diff --git a/app/src/features/account/ArchivedSettings.tsx b/app/src/features/account/ArchivedSettings.tsx index 7666c76d..cb83d019 100644 --- a/app/src/features/account/ArchivedSettings.tsx +++ b/app/src/features/account/ArchivedSettings.tsx @@ -11,6 +11,7 @@ import type { Archive, Bot, Crew } from "../../lib/protocol.gen"; import { useApi, useApp } from "../../store/context"; import { Button } from "../../ui/Button"; import { Callout } from "../../ui/Callout"; +import { IconBadge } from "../../ui/ChatCard"; import { BotAvatar } from "../bots/BotAvatar"; import { DeleteBot } from "../bots/DeleteBot"; import { DeleteCrew } from "../crews/DeleteCrew"; @@ -73,11 +74,7 @@ export function ArchivedSettings() { {archive.crews.map((crew) => ( - - - } + icon={} name={crew.name} line={s.archivedCrew(botsOf(crew).length, fromNow(crew.archivedAt ?? 0))} onDelete={() => setAsking({ crew })} diff --git a/app/src/features/bots/BotHeader.tsx b/app/src/features/bots/BotHeader.tsx index f48f272b..e54427dc 100644 --- a/app/src/features/bots/BotHeader.tsx +++ b/app/src/features/bots/BotHeader.tsx @@ -12,6 +12,7 @@ import { import { useT } from "../../i18n"; import type { Bot, Crew } from "../../lib/protocol.gen"; import { useApi, useApp } from "../../store/context"; +import { Badge } from "../../ui/Badge"; import { Button } from "../../ui/Button"; import { Menu } from "../../ui/Menu"; import { attempt } from "../../ui/toast"; @@ -91,13 +92,9 @@ export function BotHeader({ {chief && } {bot.permissionMode === "bypass_permissions" && ( - - + {t.chat.mode.badge} - + )} {bot.role || words.noRole} diff --git a/app/src/features/bots/BotStateBadge.tsx b/app/src/features/bots/BotStateBadge.tsx index 6d67eeb4..e2b67ab7 100644 --- a/app/src/features/bots/BotStateBadge.tsx +++ b/app/src/features/bots/BotStateBadge.tsx @@ -10,9 +10,10 @@ import { } from "lucide-react"; import { type Messages, t, useT } from "../../i18n"; import type { Bot, BotState } from "../../lib/protocol.gen"; +import { TONES, type Tone as ToneName } from "../../ui/tone"; import { ReadyIcon, type StateIcon, WorkingIcon } from "./StateIcons"; -type Tone = "ok" | "work" | "warn" | "danger" | "quiet"; +type Tone = Exclude; export interface StateView { label: string; @@ -57,14 +58,6 @@ export function stateView( return { ...looks[bot.state], ...words[bot.state] }; } -const toneText: Record = { - ok: "text-ok", - work: "text-work", - warn: "text-warn", - danger: "text-danger", - quiet: "text-quiet", -}; - export function BotStateBadge({ bot, crewPaused, @@ -80,7 +73,7 @@ export function BotStateBadge({ return ( crew.leadBotId === bot.id; @@ -18,12 +19,8 @@ export function ChiefBadge({ crew, compact = false }: { crew: Crew; compact?: bo ); } return ( - - + {c.badge} - + ); } diff --git a/app/src/features/browser/BrowserPanel.tsx b/app/src/features/browser/BrowserPanel.tsx index 0a05dfd7..0d21a5c8 100644 --- a/app/src/features/browser/BrowserPanel.tsx +++ b/app/src/features/browser/BrowserPanel.tsx @@ -9,8 +9,10 @@ import { useT } from "../../i18n"; import type { Bot } from "../../lib/protocol.gen"; import { SYSTEM } from "../../lib/system"; import { useApp } from "../../store/context"; +import { Badge } from "../../ui/Badge"; import { Button } from "../../ui/Button"; import { Callout } from "../../ui/Callout"; +import { EmptyState } from "../../ui/EmptyState"; import { SidePanel } from "../../ui/SidePanel"; import { BotAvatar } from "../bots/BotAvatar"; import { AddressBar } from "./AddressBar"; @@ -81,19 +83,15 @@ export function BrowserPanel({

{t.heading} {resting ? ( - - + {t.resting} - + ) : ( live && ( - + {t.live} - + ) )}

@@ -211,17 +209,6 @@ function Overlay({ busy, title, body }: { busy: boolean; title: string; body: st function Empty({ bot }: { bot: Bot }) { const t = useT().browser; return ( -
- - - - -

{t.emptyTitle(bot.name)}

-

{t.emptyBody}

-
+ ); } diff --git a/app/src/features/browser/HelpCard.tsx b/app/src/features/browser/HelpCard.tsx index fd2937b4..07d038c7 100644 --- a/app/src/features/browser/HelpCard.tsx +++ b/app/src/features/browser/HelpCard.tsx @@ -3,10 +3,11 @@ // it" answer the request; once answered it shrinks to one line. import { Ban, Check, Hand, TimerOff } from "lucide-react"; -import { useContext, useId } from "react"; +import { useContext } from "react"; import { useT } from "../../i18n"; import type { ApprovalItem, Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; +import { NoteInput, RequestCard, SettledLine } from "../../ui/ChatCard"; import { useAnswer } from "../chat/useAnswer"; import { ShowBrowser } from "./showBrowser"; @@ -28,30 +29,15 @@ function read(approval: ApprovalItem): { task: string; site: string | null } { function Answered({ approval, task }: { approval: ApprovalItem; task: string }) { const words = useT().browser.help; - const line = "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-sm"; switch (approval.status) { case "allowed": - return ( -

- - {words.doneLine(task)} -

- ); + return ; case "denied": return ( -

- - {words.wontLine(task)} - {approval.note && “{approval.note}”} -

+ ); default: - return ( -

- - {words.expired(task)} -

- ); + return ; } } @@ -63,23 +49,18 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot } allow: t.chat.approval.allowFailed, deny: t.chat.approval.denyFailed, }); - const noteId = useId(); const { task, site } = read(approval); if (approval.status !== "pending") { return ; } return ( -
-

- - - - {words.needs(bot.name)} -

{task}

@@ -89,15 +70,11 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }

)}

{words.why}

- - setNote(event.target.value)} + onChange={setNote} placeholder={t.chat.approval.notePlaceholder} - className="mt-3 h-9 w-full rounded-xl border border-line-strong bg-canvas px-3 text-sm outline-none placeholder:text-muted focus:border-muted" />
{showBrowser && ( @@ -117,6 +94,6 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot } {words.wontDo}
-
+ ); } diff --git a/app/src/features/browser/SiteCard.tsx b/app/src/features/browser/SiteCard.tsx index d2a074e8..dd962230 100644 --- a/app/src/features/browser/SiteCard.tsx +++ b/app/src/features/browser/SiteCard.tsx @@ -2,10 +2,10 @@ // address, and Allow or Deny. Once answered it shrinks to one line. import { Ban, Check, Globe, TimerOff } from "lucide-react"; -import { useId } from "react"; import { useT } from "../../i18n"; import type { ApprovalItem, Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; +import { NoteInput, RequestCard, SettledLine } from "../../ui/ChatCard"; import { useAnswer } from "../chat/useAnswer"; /** The request the daemon opens from the browser tools. */ @@ -23,30 +23,15 @@ function addressOf(input: string): string | null { function Answered({ approval, site }: { approval: ApprovalItem; site: string }) { const words = useT().browser.site; - const line = "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-sm"; switch (approval.status) { case "allowed": - return ( -

- - {words.allowed(site)} -

- ); + return ; case "denied": return ( -

- - {words.denied(site)} - {approval.note && “{approval.note}”} -

+ ); default: - return ( -

- - {words.expired(site)} -

- ); + return ; } } @@ -57,7 +42,6 @@ export function SiteCard({ approval, bot }: { approval: ApprovalItem; bot: Bot } allow: t.chat.approval.allowFailed, deny: t.chat.approval.denyFailed, }); - const noteId = useId(); const site = approval.summary; if (approval.status !== "pending") { return ; @@ -65,33 +49,27 @@ export function SiteCard({ approval, bot }: { approval: ApprovalItem; bot: Bot } const url = addressOf(approval.input); return ( -
-

- - - + {words.wants(bot.name)} {site} -

+ } + > {url && (

{url}

)}

{words.why}

- - setNote(event.target.value)} + onChange={setNote} placeholder={t.chat.approval.notePlaceholder} - className="mt-3 h-9 w-full rounded-xl border border-line-strong bg-canvas px-3 text-sm outline-none placeholder:text-muted focus:border-muted" />
-
+ ); } diff --git a/app/src/features/chat/ApprovalCard.tsx b/app/src/features/chat/ApprovalCard.tsx index 5af4c107..1536d882 100644 --- a/app/src/features/chat/ApprovalCard.tsx +++ b/app/src/features/chat/ApprovalCard.tsx @@ -2,10 +2,10 @@ // and Deny. Once answered it shrinks to one line. import { Ban, Check, CheckCheck, Hand, TimerOff } from "lucide-react"; -import { useId } from "react"; import { useT } from "../../i18n"; import type { AllowScope, ApprovalItem, Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; +import { NoteInput, RequestCard, SettledLine } from "../../ui/ChatCard"; import { Details } from "../../ui/Details"; import { HELP_TOOL, HelpCard } from "../browser/HelpCard"; import { SITE_TOOL, SiteCard } from "../browser/SiteCard"; @@ -29,35 +29,28 @@ function About({ approval }: { approval: ApprovalItem }) { function Answered({ approval }: { approval: ApprovalItem }) { const t = useT(); const label = toolAction(approval.toolName, t.tools); - const line = "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-sm"; switch (approval.status) { case "allowed": return ( -

