feat(panel): dense tooltip pass — projects, products, social, KB, business, settings, notifications (178 tips)

Density bar: every dialog field, column header, filter, badge, and
status pill explains itself — grounded in the backend (ladder
normalization, rate-limit park/probe, self-hosted fallback, reindex
scope). Disabled buttons span-wrap so tips fire; tabs re-assert
data-state.
This commit is contained in:
Renn F
2026-07-15 17:56:12 +02:00
parent 84563e84b6
commit 57254b3ce2
38 changed files with 1205 additions and 655 deletions
@@ -17,6 +17,11 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { OfflineState } from "@/components/ui/offline-state"; import { OfflineState } from "@/components/ui/offline-state";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { usePageRefresh } from "@/hooks"; import { usePageRefresh } from "@/hooks";
import { import {
Bell, Bell,
@@ -114,20 +119,26 @@ function NotificationCard({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{notification.subject}</span> <span className="font-medium">{notification.subject}</span>
<Badge <HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
className={priorityColors[notification.priority] + " text-xs"} <Badge
> className={priorityColors[notification.priority] + " text-xs"}
{notification.priority} >
</Badge> {notification.priority}
{!notification.is_read && (
<Badge variant="secondary" className="text-xs">
New
</Badge> </Badge>
</HelpTip>
{!notification.is_read && (
<HelpTip label="Hasn't been marked read yet.">
<Badge variant="secondary" className="text-xs">
New
</Badge>
</HelpTip>
)} )}
{notification.requires_ack && !notification.is_acknowledged && ( {notification.requires_ack && !notification.is_acknowledged && (
<Badge variant="destructive" className="text-xs"> <HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
Needs Ack <Badge variant="destructive" className="text-xs">
</Badge> Needs Ack
</Badge>
</HelpTip>
)} )}
{notification.related_task_id && ( {notification.related_task_id && (
<Link <Link
@@ -329,16 +340,51 @@ function NotificationsPageContent() {
) : ( ) : (
<Tabs value={activeTab} onValueChange={handleTabChange}> <Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList> <TabsList>
<TabsTrigger value="all">All</TabsTrigger> {/* TooltipTrigger's asChild Slot merge injects its own data-state
<TabsTrigger value="unread"> onto the trigger, colliding with Radix Tabs' own literal
Unread {data && data.unread_count > 0 && `(${data.unread_count})`} data-state (same trap as task-tabs.tsx) — reassert the real
</TabsTrigger> selection state explicitly so the active-tab highlight
<TabsTrigger value="pending"> survives the merge. */}
Pending{" "} <Tooltip>
{data && <TooltipTrigger asChild>
data.pending_ack_count > 0 && <TabsTrigger
`(${data.pending_ack_count})`} value="all"
</TabsTrigger> data-state={activeTab === "all" ? "active" : "inactive"}
>
All
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
Every notification you&apos;ve received, read or not
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="unread"
data-state={activeTab === "unread" ? "active" : "inactive"}
>
Unread {data && data.unread_count > 0 && `(${data.unread_count})`}
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>Notifications you haven&apos;t opened yet</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="pending"
data-state={activeTab === "pending" ? "active" : "inactive"}
>
Pending{" "}
{data &&
data.pending_ack_count > 0 &&
`(${data.pending_ack_count})`}
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
Notifications still waiting on your acknowledgement
</TooltipContent>
</Tooltip>
</TabsList> </TabsList>
<TabsContent value={activeTab} className="mt-4 space-y-3"> <TabsContent value={activeTab} className="mt-4 space-y-3">
+3 -1
View File
@@ -121,7 +121,9 @@ export default function SettingsPage() {
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Label>Collapsed Sidebar</Label> <HelpTip label="Saved to this browser only — doesn't sync across devices.">
<Label>Collapsed Sidebar</Label>
</HelpTip>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Show icons only in the sidebar Show icons only in the sidebar
</p> </p>
@@ -96,7 +96,9 @@ interface DeliverySectionProps {
function DeliverySection({ delivery }: DeliverySectionProps) { function DeliverySection({ delivery }: DeliverySectionProps) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<SectionLabel>Delivery</SectionLabel> <HelpTip label="Live task counts across the pipeline — only 'Done (30 d)' looks backward">
<SectionLabel>Delivery</SectionLabel>
</HelpTip>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<DeliveryMetric <DeliveryMetric
label="In flight" label="In flight"
@@ -147,14 +149,18 @@ function SpendSection({ spend }: SpendSectionProps) {
<SectionLabel>Spend</SectionLabel> <SectionLabel>Spend</SectionLabel>
<div className="rounded-lg border p-3 space-y-1.5"> <div className="rounded-lg border p-3 space-y-1.5">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">30-day spend</span> <HelpTip label="Total agent-session cost over the trailing 30 days (Anthropic priced; local/Ollama models cost $0)">
<span className="text-muted-foreground">30-day spend</span>
</HelpTip>
<span className="font-medium tabular-nums"> <span className="font-medium tabular-nums">
${spend_30d_usd.toFixed(2)} ${spend_30d_usd.toFixed(2)}
</span> </span>
</div> </div>
{projected_monthly_usd !== null && ( {projected_monthly_usd !== null && (
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Projected monthly</span> <HelpTip label="The 30-day spend extrapolated to a full month">
<span className="text-muted-foreground">Projected monthly</span>
</HelpTip>
<span className="font-medium tabular-nums"> <span className="font-medium tabular-nums">
${projected_monthly_usd.toFixed(2)} ${projected_monthly_usd.toFixed(2)}
</span> </span>
+46 -28
View File
@@ -101,16 +101,20 @@ function ObjectivesEditor({
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
Objective #{rowIdx + 1} Objective #{rowIdx + 1}
</span> </span>
<Button <HelpTip label="Deletes this row locally — nothing changes until you click Save charter">
type="button" <span className="inline-block">
variant="ghost" <Button
size="sm" type="button"
disabled={disabled} variant="ghost"
onClick={() => removeRow(rowIdx)} size="sm"
className="h-6 px-2 text-xs text-destructive hover:text-destructive" disabled={disabled}
> onClick={() => removeRow(rowIdx)}
Remove className="h-6 px-2 text-xs text-destructive hover:text-destructive"
</Button> >
Remove
</Button>
</span>
</HelpTip>
</div> </div>
{keys.map((key) => ( {keys.map((key) => (
<div key={key} className="space-y-1"> <div key={key} className="space-y-1">
@@ -132,15 +136,19 @@ function ObjectivesEditor({
</div> </div>
))} ))}
</div> </div>
<Button <HelpTip label="Adds a blank row using the same field keys as the first objective">
type="button" <span className="inline-block">
variant="outline" <Button
size="sm" type="button"
disabled={disabled} variant="outline"
onClick={addRow} size="sm"
> disabled={disabled}
+ Add objective onClick={addRow}
</Button> >
+ Add objective
</Button>
</span>
</HelpTip>
</div> </div>
); );
} }
@@ -297,7 +305,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
<CardContent className="space-y-5"> <CardContent className="space-y-5">
{/* North star */} {/* North star */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="north-star">North star</Label> <HelpTip label="The long-term vision every agent's briefing includes — keep it to a sentence or two">
<Label htmlFor="north-star">North star</Label>
</HelpTip>
<Textarea <Textarea
id="north-star" id="north-star"
rows={3} rows={3}
@@ -325,7 +335,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
{/* Constraints */} {/* Constraints */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="constraints">Constraints (one per line)</Label> <HelpTip label="Hard rules injected into every agent's briefing (e.g. licensing, no external data egress)">
<Label htmlFor="constraints">Constraints (one per line)</Label>
</HelpTip>
<Textarea <Textarea
id="constraints" id="constraints"
rows={3} rows={3}
@@ -361,10 +373,14 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
</div> </div>
{/* Save */} {/* Save */}
<Button onClick={handleSave} disabled={saving}> <HelpTip label="Saves north star, brand voice, objectives, constraints, and operating policy in one update">
<Save className="h-4 w-4 mr-2" /> <span className="inline-block">
{saving ? "Saving…" : "Save charter"} <Button onClick={handleSave} disabled={saving}>
</Button> <Save className="h-4 w-4 mr-2" />
{saving ? "Saving…" : "Save charter"}
</Button>
</span>
</HelpTip>
{/* Metadata */} {/* Metadata */}
<div className="border-t pt-4 space-y-1 text-xs text-muted-foreground"> <div className="border-t pt-4 space-y-1 text-xs text-muted-foreground">
@@ -377,9 +393,11 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
{goals.updated_by && ( {goals.updated_by && (
<p> <p>
Updated by:{" "} Updated by:{" "}
<span className="font-medium text-foreground font-mono"> <HelpTip label="The agent or CEO identity that last saved this charter">
{goals.updated_by} <span className="font-medium text-foreground font-mono">
</span> {goals.updated_by}
</span>
</HelpTip>
</p> </p>
)} )}
</div> </div>
+29 -18
View File
@@ -89,9 +89,12 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
{pitch.target_cells.length > 0 && ( {pitch.target_cells.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1"> <div className="flex flex-wrap gap-1 mt-1">
{pitch.target_cells.map((c) => ( {pitch.target_cells.map((c) => (
<Badge key={c} variant="outline"> <HelpTip
{c} key={c}
</Badge> label="A cell this pitch would route delivery work to if approved"
>
<Badge variant="outline">{c}</Badge>
</HelpTip>
))} ))}
</div> </div>
)} )}
@@ -111,21 +114,29 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
</div> </div>
{proposed ? ( {proposed ? (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <HelpTip label="Auto-provisions a new product + workspace for the target cells listed above">
size="sm" <span className="inline-block">
disabled={busy} <Button
onClick={() => setApproveOpen(true)} size="sm"
> disabled={busy}
<Check className="mr-1 h-4 w-4" /> Approve &amp; provision onClick={() => setApproveOpen(true)}
</Button> >
<Button <Check className="mr-1 h-4 w-4" /> Approve &amp; provision
size="sm" </Button>
variant="outline" </span>
disabled={busy} </HelpTip>
onClick={() => setRejectOpen(true)} <HelpTip label="Rejects without creating anything — recorded in the decision log below">
> <span className="inline-block">
<X className="mr-1 h-4 w-4" /> Reject <Button
</Button> size="sm"
variant="outline"
disabled={busy}
onClick={() => setRejectOpen(true)}
>
<X className="mr-1 h-4 w-4" /> Reject
</Button>
</span>
</HelpTip>
</div> </div>
) : ( ) : (
pitch.decision_notes && ( pitch.decision_notes && (
+44 -30
View File
@@ -67,21 +67,29 @@ function DirectiveCard({
{/* Actions */} {/* Actions */}
<div className="flex gap-2 pt-1"> <div className="flex gap-2 pt-1">
<Button <HelpTip label="Executes this directive as drafted (relay a message, update the charter, approve a pitch, or control a task)">
size="sm" <span className="inline-block">
disabled={busy} <Button
onClick={() => onConfirm(directive.id)} size="sm"
> disabled={busy}
<Check className="mr-1 h-4 w-4" /> Confirm onClick={() => onConfirm(directive.id)}
</Button> >
<Button <Check className="mr-1 h-4 w-4" /> Confirm
size="sm" </Button>
variant="outline" </span>
disabled={busy} </HelpTip>
onClick={() => setRejectOpen(true)} <HelpTip label="Discards the directive — the Secretary must re-propose it to try again">
> <span className="inline-block">
<X className="mr-1 h-4 w-4" /> Reject <Button
</Button> size="sm"
variant="outline"
disabled={busy}
onClick={() => setRejectOpen(true)}
>
<X className="mr-1 h-4 w-4" /> Reject
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
@@ -244,9 +252,11 @@ export function SecretaryTab() {
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3"> <CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
<CardTitle>Chat</CardTitle> <CardTitle>Chat</CardTitle>
{sessionId && ( {sessionId && (
<Button variant="outline" size="sm" onClick={() => void stop()}> <HelpTip label="Closes this chat — your next message starts a fresh Secretary session">
End session <Button variant="outline" size="sm" onClick={() => void stop()}>
</Button> End session
</Button>
</HelpTip>
)} )}
</CardHeader> </CardHeader>
<CardContent className="flex flex-1 flex-col gap-4"> <CardContent className="flex flex-1 flex-col gap-4">
@@ -284,18 +294,22 @@ export function SecretaryTab() {
</Button> </Button>
</HelpTip> </HelpTip>
) : ( ) : (
<Button <HelpTip label="Opens a new Secretary chat session — reads live company state, not a scripted bot">
onClick={() => void handleStart()} <span className="inline-block">
disabled={starting} <Button
className="h-11 shrink-0 px-6" onClick={() => void handleStart()}
aria-label={starting ? "Starting…" : undefined} disabled={starting}
> className="h-11 shrink-0 px-6"
{starting ? ( aria-label={starting ? "Starting…" : undefined}
<Loader2 className="h-4 w-4 animate-spin" /> >
) : ( {starting ? (
"Start" <Loader2 className="h-4 w-4 animate-spin" />
)} ) : (
</Button> "Start"
)}
</Button>
</span>
</HelpTip>
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -34,6 +34,15 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [
"component", "component",
]; ];
const KIND_HINTS: Record<DefinitionKind, string> = {
model: "Pydantic / ORM data models",
route: "FastAPI route handlers",
helper: "Standalone top-level functions — placement here only warns, never blocks",
business_logic: "Service-layer logic (the classes services/ owns)",
component: "Frontend components (.tsx)",
other: "Anything the classifier can't place in the kinds above",
};
function actionToast(verb: string, result: ConventionsActionResult): void { function actionToast(verb: string, result: ConventionsActionResult): void {
if (result.created && result.pr_number != null) { if (result.created && result.pr_number != null) {
toast.success( toast.success(
@@ -189,50 +198,59 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3" className="space-y-2 rounded-md border border-border p-3"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="Repo-relative directory this rule applies to (e.g. roboco/services)">
<Input
value={module.path}
placeholder="path/to/module"
onChange={(e) =>
updateModule(index, { path: e.target.value })
}
/>
</HelpTip>
<HelpTip label="Deletes this module row locally — nothing changes until Save to repo">
<Button
variant="ghost"
size="sm"
onClick={() => removeModule(index)}
>
Remove
</Button>
</HelpTip>
</div>
<HelpTip label="One-line description shown in the ambient 'Architectural Standard' prompt block every agent sees">
<Input <Input
value={module.path} value={module.purpose}
placeholder="path/to/module" placeholder="what this module is for"
onChange={(e) => onChange={(e) =>
updateModule(index, { path: e.target.value }) updateModule(index, { purpose: e.target.value })
} }
/> />
<Button </HelpTip>
variant="ghost"
size="sm"
onClick={() => removeModule(index)}
>
Remove
</Button>
</div>
<Input
value={module.purpose}
placeholder="what this module is for"
onChange={(e) =>
updateModule(index, { purpose: e.target.value })
}
/>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{FORBIDDABLE_KINDS.map((kind) => ( {FORBIDDABLE_KINDS.map((kind) => (
<Badge <HelpTip key={kind} label={KIND_HINTS[kind]}>
key={kind} <Badge
variant={ variant={
module.forbidden.includes(kind) module.forbidden.includes(kind)
? "destructive" ? "destructive"
: "outline" : "outline"
} }
className="cursor-pointer" className="cursor-pointer"
onClick={() => toggleForbidden(index, kind)} onClick={() => toggleForbidden(index, kind)}
> >
no {kind} no {kind}
</Badge> </Badge>
</HelpTip>
))} ))}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<Button variant="outline" size="sm" onClick={addModule}> <HelpTip label="Adds a blank module row to constrain another directory">
Add module <Button variant="outline" size="sm" onClick={addModule}>
</Button> Add module
</Button>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -253,9 +271,17 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
> >
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span> <span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-10 text-right text-xs text-muted-foreground"> <HelpTip
{rule.level} label={
</span> rule.level === "block"
? "Block: refuses i_am_done / pr_pass with the offending file:line"
: "Warn: informational only, never blocks the gate"
}
>
<span className="w-10 text-right text-xs text-muted-foreground">
{rule.level}
</span>
</HelpTip>
<Switch <Switch
checked={rule.level === "block"} checked={rule.level === "block"}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
@@ -285,34 +311,50 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3" className="space-y-2 rounded-md border border-border p-3"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <HelpTip label="Repo-relative path or glob this waiver exempts from the rule on the right">
value={waiver.path} <Input
placeholder="path/to/file.py" value={waiver.path}
onChange={(e) => updateWaiver(index, { path: e.target.value })} placeholder="path/to/file.py"
/> onChange={(e) =>
<Input updateWaiver(index, { path: e.target.value })
value={waiver.rule} }
placeholder="rule name" />
onChange={(e) => updateWaiver(index, { rule: e.target.value })} </HelpTip>
/> <HelpTip label="Must match a rule name above exactly for the waiver to apply">
<Button <Input
variant="ghost" value={waiver.rule}
size="sm" placeholder="rule name"
onClick={() => removeWaiver(index)} onChange={(e) =>
> updateWaiver(index, { rule: e.target.value })
Remove }
</Button> />
</HelpTip>
<HelpTip label="Deletes this waiver row locally — nothing changes until Save to repo">
<Button
variant="ghost"
size="sm"
onClick={() => removeWaiver(index)}
>
Remove
</Button>
</HelpTip>
</div> </div>
<Input <HelpTip label="Accountability note — committed with the waiver and reviewed in the PR, never a silent suppression">
value={waiver.reason} <Input
placeholder="why this is waived" value={waiver.reason}
onChange={(e) => updateWaiver(index, { reason: e.target.value })} placeholder="why this is waived"
/> onChange={(e) =>
updateWaiver(index, { reason: e.target.value })
}
/>
</HelpTip>
</div> </div>
))} ))}
<Button variant="outline" size="sm" onClick={addWaiver}> <HelpTip label="Adds a blank waiver row">
Add waiver <Button variant="outline" size="sm" onClick={addWaiver}>
</Button> Add waiver
</Button>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -332,11 +374,13 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3" className="space-y-2 rounded-md border border-border p-3"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <HelpTip label="A short, unique identifier for this rule (shown as its badge in Recent violations)">
value={rule.id} <Input
placeholder="rule-id" value={rule.id}
onChange={(e) => updateCustom(index, { id: e.target.value })} placeholder="rule-id"
/> onChange={(e) => updateCustom(index, { id: e.target.value })}
/>
</HelpTip>
<HelpTip <HelpTip
label={ label={
rule.level === "block" rule.level === "block"
@@ -354,29 +398,41 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
updateCustom(index, { level: checked ? "block" : "warn" }) updateCustom(index, { level: checked ? "block" : "warn" })
} }
/> />
<Button <HelpTip label="Deletes this custom rule row locally — nothing changes until Save to repo">
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => removeCustom(index)} size="sm"
> onClick={() => removeCustom(index)}
Remove >
</Button> Remove
</Button>
</HelpTip>
</div> </div>
<Input <HelpTip label="Regex tested against each changed file's source text — a bad pattern is skipped, never fails the gate">
value={rule.pattern} <Input
placeholder="regex pattern" value={rule.pattern}
onChange={(e) => updateCustom(index, { pattern: e.target.value })} placeholder="regex pattern"
/> onChange={(e) =>
<Input updateCustom(index, { pattern: e.target.value })
value={rule.message} }
placeholder="message shown when it matches" />
onChange={(e) => updateCustom(index, { message: e.target.value })} </HelpTip>
/> <HelpTip label="Shown next to a match in Recent violations below">
<Input
value={rule.message}
placeholder="message shown when it matches"
onChange={(e) =>
updateCustom(index, { message: e.target.value })
}
/>
</HelpTip>
</div> </div>
))} ))}
<Button variant="outline" size="sm" onClick={addCustom}> <HelpTip label="Adds a blank custom regex rule">
Add custom rule <Button variant="outline" size="sm" onClick={addCustom}>
</Button> Add custom rule
</Button>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -144,9 +144,14 @@ function UnifiedHistoryRow({ row }: { row: UnifiedRow }) {
{PLATFORM_LABELS[platform] ?? platform}: {id} {PLATFORM_LABELS[platform] ?? platform}: {id}
</a> </a>
) : ( ) : (
<span key={platform} className="text-muted-foreground"> <HelpTip
{PLATFORM_LABELS[platform] ?? platform}: {id} key={platform}
</span> label="TikTok's inbox-upload API returns no public URL — this is the platform-side post id"
>
<span className="text-muted-foreground">
{PLATFORM_LABELS[platform] ?? platform}: {id}
</span>
</HelpTip>
), ),
)} )}
</div> </div>
@@ -211,7 +216,9 @@ export function SocialHistorySection({ className }: { className?: string }) {
size="sm" size="sm"
className="w-full justify-between" className="w-full justify-between"
> >
<span>{open ? "Hide" : "Show"} history</span> <HelpTip label="Fetches both sources once, on first expand — up to 50 rows each">
<span>{open ? "Hide" : "Show"} history</span>
</HelpTip>
<ChevronDown <ChevronDown
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
/> />
@@ -76,9 +76,11 @@ function PipelineRow({ item }: { item: VideoPipelineItem }) {
prefetch={false} prefetch={false}
className="ml-auto" className="ml-auto"
> >
<Button variant="outline" size="sm"> <HelpTip label="Open the task to approve or reject the rendered video">
Review <Button variant="outline" size="sm">
</Button> Review
</Button>
</HelpTip>
</Link> </Link>
)} )}
{canRerender && ( {canRerender && (
@@ -110,7 +112,9 @@ export function VideoPipelineStrip({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" /> <Film className="h-5 w-5" />
Video Pipeline Video Pipeline
<Badge variant="secondary">{items.length}</Badge> <HelpTip label="Videos currently authoring, in review, or rendering — not yet in the queue below">
<Badge variant="secondary">{items.length}</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Every video in flight authoring, review, or rendering before it Every video in flight authoring, review, or rendering before it
@@ -483,7 +483,9 @@ function RequestVideoDialog({
<> <>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Project</Label> <HelpTip label="Only projects with the video engine enabled (Projects → Edit) can be picked">
<Label>Project</Label>
</HelpTip>
<ProjectSelector <ProjectSelector
value={effectiveProjectId} value={effectiveProjectId}
onChange={setProjectId} onChange={setProjectId}
@@ -493,7 +495,9 @@ function RequestVideoDialog({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="video-request-occasion">Occasion</Label> <HelpTip label="Short label for the trigger event — shown as a badge throughout the pipeline, queue, and history">
<Label htmlFor="video-request-occasion">Occasion</Label>
</HelpTip>
<Input <Input
id="video-request-occasion" id="video-request-occasion"
placeholder="e.g. v0.19.0 launch, Founder's Day..." placeholder="e.g. v0.19.0 launch, Founder's Day..."
@@ -502,7 +506,9 @@ function RequestVideoDialog({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="video-request-brief">Brief</Label> <HelpTip label="What the video should communicate — read by the assigned UX/UI dev when building the composition">
<Label htmlFor="video-request-brief">Brief</Label>
</HelpTip>
<Textarea <Textarea
id="video-request-brief" id="video-request-brief"
placeholder="What should this video cover?" placeholder="What should this video cover?"
@@ -512,7 +518,9 @@ function RequestVideoDialog({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Platforms</Label> <HelpTip label="Where the rendered clip can post once you approve it — editable again before approving">
<Label>Platforms</Label>
</HelpTip>
<div className="flex gap-4"> <div className="flex gap-4">
{REQUEST_PLATFORMS.map((platform) => ( {REQUEST_PLATFORMS.map((platform) => (
<div key={platform} className="flex items-center gap-2"> <div key={platform} className="flex items-center gap-2">
@@ -521,12 +529,20 @@ function RequestVideoDialog({
checked={platforms.includes(platform)} checked={platforms.includes(platform)}
onCheckedChange={() => togglePlatform(platform)} onCheckedChange={() => togglePlatform(platform)}
/> />
<Label <HelpTip
htmlFor={`video-request-${platform}`} label={
className="text-sm font-normal" platform === "x"
? "Posts via X's OAuth 1.0a API, 280-char caption"
: "Posts via TikTok's inbox-upload OAuth2 API, 2200-char caption"
}
> >
{PLATFORM_LABELS[platform]} <Label
</Label> htmlFor={`video-request-${platform}`}
className="text-sm font-normal"
>
{PLATFORM_LABELS[platform]}
</Label>
</HelpTip>
</div> </div>
))} ))}
</div> </div>
@@ -654,10 +670,12 @@ export function VideoPostQueue({ className }: { className?: string }) {
if (isLoading) return null; if (isLoading) return null;
const requestButton = ( const requestButton = (
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}> <HelpTip label="Opens a dialog to brief a UX/UI dev on an on-demand video, independent of a release or spotlight">
<Sparkles className="mr-1 h-4 w-4" /> <Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
Request a video <Sparkles className="mr-1 h-4 w-4" />
</Button> Request a video
</Button>
</HelpTip>
); );
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
@@ -696,7 +714,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" /> <Film className="h-5 w-5" />
Video Post Queue Video Post Queue
<Badge variant="secondary">{posts.length}</Badge> <HelpTip label="Rendered clips waiting for your preview, caption edits, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardAction>{requestButton}</CardAction> <CardAction>{requestButton}</CardAction>
<CardDescription> <CardDescription>
@@ -727,7 +747,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="video-reject-reason">Reason</Label> <HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="video-reject-reason">Reason</Label>
</HelpTip>
<Textarea <Textarea
id="video-reject-reason" id="video-reject-reason"
placeholder="e.g. off-brand tone, wrong occasion..." placeholder="e.g. off-brand tone, wrong occasion..."
@@ -109,17 +109,23 @@ function XPostRow({
</span> </span>
</HelpTip> </HelpTip>
{post.release_version && ( {post.release_version && (
<Badge variant="outline">v{post.release_version}</Badge> <HelpTip label="The release this post announces">
<Badge variant="outline">v{post.release_version}</Badge>
</HelpTip>
)} )}
{post.mention && ( {post.mention && (
<Badge variant="secondary" className="max-w-56 truncate"> <HelpTip label="The X mention this draft replies to">
re: {post.mention.text} <Badge variant="secondary" className="max-w-56 truncate">
</Badge> re: {post.mention.text}
</Badge>
</HelpTip>
)} )}
{post.feature && ( {post.feature && (
<Badge variant="outline" className="max-w-56 truncate"> <HelpTip label="The shipped feature this spotlight covers">
feature: {post.feature.title} <Badge variant="outline" className="max-w-56 truncate">
</Badge> feature: {post.feature.title}
</Badge>
</HelpTip>
)} )}
</div> </div>
@@ -266,7 +272,9 @@ export function XPostQueue({ className }: { className?: string }) {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />X Post Queue <Rocket className="h-5 w-5" />X Post Queue
<Badge variant="secondary">{posts.length}</Badge> <HelpTip label="Drafts waiting for your edit, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Drafted release announcements, feature spotlights, and mention Drafted release announcements, feature spotlights, and mention
@@ -297,7 +305,9 @@ export function XPostQueue({ className }: { className?: string }) {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="x-reject-reason">Reason</Label> <HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="x-reject-reason">Reason</Label>
</HelpTip>
<Textarea <Textarea
id="x-reject-reason" id="x-reject-reason"
placeholder="e.g. tone doesn't match our voice; not worth a public reply..." placeholder="e.g. tone doesn't match our voice; not worth a public reply..."
@@ -16,6 +16,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const categoryConfig: Record< const categoryConfig: Record<
@@ -135,9 +136,11 @@ export function KBCategoryNav({
<div className="flex-1 min-w-0 text-left"> <div className="flex-1 min-w-0 text-left">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="font-medium text-sm">{config.label}</span> <span className="font-medium text-sm">{config.label}</span>
<span className="text-xs text-muted-foreground"> <HelpTip label="Document count — one document can split into several chunks at embedding time, so the chunk total (Index Stats) usually runs higher.">
{count.toLocaleString()} docs <span className="text-xs text-muted-foreground w-fit">
</span> {count.toLocaleString()} docs
</span>
</HelpTip>
</div> </div>
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground truncate">
{config.description} {config.description}
@@ -86,14 +86,16 @@ function KBCategoryViewInner({ category }: { category: KBIndexType }) {
{doc.source} {doc.source}
</p> </p>
</HelpTip> </HelpTip>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground"> <HelpTip label={new Date(doc.indexed_at).toLocaleString()}>
<Clock className="h-3 w-3" /> <div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground w-fit">
<span> <Clock className="h-3 w-3" />
{formatDistanceToNow(new Date(doc.indexed_at), { <span>
addSuffix: true, {formatDistanceToNow(new Date(doc.indexed_at), {
})} addSuffix: true,
</span> })}
</div> </span>
</div>
</HelpTip>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -85,14 +85,16 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">Filter by type</span> <span className="text-sm font-medium">Filter by type</span>
{selectedTypes.length > 0 && ( {selectedTypes.length > 0 && (
<Button <HelpTip label="Reset — search every index type again instead of just these">
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => onTypesChange([])} size="sm"
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground" onClick={() => onTypesChange([])}
> className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
Clear >
</Button> Clear
</Button>
</HelpTip>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -75,19 +75,25 @@ export function KBResultCard({ result, onClick }: KBResultCardProps) {
{result.metadata && Object.keys(result.metadata).length > 0 && ( {result.metadata && Object.keys(result.metadata).length > 0 && (
<div className="flex items-center gap-2 mt-2 flex-wrap"> <div className="flex items-center gap-2 mt-2 flex-wrap">
{typeof result.metadata.language === "string" && ( {typeof result.metadata.language === "string" && (
<span className="text-xs bg-muted px-1.5 py-0.5 rounded"> <HelpTip label="Programming language this chunk was tagged with when indexed">
{result.metadata.language} <span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
</span> {result.metadata.language}
</span>
</HelpTip>
)} )}
{typeof result.metadata.agent === "string" && ( {typeof result.metadata.agent === "string" && (
<span className="text-xs bg-muted px-1.5 py-0.5 rounded"> <HelpTip label="Agent who authored this content">
@{result.metadata.agent} <span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
</span> @{result.metadata.agent}
</span>
</HelpTip>
)} )}
{typeof result.metadata.section === "string" && ( {typeof result.metadata.section === "string" && (
<span className="text-xs bg-muted px-1.5 py-0.5 rounded"> <HelpTip label="Section of the source document this chunk was extracted from">
{result.metadata.section} <span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
</span> {result.metadata.section}
</span>
</HelpTip>
)} )}
</div> </div>
)} )}
@@ -142,9 +142,11 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
</div> </div>
</div> </div>
{latestUpdated && ( {latestUpdated && (
<p className="text-xs text-muted-foreground pt-1"> <HelpTip label={new Date(latestUpdated).toLocaleString()}>
Updated {formatDistanceToNow(new Date(latestUpdated))} ago <p className="text-xs text-muted-foreground pt-1 w-fit">
</p> Updated {formatDistanceToNow(new Date(latestUpdated))} ago
</p>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -17,6 +17,11 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { import {
@@ -326,26 +331,60 @@ function KnowledgeBaseBrowserContent() {
onValueChange={(v) => handleTabChange(v as TabValue)} onValueChange={(v) => handleTabChange(v as TabValue)}
> >
<TabsList> <TabsList>
<TabsTrigger value="search" className="gap-2"> {(
<Search className="h-4 w-4" /> [
Search {
</TabsTrigger> value: "search",
<TabsTrigger value="ask" className="gap-2"> icon: Search,
<Sparkles className="h-4 w-4" /> label: "Search",
Ask AI hint: "Semantic search across indexed content, filterable by category",
</TabsTrigger> },
<TabsTrigger value="mentor" className="gap-2"> {
<Brain className="h-4 w-4" /> value: "ask",
Mentor icon: Sparkles,
</TabsTrigger> label: "Ask AI",
<TabsTrigger value="browse" className="gap-2"> hint: "One-shot Q&A: retrieves matching chunks (RAG) and generates a cited answer",
<FolderTree className="h-4 w-4" /> },
Browse {
</TabsTrigger> value: "mentor",
<TabsTrigger value="admin" className="gap-2"> icon: Brain,
<Settings className="h-4 w-4" /> label: "Mentor",
Admin hint: "Multi-turn chat that also factors in your role and your own journal entries",
</TabsTrigger> },
{
value: "browse",
icon: FolderTree,
label: "Browse",
hint: "Page through every indexed document by category, no query needed",
},
{
value: "admin",
icon: Settings,
label: "Admin",
hint: "Index health, storage stats, and manual reindex/refresh/delete controls",
},
] as const
).map((tab) => (
<Tooltip key={tab.value}>
<TooltipTrigger asChild>
{/* TooltipTrigger's asChild Slot merge injects its own
data-state onto this trigger, and Radix Tabs' internal
render spreads incoming props after its own literal
data-state — so the tooltip's value silently wins and the
data-[state=active] highlight never fires. Re-assert the
real selection state explicitly so it survives the merge. */}
<TabsTrigger
value={tab.value}
data-state={tab.value === activeTab ? "active" : "inactive"}
className="gap-2"
>
<tab.icon className="h-4 w-4" />
{tab.label}
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>{tab.hint}</TooltipContent>
</Tooltip>
))}
</TabsList> </TabsList>
{/* Search Tab */} {/* Search Tab */}
@@ -460,17 +499,27 @@ function KnowledgeBaseBrowserContent() {
<XCircle className="h-8 w-8 text-red-600" /> <XCircle className="h-8 w-8 text-red-600" />
)} )}
<div> <div>
<p className="font-medium"> <HelpTip label="Healthy requires all three subsystems below — embedding, LLM, and vector store — to each respond OK">
{health.healthy ? "Healthy" : "Unhealthy"} <p className="font-medium w-fit">
</p> {health.healthy ? "Healthy" : "Unhealthy"}
<p className="text-sm text-muted-foreground"> </p>
Embedding: {health.embedding_status} </HelpTip>
</p> <HelpTip label="Ollama-served embedding model (qwen3-embedding:0.6b) that turns text into vectors for search">
<p className="text-sm text-muted-foreground w-fit">
Embedding: {health.embedding_status}
</p>
</HelpTip>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground"> <div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div>LLM: {health.llm_status}</div> <HelpTip label="Local model that generates RAG and Mentor answers from retrieved context">
<div>Vector: {health.vector_store_status}</div> <div className="w-fit">LLM: {health.llm_status}</div>
</HelpTip>
<HelpTip label="PostgreSQL + pgvector store holding every embedded chunk for similarity search">
<div className="w-fit">
Vector: {health.vector_store_status}
</div>
</HelpTip>
</div> </div>
{( {(
[ [
@@ -507,10 +556,12 @@ function KnowledgeBaseBrowserContent() {
<span>Summary</span> <span>Summary</span>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button size="sm" variant="destructive"> <HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
<RefreshCw className="h-3 w-3 mr-1" /> <Button size="sm" variant="destructive">
Reindex All <RefreshCw className="h-3 w-3 mr-1" />
</Button> Reindex All
</Button>
</HelpTip>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -538,10 +589,14 @@ function KnowledgeBaseBrowserContent() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <HelpTip label="Sum of documents indexed across every category below">
<p className="text-2xl font-bold">{totalDocs}</p> <div className="w-fit">
<p className="text-xs text-muted-foreground">Documents</p> <p className="text-2xl font-bold">{totalDocs}</p>
</div> <p className="text-xs text-muted-foreground">
Documents
</p>
</div>
</HelpTip>
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks"> <HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
<div className="w-fit"> <div className="w-fit">
<p className="text-2xl font-bold">{totalChunks}</p> <p className="text-2xl font-bold">{totalChunks}</p>
@@ -667,14 +722,16 @@ function KnowledgeBaseBrowserContent() {
</div> </div>
<div className="grid grid-cols-3 gap-4 text-sm mb-2"> <div className="grid grid-cols-3 gap-4 text-sm mb-2">
<div> <HelpTip label="Document count for this category — see Chunks for the post-embedding split count">
<span className="text-muted-foreground"> <div className="w-fit">
Documents: <span className="text-muted-foreground">
</span>{" "} Documents:
<span className="font-medium"> </span>{" "}
{index.document_count} <span className="font-medium">
</span> {index.document_count}
</div> </span>
</div>
</HelpTip>
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks"> <HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
<div className="w-fit"> <div className="w-fit">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
@@ -685,25 +742,37 @@ function KnowledgeBaseBrowserContent() {
</span> </span>
</div> </div>
</HelpTip> </HelpTip>
<div> <HelpTip
<span className="text-muted-foreground"> label={
Updated: index.last_updated
</span>{" "} ? new Date(
<span className="font-medium"> index.last_updated,
{index.last_updated ).toLocaleString()
? formatDistanceToNow( : "This index has never been populated"
new Date(index.last_updated), }
) + " ago" >
: "Never"} <div className="w-fit">
</span> <span className="text-muted-foreground">
</div> Updated:
</span>{" "}
<span className="font-medium">
{index.last_updated
? formatDistanceToNow(
new Date(index.last_updated),
) + " ago"
: "Never"}
</span>
</div>
</HelpTip>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground"> <HelpTip label="Share of the fleet's total chunk count this category holds — not literal disk bytes">
<span>Storage usage</span> <div className="flex justify-between text-xs text-muted-foreground">
<span>{percentage}%</span> <span>Storage usage</span>
</div> <span>{percentage}%</span>
</div>
</HelpTip>
<Progress value={percentage} className="h-1" /> <Progress value={percentage} className="h-1" />
</div> </div>
</div> </div>
@@ -149,28 +149,32 @@ export function MentorAnswerDisplay({
<Brain className="h-5 w-5 text-primary shrink-0" /> <Brain className="h-5 w-5 text-primary shrink-0" />
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm"> <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
{response.agent_role && ( {response.agent_role && (
<span className="flex items-center gap-1.5"> <HelpTip label="Detected from your agent identity — shapes which standards, journals, and playbooks the mentor draws on">
<User className="h-3.5 w-3.5 text-muted-foreground" /> <span className="flex items-center gap-1.5 w-fit">
<span className="text-muted-foreground">Role:</span> <User className="h-3.5 w-3.5 text-muted-foreground" />
<Badge variant="secondary" className="font-medium"> <span className="text-muted-foreground">Role:</span>
{response.agent_role} <Badge variant="secondary" className="font-medium">
</Badge> {response.agent_role}
{response.agent_team && ( </Badge>
<span className="text-muted-foreground"> {response.agent_team && (
({response.agent_team}) <span className="text-muted-foreground">
</span> ({response.agent_team})
)} </span>
</span> )}
</span>
</HelpTip>
)} )}
{response.journal_entries_used !== undefined && {response.journal_entries_used !== undefined &&
response.journal_entries_used > 0 && ( response.journal_entries_used > 0 && (
<span className="flex items-center gap-1.5"> <HelpTip label="Entries from your own journal only — never another agent's">
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" /> <span className="flex items-center gap-1.5 w-fit">
<span className="text-muted-foreground"> <BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
{response.journal_entries_used} personal journal <span className="text-muted-foreground">
{response.journal_entries_used !== 1 ? "s" : ""} used {response.journal_entries_used} personal journal
{response.journal_entries_used !== 1 ? "s" : ""} used
</span>
</span> </span>
</span> </HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -182,9 +186,11 @@ export function MentorAnswerDisplay({
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Brain className="h-4 w-4 text-primary" /> <Brain className="h-4 w-4 text-primary" />
Mentor Response Mentor Response
<span className="text-xs text-muted-foreground font-normal ml-auto"> <HelpTip label="Knowledge-base chunks retrieved and handed to the model as context for this answer">
{response.sources.length} sources used <span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
</span> {response.sources.length} sources used
</span>
</HelpTip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -204,16 +210,17 @@ export function MentorAnswerDisplay({
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{response.suggested_followups.map((followup, index) => ( {response.suggested_followups.map((followup, index) => (
<Button <HelpTip key={index} label="Ask this next, in the same conversation">
key={index} <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-xs h-auto py-1.5 px-3" className="text-xs h-auto py-1.5 px-3"
onClick={() => onFollowUp?.(followup)} onClick={() => onFollowUp?.(followup)}
> >
<MessageCircleQuestion className="h-3 w-3 mr-1.5" /> <MessageCircleQuestion className="h-3 w-3 mr-1.5" />
{followup} {followup}
</Button> </Button>
</HelpTip>
))} ))}
</div> </div>
</div> </div>
@@ -10,6 +10,7 @@ import {
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { import {
@@ -177,15 +178,19 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
<Brain className="h-5 w-5 text-primary" /> <Brain className="h-5 w-5 text-primary" />
<span className="font-medium">Mentor Chat</span> <span className="font-medium">Mentor Chat</span>
{conversationId && ( {conversationId && (
<Badge variant="outline" className="text-xs"> <HelpTip label="Follow-up questions reuse this conversation's history for context">
Conversation active <Badge variant="outline" className="text-xs w-fit">
</Badge> Conversation active
</Badge>
</HelpTip>
)} )}
</div> </div>
<Button variant="ghost" size="sm" onClick={handleNewChat}> <HelpTip label="Clears this thread — the mentor loses this conversation's context">
<RotateCcw className="h-4 w-4 mr-1" /> <Button variant="ghost" size="sm" onClick={handleNewChat}>
New Chat <RotateCcw className="h-4 w-4 mr-1" />
</Button> New Chat
</Button>
</HelpTip>
</div> </div>
{/* Messages */} {/* Messages */}
@@ -241,19 +246,21 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
{/* Sources toggle */} {/* Sources toggle */}
{msg.sources && msg.sources.length > 0 && ( {msg.sources && msg.sources.length > 0 && (
<div> <div>
<Button <HelpTip label="The knowledge-base chunks retrieved and handed to the model as context for this answer">
variant="ghost" <Button
size="sm" variant="ghost"
className="text-xs" size="sm"
onClick={() => className="text-xs"
setExpandedSources( onClick={() =>
expandedSources === idx ? null : idx, setExpandedSources(
) expandedSources === idx ? null : idx,
} )
> }
{expandedSources === idx ? "Hide" : "Show"}{" "} >
{msg.sources.length} sources {expandedSources === idx ? "Hide" : "Show"}{" "}
</Button> {msg.sources.length} sources
</Button>
</HelpTip>
{expandedSources === idx && ( {expandedSources === idx && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{msg.sources.map((source, sidx) => ( {msg.sources.map((source, sidx) => (
@@ -278,16 +285,20 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
{msg.followups && msg.followups.length > 0 && ( {msg.followups && msg.followups.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{msg.followups.map((followup, fidx) => ( {msg.followups.map((followup, fidx) => (
<Button <HelpTip
key={fidx} key={fidx}
variant="outline" label="Ask this next, in the same conversation"
size="sm"
className="text-xs h-auto py-1.5"
onClick={() => handleFollowUp(followup)}
disabled={isLoading}
> >
{followup} <Button
</Button> variant="outline"
size="sm"
className="text-xs h-auto py-1.5"
onClick={() => handleFollowUp(followup)}
disabled={isLoading}
>
{followup}
</Button>
</HelpTip>
))} ))}
</div> </div>
)} )}
@@ -10,6 +10,7 @@ import {
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
Send, Send,
Loader2, Loader2,
@@ -60,18 +61,33 @@ export function MentorQueryInput({
</div> </div>
</div> </div>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<Badge variant="outline" className="text-xs gap-1 bg-background"> <HelpTip label="Answers factor in your agent role and team">
<User className="h-3 w-3" /> <Badge
Role-aware variant="outline"
</Badge> className="text-xs gap-1 bg-background w-fit"
<Badge variant="outline" className="text-xs gap-1 bg-background"> >
<BookMarked className="h-3 w-3" /> <User className="h-3 w-3" />
Journals Role-aware
</Badge> </Badge>
<Badge variant="outline" className="text-xs gap-1 bg-background"> </HelpTip>
<MessageCircle className="h-3 w-3" /> <HelpTip label="Searches your own journal entries — not other agents'">
Follow-ups <Badge
</Badge> variant="outline"
className="text-xs gap-1 bg-background w-fit"
>
<BookMarked className="h-3 w-3" />
Journals
</Badge>
</HelpTip>
<HelpTip label="Multi-turn — reuses this conversation's context for related questions">
<Badge
variant="outline"
className="text-xs gap-1 bg-background w-fit"
>
<MessageCircle className="h-3 w-3" />
Follow-ups
</Badge>
</HelpTip>
</div> </div>
</div> </div>
@@ -5,6 +5,7 @@ import { RAGQueryResponse } from "@/types";
import { RAGCitationCard } from "./rag-citation-card"; import { RAGCitationCard } from "./rag-citation-card";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
Bot, Bot,
BookOpen, BookOpen,
@@ -120,9 +121,11 @@ export function RAGAnswerDisplay({
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Bot className="h-4 w-4 text-primary" /> <Bot className="h-4 w-4 text-primary" />
AI Answer AI Answer
<span className="text-xs text-muted-foreground font-normal ml-auto"> <HelpTip label="Knowledge-base chunks retrieved by hybrid search and handed to the model as context for this answer">
{response.context_used} sources used <span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
</span> {response.context_used} sources used
</span>
</HelpTip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -178,20 +178,26 @@ function BellRow({ notification, onMarkRead, onAcknowledge }: BellRowProps) {
<span className="font-medium text-sm line-clamp-1 flex-1 min-w-0"> <span className="font-medium text-sm line-clamp-1 flex-1 min-w-0">
{notification.subject} {notification.subject}
</span> </span>
<Badge variant="outline" className="text-[10px] shrink-0"> <HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
{notification.priority} <Badge variant="outline" className="text-[10px] shrink-0">
</Badge> {notification.priority}
</Badge>
</HelpTip>
</div> </div>
<div className="flex flex-wrap items-center gap-1.5 mt-1.5"> <div className="flex flex-wrap items-center gap-1.5 mt-1.5">
{!notification.is_read && ( {!notification.is_read && (
<Badge variant="secondary" className="text-[10px]"> <HelpTip label="Hasn't been marked read yet.">
New <Badge variant="secondary" className="text-[10px]">
</Badge> New
</Badge>
</HelpTip>
)} )}
{needsAck && ( {needsAck && (
<Badge variant="destructive" className="text-[10px]"> <HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
Needs Ack <Badge variant="destructive" className="text-[10px]">
</Badge> Needs Ack
</Badge>
</HelpTip>
)} )}
<div className="ml-auto flex items-center gap-1"> <div className="ml-auto flex items-center gap-1">
{!notification.is_read && ( {!notification.is_read && (
@@ -26,6 +26,7 @@ import {
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Team, type ProductCellMapping } from "@/types"; import { Team, type ProductCellMapping } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
const cells: { value: Team; label: string }[] = [ const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" }, { value: Team.BACKEND, label: "Backend" },
@@ -104,12 +105,14 @@ export function CreateProductDialog() {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <HelpTip label="Groups per-cell projects (e.g. a SaaS app + its OSS core) into one deliverable for MegaTask and board routing.">
<Button> <DialogTrigger asChild>
<Plus className="h-4 w-4 mr-2" /> <Button>
New Product <Plus className="h-4 w-4 mr-2" />
</Button> New Product
</DialogTrigger> </Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogHeader> <DialogHeader>
@@ -121,7 +124,9 @@ export function CreateProductDialog() {
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
{/* Name */} {/* Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Product Name *</Label> <HelpTip label="Display name shown across the panel; renaming it never touches the slug already generated.">
<Label htmlFor="name">Product Name *</Label>
</HelpTip>
<Input <Input
id="name" id="name"
value={name} value={name}
@@ -132,7 +137,9 @@ export function CreateProductDialog() {
{/* Slug */} {/* Slug */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="slug">Slug *</Label> <HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
<Label htmlFor="slug">Slug *</Label>
</HelpTip>
<Input <Input
id="slug" id="slug"
value={slug} value={slug}
@@ -147,7 +154,9 @@ export function CreateProductDialog() {
{/* Description */} {/* Description */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="description">Description</Label> <HelpTip label="Optional context shown in the panel only — not surfaced to agents or used in task routing.">
<Label htmlFor="description">Description</Label>
</HelpTip>
<Textarea <Textarea
id="description" id="description"
value={description} value={description}
@@ -158,15 +167,19 @@ export function CreateProductDialog() {
{/* Cell -> Project mapping */} {/* Cell -> Project mapping */}
<div className="grid gap-3"> <div className="grid gap-3">
<Label>Cell Project Mapping</Label> <HelpTip label="Routes each cell's work for this product to its own repo; a cell left unmapped does no work for this product.">
<Label>Cell Project Mapping</Label>
</HelpTip>
{cells.map((cell) => ( {cells.map((cell) => (
<div key={cell.value} className="grid gap-2"> <div key={cell.value} className="grid gap-2">
<Label <HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
htmlFor={`cell-${cell.value}`} <Label
className="text-sm text-muted-foreground" htmlFor={`cell-${cell.value}`}
> className="text-sm text-muted-foreground"
{cell.label} >
</Label> {cell.label}
</Label>
</HelpTip>
<Select <Select
value={cellMapping[cell.value] ?? ""} value={cellMapping[cell.value] ?? ""}
onValueChange={(value) => onValueChange={(value) =>
@@ -30,6 +30,7 @@ import {
type ProductCellMapping, type ProductCellMapping,
type ProductUpdate, type ProductUpdate,
} from "@/types"; } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
const cells: { value: Team; label: string }[] = [ const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" }, { value: Team.BACKEND, label: "Backend" },
@@ -120,7 +121,9 @@ function EditProductForm({
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
{/* Slug (read-only) */} {/* Slug (read-only) */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="slug">Slug</Label> <HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API. Set at creation, fixed here.">
<Label htmlFor="slug">Slug</Label>
</HelpTip>
<Input <Input
id="slug" id="slug"
value={product.slug} value={product.slug}
@@ -131,7 +134,9 @@ function EditProductForm({
{/* Name */} {/* Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Product Name *</Label> <HelpTip label="Display name shown across the panel; renaming it never touches the slug.">
<Label htmlFor="name">Product Name *</Label>
</HelpTip>
<Input <Input
id="name" id="name"
value={name} value={name}
@@ -142,7 +147,9 @@ function EditProductForm({
{/* Description */} {/* Description */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="description">Description</Label> <HelpTip label="Optional context shown in the panel only — not surfaced to agents or used in task routing.">
<Label htmlFor="description">Description</Label>
</HelpTip>
<Textarea <Textarea
id="description" id="description"
value={description} value={description}
@@ -153,15 +160,19 @@ function EditProductForm({
{/* Cell -> Project mapping */} {/* Cell -> Project mapping */}
<div className="grid gap-3"> <div className="grid gap-3">
<Label>Cell Project Mapping</Label> <HelpTip label="Routes each cell's work for this product to its own repo; a cell left unmapped does no work for this product.">
<Label>Cell Project Mapping</Label>
</HelpTip>
{cells.map((cell) => ( {cells.map((cell) => (
<div key={cell.value} className="grid gap-2"> <div key={cell.value} className="grid gap-2">
<Label <HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
htmlFor={`cell-${cell.value}`} <Label
className="text-sm text-muted-foreground" htmlFor={`cell-${cell.value}`}
> className="text-sm text-muted-foreground"
{cell.label} >
</Label> {cell.label}
</Label>
</HelpTip>
<Select <Select
value={cellMapping[cell.value] ?? ""} value={cellMapping[cell.value] ?? ""}
onValueChange={(value) => onValueChange={(value) =>
@@ -126,9 +126,21 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Product</TableHead> <TableHead>
<TableHead>Cells</TableHead> <HelpTip label="Name and slug; click either to open the edit dialog.">
<TableHead>Progress</TableHead> <span>Product</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Which project each cell (Backend/Frontend/UX-UI) works on for this product.">
<span>Cells</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Done / active / blocked counts across all mapped projects' tasks.">
<span>Progress</span>
</HelpTip>
</TableHead>
<TableHead className="w-[100px]">Actions</TableHead> <TableHead className="w-[100px]">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -144,9 +156,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
> >
{product.name} {product.name}
</Button> </Button>
<p className="text-xs text-muted-foreground font-mono"> <HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
{product.slug} <p className="text-xs text-muted-foreground font-mono">
</p> {product.slug}
</p>
</HelpTip>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -188,9 +202,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
> >
{product.name} {product.name}
</Button> </Button>
<p className="text-xs text-muted-foreground font-mono"> <HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
{product.slug} <p className="text-xs text-muted-foreground font-mono">
</p> {product.slug}
</p>
</HelpTip>
</div> </div>
<HelpTip label="Edit product name, description, and cell-project mapping"> <HelpTip label="Edit product name, description, and cell-project mapping">
<Button <Button
@@ -121,12 +121,14 @@ export function CreateProjectDialog() {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <HelpTip label="Registers a git repository — sets cell ownership, branch/environment ladder, and CI/CD gate commands for agents to work on it.">
<Button> <DialogTrigger asChild>
<Plus className="h-4 w-4 mr-2" /> <Button>
New Project <Plus className="h-4 w-4 mr-2" />
</Button> New Project
</DialogTrigger> </Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="sm:max-w-[525px]"> <DialogContent className="sm:max-w-[525px]">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogHeader> <DialogHeader>
@@ -138,7 +140,9 @@ export function CreateProjectDialog() {
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
{/* Name */} {/* Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Project Name *</Label> <HelpTip label="Display name shown across the panel and CEO approval queues; changing it later never touches the slug or workspace path already derived from it.">
<Label htmlFor="name">Project Name *</Label>
</HelpTip>
<Input <Input
id="name" id="name"
value={formData.name} value={formData.name}
@@ -149,7 +153,9 @@ export function CreateProjectDialog() {
{/* Slug */} {/* Slug */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="slug">Slug *</Label> <HelpTip label="Immutable after creation — composes each agent's workspace clone path (/data/workspaces/<slug>/<cell>/<agent>) and appears in every branch name for this project.">
<Label htmlFor="slug">Slug *</Label>
</HelpTip>
<Input <Input
id="slug" id="slug"
value={formData.slug} value={formData.slug}
@@ -166,7 +172,9 @@ export function CreateProjectDialog() {
{/* Git URL */} {/* Git URL */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="git_url">Git URL *</Label> <HelpTip label="Cloned into each assigned agent's workspace on first access; use HTTPS so the encrypted token below can authenticate clone, push, and PR operations.">
<Label htmlFor="git_url">Git URL *</Label>
</HelpTip>
<Input <Input
id="git_url" id="git_url"
value={formData.git_url} value={formData.git_url}
@@ -205,7 +213,9 @@ export function CreateProjectDialog() {
{/* Assigned Cell */} {/* Assigned Cell */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="assigned_cell">Assigned Cell *</Label> <HelpTip label="Which cell owns this project — only that cell's agents can claim its tasks (enforced server-side, not just a UI filter).">
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
</HelpTip>
<Select <Select
value={formData.assigned_cell} value={formData.assigned_cell}
onValueChange={(value: Team) => onValueChange={(value: Team) =>
@@ -227,7 +237,9 @@ export function CreateProjectDialog() {
{/* Default Branch */} {/* Default Branch */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="default_branch">Default Branch</Label> <HelpTip label="Used as both head and prod when no environment ladder is set below (a degenerate single-rung ladder) — the PR review gate diffs against it and releases cut from it.">
<Label htmlFor="default_branch">Default Branch</Label>
</HelpTip>
<Input <Input
id="default_branch" id="default_branch"
value={formData.default_branch} value={formData.default_branch}
@@ -248,20 +260,24 @@ export function CreateProjectDialog() {
/> />
{/* Advanced Options Toggle */} {/* Advanced Options Toggle */}
<Button <HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
type="button" <Button
variant="ghost" type="button"
className="justify-start px-0 text-muted-foreground" variant="ghost"
onClick={() => setShowAdvanced(!showAdvanced)} className="justify-start px-0 text-muted-foreground"
> onClick={() => setShowAdvanced(!showAdvanced)}
{showAdvanced ? "Hide" : "Show"} CI/CD Commands >
</Button> {showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip>
{showAdvanced && ( {showAdvanced && (
<> <>
{/* CI/CD Commands */} {/* CI/CD Commands */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="test_command">Test Command</Label> <HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
<Label htmlFor="test_command">Test Command</Label>
</HelpTip>
<Input <Input
id="test_command" id="test_command"
value={formData.test_command || ""} value={formData.test_command || ""}
@@ -273,7 +289,9 @@ export function CreateProjectDialog() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="lint_command">Lint Command</Label> <HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="lint_command">Lint Command</Label>
</HelpTip>
<Input <Input
id="lint_command" id="lint_command"
value={formData.lint_command || ""} value={formData.lint_command || ""}
@@ -285,7 +303,9 @@ export function CreateProjectDialog() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="format_command">Format Command</Label> <HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input <Input
id="format_command" id="format_command"
value={formData.format_command || ""} value={formData.format_command || ""}
@@ -300,7 +320,9 @@ export function CreateProjectDialog() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="typecheck_command">Typecheck Command</Label> <HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="typecheck_command">Typecheck Command</Label>
</HelpTip>
<Input <Input
id="typecheck_command" id="typecheck_command"
value={formData.typecheck_command || ""} value={formData.typecheck_command || ""}
@@ -315,7 +337,9 @@ export function CreateProjectDialog() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="build_command">Build Command</Label> <HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
<Label htmlFor="build_command">Build Command</Label>
</HelpTip>
<Input <Input
id="build_command" id="build_command"
value={formData.build_command || ""} value={formData.build_command || ""}
@@ -330,7 +354,9 @@ export function CreateProjectDialog() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="quality_command">Quality Gate Command</Label> <HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
<Label htmlFor="quality_command">Quality Gate Command</Label>
</HelpTip>
<Input <Input
id="quality_command" id="quality_command"
value={formData.quality_command || ""} value={formData.quality_command || ""}
@@ -271,7 +271,9 @@ function EditProjectForm({
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
{/* Slug (read-only) */} {/* Slug (read-only) */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="slug">Slug</Label> <HelpTip label="Immutable — composes each agent's workspace clone path and appears in every branch name for this project. Set at creation, fixed here.">
<Label htmlFor="slug">Slug</Label>
</HelpTip>
<Input <Input
id="slug" id="slug"
value={project.slug} value={project.slug}
@@ -282,7 +284,9 @@ function EditProjectForm({
{/* Name */} {/* Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Project Name *</Label> <HelpTip label="Display name shown across the panel and CEO approval queues; renaming it never touches the slug or workspace path.">
<Label htmlFor="name">Project Name *</Label>
</HelpTip>
<Input <Input
id="name" id="name"
value={name} value={name}
@@ -293,7 +297,9 @@ function EditProjectForm({
{/* Git URL */} {/* Git URL */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="git_url">Git URL *</Label> <HelpTip label="Cloned into each assigned agent's workspace; use HTTPS so the token below can authenticate clone, push, and PR operations.">
<Label htmlFor="git_url">Git URL *</Label>
</HelpTip>
<Input <Input
id="git_url" id="git_url"
value={gitUrl} value={gitUrl}
@@ -348,9 +354,17 @@ function EditProjectForm({
{!clearToken && ( {!clearToken && (
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="git_token" className="text-sm"> <HelpTip
{project.has_git_token ? "Replace token" : "Set token"} label={
</Label> project.has_git_token
? "Overwrites the current token immediately on save; the previous token is discarded and cannot be recovered."
: "Required for HTTPS clone/push/PR operations if the repo is private; stored Fernet-encrypted and never re-displayed once saved."
}
>
<Label htmlFor="git_token" className="text-sm">
{project.has_git_token ? "Replace token" : "Set token"}
</Label>
</HelpTip>
<Input <Input
id="git_token" id="git_token"
type="password" type="password"
@@ -368,7 +382,9 @@ function EditProjectForm({
{/* Assigned Cell */} {/* Assigned Cell */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="assigned_cell">Assigned Cell *</Label> <HelpTip label="Which cell owns this project — only that cell's agents can claim its tasks (enforced server-side, not just a UI filter).">
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
</HelpTip>
<Select <Select
value={assignedCell} value={assignedCell}
onValueChange={(value: Team) => setAssignedCell(value)} onValueChange={(value: Team) => setAssignedCell(value)}
@@ -388,7 +404,9 @@ function EditProjectForm({
{/* Default Branch */} {/* Default Branch */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="default_branch">Default Branch</Label> <HelpTip label="Used as both head and prod when no environment ladder is set below (a degenerate single-rung ladder) — the PR review gate diffs against it and releases cut from it.">
<Label htmlFor="default_branch">Default Branch</Label>
</HelpTip>
<Input <Input
id="default_branch" id="default_branch"
value={defaultBranch} value={defaultBranch}
@@ -419,19 +437,23 @@ function EditProjectForm({
</div> </div>
{/* Advanced Options Toggle */} {/* Advanced Options Toggle */}
<Button <HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
type="button" <Button
variant="ghost" type="button"
className="justify-start px-0 text-muted-foreground" variant="ghost"
onClick={() => setShowAdvanced(!showAdvanced)} className="justify-start px-0 text-muted-foreground"
> onClick={() => setShowAdvanced(!showAdvanced)}
{showAdvanced ? "Hide" : "Show"} CI/CD Commands >
</Button> {showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip>
{showAdvanced && ( {showAdvanced && (
<> <>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="test_command">Test Command</Label> <HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
<Label htmlFor="test_command">Test Command</Label>
</HelpTip>
<Input <Input
id="test_command" id="test_command"
value={testCommand} value={testCommand}
@@ -441,7 +463,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="lint_command">Lint Command</Label> <HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="lint_command">Lint Command</Label>
</HelpTip>
<Input <Input
id="lint_command" id="lint_command"
value={lintCommand} value={lintCommand}
@@ -451,7 +475,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="format_command">Format Command</Label> <HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input <Input
id="format_command" id="format_command"
value={formatCommand} value={formatCommand}
@@ -461,7 +487,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="typecheck_command">Typecheck Command</Label> <HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="typecheck_command">Typecheck Command</Label>
</HelpTip>
<Input <Input
id="typecheck_command" id="typecheck_command"
value={typecheckCommand} value={typecheckCommand}
@@ -471,7 +499,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="build_command">Build Command</Label> <HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
<Label htmlFor="build_command">Build Command</Label>
</HelpTip>
<Input <Input
id="build_command" id="build_command"
value={buildCommand} value={buildCommand}
@@ -481,7 +511,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="quality_command">Quality Gate Command</Label> <HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
<Label htmlFor="quality_command">Quality Gate Command</Label>
</HelpTip>
<Input <Input
id="quality_command" id="quality_command"
value={qualityCommand} value={qualityCommand}
@@ -497,14 +529,16 @@ function EditProjectForm({
)} )}
{/* Autonomous Maintenance Toggle */} {/* Autonomous Maintenance Toggle */}
<Button <HelpTip label="CI-watch, video engine, dependency-update bot, and sandbox DB/Redis/Mongo opt-ins — each also needs its own fleet-wide flag armed to actually run.">
type="button" <Button
variant="ghost" type="button"
className="justify-start px-0 text-muted-foreground" variant="ghost"
onClick={() => setShowAutonomy(!showAutonomy)} className="justify-start px-0 text-muted-foreground"
> onClick={() => setShowAutonomy(!showAutonomy)}
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance >
</Button> {showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
</Button>
</HelpTip>
{showAutonomy && ( {showAutonomy && (
<> <>
@@ -522,7 +556,9 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="ci_watch_workflow">CI-watch Workflow</Label> <HelpTip label="Scopes CI-watch to one workflow's runs so a green run elsewhere can't mask a red one here; leave blank to fall back to the fleet default (ci.yml).">
<Label htmlFor="ci_watch_workflow">CI-watch Workflow</Label>
</HelpTip>
<Input <Input
id="ci_watch_workflow" id="ci_watch_workflow"
value={ciWatchWorkflow} value={ciWatchWorkflow}
@@ -549,9 +585,11 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="dep_update_command"> <HelpTip label="Dry-run only — the weekly bot runs this in a throwaway clone to detect a lockfile diff; nothing is committed until it opens a task that rides the normal PR-review flow.">
Dependency-Update Command <Label htmlFor="dep_update_command">
</Label> Dependency-Update Command
</Label>
</HelpTip>
<Input <Input
id="dep_update_command" id="dep_update_command"
value={depUpdateCommand} value={depUpdateCommand}
@@ -565,9 +603,11 @@ function EditProjectForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="dep_update_paths"> <HelpTip label="Which lockfile paths the dry-run diffs to detect a change; leave blank to auto-infer uv.lock / pnpm-lock.yaml.">
Dependency-Update Lockfile Paths <Label htmlFor="dep_update_paths">
</Label> Dependency-Update Lockfile Paths
</Label>
</HelpTip>
<Input <Input
id="dep_update_paths" id="dep_update_paths"
value={depUpdatePaths} value={depUpdatePaths}
@@ -98,52 +98,83 @@ export function EnvironmentLadderEditor({
<div className="flex flex-col"> <div className="flex flex-col">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button {/* span-wrap: disabled Button has pointer-events-none,
type="button" so the tooltip needs a hoverable wrapper to fire
variant="ghost" when isFirst disables the button. */}
size="icon" <span className="inline-block">
className="h-6 w-6" <Button
disabled={isFirst} type="button"
onClick={() => handleMove(index, -1)} variant="ghost"
aria-label="Move earlier in the flow" size="icon"
> className="h-6 w-6"
<ArrowUp className="h-3.5 w-3.5" /> disabled={isFirst}
</Button> onClick={() => handleMove(index, -1)}
aria-label="Move earlier in the flow"
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Move earlier in the flow</TooltipContent> <TooltipContent>
{isFirst
? "Already first — lands PRs, nothing to promote from."
: "Move earlier in the flow"}
</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <span className="inline-block">
type="button" <Button
variant="ghost" type="button"
size="icon" variant="ghost"
className="h-6 w-6" size="icon"
disabled={isLast} className="h-6 w-6"
onClick={() => handleMove(index, 1)} disabled={isLast}
aria-label="Move later in the flow" onClick={() => handleMove(index, 1)}
> aria-label="Move later in the flow"
<ArrowDown className="h-3.5 w-3.5" /> >
</Button> <ArrowDown className="h-3.5 w-3.5" />
</Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Move later in the flow</TooltipContent> <TooltipContent>
{isLast
? "Already last — the release target, nothing further to promote to."
: "Move later in the flow"}
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground"> <HelpTip
{role} label={
</span> role
<Input ? `This rung ${role === "PRs land" ? "is where PRs from this project land" : role === "release" ? "is where releases are cut and tagged" : "both receives PRs and is the release target (single-rung ladder)"}.`
value={rung.name} : "An intermediate rung — merged into from the rung above, promoted to the rung below."
onChange={(e) => handleUpdate(index, "name", e.target.value)} }
placeholder="Name (e.g. dev, qa, stag)" >
className="flex-1 h-8" <span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
/> {role}
<Input </span>
value={rung.branch} </HelpTip>
onChange={(e) => handleUpdate(index, "branch", e.target.value)} <HelpTip label="Display label for this rung (e.g. 'dev', 'qa') — shown in the panel only, not matched against branch names.">
placeholder="Branch (e.g. dev, master)" <span className="flex-1">
className="flex-1 h-8" <Input
/> value={rung.name}
onChange={(e) => handleUpdate(index, "name", e.target.value)}
placeholder="Name (e.g. dev, qa, stag)"
className="h-8"
/>
</span>
</HelpTip>
<HelpTip label="The real git branch this rung maps to. Must be unique across rungs; saving rejects an empty or duplicate branch.">
<span className="flex-1">
<Input
value={rung.branch}
onChange={(e) => handleUpdate(index, "branch", e.target.value)}
placeholder="Branch (e.g. dev, master)"
className="h-8"
/>
</span>
</HelpTip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@@ -166,10 +197,12 @@ export function EnvironmentLadderEditor({
</div> </div>
)} )}
<Button type="button" variant="outline" size="sm" onClick={handleAdd}> <HelpTip label="Appends a blank rung at the bottom of the ladder (the new release target) — fill in its name/branch, then reorder with the arrows.">
<Plus className="h-4 w-4 mr-1" /> <Button type="button" variant="outline" size="sm" onClick={handleAdd}>
Add rung <Plus className="h-4 w-4 mr-1" />
</Button> Add rung
</Button>
</HelpTip>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Top to bottom is the promotion flow: <strong>PRs land</strong> on the Top to bottom is the promotion flow: <strong>PRs land</strong> on the
@@ -13,6 +13,7 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Search, Filter, X } from "lucide-react"; import { Search, Filter, X } from "lucide-react";
import { Team } from "@/types"; import { Team } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
interface ProjectFiltersProps { interface ProjectFiltersProps {
searchQuery: string; searchQuery: string;
@@ -57,7 +58,9 @@ export function ProjectFilters({
<div className="flex flex-col gap-4 sm:flex-row sm:items-center"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Search */} {/* Search */}
<div className="relative flex-1 max-w-sm"> <div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <HelpTip label="Filters the list below by project name (client-side, case-insensitive substring match).">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
</HelpTip>
<Input <Input
placeholder="Search projects..." placeholder="Search projects..."
value={searchQuery} value={searchQuery}
@@ -68,17 +71,19 @@ export function ProjectFilters({
{/* Cell Filter */} {/* Cell Filter */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <HelpTip label="Show only projects assigned to the checked cells (any match).">
<Button variant="outline" className="gap-2"> <DropdownMenuTrigger asChild>
<Filter className="h-4 w-4" /> <Button variant="outline" className="gap-2">
Cell <Filter className="h-4 w-4" />
{cellFilter.length > 0 && ( Cell
<Badge variant="secondary" className="ml-1"> {cellFilter.length > 0 && (
{cellFilter.length} <Badge variant="secondary" className="ml-1">
</Badge> {cellFilter.length}
)} </Badge>
</Button> )}
</DropdownMenuTrigger> </Button>
</DropdownMenuTrigger>
</HelpTip>
<DropdownMenuContent align="start" className="w-48"> <DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filter by Cell</DropdownMenuLabel> <DropdownMenuLabel>Filter by Cell</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -95,20 +100,24 @@ export function ProjectFilters({
</DropdownMenu> </DropdownMenu>
{/* Show Inactive Toggle */} {/* Show Inactive Toggle */}
<Button <HelpTip label="Include inactive projects — hidden from the default list and skipped as the fallback for idle-agent spawns.">
variant={showInactive ? "default" : "outline"} <Button
onClick={() => onShowInactiveChange(!showInactive)} variant={showInactive ? "default" : "outline"}
className="gap-2" onClick={() => onShowInactiveChange(!showInactive)}
> className="gap-2"
{showInactive ? "Showing Inactive" : "Show Inactive"} >
</Button> {showInactive ? "Showing Inactive" : "Show Inactive"}
</Button>
</HelpTip>
{/* Clear Filters */} {/* Clear Filters */}
{hasActiveFilters && ( {hasActiveFilters && (
<Button variant="ghost" onClick={clearFilters} className="gap-2"> <HelpTip label="Resets search, cell filter, and Show Inactive back to defaults.">
<X className="h-4 w-4" /> <Button variant="ghost" onClick={clearFilters} className="gap-2">
Clear <X className="h-4 w-4" />
</Button> Clear
</Button>
</HelpTip>
)} )}
</div> </div>
); );
@@ -14,6 +14,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { FolderGit2, X } from "lucide-react"; import { FolderGit2, X } from "lucide-react";
import { HelpTip } from "@/components/ui/help-tip";
interface ProjectSelectorProps { interface ProjectSelectorProps {
value: string | null; value: string | null;
@@ -110,10 +111,12 @@ export function ProjectSelector({
<FolderGit2 className="h-4 w-4" /> <FolderGit2 className="h-4 w-4" />
<span>{selectedProject.name}</span> <span>{selectedProject.name}</span>
{selectedProject.assigned_cell && ( {selectedProject.assigned_cell && (
<Badge variant="outline" className="text-xs"> <HelpTip label="The cell that owns this project — only its agents can claim tasks against it.">
{TEAM_LABELS[selectedProject.assigned_cell] || <Badge variant="outline" className="text-xs">
selectedProject.assigned_cell} {TEAM_LABELS[selectedProject.assigned_cell] ||
</Badge> selectedProject.assigned_cell}
</Badge>
</HelpTip>
)} )}
</div> </div>
) : ( ) : (
+45 -17
View File
@@ -180,11 +180,31 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Project</TableHead> <TableHead>
<TableHead>Cell</TableHead> <HelpTip label="Name and slug; click either to open the edit dialog.">
<TableHead>Tasks</TableHead> <span>Project</span>
<TableHead>Token</TableHead> </HelpTip>
<TableHead>Status</TableHead> </TableHead>
<TableHead>
<HelpTip label="The team that owns this project — only that cell's agents can claim its tasks.">
<span>Cell</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Done / active / blocked counts across all of this project's tasks.">
<span>Tasks</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Whether a GitHub PAT is configured for clone/push/PR operations.">
<span>Token</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Inactive projects are hidden from the default list and skipped as the fallback for idle-agent spawns.">
<span>Status</span>
</HelpTip>
</TableHead>
<TableHead className="w-[100px]">Actions</TableHead> <TableHead className="w-[100px]">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -200,9 +220,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
> >
{project.name} {project.name}
</Button> </Button>
<p className="text-xs text-muted-foreground font-mono"> <HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
{project.slug} <p className="text-xs text-muted-foreground font-mono">
</p> {project.slug}
</p>
</HelpTip>
{project.ci_watch_enabled && ( {project.ci_watch_enabled && (
<div className="mt-1"> <div className="mt-1">
<CiWatchBadge enabled /> <CiWatchBadge enabled />
@@ -211,9 +233,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={teamColors[project.assigned_cell]}> <HelpTip label="Only this cell's agents can claim tasks on this project.">
{teamLabels[project.assigned_cell]} <Badge className={teamColors[project.assigned_cell]}>
</Badge> {teamLabels[project.assigned_cell]}
</Badge>
</HelpTip>
</TableCell> </TableCell>
<TableCell> <TableCell>
<TasksCell counts={project.task_counts} /> <TasksCell counts={project.task_counts} />
@@ -271,9 +295,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
> >
{project.name} {project.name}
</Button> </Button>
<p className="text-xs text-muted-foreground font-mono"> <HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
{project.slug} <p className="text-xs text-muted-foreground font-mono">
</p> {project.slug}
</p>
</HelpTip>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<HelpTip label="Edit project settings and CI/CD commands"> <HelpTip label="Edit project settings and CI/CD commands">
@@ -306,9 +332,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
</div> </div>
<div className="mt-3 divide-y"> <div className="mt-3 divide-y">
<ResponsiveTableCardRow label="Cell"> <ResponsiveTableCardRow label="Cell">
<Badge className={teamColors[project.assigned_cell]}> <HelpTip label="Only this cell's agents can claim tasks on this project.">
{teamLabels[project.assigned_cell]} <Badge className={teamColors[project.assigned_cell]}>
</Badge> {teamLabels[project.assigned_cell]}
</Badge>
</HelpTip>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Tasks"> <ResponsiveTableCardRow label="Tasks">
<TasksCell counts={project.task_counts} /> <TasksCell counts={project.task_counts} />
@@ -6,6 +6,7 @@ import { useRateLimitStore } from "@/store/rate-limit-store";
import { useRateLimitSync } from "@/hooks/use-rate-limit-sync"; import { useRateLimitSync } from "@/hooks/use-rate-limit-sync";
import { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket"; import { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket";
import type { RateLimitEntry } from "@/types/rate-limits"; import type { RateLimitEntry } from "@/types/rate-limits";
import { HelpTip } from "@/components/ui/help-tip";
// ============================================================================= // =============================================================================
// Countdown row for a single rate-limited provider // Countdown row for a single rate-limited provider
@@ -37,18 +38,26 @@ function RateLimitRow({ entry }: { entry: RateLimitEntry }) {
return ( return (
<div className="flex items-center gap-3 px-4 py-2 bg-amber-50 border-b border-amber-300 last:border-b-0"> <div className="flex items-center gap-3 px-4 py-2 bg-amber-50 border-b border-amber-300 last:border-b-0">
<AlertTriangle className="h-4 w-4 text-amber-600 shrink-0" /> <AlertTriangle className="h-4 w-4 text-amber-600 shrink-0" />
<span className="text-sm font-medium text-amber-900"> <HelpTip label="This AI provider hit a rate limit or a persistent overload; every other provider keeps working normally.">
{entry.provider} <span className="text-sm font-medium text-amber-900">
</span> {entry.provider}
{agentCount > 0 && (
<span className="text-sm text-amber-700">
{agentCount} agent{agentCount !== 1 ? "s" : ""} affected
</span> </span>
</HelpTip>
{agentCount > 0 && (
<HelpTip label="Agents that were mid-task on this provider when the limit hit; each resumes automatically once it lifts — no work is lost.">
<span className="text-sm text-amber-700">
{agentCount} agent{agentCount !== 1 ? "s" : ""} affected
</span>
</HelpTip>
)} )}
<span className="text-sm text-amber-700">{secondsLeft}s</span> <HelpTip label="Estimated time until RoboCo retries this provider, based on the retry-after it reported.">
<span className="text-sm text-amber-800 font-medium ml-auto"> <span className="text-sm text-amber-700">{secondsLeft}s</span>
operations paused resuming automatically </HelpTip>
</span> <HelpTip label="New spawns for this provider are queued, not dropped — a background probe-and-resume loop revives parked agents as soon as it's confirmed clear.">
<span className="text-sm text-amber-800 font-medium ml-auto">
operations paused resuming automatically
</span>
</HelpTip>
</div> </div>
); );
} }
@@ -527,6 +527,7 @@ export function AIRoutingCard() {
active={currentMode === "self_hosted"} active={currentMode === "self_hosted"}
onClick={flipToSelfHosted} onClick={flipToSelfHosted}
disabled={applyMode.isPending || !isSelfHostedConnected} disabled={applyMode.isPending || !isSelfHostedConnected}
labelHint="Unlike Ollama/Grok, self-hosted has no built-in fallback model — pick one in the picker below or the switch fails server-side."
/> />
<ModeButton <ModeButton
icon={<Cpu className="h-4 w-4" />} icon={<Cpu className="h-4 w-4" />}
@@ -561,9 +562,11 @@ export function AIRoutingCard() {
<> <>
<Separator /> <Separator />
<section className="space-y-2"> <section className="space-y-2">
<Label className="text-sm font-medium"> <HelpTip label="Self-hosted has no automatic fallback — clearing this back to '(use server default)' will fail the next time Self-Hosted mode is applied.">
Self-Hosted default model <Label className="text-sm font-medium">
</Label> Self-Hosted default model
</Label>
</HelpTip>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Choose which discovered model all agents should use in Choose which discovered model all agents should use in
self-hosted mode. self-hosted mode.
@@ -782,6 +785,7 @@ function ModeButton({
disabled, disabled,
onClick, onClick,
highlight, highlight,
labelHint,
}: { }: {
icon: React.ReactNode; icon: React.ReactNode;
label: string; label: string;
@@ -790,6 +794,8 @@ function ModeButton({
disabled: boolean; disabled: boolean;
onClick: () => void | Promise<void>; onClick: () => void | Promise<void>;
highlight?: boolean; highlight?: boolean;
/** Extra hover context on the label, e.g. a gotcha the always-visible description doesn't cover. */
labelHint?: string;
}) { }) {
return ( return (
<Button <Button
@@ -804,11 +810,15 @@ function ModeButton({
> >
<div className="flex w-full items-center gap-2 text-sm font-medium"> <div className="flex w-full items-center gap-2 text-sm font-medium">
{icon} {icon}
<span>{label}</span> <HelpTip label={labelHint}>
<span>{label}</span>
</HelpTip>
{active ? ( {active ? (
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs"> <HelpTip label="Currently applied fleet-wide — every agent spawns on this provider until the mode changes again.">
active <Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
</Badge> active
</Badge>
</HelpTip>
) : null} ) : null}
</div> </div>
<p className="mt-1 text-xs text-muted-foreground font-normal"> <p className="mt-1 text-xs text-muted-foreground font-normal">
@@ -289,7 +289,13 @@ export function FeatureFlagsCard() {
size="sm" size="sm"
className="w-full justify-between px-2 text-muted-foreground" className="w-full justify-between px-2 text-muted-foreground"
> >
<span className="text-sm">X (Twitter) credentials</span> {/* Tip goes on the inner span, not the
CollapsibleTrigger asChild Button — wrapping the
trigger itself would clobber its open/closed
data-state (same trap as Switch/TabsTrigger). */}
<HelpTip label="Only takes effect once x_engine_enabled above is on.">
<span className="text-sm">X (Twitter) credentials</span>
</HelpTip>
{xCredsOpen ? ( {xCredsOpen ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
) : ( ) : (
@@ -314,7 +320,9 @@ export function FeatureFlagsCard() {
size="sm" size="sm"
className="w-full justify-between px-2 text-muted-foreground" className="w-full justify-between px-2 text-muted-foreground"
> >
<span className="text-sm">TikTok credentials</span> <HelpTip label="Only used for the TikTok distribution leg — needs video_engine_enabled above on, and video only posts once you approve a rendered draft.">
<span className="text-sm">TikTok credentials</span>
</HelpTip>
{tiktokCredsOpen ? ( {tiktokCredsOpen ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
) : ( ) : (
@@ -339,7 +347,9 @@ export function FeatureFlagsCard() {
size="sm" size="sm"
className="w-full justify-between px-2 text-muted-foreground" className="w-full justify-between px-2 text-muted-foreground"
> >
<span className="text-sm">Telegram credentials</span> <HelpTip label="Only takes effect once telegram_enabled above is on — in-app notifications keep working either way.">
<span className="text-sm">Telegram credentials</span>
</HelpTip>
{telegramCredsOpen ? ( {telegramCredsOpen ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
) : ( ) : (
@@ -145,16 +145,22 @@ export function SelfHostedSection({
{/* Header */} {/* Header */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" /> <Server className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Self-Hosted LLM</Label> <HelpTip label="Routes agents to any locally-run OpenAI-compatible endpoint (Ollama, vLLM, LM Studio) instead of a cloud provider.">
<Label className="text-sm font-medium">Self-Hosted LLM</Label>
</HelpTip>
{testResult?.ok === true && ( {testResult?.ok === true && (
<Badge className="bg-emerald-500/10 text-emerald-600 border-0"> <HelpTip label="The last Test Connection call succeeded — the model list below reflects this endpoint.">
<CheckCircle2 className="h-3 w-3" /> connected <Badge className="bg-emerald-500/10 text-emerald-600 border-0">
</Badge> <CheckCircle2 className="h-3 w-3" /> connected
</Badge>
</HelpTip>
)} )}
{testResult?.ok === false && ( {testResult?.ok === false && (
<Badge className="bg-red-500/10 text-red-600 border-0"> <HelpTip label="The last Test Connection call failed — see the error detail below.">
<XCircle className="h-3 w-3" /> error <Badge className="bg-red-500/10 text-red-600 border-0">
</Badge> <XCircle className="h-3 w-3" /> error
</Badge>
</HelpTip>
)} )}
</div> </div>
@@ -216,9 +222,11 @@ export function SelfHostedSection({
</Button> </Button>
</HelpTip> </HelpTip>
</div> </div>
<Button onClick={handleSave} disabled={saveConfig.isPending}> <HelpTip label="Saves the Base URL above together with any token typed here, in one combined config — not just this token.">
{saveConfig.isPending ? "Saving…" : "Save"} <Button onClick={handleSave} disabled={saveConfig.isPending}>
</Button> {saveConfig.isPending ? "Saving…" : "Save"}
</Button>
</HelpTip>
</div> </div>
{config?.has_token && ( {config?.has_token && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
@@ -336,24 +344,26 @@ export function SelfHostedSection({
</span> </span>
</HelpTip> </HelpTip>
</p> </p>
<Button <HelpTip label="Re-queries the endpoint's model list only — doesn't re-run the connection test above.">
variant="ghost" <Button
size="sm" variant="ghost"
onClick={handleRefresh} size="sm"
disabled={refreshModels.isPending} onClick={handleRefresh}
> disabled={refreshModels.isPending}
{refreshModels.isPending ? ( >
<> {refreshModels.isPending ? (
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" /> <>
Refreshing <RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
</> Refreshing
) : ( </>
<> ) : (
<RefreshCw className="mr-1.5 h-3 w-3" /> <>
Refresh Models <RefreshCw className="mr-1.5 h-3 w-3" />
</> Refresh Models
)} </>
</Button> )}
</Button>
</HelpTip>
</div> </div>
<div className="divide-y rounded-md border"> <div className="divide-y rounded-md border">
{models.map((m) => ( {models.map((m) => (
@@ -69,19 +69,22 @@ export function TelegramCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3"> <div className="flex items-center gap-2 rounded-md border p-3">
{status?.has_credentials ? ( {status?.has_credentials ? (
<> <HelpTip label="Bot token + chat id are both stored — CEO notifications also DM via Telegram alongside the in-app bell.">
<Key className="h-4 w-4 text-green-500" /> <Key className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-600 dark:text-green-400"> </HelpTip>
Credentials are set
</span>
</>
) : ( ) : (
<> <HelpTip label="Missing — notifications stay in-app only; nothing sends over Telegram until both fields below are set.">
<KeyRound className="h-4 w-4 text-amber-500" /> <KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400"> </HelpTip>
{isLoading ? "Checking..." : "No credentials configured"} )}
</span> {status?.has_credentials ? (
</> <span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
) : (
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
)} )}
</div> </div>
@@ -88,19 +88,22 @@ export function TikTokCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3"> <div className="flex items-center gap-2 rounded-md border p-3">
{status?.has_credentials ? ( {status?.has_credentials ? (
<> <HelpTip label="All 4 OAuth2 secrets are stored — approved video drafts can post to TikTok's inbox-upload.">
<Key className="h-4 w-4 text-green-500" /> <Key className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-600 dark:text-green-400"> </HelpTip>
Credentials are set
</span>
</>
) : ( ) : (
<> <HelpTip label="Missing at least one secret — video drafts still render and queue but can't post to TikTok until all 4 are set.">
<KeyRound className="h-4 w-4 text-amber-500" /> <KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400"> </HelpTip>
{isLoading ? "Checking..." : "No credentials configured"} )}
</span> {status?.has_credentials ? (
</> <span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
) : (
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
)} )}
</div> </div>
@@ -86,19 +86,22 @@ export function XCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3"> <div className="flex items-center gap-2 rounded-md border p-3">
{status?.has_credentials ? ( {status?.has_credentials ? (
<> <HelpTip label="All 4 secrets are stored — approved posts in the X queue can actually publish.">
<Key className="h-4 w-4 text-green-500" /> <Key className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-600 dark:text-green-400"> </HelpTip>
Credentials are set
</span>
</>
) : ( ) : (
<> <HelpTip label="Missing at least one secret — drafts still queue for review but can't post until all 4 are set.">
<KeyRound className="h-4 w-4 text-amber-500" /> <KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400"> </HelpTip>
{isLoading ? "Checking..." : "No credentials configured"} )}
</span> {status?.has_credentials ? (
</> <span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
) : (
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
)} )}
</div> </div>