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 { OfflineState } from "@/components/ui/offline-state";
import { HelpTip } from "@/components/ui/help-tip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { usePageRefresh } from "@/hooks";
import {
Bell,
@@ -114,20 +119,26 @@ function NotificationCard({
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{notification.subject}</span>
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
<Badge
className={priorityColors[notification.priority] + " text-xs"}
>
{notification.priority}
</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 && (
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
<Badge variant="destructive" className="text-xs">
Needs Ack
</Badge>
</HelpTip>
)}
{notification.related_task_id && (
<Link
@@ -329,16 +340,51 @@ function NotificationsPageContent() {
) : (
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="unread">
{/* TooltipTrigger's asChild Slot merge injects its own data-state
onto the trigger, colliding with Radix Tabs' own literal
data-state (same trap as task-tabs.tsx) — reassert the real
selection state explicitly so the active-tab highlight
survives the merge. */}
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="all"
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>
<TabsTrigger value="pending">
</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>
<TabsContent value={activeTab} className="mt-4 space-y-3">
@@ -121,7 +121,9 @@ export default function SettingsPage() {
<Separator />
<div className="flex items-center justify-between">
<div>
<HelpTip label="Saved to this browser only — doesn't sync across devices.">
<Label>Collapsed Sidebar</Label>
</HelpTip>
<p className="text-sm text-muted-foreground">
Show icons only in the sidebar
</p>
@@ -96,7 +96,9 @@ interface DeliverySectionProps {
function DeliverySection({ delivery }: DeliverySectionProps) {
return (
<div className="space-y-2">
<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">
<DeliveryMetric
label="In flight"
@@ -147,14 +149,18 @@ function SpendSection({ spend }: SpendSectionProps) {
<SectionLabel>Spend</SectionLabel>
<div className="rounded-lg border p-3 space-y-1.5">
<div className="flex items-center justify-between text-sm">
<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">
${spend_30d_usd.toFixed(2)}
</span>
</div>
{projected_monthly_usd !== null && (
<div className="flex items-center justify-between text-sm">
<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">
${projected_monthly_usd.toFixed(2)}
</span>
@@ -101,6 +101,8 @@ function ObjectivesEditor({
<span className="text-xs font-medium text-muted-foreground">
Objective #{rowIdx + 1}
</span>
<HelpTip label="Deletes this row locally — nothing changes until you click Save charter">
<span className="inline-block">
<Button
type="button"
variant="ghost"
@@ -111,6 +113,8 @@ function ObjectivesEditor({
>
Remove
</Button>
</span>
</HelpTip>
</div>
{keys.map((key) => (
<div key={key} className="space-y-1">
@@ -132,6 +136,8 @@ function ObjectivesEditor({
</div>
))}
</div>
<HelpTip label="Adds a blank row using the same field keys as the first objective">
<span className="inline-block">
<Button
type="button"
variant="outline"
@@ -141,6 +147,8 @@ function ObjectivesEditor({
>
+ Add objective
</Button>
</span>
</HelpTip>
</div>
);
}
@@ -297,7 +305,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
<CardContent className="space-y-5">
{/* North star */}
<div className="space-y-2">
<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
id="north-star"
rows={3}
@@ -325,7 +335,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
{/* Constraints */}
<div className="space-y-2">
<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
id="constraints"
rows={3}
@@ -361,10 +373,14 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
</div>
{/* Save */}
<HelpTip label="Saves north star, brand voice, objectives, constraints, and operating policy in one update">
<span className="inline-block">
<Button onClick={handleSave} disabled={saving}>
<Save className="h-4 w-4 mr-2" />
{saving ? "Saving…" : "Save charter"}
</Button>
</span>
</HelpTip>
{/* Metadata */}
<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 && (
<p>
Updated by:{" "}
<HelpTip label="The agent or CEO identity that last saved this charter">
<span className="font-medium text-foreground font-mono">
{goals.updated_by}
</span>
</HelpTip>
</p>
)}
</div>
+14 -3
View File
@@ -89,9 +89,12 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
{pitch.target_cells.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{pitch.target_cells.map((c) => (
<Badge key={c} variant="outline">
{c}
</Badge>
<HelpTip
key={c}
label="A cell this pitch would route delivery work to if approved"
>
<Badge variant="outline">{c}</Badge>
</HelpTip>
))}
</div>
)}
@@ -111,6 +114,8 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
</div>
{proposed ? (
<div className="flex gap-2">
<HelpTip label="Auto-provisions a new product + workspace for the target cells listed above">
<span className="inline-block">
<Button
size="sm"
disabled={busy}
@@ -118,6 +123,10 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
>
<Check className="mr-1 h-4 w-4" /> Approve &amp; provision
</Button>
</span>
</HelpTip>
<HelpTip label="Rejects without creating anything — recorded in the decision log below">
<span className="inline-block">
<Button
size="sm"
variant="outline"
@@ -126,6 +135,8 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
>
<X className="mr-1 h-4 w-4" /> Reject
</Button>
</span>
</HelpTip>
</div>
) : (
pitch.decision_notes && (
@@ -67,6 +67,8 @@ function DirectiveCard({
{/* Actions */}
<div className="flex gap-2 pt-1">
<HelpTip label="Executes this directive as drafted (relay a message, update the charter, approve a pitch, or control a task)">
<span className="inline-block">
<Button
size="sm"
disabled={busy}
@@ -74,6 +76,10 @@ function DirectiveCard({
>
<Check className="mr-1 h-4 w-4" /> Confirm
</Button>
</span>
</HelpTip>
<HelpTip label="Discards the directive — the Secretary must re-propose it to try again">
<span className="inline-block">
<Button
size="sm"
variant="outline"
@@ -82,6 +88,8 @@ function DirectiveCard({
>
<X className="mr-1 h-4 w-4" /> Reject
</Button>
</span>
</HelpTip>
</div>
</div>
@@ -244,9 +252,11 @@ export function SecretaryTab() {
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
<CardTitle>Chat</CardTitle>
{sessionId && (
<HelpTip label="Closes this chat — your next message starts a fresh Secretary session">
<Button variant="outline" size="sm" onClick={() => void stop()}>
End session
</Button>
</HelpTip>
)}
</CardHeader>
<CardContent className="flex flex-1 flex-col gap-4">
@@ -284,6 +294,8 @@ export function SecretaryTab() {
</Button>
</HelpTip>
) : (
<HelpTip label="Opens a new Secretary chat session — reads live company state, not a scripted bot">
<span className="inline-block">
<Button
onClick={() => void handleStart()}
disabled={starting}
@@ -296,6 +308,8 @@ export function SecretaryTab() {
"Start"
)}
</Button>
</span>
</HelpTip>
)}
</div>
</CardContent>
@@ -34,6 +34,15 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [
"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 {
if (result.created && result.pr_number != null) {
toast.success(
@@ -189,6 +198,7 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3"
>
<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"
@@ -196,6 +206,8 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
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"
@@ -203,7 +215,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
>
Remove
</Button>
</HelpTip>
</div>
<HelpTip label="One-line description shown in the ambient 'Architectural Standard' prompt block every agent sees">
<Input
value={module.purpose}
placeholder="what this module is for"
@@ -211,10 +225,11 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
updateModule(index, { purpose: e.target.value })
}
/>
</HelpTip>
<div className="flex flex-wrap gap-1">
{FORBIDDABLE_KINDS.map((kind) => (
<HelpTip key={kind} label={KIND_HINTS[kind]}>
<Badge
key={kind}
variant={
module.forbidden.includes(kind)
? "destructive"
@@ -225,14 +240,17 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
>
no {kind}
</Badge>
</HelpTip>
))}
</div>
</div>
))}
</div>
<HelpTip label="Adds a blank module row to constrain another directory">
<Button variant="outline" size="sm" onClick={addModule}>
Add module
</Button>
</HelpTip>
</CardContent>
</Card>
);
@@ -253,9 +271,17 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
>
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
<div className="flex items-center gap-2">
<HelpTip
label={
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
checked={rule.level === "block"}
onCheckedChange={(checked) =>
@@ -285,16 +311,25 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3"
>
<div className="flex items-center gap-2">
<HelpTip label="Repo-relative path or glob this waiver exempts from the rule on the right">
<Input
value={waiver.path}
placeholder="path/to/file.py"
onChange={(e) => updateWaiver(index, { path: e.target.value })}
onChange={(e) =>
updateWaiver(index, { path: e.target.value })
}
/>
</HelpTip>
<HelpTip label="Must match a rule name above exactly for the waiver to apply">
<Input
value={waiver.rule}
placeholder="rule name"
onChange={(e) => updateWaiver(index, { rule: e.target.value })}
onChange={(e) =>
updateWaiver(index, { rule: e.target.value })
}
/>
</HelpTip>
<HelpTip label="Deletes this waiver row locally — nothing changes until Save to repo">
<Button
variant="ghost"
size="sm"
@@ -302,17 +337,24 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
>
Remove
</Button>
</HelpTip>
</div>
<HelpTip label="Accountability note — committed with the waiver and reviewed in the PR, never a silent suppression">
<Input
value={waiver.reason}
placeholder="why this is waived"
onChange={(e) => updateWaiver(index, { reason: e.target.value })}
onChange={(e) =>
updateWaiver(index, { reason: e.target.value })
}
/>
</HelpTip>
</div>
))}
<HelpTip label="Adds a blank waiver row">
<Button variant="outline" size="sm" onClick={addWaiver}>
Add waiver
</Button>
</HelpTip>
</CardContent>
</Card>
);
@@ -332,11 +374,13 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
className="space-y-2 rounded-md border border-border p-3"
>
<div className="flex items-center gap-2">
<HelpTip label="A short, unique identifier for this rule (shown as its badge in Recent violations)">
<Input
value={rule.id}
placeholder="rule-id"
onChange={(e) => updateCustom(index, { id: e.target.value })}
/>
</HelpTip>
<HelpTip
label={
rule.level === "block"
@@ -354,6 +398,7 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
updateCustom(index, { level: checked ? "block" : "warn" })
}
/>
<HelpTip label="Deletes this custom rule row locally — nothing changes until Save to repo">
<Button
variant="ghost"
size="sm"
@@ -361,22 +406,33 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
>
Remove
</Button>
</HelpTip>
</div>
<HelpTip label="Regex tested against each changed file's source text — a bad pattern is skipped, never fails the gate">
<Input
value={rule.pattern}
placeholder="regex pattern"
onChange={(e) => updateCustom(index, { pattern: e.target.value })}
onChange={(e) =>
updateCustom(index, { pattern: 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 })}
onChange={(e) =>
updateCustom(index, { message: e.target.value })
}
/>
</HelpTip>
</div>
))}
<HelpTip label="Adds a blank custom regex rule">
<Button variant="outline" size="sm" onClick={addCustom}>
Add custom rule
</Button>
</HelpTip>
</CardContent>
</Card>
);
@@ -144,9 +144,14 @@ function UnifiedHistoryRow({ row }: { row: UnifiedRow }) {
{PLATFORM_LABELS[platform] ?? platform}: {id}
</a>
) : (
<span key={platform} className="text-muted-foreground">
<HelpTip
key={platform}
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>
@@ -211,7 +216,9 @@ export function SocialHistorySection({ className }: { className?: string }) {
size="sm"
className="w-full justify-between"
>
<HelpTip label="Fetches both sources once, on first expand — up to 50 rows each">
<span>{open ? "Hide" : "Show"} history</span>
</HelpTip>
<ChevronDown
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
/>
@@ -76,9 +76,11 @@ function PipelineRow({ item }: { item: VideoPipelineItem }) {
prefetch={false}
className="ml-auto"
>
<HelpTip label="Open the task to approve or reject the rendered video">
<Button variant="outline" size="sm">
Review
</Button>
</HelpTip>
</Link>
)}
{canRerender && (
@@ -110,7 +112,9 @@ export function VideoPipelineStrip({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" />
Video Pipeline
<HelpTip label="Videos currently authoring, in review, or rendering — not yet in the queue below">
<Badge variant="secondary">{items.length}</Badge>
</HelpTip>
</CardTitle>
<CardDescription>
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-2">
<HelpTip label="Only projects with the video engine enabled (Projects → Edit) can be picked">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={effectiveProjectId}
onChange={setProjectId}
@@ -493,7 +495,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<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
id="video-request-occasion"
placeholder="e.g. v0.19.0 launch, Founder's Day..."
@@ -502,7 +506,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<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
id="video-request-brief"
placeholder="What should this video cover?"
@@ -512,7 +518,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<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">
{REQUEST_PLATFORMS.map((platform) => (
<div key={platform} className="flex items-center gap-2">
@@ -521,12 +529,20 @@ function RequestVideoDialog({
checked={platforms.includes(platform)}
onCheckedChange={() => togglePlatform(platform)}
/>
<HelpTip
label={
platform === "x"
? "Posts via X's OAuth 1.0a API, 280-char caption"
: "Posts via TikTok's inbox-upload OAuth2 API, 2200-char caption"
}
>
<Label
htmlFor={`video-request-${platform}`}
className="text-sm font-normal"
>
{PLATFORM_LABELS[platform]}
</Label>
</HelpTip>
</div>
))}
</div>
@@ -654,10 +670,12 @@ export function VideoPostQueue({ className }: { className?: string }) {
if (isLoading) return null;
const requestButton = (
<HelpTip label="Opens a dialog to brief a UX/UI dev on an on-demand video, independent of a release or spotlight">
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
<Sparkles className="mr-1 h-4 w-4" />
Request a video
</Button>
</HelpTip>
);
if (!posts || posts.length === 0) {
@@ -696,7 +714,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" />
Video Post Queue
<HelpTip label="Rendered clips waiting for your preview, caption edits, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle>
<CardAction>{requestButton}</CardAction>
<CardDescription>
@@ -727,7 +747,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="video-reject-reason">Reason</Label>
</HelpTip>
<Textarea
id="video-reject-reason"
placeholder="e.g. off-brand tone, wrong occasion..."
@@ -109,17 +109,23 @@ function XPostRow({
</span>
</HelpTip>
{post.release_version && (
<HelpTip label="The release this post announces">
<Badge variant="outline">v{post.release_version}</Badge>
</HelpTip>
)}
{post.mention && (
<HelpTip label="The X mention this draft replies to">
<Badge variant="secondary" className="max-w-56 truncate">
re: {post.mention.text}
</Badge>
</HelpTip>
)}
{post.feature && (
<HelpTip label="The shipped feature this spotlight covers">
<Badge variant="outline" className="max-w-56 truncate">
feature: {post.feature.title}
</Badge>
</HelpTip>
)}
</div>
@@ -266,7 +272,9 @@ export function XPostQueue({ className }: { className?: string }) {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />X Post Queue
<HelpTip label="Drafts waiting for your edit, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle>
<CardDescription>
Drafted release announcements, feature spotlights, and mention
@@ -297,7 +305,9 @@ export function XPostQueue({ className }: { className?: string }) {
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="x-reject-reason">Reason</Label>
</HelpTip>
<Textarea
id="x-reject-reason"
placeholder="e.g. tone doesn't match our voice; not worth a public reply..."
@@ -16,6 +16,7 @@ import {
} from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { cn } from "@/lib/utils";
const categoryConfig: Record<
@@ -135,9 +136,11 @@ export function KBCategoryNav({
<div className="flex-1 min-w-0 text-left">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{config.label}</span>
<span className="text-xs text-muted-foreground">
<HelpTip label="Document count — one document can split into several chunks at embedding time, so the chunk total (Index Stats) usually runs higher.">
<span className="text-xs text-muted-foreground w-fit">
{count.toLocaleString()} docs
</span>
</HelpTip>
</div>
<p className="text-xs text-muted-foreground truncate">
{config.description}
@@ -86,7 +86,8 @@ function KBCategoryViewInner({ category }: { category: KBIndexType }) {
{doc.source}
</p>
</HelpTip>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<HelpTip label={new Date(doc.indexed_at).toLocaleString()}>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground w-fit">
<Clock className="h-3 w-3" />
<span>
{formatDistanceToNow(new Date(doc.indexed_at), {
@@ -94,6 +95,7 @@ function KBCategoryViewInner({ category }: { category: KBIndexType }) {
})}
</span>
</div>
</HelpTip>
</div>
</div>
</CardContent>
@@ -85,6 +85,7 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Filter by type</span>
{selectedTypes.length > 0 && (
<HelpTip label="Reset — search every index type again instead of just these">
<Button
variant="ghost"
size="sm"
@@ -93,6 +94,7 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
>
Clear
</Button>
</HelpTip>
)}
</div>
<div className="space-y-2">
@@ -75,19 +75,25 @@ export function KBResultCard({ result, onClick }: KBResultCardProps) {
{result.metadata && Object.keys(result.metadata).length > 0 && (
<div className="flex items-center gap-2 mt-2 flex-wrap">
{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">
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
{result.metadata.language}
</span>
</HelpTip>
)}
{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">
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
@{result.metadata.agent}
</span>
</HelpTip>
)}
{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">
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
{result.metadata.section}
</span>
</HelpTip>
)}
</div>
)}
@@ -142,9 +142,11 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
</div>
</div>
{latestUpdated && (
<p className="text-xs text-muted-foreground pt-1">
<HelpTip label={new Date(latestUpdated).toLocaleString()}>
<p className="text-xs text-muted-foreground pt-1 w-fit">
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
</p>
</HelpTip>
)}
</CardContent>
</Card>
@@ -17,6 +17,11 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
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 { Progress } from "@/components/ui/progress";
import {
@@ -326,26 +331,60 @@ function KnowledgeBaseBrowserContent() {
onValueChange={(v) => handleTabChange(v as TabValue)}
>
<TabsList>
<TabsTrigger value="search" className="gap-2">
<Search className="h-4 w-4" />
Search
</TabsTrigger>
<TabsTrigger value="ask" className="gap-2">
<Sparkles className="h-4 w-4" />
Ask AI
</TabsTrigger>
<TabsTrigger value="mentor" className="gap-2">
<Brain className="h-4 w-4" />
Mentor
</TabsTrigger>
<TabsTrigger value="browse" className="gap-2">
<FolderTree className="h-4 w-4" />
Browse
</TabsTrigger>
<TabsTrigger value="admin" className="gap-2">
<Settings className="h-4 w-4" />
Admin
{(
[
{
value: "search",
icon: Search,
label: "Search",
hint: "Semantic search across indexed content, filterable by category",
},
{
value: "ask",
icon: Sparkles,
label: "Ask AI",
hint: "One-shot Q&A: retrieves matching chunks (RAG) and generates a cited answer",
},
{
value: "mentor",
icon: Brain,
label: "Mentor",
hint: "Multi-turn chat that also factors in your role and your own journal entries",
},
{
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>
{/* Search Tab */}
@@ -460,17 +499,27 @@ function KnowledgeBaseBrowserContent() {
<XCircle className="h-8 w-8 text-red-600" />
)}
<div>
<p className="font-medium">
<HelpTip label="Healthy requires all three subsystems below — embedding, LLM, and vector store — to each respond OK">
<p className="font-medium w-fit">
{health.healthy ? "Healthy" : "Unhealthy"}
</p>
<p className="text-sm text-muted-foreground">
</HelpTip>
<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 className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div>LLM: {health.llm_status}</div>
<div>Vector: {health.vector_store_status}</div>
<HelpTip label="Local model that generates RAG and Mentor answers from retrieved context">
<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>
{(
[
@@ -507,10 +556,12 @@ function KnowledgeBaseBrowserContent() {
<span>Summary</span>
<AlertDialog>
<AlertDialogTrigger asChild>
<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">
<Button size="sm" variant="destructive">
<RefreshCw className="h-3 w-3 mr-1" />
Reindex All
</Button>
</HelpTip>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -538,10 +589,14 @@ function KnowledgeBaseBrowserContent() {
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<HelpTip label="Sum of documents indexed across every category below">
<div className="w-fit">
<p className="text-2xl font-bold">{totalDocs}</p>
<p className="text-xs text-muted-foreground">Documents</p>
<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">
<div className="w-fit">
<p className="text-2xl font-bold">{totalChunks}</p>
@@ -667,7 +722,8 @@ function KnowledgeBaseBrowserContent() {
</div>
<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">
<div className="w-fit">
<span className="text-muted-foreground">
Documents:
</span>{" "}
@@ -675,6 +731,7 @@ function KnowledgeBaseBrowserContent() {
{index.document_count}
</span>
</div>
</HelpTip>
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
<div className="w-fit">
<span className="text-muted-foreground">
@@ -685,7 +742,16 @@ function KnowledgeBaseBrowserContent() {
</span>
</div>
</HelpTip>
<div>
<HelpTip
label={
index.last_updated
? new Date(
index.last_updated,
).toLocaleString()
: "This index has never been populated"
}
>
<div className="w-fit">
<span className="text-muted-foreground">
Updated:
</span>{" "}
@@ -697,13 +763,16 @@ function KnowledgeBaseBrowserContent() {
: "Never"}
</span>
</div>
</HelpTip>
</div>
<div className="space-y-1">
<HelpTip label="Share of the fleet's total chunk count this category holds — not literal disk bytes">
<div className="flex justify-between text-xs text-muted-foreground">
<span>Storage usage</span>
<span>{percentage}%</span>
</div>
</HelpTip>
<Progress value={percentage} className="h-1" />
</div>
</div>
@@ -149,7 +149,8 @@ export function MentorAnswerDisplay({
<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">
{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">
<span className="flex items-center gap-1.5 w-fit">
<User className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">Role:</span>
<Badge variant="secondary" className="font-medium">
@@ -161,16 +162,19 @@ export function MentorAnswerDisplay({
</span>
)}
</span>
</HelpTip>
)}
{response.journal_entries_used !== undefined &&
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">
<span className="flex items-center gap-1.5 w-fit">
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">
{response.journal_entries_used} personal journal
{response.journal_entries_used !== 1 ? "s" : ""} used
</span>
</span>
</HelpTip>
)}
</div>
</div>
@@ -182,9 +186,11 @@ export function MentorAnswerDisplay({
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Brain className="h-4 w-4 text-primary" />
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">
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
{response.sources.length} sources used
</span>
</HelpTip>
</CardTitle>
</CardHeader>
<CardContent>
@@ -204,8 +210,8 @@ export function MentorAnswerDisplay({
</div>
<div className="flex flex-wrap gap-2">
{response.suggested_followups.map((followup, index) => (
<HelpTip key={index} label="Ask this next, in the same conversation">
<Button
key={index}
variant="outline"
size="sm"
className="text-xs h-auto py-1.5 px-3"
@@ -214,6 +220,7 @@ export function MentorAnswerDisplay({
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
{followup}
</Button>
</HelpTip>
))}
</div>
</div>
@@ -10,6 +10,7 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Markdown } from "@/components/ui/markdown";
import {
@@ -177,15 +178,19 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
<Brain className="h-5 w-5 text-primary" />
<span className="font-medium">Mentor Chat</span>
{conversationId && (
<Badge variant="outline" className="text-xs">
<HelpTip label="Follow-up questions reuse this conversation's history for context">
<Badge variant="outline" className="text-xs w-fit">
Conversation active
</Badge>
</HelpTip>
)}
</div>
<HelpTip label="Clears this thread — the mentor loses this conversation's context">
<Button variant="ghost" size="sm" onClick={handleNewChat}>
<RotateCcw className="h-4 w-4 mr-1" />
New Chat
</Button>
</HelpTip>
</div>
{/* Messages */}
@@ -241,6 +246,7 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
{/* Sources toggle */}
{msg.sources && msg.sources.length > 0 && (
<div>
<HelpTip label="The knowledge-base chunks retrieved and handed to the model as context for this answer">
<Button
variant="ghost"
size="sm"
@@ -254,6 +260,7 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
{expandedSources === idx ? "Hide" : "Show"}{" "}
{msg.sources.length} sources
</Button>
</HelpTip>
{expandedSources === idx && (
<div className="mt-2 space-y-2">
{msg.sources.map((source, sidx) => (
@@ -278,8 +285,11 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
{msg.followups && msg.followups.length > 0 && (
<div className="flex flex-wrap gap-2">
{msg.followups.map((followup, fidx) => (
<Button
<HelpTip
key={fidx}
label="Ask this next, in the same conversation"
>
<Button
variant="outline"
size="sm"
className="text-xs h-auto py-1.5"
@@ -288,6 +298,7 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
>
{followup}
</Button>
</HelpTip>
))}
</div>
)}
@@ -10,6 +10,7 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import {
Send,
Loader2,
@@ -60,18 +61,33 @@ export function MentorQueryInput({
</div>
</div>
<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">
<Badge
variant="outline"
className="text-xs gap-1 bg-background w-fit"
>
<User className="h-3 w-3" />
Role-aware
</Badge>
<Badge variant="outline" className="text-xs gap-1 bg-background">
</HelpTip>
<HelpTip label="Searches your own journal entries — not other agents'">
<Badge
variant="outline"
className="text-xs gap-1 bg-background w-fit"
>
<BookMarked className="h-3 w-3" />
Journals
</Badge>
<Badge variant="outline" className="text-xs gap-1 bg-background">
</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>
@@ -5,6 +5,7 @@ import { RAGQueryResponse } from "@/types";
import { RAGCitationCard } from "./rag-citation-card";
import { Markdown } from "@/components/ui/markdown";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import {
Bot,
BookOpen,
@@ -120,9 +121,11 @@ export function RAGAnswerDisplay({
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Bot className="h-4 w-4 text-primary" />
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">
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
{response.context_used} sources used
</span>
</HelpTip>
</CardTitle>
</CardHeader>
<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">
{notification.subject}
</span>
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
<Badge variant="outline" className="text-[10px] shrink-0">
{notification.priority}
</Badge>
</HelpTip>
</div>
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
{!notification.is_read && (
<HelpTip label="Hasn't been marked read yet.">
<Badge variant="secondary" className="text-[10px]">
New
</Badge>
</HelpTip>
)}
{needsAck && (
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
<Badge variant="destructive" className="text-[10px]">
Needs Ack
</Badge>
</HelpTip>
)}
<div className="ml-auto flex items-center gap-1">
{!notification.is_read && (
@@ -26,6 +26,7 @@ import {
import { Plus } from "lucide-react";
import { toast } from "sonner";
import { Team, type ProductCellMapping } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" },
@@ -104,12 +105,14 @@ export function CreateProductDialog() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<HelpTip label="Groups per-cell projects (e.g. a SaaS app + its OSS core) into one deliverable for MegaTask and board routing.">
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
New Product
</Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSubmit}>
<DialogHeader>
@@ -121,7 +124,9 @@ export function CreateProductDialog() {
<div className="grid gap-4 py-4">
{/* Name */}
<div className="grid gap-2">
<HelpTip label="Display name shown across the panel; renaming it never touches the slug already generated.">
<Label htmlFor="name">Product Name *</Label>
</HelpTip>
<Input
id="name"
value={name}
@@ -132,7 +137,9 @@ export function CreateProductDialog() {
{/* Slug */}
<div className="grid gap-2">
<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
id="slug"
value={slug}
@@ -147,7 +154,9 @@ export function CreateProductDialog() {
{/* Description */}
<div className="grid gap-2">
<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
id="description"
value={description}
@@ -158,15 +167,19 @@ export function CreateProductDialog() {
{/* Cell -> Project mapping */}
<div className="grid gap-3">
<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) => (
<div key={cell.value} className="grid gap-2">
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
<Label
htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground"
>
{cell.label}
</Label>
</HelpTip>
<Select
value={cellMapping[cell.value] ?? ""}
onValueChange={(value) =>
@@ -30,6 +30,7 @@ import {
type ProductCellMapping,
type ProductUpdate,
} from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" },
@@ -120,7 +121,9 @@ function EditProductForm({
<div className="grid gap-4 py-4">
{/* Slug (read-only) */}
<div className="grid gap-2">
<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
id="slug"
value={product.slug}
@@ -131,7 +134,9 @@ function EditProductForm({
{/* Name */}
<div className="grid gap-2">
<HelpTip label="Display name shown across the panel; renaming it never touches the slug.">
<Label htmlFor="name">Product Name *</Label>
</HelpTip>
<Input
id="name"
value={name}
@@ -142,7 +147,9 @@ function EditProductForm({
{/* Description */}
<div className="grid gap-2">
<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
id="description"
value={description}
@@ -153,15 +160,19 @@ function EditProductForm({
{/* Cell -> Project mapping */}
<div className="grid gap-3">
<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) => (
<div key={cell.value} className="grid gap-2">
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
<Label
htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground"
>
{cell.label}
</Label>
</HelpTip>
<Select
value={cellMapping[cell.value] ?? ""}
onValueChange={(value) =>
@@ -126,9 +126,21 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Cells</TableHead>
<TableHead>Progress</TableHead>
<TableHead>
<HelpTip label="Name and slug; click either to open the edit dialog.">
<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>
</TableRow>
</TableHeader>
@@ -144,9 +156,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
>
{product.name}
</Button>
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
<p className="text-xs text-muted-foreground font-mono">
{product.slug}
</p>
</HelpTip>
</div>
</TableCell>
<TableCell>
@@ -188,9 +202,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
>
{product.name}
</Button>
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
<p className="text-xs text-muted-foreground font-mono">
{product.slug}
</p>
</HelpTip>
</div>
<HelpTip label="Edit product name, description, and cell-project mapping">
<Button
@@ -121,12 +121,14 @@ export function CreateProjectDialog() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<HelpTip label="Registers a git repository — sets cell ownership, branch/environment ladder, and CI/CD gate commands for agents to work on it.">
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
New Project
</Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="sm:max-w-[525px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
@@ -138,7 +140,9 @@ export function CreateProjectDialog() {
<div className="grid gap-4 py-4">
{/* Name */}
<div className="grid gap-2">
<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
id="name"
value={formData.name}
@@ -149,7 +153,9 @@ export function CreateProjectDialog() {
{/* Slug */}
<div className="grid gap-2">
<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
id="slug"
value={formData.slug}
@@ -166,7 +172,9 @@ export function CreateProjectDialog() {
{/* Git URL */}
<div className="grid gap-2">
<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
id="git_url"
value={formData.git_url}
@@ -205,7 +213,9 @@ export function CreateProjectDialog() {
{/* Assigned Cell */}
<div className="grid gap-2">
<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
value={formData.assigned_cell}
onValueChange={(value: Team) =>
@@ -227,7 +237,9 @@ export function CreateProjectDialog() {
{/* Default Branch */}
<div className="grid gap-2">
<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
id="default_branch"
value={formData.default_branch}
@@ -248,6 +260,7 @@ export function CreateProjectDialog() {
/>
{/* Advanced Options Toggle */}
<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.">
<Button
type="button"
variant="ghost"
@@ -256,12 +269,15 @@ export function CreateProjectDialog() {
>
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip>
{showAdvanced && (
<>
{/* CI/CD Commands */}
<div className="grid gap-2">
<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
id="test_command"
value={formData.test_command || ""}
@@ -273,7 +289,9 @@ export function CreateProjectDialog() {
</div>
<div className="grid gap-2">
<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
id="lint_command"
value={formData.lint_command || ""}
@@ -285,7 +303,9 @@ export function CreateProjectDialog() {
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input
id="format_command"
value={formData.format_command || ""}
@@ -300,7 +320,9 @@ export function CreateProjectDialog() {
</div>
<div className="grid gap-2">
<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
id="typecheck_command"
value={formData.typecheck_command || ""}
@@ -315,7 +337,9 @@ export function CreateProjectDialog() {
</div>
<div className="grid gap-2">
<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
id="build_command"
value={formData.build_command || ""}
@@ -330,7 +354,9 @@ export function CreateProjectDialog() {
</div>
<div className="grid gap-2">
<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
id="quality_command"
value={formData.quality_command || ""}
@@ -271,7 +271,9 @@ function EditProjectForm({
<div className="grid gap-4 py-4">
{/* Slug (read-only) */}
<div className="grid gap-2">
<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
id="slug"
value={project.slug}
@@ -282,7 +284,9 @@ function EditProjectForm({
{/* Name */}
<div className="grid gap-2">
<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
id="name"
value={name}
@@ -293,7 +297,9 @@ function EditProjectForm({
{/* Git URL */}
<div className="grid gap-2">
<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
id="git_url"
value={gitUrl}
@@ -348,9 +354,17 @@ function EditProjectForm({
{!clearToken && (
<div className="grid gap-2">
<HelpTip
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
id="git_token"
type="password"
@@ -368,7 +382,9 @@ function EditProjectForm({
{/* Assigned Cell */}
<div className="grid gap-2">
<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
value={assignedCell}
onValueChange={(value: Team) => setAssignedCell(value)}
@@ -388,7 +404,9 @@ function EditProjectForm({
{/* Default Branch */}
<div className="grid gap-2">
<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
id="default_branch"
value={defaultBranch}
@@ -419,6 +437,7 @@ function EditProjectForm({
</div>
{/* Advanced Options Toggle */}
<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.">
<Button
type="button"
variant="ghost"
@@ -427,11 +446,14 @@ function EditProjectForm({
>
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip>
{showAdvanced && (
<>
<div className="grid gap-2">
<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
id="test_command"
value={testCommand}
@@ -441,7 +463,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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
id="lint_command"
value={lintCommand}
@@ -451,7 +475,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input
id="format_command"
value={formatCommand}
@@ -461,7 +487,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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
id="typecheck_command"
value={typecheckCommand}
@@ -471,7 +499,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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
id="build_command"
value={buildCommand}
@@ -481,7 +511,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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
id="quality_command"
value={qualityCommand}
@@ -497,6 +529,7 @@ function EditProjectForm({
)}
{/* Autonomous Maintenance Toggle */}
<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.">
<Button
type="button"
variant="ghost"
@@ -505,6 +538,7 @@ function EditProjectForm({
>
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
</Button>
</HelpTip>
{showAutonomy && (
<>
@@ -522,7 +556,9 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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
id="ci_watch_workflow"
value={ciWatchWorkflow}
@@ -549,9 +585,11 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<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.">
<Label htmlFor="dep_update_command">
Dependency-Update Command
</Label>
</HelpTip>
<Input
id="dep_update_command"
value={depUpdateCommand}
@@ -565,9 +603,11 @@ function EditProjectForm({
</div>
<div className="grid gap-2">
<HelpTip label="Which lockfile paths the dry-run diffs to detect a change; leave blank to auto-infer uv.lock / pnpm-lock.yaml.">
<Label htmlFor="dep_update_paths">
Dependency-Update Lockfile Paths
</Label>
</HelpTip>
<Input
id="dep_update_paths"
value={depUpdatePaths}
@@ -98,6 +98,10 @@ export function EnvironmentLadderEditor({
<div className="flex flex-col">
<Tooltip>
<TooltipTrigger asChild>
{/* span-wrap: disabled Button has pointer-events-none,
so the tooltip needs a hoverable wrapper to fire
when isFirst disables the button. */}
<span className="inline-block">
<Button
type="button"
variant="ghost"
@@ -109,11 +113,17 @@ export function EnvironmentLadderEditor({
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
</span>
</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>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
type="button"
variant="ghost"
@@ -125,25 +135,46 @@ export function EnvironmentLadderEditor({
>
<ArrowDown className="h-3.5 w-3.5" />
</Button>
</span>
</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>
</div>
<HelpTip
label={
role
? `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)"}.`
: "An intermediate rung — merged into from the rung above, promoted to the rung below."
}
>
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
{role}
</span>
</HelpTip>
<HelpTip label="Display label for this rung (e.g. 'dev', 'qa') — shown in the panel only, not matched against branch names.">
<span className="flex-1">
<Input
value={rung.name}
onChange={(e) => handleUpdate(index, "name", e.target.value)}
placeholder="Name (e.g. dev, qa, stag)"
className="flex-1 h-8"
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="flex-1 h-8"
className="h-8"
/>
</span>
</HelpTip>
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -166,10 +197,12 @@ export function EnvironmentLadderEditor({
</div>
)}
<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.">
<Button type="button" variant="outline" size="sm" onClick={handleAdd}>
<Plus className="h-4 w-4 mr-1" />
Add rung
</Button>
</HelpTip>
<p className="text-xs text-muted-foreground">
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 { Search, Filter, X } from "lucide-react";
import { Team } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
interface ProjectFiltersProps {
searchQuery: string;
@@ -57,7 +58,9 @@ export function ProjectFilters({
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Search */}
<div className="relative flex-1 max-w-sm">
<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
placeholder="Search projects..."
value={searchQuery}
@@ -68,6 +71,7 @@ export function ProjectFilters({
{/* Cell Filter */}
<DropdownMenu>
<HelpTip label="Show only projects assigned to the checked cells (any match).">
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Filter className="h-4 w-4" />
@@ -79,6 +83,7 @@ export function ProjectFilters({
)}
</Button>
</DropdownMenuTrigger>
</HelpTip>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filter by Cell</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -95,6 +100,7 @@ export function ProjectFilters({
</DropdownMenu>
{/* Show Inactive Toggle */}
<HelpTip label="Include inactive projects — hidden from the default list and skipped as the fallback for idle-agent spawns.">
<Button
variant={showInactive ? "default" : "outline"}
onClick={() => onShowInactiveChange(!showInactive)}
@@ -102,13 +108,16 @@ export function ProjectFilters({
>
{showInactive ? "Showing Inactive" : "Show Inactive"}
</Button>
</HelpTip>
{/* Clear Filters */}
{hasActiveFilters && (
<HelpTip label="Resets search, cell filter, and Show Inactive back to defaults.">
<Button variant="ghost" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" />
Clear
</Button>
</HelpTip>
)}
</div>
);
@@ -14,6 +14,7 @@ import {
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { FolderGit2, X } from "lucide-react";
import { HelpTip } from "@/components/ui/help-tip";
interface ProjectSelectorProps {
value: string | null;
@@ -110,10 +111,12 @@ export function ProjectSelector({
<FolderGit2 className="h-4 w-4" />
<span>{selectedProject.name}</span>
{selectedProject.assigned_cell && (
<HelpTip label="The cell that owns this project — only its agents can claim tasks against it.">
<Badge variant="outline" className="text-xs">
{TEAM_LABELS[selectedProject.assigned_cell] ||
selectedProject.assigned_cell}
</Badge>
</HelpTip>
)}
</div>
) : (
@@ -180,11 +180,31 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Cell</TableHead>
<TableHead>Tasks</TableHead>
<TableHead>Token</TableHead>
<TableHead>Status</TableHead>
<TableHead>
<HelpTip label="Name and slug; click either to open the edit dialog.">
<span>Project</span>
</HelpTip>
</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>
</TableRow>
</TableHeader>
@@ -200,9 +220,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
>
{project.name}
</Button>
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
<p className="text-xs text-muted-foreground font-mono">
{project.slug}
</p>
</HelpTip>
{project.ci_watch_enabled && (
<div className="mt-1">
<CiWatchBadge enabled />
@@ -211,9 +233,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
</div>
</TableCell>
<TableCell>
<HelpTip label="Only this cell's agents can claim tasks on this project.">
<Badge className={teamColors[project.assigned_cell]}>
{teamLabels[project.assigned_cell]}
</Badge>
</HelpTip>
</TableCell>
<TableCell>
<TasksCell counts={project.task_counts} />
@@ -271,9 +295,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
>
{project.name}
</Button>
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
<p className="text-xs text-muted-foreground font-mono">
{project.slug}
</p>
</HelpTip>
</div>
<div className="flex shrink-0 items-center gap-1">
<HelpTip label="Edit project settings and CI/CD commands">
@@ -306,9 +332,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
</div>
<div className="mt-3 divide-y">
<ResponsiveTableCardRow label="Cell">
<HelpTip label="Only this cell's agents can claim tasks on this project.">
<Badge className={teamColors[project.assigned_cell]}>
{teamLabels[project.assigned_cell]}
</Badge>
</HelpTip>
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Tasks">
<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 { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket";
import type { RateLimitEntry } from "@/types/rate-limits";
import { HelpTip } from "@/components/ui/help-tip";
// =============================================================================
// Countdown row for a single rate-limited provider
@@ -37,18 +38,26 @@ function RateLimitRow({ entry }: { entry: RateLimitEntry }) {
return (
<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" />
<HelpTip label="This AI provider hit a rate limit or a persistent overload; every other provider keeps working normally.">
<span className="text-sm font-medium text-amber-900">
{entry.provider}
</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>
)}
<HelpTip label="Estimated time until RoboCo retries this provider, based on the retry-after it reported.">
<span className="text-sm text-amber-700">{secondsLeft}s</span>
</HelpTip>
<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>
);
}
@@ -527,6 +527,7 @@ export function AIRoutingCard() {
active={currentMode === "self_hosted"}
onClick={flipToSelfHosted}
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
icon={<Cpu className="h-4 w-4" />}
@@ -561,9 +562,11 @@ export function AIRoutingCard() {
<>
<Separator />
<section className="space-y-2">
<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.">
<Label className="text-sm font-medium">
Self-Hosted default model
</Label>
</HelpTip>
<p className="text-xs text-muted-foreground">
Choose which discovered model all agents should use in
self-hosted mode.
@@ -782,6 +785,7 @@ function ModeButton({
disabled,
onClick,
highlight,
labelHint,
}: {
icon: React.ReactNode;
label: string;
@@ -790,6 +794,8 @@ function ModeButton({
disabled: boolean;
onClick: () => void | Promise<void>;
highlight?: boolean;
/** Extra hover context on the label, e.g. a gotcha the always-visible description doesn't cover. */
labelHint?: string;
}) {
return (
<Button
@@ -804,11 +810,15 @@ function ModeButton({
>
<div className="flex w-full items-center gap-2 text-sm font-medium">
{icon}
<HelpTip label={labelHint}>
<span>{label}</span>
</HelpTip>
{active ? (
<HelpTip label="Currently applied fleet-wide — every agent spawns on this provider until the mode changes again.">
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
active
</Badge>
</HelpTip>
) : null}
</div>
<p className="mt-1 text-xs text-muted-foreground font-normal">
@@ -289,7 +289,13 @@ export function FeatureFlagsCard() {
size="sm"
className="w-full justify-between px-2 text-muted-foreground"
>
{/* 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 ? (
<ChevronDown className="h-4 w-4" />
) : (
@@ -314,7 +320,9 @@ export function FeatureFlagsCard() {
size="sm"
className="w-full justify-between px-2 text-muted-foreground"
>
<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 ? (
<ChevronDown className="h-4 w-4" />
) : (
@@ -339,7 +347,9 @@ export function FeatureFlagsCard() {
size="sm"
className="w-full justify-between px-2 text-muted-foreground"
>
<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 ? (
<ChevronDown className="h-4 w-4" />
) : (
@@ -145,16 +145,22 @@ export function SelfHostedSection({
{/* Header */}
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" />
<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 && (
<HelpTip label="The last Test Connection call succeeded — the model list below reflects this endpoint.">
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
<CheckCircle2 className="h-3 w-3" /> connected
</Badge>
</HelpTip>
)}
{testResult?.ok === false && (
<HelpTip label="The last Test Connection call failed — see the error detail below.">
<Badge className="bg-red-500/10 text-red-600 border-0">
<XCircle className="h-3 w-3" /> error
</Badge>
</HelpTip>
)}
</div>
@@ -216,9 +222,11 @@ export function SelfHostedSection({
</Button>
</HelpTip>
</div>
<HelpTip label="Saves the Base URL above together with any token typed here, in one combined config — not just this token.">
<Button onClick={handleSave} disabled={saveConfig.isPending}>
{saveConfig.isPending ? "Saving…" : "Save"}
</Button>
</HelpTip>
</div>
{config?.has_token && (
<p className="text-xs text-muted-foreground">
@@ -336,6 +344,7 @@ export function SelfHostedSection({
</span>
</HelpTip>
</p>
<HelpTip label="Re-queries the endpoint's model list only — doesn't re-run the connection test above.">
<Button
variant="ghost"
size="sm"
@@ -354,6 +363,7 @@ export function SelfHostedSection({
</>
)}
</Button>
</HelpTip>
</div>
<div className="divide-y rounded-md border">
{models.map((m) => (
@@ -69,19 +69,22 @@ export function TelegramCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3">
{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" />
</HelpTip>
) : (
<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" />
</HelpTip>
)}
{status?.has_credentials ? (
<span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
</>
) : (
<>
<KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
</>
)}
</div>
@@ -88,19 +88,22 @@ export function TikTokCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3">
{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" />
</HelpTip>
) : (
<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" />
</HelpTip>
)}
{status?.has_credentials ? (
<span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
</>
) : (
<>
<KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
</>
)}
</div>
@@ -86,19 +86,22 @@ export function XCredentialsForm() {
<div className="flex items-center gap-2 rounded-md border p-3">
{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" />
</HelpTip>
) : (
<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" />
</HelpTip>
)}
{status?.has_credentials ? (
<span className="text-sm text-green-600 dark:text-green-400">
Credentials are set
</span>
</>
) : (
<>
<KeyRound className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-600 dark:text-amber-400">
{isLoading ? "Checking..." : "No credentials configured"}
</span>
</>
)}
</div>