- - {t.chat.approval.allowed(label)} - -

+ } + /> ); case "denied": return ( -

- - {t.chat.approval.denied(label)} - {approval.note ? ( - {`“${approval.note}”`} - ) : ( - - )} -

+ } + /> ); default: - return ( -

- - {t.chat.approval.expired(label)} -

- ); + return ; } } @@ -134,23 +127,18 @@ function ToolApproval({ approval, bot }: { approval: ApprovalItem; bot: Bot }) { allow: t.chat.approval.allowFailed, deny: t.chat.approval.denyFailed, }); - const noteId = useId(); if (approval.status !== "pending") { return ; } const label = toolAction(approval.toolName, t.tools); return ( -
-

- - - - {t.chat.approval.wants(bot.name, label)} -

{isCommand(approval.toolName) ? ( ) : ( @@ -165,15 +153,11 @@ function ToolApproval({ approval, bot }: { approval: ApprovalItem; bot: Bot }) { )} - - setNote(event.target.value)} + onChange={setNote} placeholder={t.chat.approval.notePlaceholder} - className="mt-3 h-9 w-full rounded-xl border border-line-strong bg-canvas px-3 text-sm outline-none placeholder:text-muted focus:border-muted" />
-
+ ); } diff --git a/app/src/features/chat/NoticeRow.tsx b/app/src/features/chat/NoticeRow.tsx index 7f8ecddf..ffb39fee 100644 --- a/app/src/features/chat/NoticeRow.tsx +++ b/app/src/features/chat/NoticeRow.tsx @@ -6,11 +6,12 @@ import { memo } from "react"; import { type Messages, useT } from "../../i18n"; import type { NoticeItem } from "../../lib/protocol.gen"; import { useArrival } from "../../ui/motion"; +import { TONES } from "../../ui/tone"; const LOOK = { - info: { icon: Info, tone: "text-work", frame: "border-line bg-panel" }, - warning: { icon: TriangleAlert, tone: "text-warn", frame: "border-warn/40 bg-warn/6" }, - error: { icon: CircleAlert, tone: "text-danger", frame: "border-danger/40 bg-danger/6" }, + info: { icon: Info, tone: TONES.work.text, frame: TONES.quiet.frame }, + warning: { icon: TriangleAlert, tone: TONES.warn.text, frame: TONES.warn.frame }, + error: { icon: CircleAlert, tone: TONES.danger.text, frame: TONES.danger.frame }, } as const; /** `at` is when the notice came, so a new one animates in. */ diff --git a/app/src/features/chat/PlanCard.tsx b/app/src/features/chat/PlanCard.tsx index 44157ecd..e37252ab 100644 --- a/app/src/features/chat/PlanCard.tsx +++ b/app/src/features/chat/PlanCard.tsx @@ -2,11 +2,11 @@ // it as the ExitPlanMode tool, with the plan. Approving lets the bot work; // sending it back keeps it planning, with what should change. -import { Check, ChevronRight, ListTodo, PencilLine, TimerOff } from "lucide-react"; -import { useId } from "react"; +import { Check, ListTodo, PencilLine, TimerOff } from "lucide-react"; import { useT } from "../../i18n"; import type { ApprovalItem, Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; +import { NoteArea, SectionCard, SettledLine } from "../../ui/ChatCard"; import { Markdown } from "./Markdown"; import { useAnswer } from "./useAnswer"; @@ -34,77 +34,58 @@ export function PlanCard({ approval, bot }: { approval: ApprovalItem; bot: Bot } allow: p.approveFailed, deny: p.keepFailed, }); - const noteId = useId(); const plan = planText(approval.input); if (approval.status !== "pending") { return ; } return ( -
+ +
+ + +
+ + } > -

- - - - {p.ready(bot.name)} -

-
- -