This commit is contained in:
ZacharyZhang-NY committed 2026-03-23 16:27:55 -04:00
1 parent b15f1c0e5e
commit b56aaa1498
71 files changed
+866 -659

No files matched your search

+2
View File
@@ -46,3 +46,5 @@ yarn-debug.log*
yarn-error.log*
CLAUDE.md
build/release/AgentCompany-0.4.61-arm64.dmg
CLAUDE.md
CLAUDE.md
+32
View File
@@ -121,3 +121,35 @@ Do not make direct repo edits outside a GSD workflow unless the user explicitly
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
> This section is managed by `generate-claude-profile` -- do not edit manually.
<!-- GSD:profile-end -->
## Design Context
### Users
Builders and operators who want to spin up autonomous AI companies without managing infrastructure. They arrive with a goal, expect the system to self-organize, and watch their AI workforce execute. They care about output quality and want to feel like they're running something real — not playing with a demo.
### Brand Personality
**Bold, futuristic, ambitious.** AgentCompany is the command layer for the next era of work. The interface should project quiet authority — this is a serious tool that happens to do something extraordinary.
### Emotional Goal
**Professional pride.** The user should feel "I built something real." The interface should reinforce that their AI company is a legitimate operation — not a toy, not a gimmick, but a functioning organization they orchestrated.
### Aesthetic Direction
- **Reference**: Notion — content-first layout, gentle hierarchy, flexible workspace feel
- **Accent**: Steel slate (#4a5d7a light, #7b9cc4 dark) — formal, authoritative, professional
- **Theme**: Full light + dark mode via CSS custom properties
- **Typography**: System sans-serif for UI, JetBrains Mono for code/data
- **Icons**: Phosphor (regular + fill weights), 16-18px standard
- **Motion**: Framer Motion with cubic-bezier(0.16, 1, 0.3, 1), reduced-motion aware
### Anti-References (what this must NOT be)
- **Not toy/demo**: No cartoon illustrations, rainbow gradients, or playful AI mascots
- **Not enterprise bloat**: No overwhelming dashboards, feature overload, or Salesforce-style complexity
- **Not generic SaaS**: No cookie-cutter component libraries, stock photo energy, or bland templates
- **Not sci-fi gimmick**: No glowing neon, Matrix terminals, or over-the-top futuristic theming
### Design Principles
1. **Content over chrome** — Information density is a feature. Every element earns its place. No decorative filler.
2. **Quiet authority** — The interface should feel confident without shouting. Subtle borders, restrained color, generous whitespace.
3. **Real, not theatrical** — Agent activity should feel like watching a real company operate, not a sci-fi simulation. Status over spectacle.
4. **Desktop-native craft** — Keyboard-first, focus rings, platform-aware shortcuts, proper window management. No web-app-in-a-wrapper feel.
5. **Accessible by default** — Reduced motion, color contrast, semantic HTML, focus traps. Not an afterthought.
+1 -1
View File
@@ -4,7 +4,7 @@
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
},
"extraMetadata": {
"version": "0.4.61"
"version": "0.4.77"
},
"mac": {
"hardenedRuntime": false,
+7 -10
View File
@@ -1,11 +1,8 @@
version: 0.4.61
version: 0.4.77
files:
- url: AgentCompany-0.4.61-arm64-mac.zip
sha512: 9qHqwpYcc4lCuAfqazR5duwAIYvMoMWufWtw+Wm1gyVGfwyb58szRmzgWHBErYHx/51eQHZL7i6RhJswmIvIrg==
size: 113638909
- url: AgentCompany-0.4.61-arm64.dmg
sha512: tuE2rcS0lPGMvB0lOnxX3udIl+oESrZWqRrYXuzIV7gsBfp8NL26fLxjppt6BhQbVT6GeKHi7p7OdViZKxYL2Q==
size: 118393537
path: AgentCompany-0.4.61-arm64-mac.zip
sha512: 9qHqwpYcc4lCuAfqazR5duwAIYvMoMWufWtw+Wm1gyVGfwyb58szRmzgWHBErYHx/51eQHZL7i6RhJswmIvIrg==
releaseDate: '2026-03-23T16:31:23.663Z'
- url: AgentCompany-0.4.77-arm64-mac.zip
sha512: rM2oPamJ0mHhH4bkIrcdJT+raflsJpiKI331HDD/flZvGnB0L3zveHEjkG6eZkIvpQOh78d9/HjPRUuB2lGZ+w==
size: 113614712
path: AgentCompany-0.4.77-arm64-mac.zip
sha512: rM2oPamJ0mHhH4bkIrcdJT+raflsJpiKI331HDD/flZvGnB0L3zveHEjkG6eZkIvpQOh78d9/HjPRUuB2lGZ+w==
releaseDate: '2026-03-23T20:26:33.820Z'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.61",
"version": "0.4.77",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
+63 -58
View File
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
ArrowClockwise,
CaretUpDown,
ChatCircle,
Check,
@@ -15,7 +14,7 @@ import {
MagnifyingGlass,
Megaphone,
Plus,
Robot,
UsersThree,
ShieldCheck,
Trash,
UserPlus,
@@ -23,6 +22,7 @@ import {
Target,
SidebarSimple,
TerminalWindow,
CircleNotch,
WarningCircle,
X,
} from "@phosphor-icons/react";
@@ -77,6 +77,7 @@ import { CommunicationHub } from "./components/communication";
import { HiringPipeline } from "./components/hiring";
import { DocumentCenter, KnowledgeBaseManager, MeetingsManager, SprintManager } from "./components/company-operations";
import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionButton, Select, StatusPill, useFocusTrap, useReducedMotion } from "./components/ui";
import ToastContainer from "./components/Toast";
import { unwrap } from "./lib/desktop";
import { formatMoney, formatTime } from "./lib/formatters";
@@ -90,7 +91,7 @@ type PaletteAction = {
const sidebarNavDefs: NavItemDef[] = [
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
{ id: "agents", labelKey: "nav.agents", icon: Robot },
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
@@ -101,8 +102,8 @@ const sidebarNavDefs: NavItemDef[] = [
export function App() {
const reducedMotion = useReducedMotion();
const noMotion = { duration: 0 };
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };
const pageTransition = reducedMotion ? noMotion : { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const };
// -- Zustand store bindings (replaces 31 useState calls) --
const locale = useAppStore((s) => s.locale);
@@ -556,24 +557,11 @@ export function App() {
setInitialOnboardingLocked(false);
await refresh();
setError(null);
setSection("overview");
if (summary.runId) {
setSection("runs");
setSelectedRunId(summary.runId);
setConsoleOpen(true);
return;
}
if (summary.taskId) {
setSection("tasks");
setSelectedTaskId(summary.taskId);
return;
}
if (summary.projectId) {
setSection("projects");
setSelectedProjectId(summary.projectId);
return;
}
if (summary.leadAgentId) {
setSection("agents");
setSelectedAgentId(summary.leadAgentId);
}
}, [refresh]);
@@ -582,8 +570,9 @@ export function App() {
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
{t("app.loading")}
<div className="flex min-h-screen flex-col items-center justify-center gap-3">
<CircleNotch size={24} className="animate-spin text-[color:var(--accent)]" />
<span className="text-[13px] text-[color:var(--muted)]">{t("app.loading")}</span>
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
@@ -616,7 +605,7 @@ export function App() {
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className={`grid h-[100dvh] overflow-hidden bg-[color:var(--bg)] ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]">
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line-strong)] bg-[color:var(--panel)]">
{sidebarCollapsed ? (
<div className="sidebar-drag flex items-center justify-center px-2 pb-3 pt-[38px]">
<button
@@ -651,7 +640,7 @@ export function App() {
<span className="flex-1 truncate text-[12px] text-[color:var(--danger)]">Delete {company.name}?</span>
<button
type="button"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)] hover:text-white"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"
onClick={() => void deleteCompany(company.id)}
>
Yes
@@ -678,7 +667,7 @@ export function App() {
</button>
<button
type="button"
className="mr-2 rounded p-1 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger)] hover:text-white group-hover:opacity-100"
className="mr-2 rounded p-1 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)] group-hover:opacity-100"
title={`Delete ${company.name}`}
onClick={() => setConfirmDeleteCompanyId(company.id)}
>
@@ -749,56 +738,51 @@ export function App() {
</aside>
<main aria-label="Content" className="flex min-w-0 flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
<div className="mx-auto flex w-full max-w-[1040px] flex-col px-6 py-6 lg:px-10 lg:py-8">
<header className="mb-8 flex items-start justify-between gap-4 border-b border-[color:var(--line)] pb-5 lg:mb-10 lg:gap-6">
<h1 className="text-[24px] tracking-[-0.03em] text-[color:var(--text)] lg:text-[31px]">
{navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
</h1>
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
<button
type="button"
aria-label="Refresh"
title="Refresh"
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
onClick={() => void refresh()}
>
<ArrowClockwise size={16} />
<span className="hidden lg:inline">{t("app.refresh")}</span>
</button>
<button
type="button"
aria-label={consoleOpen ? "Hide console" : "Show console"}
title={consoleOpen ? "Hide console" : "Show console"}
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
onClick={() => setConsoleOpen(!consoleOpen)}
>
<TerminalWindow size={16} />
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
</button>
<header className="sidebar-drag shrink-0 border-b border-[color:var(--line)] bg-[color:var(--bg)]">
<div className="mx-auto flex w-full max-w-[1040px] items-start justify-between gap-4 px-6 pb-5 pt-6 lg:gap-6 lg:px-10 lg:pt-8">
<h1 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">
{navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
</h1>
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
<button
type="button"
aria-label={consoleOpen ? "Hide console" : "Show console"}
title={consoleOpen ? "Hide console" : "Show console"}
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
onClick={() => setConsoleOpen(!consoleOpen)}
>
<TerminalWindow size={16} />
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
</button>
</div>
</div>
</header>
<div className={`flex-1 ${section === "communication" ? "flex flex-col overflow-hidden" : "overflow-auto"}`}>
<div className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "mx-auto flex w-full max-w-[1040px] flex-col px-6 py-6 lg:px-10 lg:py-8"}>
{error ? (
<div className="mb-6 flex items-center gap-3 rounded-[10px] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className={`flex items-center gap-3 rounded-[8px] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)] ${section === "communication" ? "mx-6 mt-4" : "mb-6"}`}>
<WarningCircle size={18} />
<span>{error}</span>
</div>
) : null}
<div className="min-h-0 flex-1">
<div className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "min-h-0 flex-1"}>
<ErrorBoundary>
<AnimatePresence mode="wait">
<motion.div
key={section}
initial={{ opacity: 0, y: 12 }}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
exit={{ opacity: 0, y: -12 }}
transition={pageTransition}
className="space-y-14"
className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "space-y-14"}
style={section === "communication" ? { height: "100%" } : undefined}
>
{section === "overview" ? (
<Dashboard
companyName={currentCompany?.name}
agents={companyAgents}
tasks={tasks}
runs={runs}
@@ -992,6 +976,26 @@ export function App() {
{section === "tasks" ? (
<>
<Panel title="Goals" action={<ActionButton label="New goal" icon={Target} tone="accent" onClick={() => setSelectedGoalId("__new__")} />}>
<GoalList goals={companyGoals} agents={companyAgents} selectedGoalId={selectedGoalId} onSelect={setSelectedGoalId} />
</Panel>
<Modal open={selectedGoalId === "__new__" && section === "tasks"} title="New goal" onClose={() => setSelectedGoalId(null)}>
<GoalCreator companyId={currentCompany!.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
</Modal>
<Modal open={!!selectedGoal && section === "tasks"} title={selectedGoal ? `Edit: ${selectedGoal.title}` : ""} onClose={() => setSelectedGoalId(null)}>
{selectedGoal ? (
<GoalEditor
key={selectedGoalId}
goal={selectedGoal}
goals={companyGoals}
agents={companyAgents}
companyId={currentCompany!.id}
onSaved={async () => { setSelectedGoalId(null); await refresh(); }}
onDeleted={async () => { setSelectedGoalId(null); await refresh(); }}
/>
) : null}
</Modal>
<Panel
title="Task queue"
action={
@@ -1387,7 +1391,7 @@ export function App() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.98 }}
transition={panelTransition}
className="w-full max-w-[560px] rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
className="w-full max-w-[560px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center gap-3">
@@ -1458,7 +1462,7 @@ export function App() {
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<Robot size={16} />
<UsersThree size={16} />
<span className="text-[13px] tracking-[0.005em]">{agent.name}</span>
<span className="text-[11px] text-[color:var(--muted)]">{agent.role}</span>
</div>
@@ -1594,6 +1598,7 @@ export function App() {
) : null}
</AnimatePresence>
<ToastContainer />
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
@@ -28,14 +28,14 @@ export function ClaudeSettingsDialog({
}: ClaudeSettingsDialogProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--bg)]/72 backdrop-blur-sm" onClick={onClose}>
<div className="w-full max-w-[420px] rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
<div className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[16px] font-semibold text-[color:var(--text)]">Claude Code {currentLocale === "zh" ? "设置" : "Settings"}</h3>
<button type="button" onClick={onClose} className="rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={16} /></button>
</div>
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">API Key</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">API Key</label>
<input
type="password"
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
@@ -46,7 +46,7 @@ export function ClaudeSettingsDialog({
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}</div>
</div>
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Base URL</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Base URL</label>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={claudeBaseUrl}
@@ -56,7 +56,7 @@ export function ClaudeSettingsDialog({
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}</div>
</div>
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Model</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Model</label>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={claudeModel}
@@ -74,7 +74,7 @@ export function ClaudeSettingsDialog({
type="button"
onClick={onSave}
disabled={saving}
className="rounded-[8px] bg-[color:var(--accent)] px-4 py-2 text-[13px] font-medium text-white transition hover:opacity-90 disabled:opacity-50"
className="rounded-[8px] bg-[color:var(--accent)] px-4 py-2 text-[13px] font-medium text-[color:var(--text-on-accent)] transition hover:opacity-90 disabled:opacity-50"
>
{saving ? (currentLocale === "zh" ? "保存中..." : "Saving...") : (currentLocale === "zh" ? "保存" : "Save")}
</button>
@@ -54,7 +54,7 @@ export function CompanyStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
className="surface w-full rounded-[8px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{t("onboarding.stepLabel1")} 1 / 3
@@ -87,7 +87,7 @@ export function CompanyStep({
label: connector.label,
}))}
/>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
{t("onboarding.connectorInfo")}
</div>
<div>
@@ -130,7 +130,7 @@ export function CompanyStep({
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
{error}
</div>
) : null}
@@ -75,7 +75,7 @@ export function ConnectorReadiness({
})}
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
<div className="mt-4 rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
{t("onboarding.connectorWarning")}
</div>
) : null}
@@ -24,7 +24,7 @@ export function LanguageStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
className="surface w-full rounded-[8px] p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<GlobeSimple size={12} />
@@ -41,7 +41,7 @@ export function LanguageStep({
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-[10px] border px-4 py-3.5 text-left transition ${
className={`flex w-full items-center gap-3 rounded-[8px] border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
@@ -52,7 +52,7 @@ export function LanguageStep({
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
: "border-[color:var(--line)]"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-white" /> : null}
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-[color:var(--text-on-accent)]" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
@@ -26,7 +26,7 @@ export function LaunchStep({
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
className="surface w-full rounded-[8px] p-7"
>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[color:var(--success-soft)] text-[color:var(--success)]">
<CheckCircle size={24} weight="fill" />
@@ -39,31 +39,31 @@ export function LaunchStep({
</p>
<div className="mt-6 grid gap-3 md:grid-cols-2">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{selectedConnector?.label ?? t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
@@ -72,7 +72,7 @@ export function LaunchStep({
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
{error}
</div>
) : null}
@@ -32,7 +32,7 @@ export function MissionStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
className="surface w-full rounded-[8px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{t("onboarding.stepLabel2")} 2 / 3
@@ -70,7 +70,7 @@ export function MissionStep({
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
{error}
</div>
) : null}
@@ -41,34 +41,83 @@ export function OnboardingHero({
}));
return (
<div className="flex min-h-0 items-center justify-center overflow-y-auto px-8 py-12 md:px-16">
<div className="relative flex min-h-0 items-center justify-center overflow-y-auto px-8 py-12 md:px-16">
{/* Ambient gradient - subtle animated accent wash */}
<div
className="pointer-events-none absolute inset-0 opacity-[0.035]"
style={{
background: "radial-gradient(ellipse 80% 60% at 30% 20%, var(--accent) 0%, transparent 70%)",
}}
/>
<div
className="pointer-events-none absolute inset-0 opacity-[0.025]"
style={{
background: "radial-gradient(ellipse 60% 80% at 70% 80%, var(--accent) 0%, transparent 70%)",
animation: "hero-drift 12s ease-in-out infinite alternate",
}}
/>
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
className="mx-auto w-full max-w-[460px]"
className="relative z-10 mx-auto w-full max-w-[460px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{t("onboarding.subtitle")}
</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
{/* Signature line */}
<motion.div
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
className="mb-6 flex items-center gap-3"
>
<div
className="h-px flex-1 max-w-[32px]"
style={{
background: "linear-gradient(90deg, var(--accent), transparent)",
}}
/>
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[color:var(--muted)]">
{t("onboarding.subtitle")}
</span>
</motion.div>
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15, ease: [0.16, 1, 0.3, 1] }}
className="text-[36px] font-bold leading-[1.05] tracking-[-0.03em] text-[color:var(--text)] md:text-[44px]"
>
{t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
))}
</h1>
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
{t("onboarding.description")}
</p>
</motion.h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.25, ease: [0.16, 1, 0.3, 1] }}
className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]"
>
{t("onboarding.description")}
</motion.p>
{/* Step progress */}
{step !== "language" ? (
<div className="mt-10 flex items-center gap-3">
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.35, ease: [0.16, 1, 0.3, 1] }}
className="mt-10 flex items-center gap-3"
>
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
<div className={`h-px w-6 transition-colors duration-300 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium transition-colors duration-300 ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
@@ -81,9 +130,10 @@ export function OnboardingHero({
</div>
</div>
))}
</div>
</motion.div>
) : null}
{/* Connector readiness */}
<ConnectorReadiness
connectors={connectors}
currentLocale={currentLocale}
@@ -91,7 +141,41 @@ export function OnboardingHero({
onOpenClaudeSettings={onOpenClaudeSettings}
t={t}
/>
{/* ELYDORA signature */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mt-12 flex items-center gap-3"
>
<div
className="h-px w-8"
style={{
background: "linear-gradient(90deg, var(--accent), transparent)",
}}
/>
<span
className="select-none text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{
background: "linear-gradient(135deg, var(--accent) 0%, var(--muted) 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
by ELYDORA
</span>
</motion.div>
</motion.div>
{/* Inline keyframe for ambient drift */}
<style>{`
@keyframes hero-drift {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(-4%, 3%) scale(1.05); }
}
`}</style>
</div>
);
}
@@ -184,7 +184,9 @@ export function CompanyOnboarding({
}, [launchSummary, onDone, t]);
return (
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
<div className="relative grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
{/* Drag region for macOS frameless title bar */}
<div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" />
<OnboardingHero
step={step}
connectors={connectors}
+54
View File
@@ -0,0 +1,54 @@
import { AnimatePresence, motion } from "framer-motion"
import { CheckCircle, Warning, X, XCircle } from "@phosphor-icons/react"
import { useAppStore } from "../state/store"
import { useReducedMotion } from "./ui"
const TONE_CONFIG = {
success: { icon: CheckCircle, bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" },
danger: { icon: XCircle, bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" },
warn: { icon: Warning, bg: "var(--warn-soft)", border: "var(--warn)", text: "var(--warn)" },
} as const
export default function ToastContainer() {
const toasts = useAppStore((s) => s.toasts)
const dismiss = useAppStore((s) => s.dismissToast)
const reduced = useReducedMotion()
if (toasts.length === 0) return null
return (
<div className="fixed bottom-4 right-4 z-[60] flex flex-col gap-2" aria-live="polite">
<AnimatePresence>
{toasts.map((toast) => {
const cfg = TONE_CONFIG[toast.tone]
const Icon = cfg.icon
return (
<motion.div
key={toast.id}
initial={reduced ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.96 }}
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
className="flex w-[320px] items-start gap-2.5 rounded-[8px] border px-4 py-3 shadow-lg"
style={{ backgroundColor: cfg.bg, borderColor: cfg.border }}
>
<Icon size={16} weight="fill" className="mt-px shrink-0" style={{ color: cfg.text }} />
<span className="min-w-0 flex-1 break-words text-[13px] font-medium" style={{ color: cfg.text }}>
{toast.message}
</span>
<button
type="button"
onClick={() => dismiss(toast.id)}
className="shrink-0 rounded-[4px] p-0.5 opacity-60 transition hover:opacity-100"
style={{ color: cfg.text }}
aria-label="Dismiss"
>
<X size={12} />
</button>
</motion.div>
)
})}
</AnimatePresence>
</div>
)
}
@@ -15,7 +15,7 @@ function DeleteConfirmDialog({ open, entityName, onConfirm, onCancel }: { open:
if (!open) return null
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onCancel}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-[14px] border bg-[color:var(--panel)] p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-[8px] border bg-[color:var(--panel)] p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-[color:var(--danger-soft)]"><Trash size={16} className="text-[color:var(--danger)]" /></div><h3 className="text-[15px] font-bold text-[color:var(--text)]">Delete</h3></div>
<p className="mt-2 text-[13px] leading-relaxed text-[color:var(--muted)]">Are you sure you want to delete <strong className="text-[color:var(--text)]">{entityName}</strong>? This action cannot be undone.</p>
<div className="mt-5 flex justify-end gap-3"><button type="button" onClick={onCancel} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">Cancel</button><ActionButton label="Delete" onClick={onConfirm} icon={Trash} tone="danger" /></div>
@@ -47,13 +47,13 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-white"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Automation Rules</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--success)" }].map((s) => <div key={s.l} className="rounded-[10px] border bg-[color:var(--panel)] px-4 py-3" style={{ borderColor: LINE }}><div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.l}</div><div className="text-[22px] font-bold" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Automation Rules</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--success)" }].map((s) => <div key={s.l} className="rounded-[8px] border bg-[color:var(--panel)] px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Activity Log ({actLog.length})</button></div>
{activeTab === "rules" ? <>
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-white" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
: filtered.length === 0 ? <div className="py-8 text-center text-[13px] text-[color:var(--muted)]">No rules match the current filter.</div>
: <div className="space-y-2"><AnimatePresence>{filtered.map((r) => <RuleCard key={r.id} rule={r} onToggle={() => void handleToggle(r)} onDelete={() => setDelTarget(r)} onEdit={() => setEditRule(r)} />)}</AnimatePresence></div>}
</> : <AutomationActivityLog entries={actLog} />}
@@ -71,9 +71,9 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-white"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Workflow Pipelines</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Workflow Pipelines</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
: <div className="space-y-2"><AnimatePresence>{workflows.map((wf) => <WorkflowCard key={wf.id} workflow={wf} isRunning={runningId === wf.id} onDelete={() => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}</AnimatePresence></div>}
<AnimatePresence>{showCreate ? <WorkflowFormModal open={showCreate} editingWorkflow={null} companyId={companyId} onClose={() => setShowCreate(false)} onSaved={onRefresh} /> : null}{editWf ? <WorkflowFormModal open={!!editWf} editingWorkflow={editWf} companyId={companyId} onClose={() => setEditWf(null)} onSaved={onRefresh} /> : null}</AnimatePresence>
<AnimatePresence>{delTarget ? <DeleteConfirmDialog open={!!delTarget} entityName={delTarget.name} onConfirm={() => void handleDelete()} onCancel={() => setDelTarget(null)} /> : null}</AnimatePresence>
@@ -85,8 +85,8 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
const t = useT(); const [activeSection, setActiveSection] = useState<"rules"|"workflows">("rules")
return (
<div className="space-y-6">
<div className="rounded-[12px] border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
<div className="flex gap-1 rounded-[10px] bg-[color:var(--panel-soft)] p-1">
<div className="rounded-[8px] border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1">
<button type="button" onClick={() => setActiveSection("rules")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} style={activeSection === "rules" ? { color: ACCENT } : undefined} />Automation Rules ({rules.length})</button>
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} style={activeSection === "workflows" ? { color: ACCENT } : undefined} />Workflows ({workflows.length})</button>
</div>
@@ -9,13 +9,13 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
const TriggerIcon = triggerInfo?.icon ?? Lightning; const colors = statusColors[rule.status]
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[10px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition duration-150 hover:shadow-md hover:-translate-y-px" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={onEdit} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-semibold text-[color:var(--text)]">{rule.name}</span>
<span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{rule.status}</span>
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{rule.name}</span>
<span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} aria-hidden="true" />{rule.status}</span>
</div>
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[color:var(--muted)]">
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? rule.trigger}</span>
@@ -23,7 +23,7 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Gear size={10} />{actionInfo?.label ?? rule.action}</span>
{rule.sourceDepartment || rule.targetDepartment ? <><span className="text-[color:var(--line-strong)]">|</span><span className="inline-flex items-center gap-1"><span className="font-medium text-[color:var(--muted-strong)]">{rule.sourceDepartment ?? "Any"}</span><CaretRight size={8} /><span className="font-medium text-[color:var(--muted-strong)]">{rule.targetDepartment ?? "Same"}</span></span></> : null}
</div>
<div className="mt-1 flex items-center gap-3 text-[10px] text-[color:var(--muted)]"><span>{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Priority {rule.priority}</span></div>
<div className="mt-1 flex items-center gap-3 text-[10px] text-[color:var(--muted)]"><span className="tabular-nums">{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span className="tabular-nums">Priority {rule.priority}</span></div>
</button>
<div className="flex shrink-0 items-center gap-1">
<button type="button" onClick={onToggle} aria-label={rule.status === "active" ? "Pause rule" : "Activate rule"} title={rule.status === "active" ? "Pause rule" : "Activate rule"} className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]">{rule.status === "active" ? <Pause size={15} /> : <Play size={15} />}</button>
@@ -25,7 +25,7 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved }
const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action]
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-[14px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Automation Rule" : "New Automation Rule"}</h2></div>
<button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button>
@@ -25,7 +25,7 @@ export function WorkflowFormModal({ open, editingWorkflow, companyId, onClose, o
const statusOptions = [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "paused", label: "Paused" }]
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-[14px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}><div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><ArrowsClockwise size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Workflow" : "New Workflow"}</h2></div><button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button></div>
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
{error ? <div className="rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]">{error}</div> : null}
@@ -35,9 +35,9 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
const steps: WorkflowStep[] = useMemo(() => { const parsed = safeJsonParse(workflow.stepsJson); return Array.isArray(parsed) ? (parsed as WorkflowStep[]) : [] }, [workflow.stepsJson])
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[10px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={() => setExpanded((p) => !p)} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2"><span className="truncate text-[13px] font-semibold text-[color:var(--text)]">{workflow.name}</span><span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{workflow.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-[color:var(--muted)]"><span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? workflow.triggerType}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{steps.length} step{steps.length !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{workflow.runCount} run{workflow.runCount !== 1 ? "s" : ""}</span>{workflow.lastRunAt ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(workflow.lastRunAt)}</span></> : null}</div>
@@ -1,17 +1,7 @@
import type { AgentMessageRecord, CommentRecord, TaskRecord, MessageChannel } from "@shared/types"
import { ChatCircle, Hash, CaretDown } from "@phosphor-icons/react"
import { CaretDown } from "@phosphor-icons/react"
import { useT } from "../../i18n"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct Messages", icon: ChatCircle, color: "var(--accent)" },
department: { label: "Department", icon: ChatCircle, color: "#6366f1" },
company: { label: "Company-wide", icon: ChatCircle, color: "#d97745" },
project: { label: "Project", icon: Hash, color: "#10b981" },
incident: { label: "Incident", icon: ChatCircle, color: "var(--danger)" },
}
const TERRACOTTA = "#d97745"
export interface ChannelDescriptor {
key: string
kind: "message"
@@ -70,51 +60,19 @@ export function ChannelHeader({
) : null}
{activeChannel ? (
<>
{activeChannel.kind === "message" ? (() => {
const Icon = channelConfig[activeChannel.channel].icon
return (
<Icon
size={16}
weight="fill"
style={{
color: channelConfig[activeChannel.channel].color,
}}
/>
)
})() : (
<Hash size={16} weight="fill" style={{ color: TERRACOTTA }} />
)}
<div className="min-w-0 flex-1">
<div className="text-[13px] font-semibold text-[color:var(--text)]">
{activeChannel.label}
</div>
<div className="text-[10px] text-[color:var(--muted)]">
{activeChannel.kind === "message"
? `${channelMessages.length} message${channelMessages.length !== 1 ? "s" : ""}${(() => {
const stats = unreadByChannel.get(activeChannel.key)
return stats?.unread ? ` (${stats.unread} unread)` : ""
})()}`
: `${taskDiscussionComments.length} comment${taskDiscussionComments.length !== 1 ? "s" : ""}${activeTask ? ` · ${activeTask.status.replaceAll("_", " ")}` : ""}`}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-semibold text-[color:var(--text)]">{activeChannel.label}</div>
{activeChannel.kind === "task" && activeTask ? (
<button
type="button"
onClick={() => onNavigateToTask(activeTask.id)}
className="rounded-[6px] border border-[color:var(--line)] px-2.5 py-1 text-[11px] font-medium text-[color:var(--muted-strong)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
>
<button type="button" onClick={() => onNavigateToTask(activeTask.id)} className="mt-0.5 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
Open task
</button>
) : null}
</>
</div>
) : (
<span className="text-[13px] text-[color:var(--muted)]">
{t("comm.selectChannel")}
</span>
<span className="text-[13px] text-[color:var(--muted)]">{t("comm.selectChannel")}</span>
)}
<div className="ml-auto w-[320px]">
<div className="ml-auto w-full max-w-[320px]">
{filterBar}
</div>
</div>
@@ -1,18 +1,16 @@
import { useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { EnvelopeSimple, Hash, Buildings, Megaphone, Fire, CaretDown, MagnifyingGlass, Funnel } from "@phosphor-icons/react"
import { EnvelopeSimple, Hash, Buildings, Megaphone, Fire, MagnifyingGlass, Funnel } from "@phosphor-icons/react"
import type { AgentRecord, MessageChannel } from "@shared/types"
import { AgentAvatar } from "./MessageItem"
import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeader"
import { useT } from "../../i18n"
const TERRACOTTA = "#d97745"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--accent)" },
department: { label: "Department", icon: Buildings, color: "#6366f1" },
company: { label: "Company-wide", icon: Megaphone, color: TERRACOTTA },
project: { label: "Project", icon: Hash, color: "#10b981" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
company: { label: "Company-wide", icon: Megaphone, color: "var(--accent)" },
project: { label: "Project", icon: Hash, color: "var(--success)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
}
@@ -40,8 +38,8 @@ function ChannelItem({
type="button"
onClick={onClick}
aria-current={active ? "page" : undefined}
className={`group flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
active ? "bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
className={`group relative flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
active ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
}`}
>
{descriptor.channel === "direct" && agent ? (
@@ -56,7 +54,7 @@ function ChannelItem({
)}
<span
className={`min-w-0 flex-1 truncate text-[12px] ${
className={`min-w-0 flex-1 truncate text-[13px] ${
active
? "font-semibold text-[color:var(--text)]"
: unreadCount > 0
@@ -71,8 +69,8 @@ function ChannelItem({
<span className="h-2 w-2 shrink-0 rounded-full bg-[color:var(--danger)]" />
) : unreadCount > 0 ? (
<span
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-white"
style={{ backgroundColor: TERRACOTTA }}
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-[color:var(--text-on-accent)]"
style={{ backgroundColor: "var(--accent)" }}
>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
@@ -113,24 +111,10 @@ export function ChannelSidebar({
className="flex h-full shrink-0 flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]"
style={{ width: 260 }}
>
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
<span className="text-[13px] font-semibold text-[color:var(--text)]">
{t("comm.channels")}
</span>
<button
type="button"
onClick={onHideSidebar}
className="rounded-[4px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
aria-label="Hide sidebar"
>
<CaretDown size={12} className="-rotate-90" />
</button>
</div>
<div className="flex-1 overflow-y-auto px-2 py-2">
<div className="flex-1 overflow-y-auto px-2 py-3">
{channelGroups.map((group) => (
<div key={group.type} className="mb-3">
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{group.label}
</div>
{group.items.map((ch) => {
@@ -152,7 +136,7 @@ export function ChannelSidebar({
{taskDiscussionChannels.length > 0 ? (
<div className="mb-3">
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{t("comm.taskDiscussions")}
</div>
{taskDiscussionChannels.map((discussion) => (
@@ -164,10 +148,10 @@ export function ChannelSidebar({
selectedChannelKey === discussion.key ? "bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
}`}
>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]" style={{ backgroundColor: `${TERRACOTTA}18` }}>
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: TERRACOTTA }} />
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]" style={{ backgroundColor: "var(--accent-soft)" }}>
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--accent)" }} />
</div>
<span className={`min-w-0 flex-1 truncate text-[12px] ${selectedChannelKey === discussion.key ? "font-semibold text-[color:var(--text)]" : "font-medium text-[color:var(--muted-strong)]"}`}>
<span className={`min-w-0 flex-1 truncate text-[13px] ${selectedChannelKey === discussion.key ? "font-semibold text-[color:var(--text)]" : "font-medium text-[color:var(--muted-strong)]"}`}>
{discussion.label}
</span>
</button>
@@ -183,7 +167,7 @@ export function ChannelSidebar({
</div>
<div className="border-t border-[color:var(--line)] px-4 py-2">
<div className="flex items-center justify-between text-[10px] text-[color:var(--muted)]">
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<span>{totalChannels} channels</span>
<span>{agents.length} agents</span>
</div>
@@ -225,21 +209,21 @@ export function FilterBar({
onKeyDown={onSearchKeyDown}
placeholder={t("comm.searchMessages")}
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
style={{ focusRingColor: "var(--accent)" } as React.CSSProperties}
/>
</div>
{scopeToggle}
<button
type="button"
onClick={() => setExpanded(!expanded)}
className={`flex items-center gap-1.5 rounded-[6px] border px-2.5 py-1.5 text-[10px] font-medium transition ${
className={`flex items-center gap-1.5 rounded-[6px] border px-2.5 py-1.5 text-[11px] font-medium transition ${
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? "border-transparent text-white"
? "border-transparent text-[color:var(--text-on-accent)]"
: "border-[color:var(--line)] text-[color:var(--muted-strong)] hover:border-[color:var(--line-strong)]"
}`}
style={
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? { backgroundColor: TERRACOTTA }
? { backgroundColor: "var(--accent)" }
: undefined
}
>
@@ -259,7 +243,7 @@ export function FilterBar({
<select
value={filters.channelType}
onChange={(e) => onChangeFilters({ ...filters, channelType: e.target.value as MessageChannel | "all" })}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
@@ -271,7 +255,7 @@ export function FilterBar({
<select
value={filters.priority}
onChange={(e) => onChangeFilters({ ...filters, priority: e.target.value })}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
@@ -281,10 +265,10 @@ export function FilterBar({
<button
type="button"
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition ${
filters.unreadOnly ? "text-white" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${
filters.unreadOnly ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
style={filters.unreadOnly ? { backgroundColor: "var(--accent)" } : undefined}
>
{t("comm.unreadOnly")}
</button>
@@ -292,7 +276,7 @@ export function FilterBar({
<button
type="button"
onClick={() => onChangeFilters({ search: filters.search, channelType: "all", priority: "all", unreadOnly: false })}
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
className="text-[11px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
>
{t("comm.clearFilters")}
</button>
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ChatCircle, Plus } from "@phosphor-icons/react"
import type { AgentMessageRecord, AgentRecord, CommentRecord, MessageChannel, ProjectRecord, TaskRecord } from "@shared/types"
import { useAppStore } from "../../state/store"
import { unwrap } from "../../lib/desktop"
@@ -9,12 +8,10 @@ import { ChannelSidebar, FilterBar, type FilterState } from "./ChannelSidebar"
import type { ChannelDescriptor, TaskDiscussionDescriptor, AnyChannel } from "./ChannelHeader"
import { ChannelHeader } from "./ChannelHeader"
import { MessageList } from "./MessageList"
import { MessageComposer, TaskDiscussionComposer, NoChannelComposer, ComposeModal } from "./MessageComposer"
import { MessageComposer, TaskDiscussionComposer, NoChannelComposer } from "./MessageComposer"
import { SearchResults } from "./SearchResults"
import { deriveChannels, deriveTaskDiscussionChannels, getChannelMessages } from "./channel-utils"
const TERRACOTTA = "#d97745"
const TERRACOTTA_SOFT = "#d9774518"
interface CommunicationHubProps {
messages: AgentMessageRecord[]; comments: CommentRecord[]; tasks: TaskRecord[]; agents: AgentRecord[]
@@ -35,9 +32,9 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
aria-checked={scope === "global"}
onClick={() => onChangeScope("global")}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("global") } }}
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition-colors duration-150 ${
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "global"
? "bg-[color:var(--accent)] text-white"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}
>
@@ -49,9 +46,9 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
aria-checked={scope === "channel"}
onClick={() => onChangeScope("channel")}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("channel") } }}
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition-colors duration-150 ${
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "channel"
? "bg-[color:var(--accent)] text-white"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}
>
@@ -65,7 +62,6 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
const t = useT()
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
const [showSidebar, setShowSidebar] = useState(true)
const [showCompose, setShowCompose] = useState(false)
const [replyTo, setReplyTo] = useState<AgentMessageRecord | null>(null)
const [optimisticReadIds, setOptimisticReadIds] = useState<Set<string>>(new Set())
const pendingReadIdsRef = useRef<Set<string>>(new Set())
@@ -218,7 +214,6 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
return counts
}, [channels, effectiveMessages])
const totalUnread = useMemo(() => effectiveMessages.filter((m) => !m.readAt).length, [effectiveMessages])
const channelGroups = useMemo(() => {
const groups: Array<{ type: MessageChannel; label: string; items: ChannelDescriptor[] }> = []
@@ -235,64 +230,47 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
) : null
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: TERRACOTTA_SOFT }}>
<ChatCircle size={20} weight="fill" style={{ color: TERRACOTTA }} />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("comm.communicationHub")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">{messages.length} message{messages.length !== 1 ? "s" : ""}{totalUnread > 0 ? <span style={{ color: TERRACOTTA }}> ({totalUnread} unread)</span> : null}</p>
</div>
<div className="flex h-full min-h-0 overflow-hidden">
<AnimatePresence initial={false}>
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={setSelectedChannelKey} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
</AnimatePresence>
<div className="flex min-w-0 flex-1 flex-col">
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
<div className="flex-1 overflow-y-auto px-2 py-3">
<AnimatePresence mode="wait">
{isSearchActive ? (
<motion.div
key="search-results"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="h-full"
>
<SearchResults
results={searchResults}
query={searchQuery}
loading={searchLoading}
agents={agents}
onNavigateToResult={handleNavigateToResult}
/>
</motion.div>
) : (
<motion.div
key="message-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="h-full"
>
<MessageList activeChannel={activeChannel} filteredMessages={filteredMessages} taskDiscussionComments={taskComments} activeTask={activeTask} agentMap={agentMap} filters={filters} onReply={setReplyTo} messageCount={effectiveMessages.length} selectedChannelKey={selectedChannelKey} />
</motion.div>
)}
</AnimatePresence>
</div>
<button type="button" onClick={() => setShowCompose(true)} className="inline-flex items-center gap-2 rounded-[8px] px-3 py-2 text-[11px] font-semibold text-white transition hover:brightness-90" style={{ backgroundColor: TERRACOTTA }}>
<Plus size={13} weight="bold" />{t("comm.newMessage")}
</button>
{activeChannel ? (activeChannel.kind === "message" ? <MessageComposer agents={agents} companyId={companyId} channel={activeChannel.channel} channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId} directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []} replyTo={replyTo} onSent={() => { setReplyTo(null); onRefresh() }} onCancelReply={() => setReplyTo(null)} /> : <TaskDiscussionComposer companyId={companyId} taskId={activeChannel.taskId} onSent={onRefresh} />) : <NoChannelComposer />}
</div>
<div className="flex overflow-hidden rounded-[12px] border border-[color:var(--line)]" style={{ height: "calc(100vh - 220px)", minHeight: "480px" }}>
<AnimatePresence initial={false}>
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={setSelectedChannelKey} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
</AnimatePresence>
<div className="flex min-w-0 flex-1 flex-col bg-[color:var(--bg)]">
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
<div className="flex-1 overflow-y-auto px-2 py-3">
<AnimatePresence mode="wait">
{isSearchActive ? (
<motion.div
key="search-results"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="h-full"
>
<SearchResults
results={searchResults}
query={searchQuery}
loading={searchLoading}
agents={agents}
onNavigateToResult={handleNavigateToResult}
/>
</motion.div>
) : (
<motion.div
key="message-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="h-full"
>
<MessageList activeChannel={activeChannel} filteredMessages={filteredMessages} taskDiscussionComments={taskComments} activeTask={activeTask} agentMap={agentMap} filters={filters} onReply={setReplyTo} messageCount={effectiveMessages.length} selectedChannelKey={selectedChannelKey} />
</motion.div>
)}
</AnimatePresence>
</div>
{activeChannel ? (activeChannel.kind === "message" ? <MessageComposer agents={agents} companyId={companyId} channel={activeChannel.channel} channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId} directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []} replyTo={replyTo} onSent={() => { setReplyTo(null); onRefresh() }} onCancelReply={() => setReplyTo(null)} /> : <TaskDiscussionComposer companyId={companyId} taskId={activeChannel.taskId} onSent={onRefresh} />) : <NoChannelComposer />}
</div>
</div>
<ComposeModal open={showCompose} onClose={() => setShowCompose(false)} agents={agents} projects={projects} companyId={companyId} onSent={onRefresh} />
</div>
)
}
@@ -7,11 +7,9 @@ import { InlineNotice, Modal, Select, Input, TextArea } from "../ui"
import { AgentAvatar } from "./MessageItem"
import { useT } from "../../i18n"
const TERRACOTTA = "#d97745"
function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) {
return (
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" style={{ backgroundColor: TERRACOTTA }} aria-label={label}>
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" aria-label={label}>
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={18} weight="fill" />}
</button>
)
@@ -57,7 +55,7 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
<AnimatePresence>
{replyTo ? (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="mb-2 flex items-center gap-2 overflow-hidden rounded-[6px] bg-[color:var(--panel-soft)] px-3 py-2">
<ArrowBendUpLeft size={12} style={{ color: TERRACOTTA }} />
<ArrowBendUpLeft size={12} className="text-[color:var(--accent)]" />
<span className="min-w-0 flex-1 truncate text-[11px] text-[color:var(--muted-strong)]">Replying to <span className="font-semibold">{agents.find((a) => a.id === replyTo.fromAgentId)?.name ?? "agent"}</span>{replyTo.subject ? `: ${replyTo.subject}` : ""}</span>
<button type="button" onClick={onCancelReply} className="shrink-0 rounded-[4px] p-0.5 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={12} /></button>
</motion.div>
@@ -71,10 +69,10 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
{!replyTo ? <input type="text" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Subject (optional)" className="min-w-0 flex-1 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" /> : null}
</div>
<div className="flex items-end gap-2">
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[12px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: TERRACOTTA } as React.CSSProperties} />
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
<SendButton sending={sending} disabled={!body.trim() || !fromAgentId || (channel === "direct" && !directRecipientId)} onClick={() => void handleSend()} label="Send message" />
</div>
<div className="mt-1 text-[9px] text-[color:var(--muted)]">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
</div>
)
}
@@ -89,7 +87,7 @@ export function TaskDiscussionComposer({ companyId, taskId, onSent }: { companyI
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
<div className="flex items-end gap-2">
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[12px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: TERRACOTTA } as React.CSSProperties} />
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
<SendButton sending={sending} disabled={!body.trim()} onClick={() => void handleSend()} label="Send task comment" />
</div>
</div>
@@ -132,8 +130,8 @@ export function ComposeModal({ open, onClose, agents, projects, companyId, onSen
<TextArea label={t("comm.message")} value={body} onChange={setBody} placeholder={t("comm.writeMessage")} rows={5} />
<Select label="Priority" value={priority} onChange={(v) => setPriority(v as MessagePriority)} options={[{ value: "low", label: "Low" }, { value: "normal", label: "Normal" }, { value: "urgent", label: "Urgent" }]} />
<div className="flex justify-end gap-3 pt-2">
<button type="button" onClick={onClose} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">{t("common.cancel")}</button>
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-[8px] px-4 py-2 text-[12px] font-semibold text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" style={{ backgroundColor: TERRACOTTA }}>
<button type="button" onClick={onClose} className="rounded-[8px] px-3 py-2 text-[13px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">{t("common.cancel")}</button>
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] font-semibold bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none">
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={14} weight="fill" />}{t("comm.sendMessage")}
</button>
</div>
@@ -1,3 +1,4 @@
import { memo } from "react"
import { motion } from "framer-motion"
import {
ChatCircle,
@@ -11,7 +12,6 @@ import type { AgentMessageRecord, AgentRecord, CommentRecord, DepartmentId } fro
import { getAgentInitials, formatMessageTime } from "../../lib/formatters"
import { DEPARTMENT_COLORS } from "@shared/constants"
const TERRACOTTA = "#d97745"
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
urgent: { label: "Urgent", color: "var(--danger)", bg: "var(--danger-soft)" },
@@ -24,7 +24,7 @@ function getDepartmentColor(department: DepartmentId | null | undefined): string
return DEPARTMENT_COLORS[department] ?? "var(--muted)"
}
export function AgentAvatar({
export const AgentAvatar = memo(function AgentAvatar({
agent,
size = "md",
}: {
@@ -43,9 +43,9 @@ export function AgentAvatar({
{initials}
</div>
)
}
})
export function MessageBubble({
export const MessageBubble = memo(function MessageBubble({
message,
agent,
isThread,
@@ -67,7 +67,7 @@ export function MessageBubble({
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={`group relative flex gap-3 rounded-[10px] px-4 py-3 transition ${
className={`group relative flex gap-3 rounded-[8px] px-4 py-3 transition ${
isThread ? "ml-11" : ""
} ${
isUnread
@@ -82,21 +82,21 @@ export function MessageBubble({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold" style={{ color: deptColor }}>
<span className="truncate text-[13px] font-semibold" style={{ color: deptColor }}>
{agent?.name ?? "Unknown Agent"}
</span>
{agent?.title ? (
<span className="text-[10px] text-[color:var(--muted)]">{agent.title}</span>
<span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span>
) : null}
{agent?.department ? (
<span
className="rounded-full px-1.5 py-0.5 text-[8px] font-bold uppercase tracking-[0.08em]"
className="shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em]"
style={{ backgroundColor: `${deptColor}15`, color: deptColor }}
>
{agent.department.replace("_", " ")}
</span>
) : null}
<span className="text-[10px] text-[color:var(--muted)]">
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">
{formatMessageTime(message.createdAt)}
</span>
{message.priority !== "normal" ? (
@@ -112,23 +112,23 @@ export function MessageBubble({
</span>
) : null}
{isUnread ? (
<Circle size={8} weight="fill" className="shrink-0" style={{ color: TERRACOTTA }} />
<Circle size={8} weight="fill" className="shrink-0" style={{ color: "var(--accent)" }} aria-label="Unread" />
) : (
<CheckCircle
size={10}
weight="fill"
className="shrink-0 text-[color:var(--muted)] opacity-0 group-hover:opacity-60"
className="shrink-0 text-[color:var(--muted)] opacity-0 group-hover:opacity-60 group-focus-within:opacity-60"
/>
)}
</div>
{message.subject ? (
<div className="mt-0.5 text-[13px] font-medium text-[color:var(--text)]">
<div className="mt-0.5 break-words text-[13px] font-medium text-[color:var(--text)]">
{message.subject}
</div>
) : null}
<div className="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
{message.body}
</div>
@@ -142,7 +142,7 @@ export function MessageBubble({
type="button"
onClick={onReply}
className="flex items-center gap-1.5 text-[11px] font-medium transition hover:opacity-80"
style={{ color: TERRACOTTA }}
style={{ color: "var(--accent)" }}
>
<ChatCircle size={12} />
{replyCount} {replyCount === 1 ? "reply" : "replies"}
@@ -151,7 +151,7 @@ export function MessageBubble({
<button
type="button"
onClick={onReply}
className="flex items-center gap-1 rounded-[5px] px-2 py-0.5 text-[10px] font-medium text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] group-hover:opacity-100"
className="flex items-center gap-1 rounded-[6px] px-2 py-0.5 text-[11px] font-medium text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] group-hover:opacity-100"
>
<ArrowBendUpLeft size={10} />
Reply
@@ -160,7 +160,7 @@ export function MessageBubble({
</div>
</motion.div>
)
}
})
export function TaskCommentBubble({
comment,
@@ -177,19 +177,19 @@ export function TaskCommentBubble({
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.18 }}
className="group relative flex gap-3 rounded-[10px] border-l-[3px] bg-[color:var(--panel)] px-4 py-3"
style={{ borderLeftColor: isBoard ? TERRACOTTA : deptColor }}
className="group relative flex gap-3 rounded-[8px] border-l-[3px] bg-[color:var(--panel)] px-4 py-3"
style={{ borderLeftColor: isBoard ? "var(--accent)" : deptColor }}
>
<AgentAvatar agent={agent} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold" style={{ color: isBoard ? TERRACOTTA : deptColor }}>
<span className="truncate text-[13px] font-semibold" style={{ color: isBoard ? "var(--accent)" : deptColor }}>
{comment.authorName}
</span>
{agent?.title ? <span className="text-[10px] text-[color:var(--muted)]">{agent.title}</span> : null}
<span className="text-[10px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span>
{agent?.title ? <span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span> : null}
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span>
</div>
<div className="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
{comment.body}
</div>
</div>
@@ -211,7 +211,7 @@ function AttachmentList({ json }: { json: string }) {
{items.map((item, i) => (
<span
key={`${item.id}-${i}`}
className="inline-flex items-center gap-1 rounded-[5px] bg-[color:var(--panel-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--muted-strong)]"
className="inline-flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] px-2 py-1 text-[11px] font-medium text-[color:var(--muted-strong)]"
>
<At size={10} className="text-[color:var(--muted)]" />
{item.title || `${item.type}:${item.id.slice(0, 8)}`}
@@ -121,7 +121,7 @@ export function MessageList({
: t("comm.noMessagesChannel")}
</div>
{(filters.search || filters.unreadOnly || filters.priority !== "all") ? (
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("comm.adjustFilters")}</div>
<div className="mt-1 text-[13px] text-[color:var(--muted)]">{t("comm.adjustFilters")}</div>
) : null}
</div>
)
@@ -10,9 +10,9 @@ import { useRovingTabIndex } from "../../lib/keyboard"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--accent)" },
department: { label: "Department", icon: Buildings, color: "#6366f1" },
company: { label: "Company", icon: Megaphone, color: "#d97745" },
project: { label: "Project", icon: Hash, color: "#10b981" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
company: { label: "Company", icon: Megaphone, color: "var(--accent)" },
project: { label: "Project", icon: Hash, color: "var(--success)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
}
@@ -152,28 +152,28 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
<div className="min-w-0 flex-1">
{/* Row 1: Agent name, channel badge, timestamp */}
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold text-[color:var(--text)]">
<span className="truncate text-[11px] font-semibold text-[color:var(--text)]">
{agent?.name ?? "Unknown Agent"}
</span>
<span
className="inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium"
className="inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium"
style={{ backgroundColor: `${config.color}18`, color: config.color }}
>
<ChannelIcon size={10} weight="bold" />
{config.label}
</span>
<span className="ml-auto shrink-0 font-mono text-[10px] text-[color:var(--muted)]">
<span className="ml-auto whitespace-nowrap shrink-0 font-mono text-[11px] text-[color:var(--muted)]">
{formatMessageTime(message.createdAt)}
</span>
</div>
{/* Row 2: Subject */}
<div className="mt-0.5 truncate text-[12px] font-semibold text-[color:var(--text)]">
<div className="mt-0.5 truncate text-[13px] font-semibold text-[color:var(--text)]">
{highlightMatch(message.subject, query)}
</div>
{/* Row 3: Body snippet */}
<div className="mt-0.5 line-clamp-2 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="mt-0.5 line-clamp-2 break-words text-[13px] leading-relaxed text-[color:var(--muted)]">
{highlightMatch(message.body, query)}
</div>
</div>
@@ -89,8 +89,8 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
</button>
<div className="flex items-center gap-2">
<StatusPill status={document.status} />
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
<button type="button" aria-label="Edit document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
@@ -77,8 +77,8 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
</button>
<div className="flex items-center gap-2">
<StatusPill status={entry.importance} />
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
<button type="button" aria-label="Edit entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
+2 -2
View File
@@ -137,7 +137,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
<div className="grid gap-4">
{submitError ? <InlineNotice message={submitError} /> : null}
{needsLogin ? (
<div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-4 text-[12px] text-[color:var(--warn)]">
<div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-4 text-[12px] text-[color:var(--warn)]">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em]">{authActionCopy.title}</div>
<div className="mt-1.5 leading-relaxed">{authActionCopy.message}</div>
<div className="mt-3">
@@ -249,7 +249,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
disabled={requiresExplicitModel && !model.trim()}
/>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-4 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-4 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Configuration doc</div>
<pre className="mono whitespace-pre-wrap">{connector.configurationDoc}</pre>
</div>
@@ -65,7 +65,7 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
) : null}
</div>
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
<div className="mt-0.5 whitespace-nowrap text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
</div>
</motion.div>
);
@@ -49,7 +49,7 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
<div
role="alert"
aria-live="assertive"
className="rounded-[10px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
>
<div className="flex items-start gap-3">
{/* Icon */}
@@ -69,11 +69,12 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
{agentsOverThreshold.map((agent) => (
<span
key={agent.id}
className={`text-[11px] font-[family-name:var(--font-mono)] ${
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
agent.pct >= 100
? "text-[color:var(--danger)]"
: "text-[color:var(--warn)]"
}`}
style={{ fontVariantNumeric: "tabular-nums" }}
>
{agent.name} ({agent.pct}%)
</span>
+45 -16
View File
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { motion } from "framer-motion";
import {
ArrowRight,
Briefcase,
@@ -28,6 +29,7 @@ import { RecoveryBanner } from "./RecoveryBanner";
import { BudgetAlertBanner } from "./BudgetAlertBanner";
export interface DashboardProps {
companyName?: string;
agents: AgentRecord[];
tasks: TaskRecord[];
runs: RunRecord[];
@@ -42,7 +44,10 @@ export interface DashboardProps {
export function Dashboard(props: DashboardProps) {
const t = useT();
const { agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
const { companyName, agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
const taskMap = useMemo(() => new Map(tasks.map((t) => [t.id, t])), [tasks]);
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
@@ -59,15 +64,35 @@ export function Dashboard(props: DashboardProps) {
return runs
.filter((r) => r.status === "running" || r.status === "queued")
.map((run) => {
const agent = agents.find((a) => a.id === run.agentId);
const task = tasks.find((t) => t.id === run.taskId);
const agent = agentMap.get(run.agentId);
const task = taskMap.get(run.taskId);
return { run, agent, task };
})
.slice(0, 8);
}, [runs, agents, tasks]);
}, [runs, agentMap, taskMap]);
return (
<div className="space-y-8">
<div className="space-y-6">
{/* Company status line */}
{companyName ? (
<div>
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{companyName}</h2>
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-[color:var(--muted)]">
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
</span>
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
{activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
</span>
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
<span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
</div>
</div>
) : null}
{/* Conditional banners */}
<RecoveryBanner />
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} />
@@ -109,15 +134,19 @@ export function Dashboard(props: DashboardProps) {
{/* Needs Attention — moved to top, only shows when items exist */}
{hasAttentionItems ? (
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--warn)]" />
{t("dash.needsAttentionSection")}
</div>
<div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
{pendingApprovals.slice(0, 5).map((approval, i) => {
const requester = approval.requestedByAgentId ? agentMap.get(approval.requestedByAgentId) : undefined;
return (
<button
<motion.button
key={approval.id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
type="button"
onClick={() => onNavigate("approvals", approval.id)}
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
@@ -131,11 +160,11 @@ export function Dashboard(props: DashboardProps) {
</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
</motion.button>
);
})}
{failedRuns.slice(0, 3).map((run) => {
const agent = agents.find((a) => a.id === run.agentId);
const agent = agentMap.get(run.agentId);
return (
<button
key={run.id}
@@ -155,7 +184,7 @@ export function Dashboard(props: DashboardProps) {
);
})}
{urgentMessages.slice(0, 3).map((msg) => {
const sender = agents.find((a) => a.id === msg.fromAgentId);
const sender = agentMap.get(msg.fromAgentId);
return (
<button
key={msg.id}
@@ -189,7 +218,7 @@ export function Dashboard(props: DashboardProps) {
</button>
</div>
{activeWork.length > 0 ? (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
{activeWork.map(({ run, agent, task }) => (
<button
key={run.id}
@@ -201,7 +230,7 @@ export function Dashboard(props: DashboardProps) {
<Robot size={16} className="text-[color:var(--success)]" />
{run.status === "running" ? (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
</span>
) : null}
@@ -218,7 +247,7 @@ export function Dashboard(props: DashboardProps) {
))}
</div>
) : (
<div className="rounded-[10px] border border-dashed border-[color:var(--line)] py-8 text-center">
<div className="rounded-[8px] border border-dashed border-[color:var(--line)] py-8 text-center">
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div>
</div>
@@ -232,7 +261,7 @@ export function Dashboard(props: DashboardProps) {
{t("dash.recentActivity")}
</div>
</div>
<div className="max-h-[360px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
<div className="max-h-[360px] overflow-y-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
<ActivityFeed activity={activity} agents={agents} limit={10} />
</div>
</section>
@@ -19,12 +19,12 @@ const DEPARTMENT_INFO: Record<string, { label: string; icon: React.ElementType;
engineering: { label: "Engineering", icon: Cpu, color: "#6366f1" },
product: { label: "Product", icon: Rocket, color: "#8b5cf6" },
design: { label: "Design", icon: Target, color: "#ec4899" },
marketing: { label: "Marketing", icon: TrendUp, color: "#f59e0b" },
marketing: { label: "Marketing", icon: TrendUp, color: "#0ea5e9" },
sales: { label: "Sales", icon: ChartLineUp, color: "#10b981" },
hr: { label: "People", icon: Users, color: "#06b6d4" },
finance: { label: "Finance", icon: CurrencyCircleDollar, color: "#14b8a6" },
legal: { label: "Legal", icon: ShieldCheck, color: "#64748b" },
operations: { label: "Operations", icon: Lightning, color: "#f97316" },
operations: { label: "Operations", icon: Lightning, color: "#475569" },
customer_support: { label: "Support", icon: Heartbeat, color: "#3b82f6" },
research: { label: "Research", icon: Target, color: "#a855f7" },
};
@@ -88,12 +88,12 @@ export function DepartmentOverview({
key={deptId}
type="button"
onClick={() => onNavigate("agents")}
className="focus-ring group relative overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
className="focus-ring group relative overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className="absolute left-0 top-0 h-full w-[3px]" style={{ backgroundColor: info.color }} />
<div className="flex items-center gap-2 pl-1">
<Icon size={14} style={{ color: info.color }} />
<span className="text-[11px] font-semibold text-[color:var(--text)]">{info.label}</span>
<Icon size={15} weight="fill" style={{ color: info.color }} />
<span className="truncate text-[11px] font-medium text-[color:var(--text)]">{info.label}</span>
{runningAgents > 0 && (
<span className="ml-auto flex items-center gap-1">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
@@ -103,18 +103,18 @@ export function DepartmentOverview({
</div>
<div className="mt-2 grid grid-cols-3 gap-1 pl-1">
<div>
<div className="text-[14px] font-bold text-[color:var(--text)]">{data.agents.length}</div>
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">People</div>
<div className="text-[16px] font-bold tabular-nums text-[color:var(--text)]">{data.agents.length}</div>
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">People</div>
</div>
<div>
<div className="text-[14px] font-bold text-[color:var(--accent)]">{activeTasks}</div>
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">Active</div>
<div className="text-[16px] font-bold tabular-nums text-[color:var(--accent)]">{activeTasks}</div>
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Active</div>
</div>
<div>
<div className={`text-[14px] font-bold ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
<div className={`text-[16px] font-bold tabular-nums ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
{blockedTasks > 0 ? blockedTasks : doneTasks}
</div>
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{blockedTasks > 0 ? "Blocked" : "Done"}
</div>
</div>
@@ -1,5 +1,4 @@
import { useMemo } from "react";
import { motion } from "framer-motion";
import { Target } from "@phosphor-icons/react";
import type { GoalRecord, SectionId, TaskRecord } from "@shared/types";
@@ -21,24 +20,22 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
key={goal.id}
type="button"
onClick={() => onNavigate("goals")}
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)]"
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target size={14} className="text-[color:var(--accent)]" />
<span className="text-[12px] font-medium text-[color:var(--text)]">{goal.title}</span>
<Target size={15} weight="fill" className="text-[color:var(--accent)]" />
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span>
</div>
<span className="text-[11px] font-bold text-[color:var(--accent)]">{pct}%</span>
<span className="whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
</div>
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<motion.div
className="h-full rounded-full bg-[color:var(--accent)]"
initial={{ width: 0 }}
animate={{ width: `${pct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<div
className="h-full rounded-full bg-[color:var(--accent)] transition-all duration-700 ease-out"
style={{ width: `${pct}%` }}
/>
</div>
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{done}/{total} tasks complete</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>{done}/{total} tasks complete</div>
</button>
);
})}
@@ -44,7 +44,7 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
if (tasks.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
@@ -57,11 +57,11 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
{KANBAN_COLUMNS.map((col) => {
const colTasks = tasksByStatus[col.status] ?? [];
return (
<div key={col.status} className="flex w-[220px] shrink-0 flex-col">
<div key={col.status} className="flex w-[220px] min-w-[180px] shrink-0 flex-col">
<div className="mb-2 flex items-center gap-2 px-1">
<span className={`h-2 w-2 rounded-full ${col.dotColor}`} />
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[10px] font-bold text-[color:var(--muted)]">
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-bold tabular-nums text-[color:var(--muted-strong)]">
{colTasks.length}
</span>
</div>
@@ -75,9 +75,9 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
key={task.id}
type="button"
onClick={() => onSelectTask(task.id)}
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
>
<div className="text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
<div className="truncate text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
<div className="mt-1.5 flex items-center gap-2">
{assignee ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
@@ -88,8 +88,8 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
)}
{isRunning ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--success)]">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-[color:var(--success)]">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" aria-hidden="true" />
Running
</span>
) : null}
@@ -30,19 +30,27 @@ export function MetricCard({
accent: "bg-[color:var(--accent-soft)]",
};
const borderMap = {
default: "border-l-transparent",
success: "border-l-[color:var(--success)]",
warn: "border-l-[color:var(--warn)]",
danger: "border-l-[color:var(--danger)]",
accent: "border-l-[color:var(--accent)]",
};
return (
<button
type="button"
onClick={onClick}
className="focus-ring flex items-center gap-3.5 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
className={`focus-ring flex items-center gap-4 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${borderMap[tone]}`}
>
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
<Icon size={20} weight="fill" className={toneMap[tone]} />
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
<Icon size={24} weight="fill" className={toneMap[tone]} />
</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[20px] font-bold tracking-[-0.02em] leading-tight ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{subtitle}</div> : null}
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[28px] font-bold tracking-[-0.02em] leading-none ${toneMap[tone]}`} style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-[color:var(--muted)]">{subtitle}</div> : null}
</div>
</button>
);
@@ -121,7 +121,7 @@ export function NeedsAttention({
<Cpu size={18} className="shrink-0 text-[color:var(--warn)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{action.resultSummary || action.actionType}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
@@ -136,7 +136,7 @@ export function NeedsAttention({
<Lightning size={18} className="shrink-0 text-[color:var(--warn)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{workflow.name}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
@@ -20,17 +20,17 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
<button
type="button"
onClick={onClick}
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3.5 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
>
<div className="relative">
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[14px] font-bold ${
<div className={`flex h-12 w-12 items-center justify-center rounded-full text-[15px] font-bold ${
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{initials}
</div>
{agent.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
</span>
) : (
@@ -61,7 +61,7 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
if (agents.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-10 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-10 text-center">
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
</div>
@@ -41,7 +41,7 @@ export function RecoveryBanner() {
const handleDismissAll = () => setDismissed(true);
return (
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[10px] p-4 mb-4">
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[8px] px-4 py-4 mb-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 text-[color:var(--warn)] font-medium">
<Warning size={18} weight="bold" />
@@ -53,10 +53,10 @@ export function RecoveryBanner() {
</div>
<div className="space-y-2">
{runs.map(run => (
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded px-3 py-2 text-sm">
<div>
<span className="text-[color:var(--text)]">{run.taskTitle}</span>
<span className="text-[color:var(--muted)] ml-2">({run.agentName})</span>
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded-[4px] px-3 py-2 text-[13px]">
<div className="min-w-0 flex-1">
<span className="truncate text-[color:var(--text)]">{run.taskTitle}</span>
<span className="whitespace-nowrap text-[color:var(--muted)] ml-2">({run.agentName})</span>
</div>
<div className="flex gap-2">
<button
@@ -30,11 +30,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
}, [tasks]);
return (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CalendarCheck size={16} className="text-[color:var(--accent)]" />
<span className="text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
<span className="truncate text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
<StatusPill status={sprint.status} />
</div>
<div className="flex items-center gap-1 text-[11px] text-[color:var(--muted)]">
@@ -53,11 +53,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
</div>
<div className="mt-2 flex items-center justify-between text-[10px] text-[color:var(--muted)]">
<span>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
<span style={{ fontVariantNumeric: "tabular-nums" }}>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
<div className="flex items-center gap-3">
<span className="text-[color:var(--success)]">{sprintTasks.done} done</span>
<span className="text-[color:var(--accent)]">{sprintTasks.active} active</span>
<span>{sprintTasks.total} total</span>
<span className="whitespace-nowrap text-[color:var(--success)]">{sprintTasks.done} done</span>
<span className="whitespace-nowrap text-[color:var(--accent)]">{sprintTasks.active} active</span>
<span className="whitespace-nowrap">{sprintTasks.total} total</span>
</div>
</div>
@@ -23,7 +23,7 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
{stages.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-1.5">
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
<span className={`text-[18px] font-bold ${stage.tone}`}>{stage.count}</span>
<span className={`text-[18px] font-bold ${stage.tone}`} style={{ fontVariantNumeric: "tabular-nums" }}>{stage.count}</span>
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
</div>
{i < stages.length - 1 ? (
@@ -38,7 +38,7 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
overscan: 15,
})
if (activity.length === 0) return <EmptyState title={t("exec.noActivityYet")} />
if (activity.length === 0) return <EmptyState title={t("exec.noActivityYet")} detail={t("exec.noActivityDetail")} />
return (
<div className="space-y-3">
<div className="flex items-center gap-3">
@@ -81,19 +81,19 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<EntityIcon size={14} weight="duotone" className="text-[color:var(--muted)]" />
<div className="text-[14px] text-[color:var(--text)]">{entry.action}</div>
<div className="flex min-w-0 items-center gap-2">
<EntityIcon size={14} weight="duotone" className="shrink-0 text-[color:var(--muted)]" />
<div className="truncate text-[13px] text-[color:var(--text)]">{entry.action}</div>
</div>
<div className="mono text-[11px] text-[color:var(--muted)]">{formatTime(entry.createdAt)}</div>
</div>
<div className="flex items-center gap-2 text-[13px] text-[color:var(--muted)]">
<span className="font-medium text-[color:var(--muted-strong)]">{entry.actor}</span>
<span>·</span>
<span>{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
<span className="truncate">{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
</div>
</div>
)
@@ -7,6 +7,7 @@ import { unwrap } from "../../lib/desktop"
import { ActionButton, ConfirmDialog, InlineNotice, StatusPill, TextArea } from "../ui"
import { describeActionError, parseApprovalPayloadJson } from "./execution-utils"
import { SocialDraftPreview } from "./SocialDraftPreview"
import { useAppStore } from "../../state/store"
function getTypeBadgeClass(type: string): string {
if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
@@ -21,6 +22,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>; onDeleted: () => Promise<void>
}) {
const t = useT()
const addToast = useAppStore((s) => s.addToast)
const [decisionNote, setDecisionNote] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false)
const [actionError, setActionError] = useState<string | null>(null); const [acting, setActing] = useState<"approved"|"rejected"|"revision_requested"|"delete"|null>(null)
const [scheduleMode, setScheduleMode] = useState<"immediate" | "scheduled">("immediate")
@@ -59,6 +61,9 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo
}
await onDecide(state, noteForDecision)
if (state === "approved") addToast("Approval granted — agent is moving forward", "success")
else if (state === "rejected") addToast("Approval rejected", "warn")
else if (state === "revision_requested") addToast("Revision requested — agent will revise and resubmit", "warn")
} catch (e) {
setActionError(describeActionError(e))
} finally {
@@ -80,15 +85,15 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
})()
return (
<div className="space-y-5">
<div className="space-y-6">
<ConfirmDialog open={confirmDelete} title={t("exec.deleteApproval")} message={t("exec.deleteApprovalConfirm")} errorMessage={actionError} onConfirm={() => void handleDelete()} onCancel={() => { setActionError(null); setConfirmDelete(false) }} />
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
<div className="border-y border-[color:var(--line)] py-4 space-y-4">
<div className="flex items-center gap-3">
<span className={`rounded-full px-3 py-1 text-[11px] font-bold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
<StatusPill status={approval.state} />
</div>
<div className="text-[14px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
{approval.impactSummary ? <div className="text-[13px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null}
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-[color:var(--muted)]"><Robot size={14} />Requested by: <span className="font-medium text-[color:var(--text)]">{requestingAgent?.name ?? "Unknown"}</span>{requestingAgent?.role ? <span className="text-[10px] text-[color:var(--muted)]">({requestingAgent.role})</span> : null}</div>
@@ -97,7 +102,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
{approval.updatedAt !== approval.createdAt ? <div className="text-[color:var(--muted)]">Updated: <span className="mono text-[color:var(--text)]">{formatTime(approval.updatedAt)}</span></div> : null}
</div>
{approval.type === "hire_agent" ? (() => { const payload = parseApprovalPayloadJson(approval); const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null; const reqSnap = payload?.requestedConfigurationSnapshot && typeof payload.requestedConfigurationSnapshot === "object" && !Array.isArray(payload.requestedConfigurationSnapshot) ? payload.requestedConfigurationSnapshot as Record<string, unknown> : null; const proposedAgent = (approval.relatedAgentId ? (agents.find((a) => a.id === approval.relatedAgentId) ?? null) : null) ?? (payloadAgentId ? (agents.find((a) => a.id === payloadAgentId) ?? null) : null); const mgrId = proposedAgent?.reportsTo ?? (typeof reqSnap?.reportsTo === "string" ? reqSnap.reportsTo : null); const mgr = mgrId ? (agents.find((a) => a.id === mgrId) ?? null) : null; if (!proposedAgent && !reqSnap) return null; return (
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--accent)]">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent)]">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
<div><span className="text-[color:var(--muted)]">Name:</span> <span className="font-medium text-[color:var(--text)]">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
<div><span className="text-[color:var(--muted)]">Role:</span> <span className="text-[color:var(--text)]">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-[color:var(--muted)]">Title:</span> <span className="text-[color:var(--text)]">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
@@ -106,7 +111,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
<div><span className="text-[color:var(--muted)]">Budget:</span> <span className="mono text-[color:var(--text)]">{formatMoney(proposedAgent?.budgetMonthlyUsd ?? (typeof reqSnap?.budgetMonthlyUsd === "number" ? reqSnap.budgetMonthlyUsd : 0))}/mo</span></div>
{mgr ? <div><span className="text-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{mgr.name}</span></div> : null}
</div></div>) })() : null}
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
{isSocialPost && approval.payloadJson ? (
<SocialDraftPreview
payloadJson={approval.payloadJson}
@@ -129,7 +134,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
onClick={() => { setScheduleMode("immediate"); setScheduleError(null) }}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { setScheduleMode("immediate"); setScheduleError(null) } }}
>
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" /> : null}
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-[color:var(--text)]">Post immediately</span>
</label>
@@ -142,7 +147,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
onClick={() => setScheduleMode("scheduled")}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setScheduleMode("scheduled") }}
>
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" /> : null}
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-[color:var(--text)]">Schedule for later</span>
</label>
@@ -163,7 +168,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
</div>
) : null}
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold text-[color:var(--muted)]">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
<div className="flex flex-wrap gap-3">
{!isDecided ? <>
<ActionButton
@@ -17,13 +17,13 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
overscan: 10,
})
if (approvals.length === 0) return <EmptyState title={t("exec.noApprovals")} />
if (approvals.length === 0) return <EmptyState title={t("exec.noApprovals")} detail={t("exec.noApprovalsDetail")} />
return (
<div
ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]"
role="listbox"
aria-label={t("exec.noApprovals")}
aria-label="Pending approvals"
onKeyDown={handleKeyDown}
>
<div
@@ -53,13 +53,13 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
>
<button
className={`relative w-full border-b border-[color:var(--line)] px-5 py-4 text-left transition last:border-b-0 ${selectedApprovalId === approval.id ? "bg-transparent" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedApprovalId === approval.id ? "bg-[color:var(--warn-soft)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
onClick={() => onSelect(approval.id)}
tabIndex={-1}
>
{selectedApprovalId === approval.id ? <span className="absolute inset-y-3 left-0 w-0.5 rounded-full bg-[color:var(--warn)]" /> : null}
{selectedApprovalId === approval.id ? <span className="absolute inset-y-0 left-0 w-[3px] bg-[color:var(--warn)]" /> : null}
<div className="flex items-center justify-between gap-4">
<div className={`text-[15px] text-[color:var(--text)] ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
<StatusPill status={approval.state} />
</div>
</button>
+10 -10
View File
@@ -28,7 +28,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
overscan: 10,
})
if (costs.length === 0) return <EmptyState title={t("exec.noCostData")} />
if (costs.length === 0) return <EmptyState title={t("exec.noCostData")} detail={t("exec.noCostDataDetail")} />
return (
<div className="space-y-3">
<div className="flex items-center gap-3">
@@ -66,11 +66,11 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
>
<div className="flex items-center justify-between">
<div className="text-[14px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div>
<div className="mono text-[14px] font-semibold text-[color:var(--text)]">{formatMoney(total)}</div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div>
<div className="mono tabular-nums text-[13px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{formatMoney(total)}</div>
</div>
</div>
)
@@ -91,15 +91,15 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
>
<div className="flex items-center justify-between">
<div>
<div className="text-[14px] text-[color:var(--text)]">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
<div className="text-[12px] text-[color:var(--muted)]">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] text-[color:var(--text)]">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
<div className="text-[11px] text-[color:var(--muted)]">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
</div>
<div className="text-right">
<div className="mono text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div>
<div className="shrink-0 text-right">
<div className="mono tabular-nums text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div>
{entry.unattributed ? <div className="text-[11px] text-[color:var(--warn)]">unattributed</div> : null}
</div>
</div>
@@ -6,18 +6,18 @@ import { EmptyState, ListFrame } from "../ui"
export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentRecord[] }) {
const t = useT()
const reviewTasks = tasks.filter((tk) => tk.status === "in_review")
if (reviewTasks.length === 0) return <EmptyState title={t("exec.noReviewsInProgress")} />
if (reviewTasks.length === 0) return <EmptyState title={t("exec.noReviewsInProgress")} detail={t("exec.noReviewsDetail")} />
return (
<ListFrame>
{reviewTasks.map((task) => {
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null
const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
return (
<div key={task.id} className="flex items-start gap-3 border-b border-[color:var(--line)] px-5 py-4 last:border-b-0">
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-blue-500" />
<div key={task.id} className="flex items-start gap-3 border-b border-[color:var(--line)] px-4 py-3 last:border-b-0">
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<div className="min-w-0 flex-1">
<div className="text-[13px] font-medium text-[color:var(--text)] truncate">{task.title}</div>
<div className="mt-0.5 text-[12px] text-[color:var(--muted)]">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> &rarr; reviewed by <span className="text-[color:var(--muted-strong)]">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{task.title}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> &rarr; reviewed by <span className="text-[color:var(--muted-strong)]">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
</div>
</div>
)
@@ -8,38 +8,39 @@ import { formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
import { ActionButton, ConfirmDialog, EmptyState, InlineNotice, Select } from "../ui"
import { describeActionError } from "./execution-utils"
import { useAppStore } from "../../state/store"
export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapshot: ProfileSnapshot; secrets: SecretRecord[]; companyId: string; onSaved: () => Promise<void> }) {
const t = useT(); const currentLocale = useLocale()
const t = useT(); const currentLocale = useLocale(); const addToast = useAppStore((s) => s.addToast)
const [theme, setTheme] = useState(snapshot.theme); const [selectedLocale, setSelectedLocale] = useState<AppLocale>(snapshot.locale ?? currentLocale); const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled)
const [confirmDeleteSecretId, setConfirmDeleteSecretId] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null); const [actionNotice, setActionNotice] = useState<string | null>(null)
const [deleteSecretError, setDeleteSecretError] = useState<string | null>(null); const [loadingAction, setLoadingAction] = useState<"save"|"check"|"backup"|"restore"|null>(null)
const secretToDelete = secrets.find((s) => s.id === confirmDeleteSecretId) ?? null
useEffect(() => { setTheme(snapshot.theme); setUpdatesEnabled(snapshot.updatesEnabled); if (snapshot.locale) setSelectedLocale(snapshot.locale) }, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale])
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice("Secret deleted."); await onSaved() } catch (e) { setDeleteSecretError(describeActionError(e)) } }
async function handleSaveSettings() { setActionError(null); setActionNotice(null); setLoadingAction("save"); try { unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled })); await onSaved(); setActionNotice("Settings saved.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice("Secret deleted."); addToast("Secret deleted", "success"); await onSaved() } catch (e) { setDeleteSecretError(describeActionError(e)) } }
async function handleSaveSettings() { setActionError(null); setActionNotice(null); setLoadingAction("save"); try { unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled })); await onSaved(); setActionNotice("Settings saved."); addToast("Settings saved", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleCheckForUpdates() { setActionError(null); setActionNotice(null); setLoadingAction("check"); try { const started = unwrap(await window.agentCompany.checkForUpdates()); setActionNotice(started ? "Update check started." : "Update check skipped because no release update feed is configured.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleBackupProfile() { setActionError(null); setActionNotice(null); setLoadingAction("backup"); try { const backupPath = unwrap(await window.agentCompany.backupProfile()); if (backupPath) setActionNotice(`Profile backup created at ${backupPath}.`) } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleRestoreProfile() { setActionError(null); setActionNotice(null); setLoadingAction("restore"); try { const restored = unwrap(await window.agentCompany.restoreProfile()); if (!restored) return; await onSaved(); setActionNotice("Profile restored.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleBackupProfile() { setActionError(null); setActionNotice(null); setLoadingAction("backup"); try { const backupPath = unwrap(await window.agentCompany.backupProfile()); if (backupPath) { setActionNotice(`Profile backup created at ${backupPath}.`); addToast("Profile backup created", "success") } } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
async function handleRestoreProfile() { setActionError(null); setActionNotice(null); setLoadingAction("restore"); try { const restored = unwrap(await window.agentCompany.restoreProfile()); if (!restored) return; await onSaved(); setActionNotice("Profile restored."); addToast("Profile restored", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
return (
<div className="space-y-6">
<ConfirmDialog open={confirmDeleteSecretId !== null} title="Delete secret" message={`Permanently delete secret "${secretToDelete?.name ?? ""}"? Any connectors referencing it will lose access.`} errorMessage={deleteSecretError} onConfirm={() => confirmDeleteSecretId && void handleDeleteSecret(confirmDeleteSecretId)} onCancel={() => { setDeleteSecretError(null); setConfirmDeleteSecretId(null) }} />
{actionError ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.profile")}</div><div className="border-y border-[color:var(--line)] py-4"><div className="mb-2 text-[12px] text-[color:var(--muted)]">{t("exec.profilePath")}</div><div className="mono text-[12px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div></div></div>
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(v) => setTheme(v as ProfileSnapshot["theme"])} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.profile")}</div><div className="border-y border-[color:var(--line)] py-4"><div className="mb-2 text-[11px] text-[color:var(--muted)]">{t("exec.profilePath")}</div><div className="mono truncate text-[11px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.dataset.theme = next; root.style.colorScheme = next === "system" ? "light dark" : next; void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved()) }} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div className="mt-3"><Select label={t("settings.language")} value={selectedLocale} onChange={(v) => setSelectedLocale(v as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} /></div>
<div className="border-y border-[color:var(--line)] py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-[color:var(--muted-strong)]"><span>{t("exec.enableUpdater")}</span><input type="checkbox" checked={updatesEnabled} onChange={(e) => setUpdatesEnabled(e.target.checked)} /></label></div>
</div>
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; return (
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.autoApproveDesc")}</div></div></label></div></div>) })()}
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
</div>
)
}
@@ -7,6 +7,7 @@ import { unwrap } from "../../lib/desktop"
import { ActionButton, ConfirmDialog, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
import { describeActionError, PriorityDot } from "./execution-utils"
import { TaskComments } from "./TaskComments"
import { useAppStore } from "../../state/store"
export function TaskInspector({ task, tasks, runs, agents, goals, projects, workspaces, companyId, onRun, onCancelRun, onCreateApproval, onOpenDiscussion, comments, onRefresh, onDeleted }: {
task: TaskRecord; tasks: TaskRecord[]; runs: RunRecord[]; agents: ProfileSnapshot["agents"]; goals: GoalRecord[]; projects: ProjectRecord[]; workspaces: WorkspaceRecord[]; companyId: string
@@ -14,32 +15,34 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
comments: CommentRecord[]; onRefresh: () => Promise<void>; onDeleted: () => Promise<void>
}) {
const t = useT()
const addToast = useAppStore((s) => s.addToast)
const [editing, setEditing] = useState(false); const [editTitle, setEditTitle] = useState(task.title); const [editDescription, setEditDescription] = useState(task.description)
const [editPriority, setEditPriority] = useState(task.priority); const [editStatus, setEditStatus] = useState<TaskStatus>(task.status); const [editAssignee, setEditAssignee] = useState(task.assigneeAgentId ?? "")
const [editWorkspace, setEditWorkspace] = useState(task.workspaceId ?? ""); const [editProject, setEditProject] = useState(task.projectId ?? ""); const [editGoal, setEditGoal] = useState(task.goalId ?? "")
const [confirmDelete, setConfirmDelete] = useState(false); const [actionError, setActionError] = useState<string | null>(null); const [actionNotice, setActionNotice] = useState<string | null>(null)
const [deleteError, setDeleteError] = useState<string | null>(null); const [savingChanges, setSavingChanges] = useState(false); const [updatingStatus, setUpdatingStatus] = useState<TaskStatus | null>(null); const [creatingApproval, setCreatingApproval] = useState(false)
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null; const project = projects.find((e) => e.id === task.projectId) ?? null
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
const assignee = agentMap.get(task.assigneeAgentId ?? "") ?? null; const project = projects.find((e) => e.id === task.projectId) ?? null
const goal = goals.find((e) => e.id === task.goalId) ?? null; const workspace = workspaces.find((e) => e.id === task.workspaceId) ?? null
const activeRun = runs.find((r) => r.id === task.activeRunId) ?? null; const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
const activeRun = runs.find((r) => r.id === task.activeRunId) ?? null; const reviewer = assignee?.reportsTo ? (agentMap.get(assignee.reportsTo) ?? null) : null
const needsGovernance = task.status === "blocked" || (task.status === "in_review" && !reviewer)
const budgetWarning = assignee && assignee.budgetMonthlyUsd > 0 ? assignee.spentMonthlyUsd / assignee.budgetMonthlyUsd : 0
function startEdit() { setActionError(null); setActionNotice(null); setEditTitle(task.title); setEditDescription(task.description); setEditPriority(task.priority); setEditStatus(task.status); setEditAssignee(task.assigneeAgentId ?? ""); setEditWorkspace(task.workspaceId ?? ""); setEditProject(task.projectId ?? ""); setEditGoal(task.goalId ?? ""); setEditing(true) }
async function saveChanges() { setActionError(null); setActionNotice(null); setSavingChanges(true); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: editTitle, description: editDescription, priority: editPriority as TaskRecord["priority"], status: editStatus, assigneeAgentId: editAssignee || null, workspaceId: editWorkspace || null, projectId: editProject || null, goalId: editGoal || null })); await onRefresh(); setEditing(false); setActionNotice("Task changes saved.") } catch (e) { setActionError(describeActionError(e)) } finally { setSavingChanges(false) } }
async function saveChanges() { setActionError(null); setActionNotice(null); setSavingChanges(true); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: editTitle, description: editDescription, priority: editPriority as TaskRecord["priority"], status: editStatus, assigneeAgentId: editAssignee || null, workspaceId: editWorkspace || null, projectId: editProject || null, goalId: editGoal || null })); await onRefresh(); setEditing(false); setActionNotice("Task changes saved."); addToast("Task changes saved", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setSavingChanges(false) } }
async function quickStatus(status: TaskStatus) { setActionError(null); setActionNotice(null); setUpdatingStatus(status); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: task.title, description: task.description, priority: task.priority, status, assigneeAgentId: task.assigneeAgentId, workspaceId: task.workspaceId, projectId: task.projectId, goalId: task.goalId })); await onRefresh(); setActionNotice(`Task moved to ${status.replaceAll("_", " ")}.`) } catch (e) { setActionError(describeActionError(e)) } finally { setUpdatingStatus(null) } }
async function handleCreateApproval() { setActionError(null); setActionNotice(null); setCreatingApproval(true); try { await onCreateApproval(); setActionNotice(task.status === "blocked" ? "Approval created and routed to leadership." : "Approval created.") } catch (e) { setActionError(describeActionError(e)) } finally { setCreatingApproval(false) } }
async function handleDelete() { setDeleteError(null); try { unwrap(await window.agentCompany.deleteTask({ id: task.id, companyId })); setConfirmDelete(false); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
async function handleCreateApproval() { setActionError(null); setActionNotice(null); setCreatingApproval(true); try { await onCreateApproval(); setActionNotice(task.status === "blocked" ? "Approval created and routed to leadership." : "Approval created."); addToast("Approval created and routed to leadership", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setCreatingApproval(false) } }
async function handleDelete() { setDeleteError(null); try { unwrap(await window.agentCompany.deleteTask({ id: task.id, companyId })); setConfirmDelete(false); addToast("Task deleted", "success"); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
return (
<div className="space-y-5">
<div className="space-y-6">
<ConfirmDialog open={confirmDelete} title={t("exec.deleteTask")} message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`} errorMessage={deleteError} onConfirm={() => void handleDelete()} onCancel={() => { setDeleteError(null); setConfirmDelete(false) }} />
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-[8px] border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"}`}><Warning size={16} weight="fill" />{assignee.name} has used {Math.round(budgetWarning * 100)}% of their monthly budget ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
{actionError ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
{editing ? (
<div className="grid gap-3 border-y border-[color:var(--line)] py-4 md:grid-cols-2">
<div className="grid gap-3 md:grid-cols-2">
<div className="md:col-span-2"><Input label={t("exec.title")} value={editTitle} onChange={setEditTitle} placeholder={t("exec.taskTitle")} /></div>
<div className="md:col-span-2"><TextArea label={t("exec.description")} value={editDescription} onChange={setEditDescription} placeholder={t("exec.description")} rows={4} /></div>
<Select label={t("exec.priority")} value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical","high","medium","low"].map(v => ({ value: v, label: v }))} />
@@ -52,18 +55,18 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
</div>
) : (
<>
<div className="grid gap-x-6 gap-y-1 text-[13px] md:grid-cols-2">
<div className="grid gap-x-6 gap-y-2 text-[13px] md:grid-cols-2">
{assignee && <div className="text-[color:var(--muted)]">Assignee: <span className="text-[color:var(--text)]">{assignee.name}</span></div>}
{project && <div className="text-[color:var(--muted)]">Project: <span className="text-[color:var(--text)]">{project.name}</span></div>}
{goal && <div className="text-[color:var(--muted)]">Goal: <span className="text-[color:var(--text)]">{goal.title}</span></div>}
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-[color:var(--muted)]">Parent: <span className="text-[color:var(--text)]">{parent.title}</span></div> : null })()}
{workspace && <div className="text-[color:var(--muted)]">Workspace: <span className="text-[color:var(--text)]">{workspace.name}</span></div>}
{task.costUsd > 0 && <div className="text-[color:var(--muted)]">Cost: <span className="mono text-[color:var(--text)]">{formatMoney(task.costUsd)}</span></div>}
{task.costUsd > 0 && <div className="text-[color:var(--muted)]">Cost: <span className="mono tabular-nums text-[color:var(--text)]">{formatMoney(task.costUsd)}</span></div>}
</div>
{task.description ? <div className="text-[13px] leading-relaxed text-[color:var(--muted-strong)] whitespace-pre-wrap">{task.description}</div> : null}
{task.status === "in_review" && reviewer ? <div className="rounded-[10px] border border-blue-200 bg-blue-50 px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-blue-700"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-blue-600/80">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
: needsGovernance ? <div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
<div className="flex items-center gap-2 pt-1">
{task.status === "in_review" && reviewer ? <div className="rounded-[8px] border border-[color:var(--accent-soft)] bg-[color:var(--accent-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--accent-text)]"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--accent-text)]">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
: needsGovernance ? <div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
<div className="flex items-center gap-3">
{activeRun ? <ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" /> : <>
{["todo","in_progress","blocked"].includes(task.status) && <ActionButton label={t("exec.run")} onClick={() => void onRun(task.id)} icon={Play} tone="accent" />}
{task.status === "in_review" && <ActionButton label={t("exec.done")} onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />}
@@ -79,17 +82,17 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
<div className="space-y-2">
<div className="flex items-center gap-2"><ListChecks size={14} className="text-[color:var(--muted)]" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-[color:var(--success)]">{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done</span></div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]"><div className="h-full rounded-full bg-[color:var(--success)] transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agents.find((a) => a.id === sub.assigneeAgentId); return (
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agentMap.get(sub.assigneeAgentId ?? ""); return (
<div key={sub.id} className="flex items-center gap-2 rounded-[6px] px-3 py-2 text-[12px] bg-[color:var(--panel-soft)]">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${sub.status === "done" ? "bg-[color:var(--success)]" : sub.status === "in_progress" ? "bg-[color:var(--accent)]" : sub.status === "blocked" ? "bg-[color:var(--danger)]" : sub.status === "in_review" ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)]"}`} />
<span className={`flex-1 truncate ${sub.status === "done" ? "line-through text-[color:var(--muted)]" : "text-[color:var(--text)]"}`}>{sub.title}</span>
{subAssignee ? <span className="shrink-0 text-[10px] text-[color:var(--muted)]">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
</div>) })}</div></div>) })()}
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} />
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agents.find((a) => a.id === run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
<div key={run.id} className={`rounded-[8px] border px-3 py-2.5 ${isActive ? "border-[color:var(--success)] bg-[color:var(--success-soft)]" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--danger)] border-opacity-30 bg-[color:var(--danger-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)]"}`}>
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="mono">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
{run.errorMessage ? <div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)]">{run.errorMessage}</div> : null}
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
{run.errorMessage ? <div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)] break-words">{run.errorMessage}</div> : null}
</div>) })}{runs.length > 8 && <div className="text-[11px] text-[color:var(--muted)] text-center">+{runs.length - 8} more runs</div>}</div></div>}
</div>
)
@@ -18,13 +18,13 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
overscan: 10,
})
if (tasks.length === 0) return <EmptyState title={t("exec.taskQueueEmpty")} />
if (tasks.length === 0) return <EmptyState title={t("exec.taskQueueEmpty")} detail={t("exec.taskQueueEmptyDetail")} />
return (
<div
ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]"
role="listbox"
aria-label={t("exec.taskQueueEmpty")}
aria-label="Task queue"
onKeyDown={handleKeyDown}
>
<div
@@ -55,7 +55,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
>
<button
className={`relative w-full border-b border-[color:var(--line)] px-5 py-4 text-left transition last:border-b-0 ${selectedTaskId === task.id ? "bg-transparent" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedTaskId === task.id ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
onClick={() => onSelect(task.id)}
tabIndex={-1}
>
@@ -63,7 +63,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2.5">
<PriorityDot priority={task.priority} />
<div className={`truncate text-[14px] text-[color:var(--text)] ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
</div>
<StatusPill status={activeRun?.status ?? task.status} />
</div>
@@ -15,18 +15,18 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents,
return (
<div className="space-y-5">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="flex items-start justify-between gap-3"><div className="min-w-0"><div className="flex items-center gap-2"><div className="text-[15px] font-semibold text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div><div className="mono mt-1 text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div><div className="mt-2 text-[12px] text-[color:var(--muted)]">{project ? <>Project: <span className="text-[color:var(--text)]">{project.name}</span></> : "No linked project"}{workspace.repoUrl ? <span> · Repo: <span className="text-[color:var(--text)]">{workspace.repoUrl}</span></span> : null}{workspace.repoRef ? <span> · Ref: <span className="text-[color:var(--text)]">{workspace.repoRef}</span></span> : null}</div></div><ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} /></div>
</div>
<div className="grid gap-3 md:grid-cols-4">
{[{ label: t("exec.agents"), val: wAgents.length }, { label: t("exec.tasks"), val: wTasks.length }, { label: t("exec.runs"), val: wRuns.length }, { label: t("exec.docs"), val: wDocs.length }].map((s) => (
<div key={s.label} className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{s.val}</div></div>
<div key={s.label} className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{s.val}</div></div>
))}
</div>
<div className="grid gap-4 xl:grid-cols-3">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div><div className="text-[12px] font-medium text-[color:var(--text)]">{a.name}</div><div className="text-[11px] text-[color:var(--muted)]">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-[color:var(--text)]">{tk.title}</span></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-[color:var(--text)]">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div><div className="text-[12px] font-medium text-[color:var(--text)]">{a.name}</div><div className="text-[11px] text-[color:var(--muted)]">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-[color:var(--text)]">{tk.title}</span></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-[color:var(--text)]">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
</div>
</div>
)
@@ -15,7 +15,7 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
const toDelete = workspaces.find((w) => w.id === confirmDeleteId) ?? null
async function handleDelete(id: string) { setDeleteError(null); try { unwrap(await window.agentCompany.deleteWorkspace({ id, companyId })); setConfirmDeleteId(null); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
if (workspaces.length === 0) return <EmptyState title={t("exec.noWorkspacesYet")} />
if (workspaces.length === 0) return <EmptyState title={t("exec.noWorkspacesYet")} detail={t("exec.noWorkspacesDetail")} />
return (
<>
<ConfirmDialog open={confirmDeleteId !== null} title={t("exec.deleteWorkspace")} message={t("exec.deleteWorkspaceConfirm").replace("{0}", toDelete?.name ?? "")} errorMessage={deleteError} onConfirm={() => confirmDeleteId && void handleDelete(confirmDeleteId)} onCancel={() => { setDeleteError(null); setConfirmDeleteId(null) }} />
@@ -27,8 +27,8 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
<div className={`mb-2 rounded-[8px] p-1 ${selectedWorkspaceId === workspace.id ? "bg-[color:var(--accent-soft)]" : ""}`}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2"><div className="text-[14px] text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
<div className="mono text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div>
<div className="flex items-center gap-2"><div className="truncate text-[13px] text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
<div className="mono truncate text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div>
</div>
<div className="flex items-center gap-2">
{workspace.isPrimary ? <StatusPill status="ready" /> : null}
@@ -37,7 +37,7 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
</div>
</div>
<div className="text-[12px] text-[color:var(--muted)]">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-[color:var(--text)]">{linkedProject.name}</span></span> : null}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-[color:var(--text)]">{linkedProject.name}</span></span> : null}</div>
</div>
</ListRow>
)
+10 -10
View File
@@ -58,14 +58,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
];
return (
<div className="space-y-4">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
<h2 className="text-[18px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
@@ -81,14 +81,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
onClick={() => setFilter(btn.key)}
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
filter === btn.key
? "bg-[color:var(--accent)] text-white"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"
}`}
>
{btn.label}
{btn.count > 0 ? (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${
filter === btn.key ? "bg-white/20 text-white" : "bg-[color:var(--line)] text-[color:var(--muted)]"
filter === btn.key ? "bg-[color:var(--text-on-accent)]/20 text-[color:var(--text-on-accent)]" : "bg-[color:var(--line)] text-[color:var(--muted)]"
}`}>
{btn.count}
</span>
@@ -98,7 +98,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
</div>
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
@@ -115,7 +115,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 20 }}
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm ${severityColors[item.severity] ?? ""}`}
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px ${severityColors[item.severity] ?? ""}`}
>
<div
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]"
@@ -144,11 +144,11 @@ export function Inbox({ items, onNavigate }: InboxProps) {
{item.agentName}
</span>
) : null}
<span>{item.subtitle.slice(0, 80)}</span>
<span className="shrink-0">{timeAgo(item.createdAt)}</span>
<span className="truncate">{item.subtitle.slice(0, 80)}</span>
<span className="shrink-0 tabular-nums">{timeAgo(item.createdAt)}</span>
</div>
</button>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100" />
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100 group-focus-within:opacity-100" />
</motion.div>
);
})}
@@ -13,7 +13,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
@@ -48,7 +48,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
/>
</div>
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div>
<div className="flex items-center gap-6">
<div className="flex-1">
@@ -73,7 +73,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div>
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)]">
<div className="overflow-x-auto rounded-[8px] border border-[color:var(--line)]">
<table className="w-full text-left">
<thead>
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]">
@@ -18,7 +18,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
@@ -28,9 +28,9 @@ export function StandupView({ report, onNavigate }: StandupProps) {
return (
<div className="space-y-6">
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
@@ -43,22 +43,22 @@ export function StandupView({ report, onNavigate }: StandupProps) {
<div className="text-[14px] font-medium text-[color:var(--text)]">{report.summary}</div>
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[18px] font-bold tabular-nums text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[18px] font-bold tabular-nums text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[18px] font-bold tabular-nums text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
<div className={`text-[18px] font-bold tabular-nums ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
@@ -114,7 +114,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
</div>
{ar.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[color:var(--success)]" />
</span>
) : null}
@@ -128,7 +128,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</span> : null}
{ar.tasksBlocked > 0 ? <span className="text-[color:var(--danger)]">{ar.tasksBlocked} blocked</span> : null}
{ar.costLast24h > 0 ? <span>${ar.costLast24h.toFixed(2)}</span> : null}
{ar.costLast24h > 0 ? <span className="tabular-nums">${ar.costLast24h.toFixed(2)}</span> : null}
</div>
{ar.highlights.length > 0 ? (
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{ar.highlights.join(" \u00B7 ")}</div>
@@ -40,7 +40,7 @@ export function CompactMetric({ label, value, tone = "default", subtitle }: {
};
return (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[22px] font-bold ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="text-[10px] text-[color:var(--muted)]">{subtitle}</div> : null}
@@ -32,7 +32,7 @@ export function AgentRoster({
})
if (agents.length === 0) {
return <EmptyState title={t("org.noAgentsYet")} />
return <EmptyState title={t("org.noAgentsYet")} detail={t("org.noAgentsDetail")} />
}
return (
@@ -79,7 +79,7 @@ export function AgentRoster({
type="button"
onClick={() => onSelect(agent.id)}
tabIndex={-1}
className={`block w-full border-b border-[color:var(--line)] px-5 py-4 text-left last:border-b-0 cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
className={`block w-full border-b border-[color:var(--line)] px-4 py-3 text-left last:border-b-0 cursor-pointer transition-colors duration-150 ${isSelected ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "hover:bg-[color:var(--panel-soft)]"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -87,15 +87,15 @@ export function AgentRoster({
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div>
<div className="text-[15px] text-[color:var(--text)]">{agent.name}</div>
<div className="text-[12px] text-[color:var(--muted)]">
<div className={`truncate text-[14px] font-semibold text-[color:var(--text)]`}>{agent.name}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">
{agent.role} · {agent.title || "No title"}
</div>
</div>
</div>
<StatusPill status={agent.status} />
</div>
<div className="flex items-center justify-between text-[12px] text-[color:var(--muted)]">
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<span>Connector: {connector?.label ?? agent.connectorId}</span>
{workspace && <span>Workspace: {workspace.name}</span>}
</div>
@@ -16,7 +16,7 @@ export function GoalList({
}) {
const t = useT();
if (goals.length === 0) {
return <EmptyState title={t("org.noGoalsYet")} />;
return <EmptyState title={t("org.noGoalsYet")} detail={t("org.noGoalsDetail")} />;
}
return (
<ListFrame>
@@ -37,12 +37,12 @@ export function GoalList({
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div className="text-[15px] text-[color:var(--text)]">{goal.title}</div>
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{goal.title}</div>
</div>
<StatusPill status={goal.status} />
</div>
<div className="text-[13px] text-[color:var(--muted)]">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[12px] text-[color:var(--muted)]">
<div className="line-clamp-2 text-[13px] text-[color:var(--muted)]">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[11px] text-[color:var(--muted)]">
{parentGoal && <span>Parent: {parentGoal.title}</span>}
{ownerAgent && <span>Owner: {ownerAgent.name}</span>}
</div>
@@ -67,13 +67,13 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
setZoom(nz);
}, [zoom, pan]);
if (agents.length === 0) return <EmptyState title={t("org.noAgentsToDisplay")} />;
if (agents.length === 0) return <EmptyState title={t("org.noAgentsToDisplay")} detail={t("org.noAgentsToDisplayDetail")} />;
const zoomIn = () => { const nz = Math.min(zoom * 1.2, 2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
const zoomOut = () => { const nz = Math.max(zoom * 0.8, 0.2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
return (
<div ref={containerRef} className="relative h-[480px] w-full overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
<div ref={containerRef} className="relative min-h-[400px] h-[clamp(400px,60vh,720px)] w-full overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
<div className="absolute right-3 top-3 z-10 flex flex-col gap-1">
{[{ label: "+", ariaLabel: "Zoom in", action: zoomIn }, { label: "\u2212", ariaLabel: "Zoom out", action: zoomOut }, { label: "Fit", ariaLabel: "Fit chart to screen", action: fitToScreen }].map((btn) => (
<button key={btn.ariaLabel} className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] text-[12px] text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]" onClick={btn.action} aria-label={btn.ariaLabel}>{btn.label}</button>
@@ -100,22 +100,23 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
return (
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[10px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
<div className={`h-[3px] rounded-t-[9px] ${isCEO ? "bg-[color:var(--accent)]" : node.children.length > 0 ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)] opacity-30"}`} />
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[8px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
<div className={`rounded-t-[7px] ${isCEO ? "h-[4px] bg-[color:var(--accent)]" : node.children.length > 0 ? "h-[3px] bg-[color:var(--warn)]" : "h-[2px] bg-[color:var(--muted)] opacity-30"}`} />
<div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
{isRunning ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor }} />
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5" aria-hidden="true">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor, willChange: "transform" }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
</span>
) : (
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} aria-hidden="true" />
)}
<span className="sr-only">{`Status: ${node.agent.status}`}</span>
</div>
<div className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="truncate text-[14px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">{node.agent.title || node.agent.role}</span>
<div className="mt-1.5 flex items-center gap-2">
<span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{connectorLabel}</span>
@@ -20,7 +20,7 @@ export function ProjectList({
}) {
const t = useT();
if (projects.length === 0) {
return <EmptyState title={t("org.noProjectsYet")} />;
return <EmptyState title={t("org.noProjectsYet")} detail={t("org.noProjectsDetail")} />;
}
return (
<ListFrame>
@@ -38,12 +38,12 @@ export function ProjectList({
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
)}
<div className="text-[15px] text-[color:var(--text)]">{project.name}</div>
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{project.name}</div>
</div>
<StatusPill status={project.status} />
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">{project.description}</div>
<div className="flex flex-wrap gap-4 text-[12px] text-[color:var(--muted)]">
<div className="flex flex-wrap gap-4 text-[11px] text-[color:var(--muted)]">
<span>
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
</span>
@@ -86,12 +86,13 @@ export function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; c
}
export const statusDotColor: Record<string, string> = {
running: "#22d3ee",
active: "#4ade80",
paused: "#facc15",
idle: "#facc15",
error: "#f87171",
terminated: "#a3a3a3",
running: "var(--accent)",
active: "var(--success)",
paused: "var(--warn)",
idle: "var(--muted)",
error: "var(--danger)",
terminated: "var(--muted)",
pending_approval: "var(--warn)",
};
export const adapterLabels: Record<string, string> = {
+3 -3
View File
@@ -82,7 +82,7 @@ export function RunDetail({
<DetailRow label="Cost">{formatMoney(selectedRun.costUsd)}</DetailRow>
</div>
{selectedRun.errorMessage ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-4 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
{selectedRun.errorMessage}
</div>
) : null}
@@ -118,14 +118,14 @@ export function RunDetail({
)}
>
{logState.error ? (
<div className="rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
{logState.error}
</div>
) : null}
<div className="mb-3 flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<span>{logState.loading ? "Refreshing\u2026" : logState.eof ? "At latest tail" : "Tail window loaded"}</span>
</div>
<div className="max-h-[420px] overflow-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="max-h-[420px] overflow-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
{logState.content || "No log output captured yet."}
</pre>
+3 -3
View File
@@ -45,7 +45,7 @@ export function RunSummaryCard({
tone?: "default" | "danger";
}) {
return (
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
@@ -54,7 +54,7 @@ export function RunSummaryCard({
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{detail}</div>
</div>
<div className={`flex h-10 w-10 items-center justify-center rounded-[10px] ${tone === "danger" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>
<div className={`flex h-10 w-10 items-center justify-center rounded-[8px] ${tone === "danger" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>
<Icon size={18} />
</div>
</div>
@@ -64,7 +64,7 @@ export function RunSummaryCard({
export function RunMeta({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className="mt-1 text-[13px] text-[color:var(--text)]">{value}</div>
</div>
@@ -17,7 +17,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
const cardContent = (
<>
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[color:var(--text)]">{account.displayName || account.accountName}</span>
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{account.displayName || account.accountName}</span>
<span
className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: platform.bgColor, color: platform.color }}
@@ -26,7 +26,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
</span>
</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<span>@{account.accountName}</span>
<span className="truncate">@{account.accountName}</span>
{account.requireApproval ? (
<span className="flex items-center gap-1 text-[color:var(--warn)]">
<ShieldCheck size={10} />
@@ -39,9 +39,9 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
);
return (
<div className="group flex items-center gap-3 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm">
<div className="group flex items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px">
<div
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[10px]"
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[8px]"
style={{ backgroundColor: platform.bgColor, color: platform.color }}
>
<PlatformIcon size={18} weight="fill" />
@@ -78,7 +78,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<button
type="button"
onClick={onLogin}
className="flex items-center gap-1 rounded-[6px] bg-[color:var(--accent)] px-2.5 py-1.5 text-[10px] font-semibold text-white transition hover:opacity-90"
className="flex items-center gap-1 rounded-[6px] bg-[color:var(--accent)] px-2.5 py-1.5 text-[10px] font-semibold text-[color:var(--text-on-accent)] transition hover:opacity-90"
>
<SignIn size={12} />
Login
@@ -88,7 +88,8 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<button
type="button"
onClick={onDelete}
className="rounded-[6px] p-1.5 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100"
aria-label="Delete account"
className="rounded-[6px] p-1.5 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100 group-focus-within:opacity-100"
>
<Trash size={14} />
</button>
@@ -28,7 +28,7 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="w-full max-w-[420px] rounded-[14px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-xl"
className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-5 flex items-center justify-between">
@@ -52,7 +52,7 @@ export function SocialAccountsPanel({
<div className="space-y-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" />
</div>
<div>
@@ -70,7 +70,7 @@ export function SocialAccountsPanel({
/>
</div>
<div className="rounded-[10px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3">
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3">
<div className="flex items-start gap-2 text-[12px] text-[color:var(--accent)]">
<Browser size={16} className="mt-0.5 shrink-0" />
<div>
@@ -111,7 +111,7 @@ export function SocialAccountsPanel({
{activeTab === "accounts" ? (
accounts.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Megaphone size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
+27 -19
View File
@@ -2,7 +2,7 @@ import { Component, useEffect, useRef, type ElementType, type ErrorInfo, type Re
export function Badge({ count, tone = "default" }: { count: number; tone?: "default" | "warn" | "danger" }) {
if (count <= 0) return null;
const toneClass = tone === "danger" ? "bg-[color:var(--danger)] text-white" : tone === "warn" ? "bg-[color:var(--warn)] text-white" : "bg-[color:var(--muted)] text-white";
const toneClass = tone === "danger" ? "bg-[color:var(--danger)] text-[color:var(--text-on-accent)]" : tone === "warn" ? "bg-[color:var(--warn)] text-[color:var(--text-on-accent)]" : "bg-[color:var(--muted)] text-[color:var(--text-on-accent)]";
return (
<span className={`ml-auto inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1.5 text-[10px] font-bold ${toneClass}`}>
{count > 99 ? "99+" : count}
@@ -52,15 +52,16 @@ export function SectionButton({
return (
<button
aria-current={active ? "page" : undefined}
className={`focus-ring flex w-full items-center gap-2.5 rounded-[6px] px-3 py-[7px] text-left transition duration-150 ${
className={`focus-ring relative flex w-full items-center gap-2.5 rounded-[6px] px-3 py-[7px] text-left transition duration-150 ${
active
? "bg-[color:var(--panel-soft)] text-[color:var(--text)]"
? "bg-[color:var(--accent-soft)] text-[color:var(--text)]"
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
}`}
onClick={onClick}
>
{active ? <span className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
<Icon size={17} weight={active ? "fill" : "regular"} className={active ? "text-[color:var(--accent)]" : ""} />
<span className={`text-[13px] tracking-[0.005em] ${active ? "font-semibold" : "font-medium"}`}>{label}</span>
<span className={`text-[13px] tracking-[0.005em] ${active ? "font-bold" : "font-medium"}`}>{label}</span>
{badge ? <Badge count={badge} tone={badgeTone} /> : null}
</button>
);
@@ -85,8 +86,8 @@ export function Panel({
<section className={`space-y-4 ${className}`}>
<div className={`flex items-end justify-between gap-4 ${tone === "surface" ? "border-b border-[color:var(--line)] pb-3" : ""}`}>
<div>
{eyebrow ? <div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{eyebrow}</div> : null}
<h2 className="text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">{title}</h2>
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)]">{eyebrow}</div> : null}
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-[color:var(--text)]">{title}</h2>
</div>
{action}
</div>
@@ -127,12 +128,12 @@ export function Modal({
role="dialog"
aria-modal="true"
aria-label={title}
className="flex w-full flex-col rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-xl"
className="flex w-full flex-col rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-xl"
style={{ maxWidth: width, maxHeight: "80vh" }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-6 py-4">
<h2 className="text-[16px] font-semibold text-[color:var(--text)]">{title}</h2>
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-5 py-3.5">
<h2 className="text-[16px] font-bold tracking-[-0.015em] text-[color:var(--text)]">{title}</h2>
<button
type="button"
className="focus-ring rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
@@ -142,7 +143,7 @@ export function Modal({
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg>
</button>
</div>
<div className="overflow-y-auto px-6 py-5">{children}</div>
<div className="overflow-y-auto px-5 py-4">{children}</div>
</div>
</div>
);
@@ -303,14 +304,14 @@ export function ActionButton({
const isDisabled = loading || disabled;
const toneClass =
tone === "accent"
? "border-transparent bg-[color:var(--accent)] text-white hover:brightness-90"
? "border-transparent bg-[color:var(--accent)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(74,93,122,0.35)] hover:bg-[color:var(--accent-hover)]"
: tone === "danger"
? "border-transparent bg-[color:var(--danger)] text-white hover:brightness-90"
? "border-transparent bg-[color:var(--danger)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(220,53,69,0.3)] hover:brightness-90"
: "border-[color:var(--line)] bg-[color:var(--panel)] text-[color:var(--text)] hover:bg-[color:var(--panel-soft)]";
return (
<button
className={`focus-ring inline-flex items-center gap-2 rounded-[7px] border px-3.5 py-2 text-[12px] font-semibold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""}`}
className={`focus-ring inline-flex items-center gap-2 rounded-[8px] border px-4 py-2.5 text-[12px] font-bold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
onClick={onClick}
disabled={isDisabled}
aria-busy={loading}
@@ -351,11 +352,11 @@ export function StatusPill({ status }: { status: string }) {
const displayLabel = checking ? "checking" : status.replaceAll("_", " ");
return (
<span className={`inline-flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] ${toneClass}`}>
<span className={`inline-flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] ${toneClass}`}>
{checking ? (
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--muted)]" aria-hidden="true" />
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--muted)]" aria-hidden="true" />
) : (
<span className={`h-1.5 w-1.5 rounded-full ${dotClass}`} aria-hidden="true" />
<span className={`h-2 w-2 rounded-full ${dotClass}`} aria-hidden="true" />
)}
{displayLabel}
</span>
@@ -365,9 +366,11 @@ export function StatusPill({ status }: { status: string }) {
export function InlineNotice({
message,
tone = "danger",
action,
}: {
message: string;
tone?: "danger" | "success" | "warn";
action?: ReactNode;
}) {
const toneClass = tone === "success"
? "border-[color:var(--success)] bg-[color:var(--success-soft)] text-[color:var(--success)]"
@@ -375,8 +378,13 @@ export function InlineNotice({
? "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
: "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]";
return (
<div className={`rounded-[10px] border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
{message}
<div className={`rounded-[8px] border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
{action ? (
<div className="flex items-start justify-between gap-3">
<span>{message}</span>
{action}
</div>
) : message}
</div>
);
}
@@ -408,7 +416,7 @@ export function ConfirmDialog({ open, title, message, errorMessage, onConfirm, o
role="dialog"
aria-modal="true"
aria-labelledby="confirm-dialog-title"
className="w-full max-w-[400px] rounded-[12px] bg-[color:var(--panel)] border border-[color:var(--line)] p-6 shadow-lg"
className="w-full max-w-[400px] rounded-[8px] bg-[color:var(--panel)] border border-[color:var(--line)] p-6 shadow-lg"
onClick={e => e.stopPropagation()}
>
<div id="confirm-dialog-title" className="text-[16px] font-semibold text-[color:var(--text)] mb-2">{title}</div>
+32 -20
View File
@@ -35,7 +35,7 @@ export const en: Record<string, string> = {
"nav.group.infrastructure": "Infrastructure",
// ── App-level strings ──
"app.loading": "Loading local profile...",
"app.loading": "Setting up your workspace...",
"app.selectCompany": "Select company",
"app.newCompany": "New company",
"app.commandPalette": "Command palette",
@@ -107,9 +107,9 @@ export const en: Record<string, string> = {
"onboarding.enterCompanyName": "Enter a company name.",
"onboarding.connectorRequired": "A ready connector is required to continue.",
"onboarding.describeFirstGoal": "Describe the first real company goal.",
"onboarding.unableToBootstrap": "Unable to bootstrap the company.",
"onboarding.unableToBootstrap": "Unable to launch the company. Check your connector authentication and try again.",
"onboarding.unableToOpenControlCenter": "Unable to open the control center.",
"onboarding.connectorNotInstalled": "Install and authenticate a CLI runtime first.",
"onboarding.connectorNotInstalled": "No CLI runtime detected. Install and authenticate a supported runtime to continue.",
"onboarding.connectorWarning": "Install and authenticate a CLI runtime first.",
"onboarding.languageSelection": "Language",
"onboarding.selectLanguage": "Select language",
@@ -289,12 +289,13 @@ export const en: Record<string, string> = {
// ── Common panel titles ──
"panel.choose": "Choose",
"panel.noData": "No data",
"panel.noData": "Nothing to show yet",
"panel.empty": "Empty",
"panel.loading": "Loading...",
// ── Execution / Task Inspector ──
"exec.taskQueueEmpty": "Task queue is empty",
"exec.taskQueueEmpty": "All caught up",
"exec.taskQueueEmptyDetail": "New tasks will appear here as agents plan and break down work.",
"exec.deleteTask": "Delete task",
"exec.deleteTaskConfirm": "Permanently delete \"{0}\"? This cannot be undone.",
"exec.budgetUsed": "has used {0}% of their monthly budget",
@@ -348,10 +349,12 @@ export const en: Record<string, string> = {
"exec.send": "Send",
"exec.createTask": "Create task",
"exec.taskTitle": "Task title",
"exec.noReviewsInProgress": "No agent reviews in progress",
"exec.noReviewsInProgress": "No reviews pending",
"exec.noReviewsDetail": "Tasks waiting for agent review will appear here.",
"exec.awaitingReview": "awaiting review",
"exec.reviewedBy": "reviewed by",
"exec.noApprovals": "No approvals",
"exec.noApprovals": "No pending approvals",
"exec.noApprovalsDetail": "Approval requests from agents will appear here when they need your sign-off.",
"exec.deleteApproval": "Delete approval",
"exec.deleteApprovalConfirm": "Permanently delete this approval? This cannot be undone.",
"exec.requestedBy": "Requested by:",
@@ -373,7 +376,8 @@ export const en: Record<string, string> = {
"exec.requestRevision": "Request revision",
"exec.reject": "Reject",
"exec.lastNote": "Last note:",
"exec.noCostData": "No cost data yet",
"exec.noCostData": "No costs recorded yet",
"exec.noCostDataDetail": "API usage and token costs will be tracked here once agents start running.",
"exec.allConnectors": "All connectors",
"exec.runCost": "Run cost",
"exec.unknownAgent": "Unknown agent",
@@ -381,19 +385,21 @@ export const en: Record<string, string> = {
"exec.totalEntries": "Total ({0} entries)",
"exec.knownSpend": "Known spend",
"exec.unattributedEntries": "Unattributed entries",
"exec.noActivityYet": "No activity yet",
"exec.noActivityYet": "No activity recorded yet",
"exec.noActivityDetail": "Agent actions, task updates, and system events will stream here.",
"exec.searchActivity": "Search activity...",
"exec.allTypes": "All types",
"exec.noMatchingActivity": "No matching activity",
"exec.noMatchingActivity": "No activity matches this filter",
"exec.loadMore": "Load more ({0} remaining)",
"exec.noWorkspacesYet": "No workspaces yet",
"exec.noWorkspacesYet": "No workspaces created yet",
"exec.noWorkspacesDetail": "Workspaces are created automatically when your company launches.",
"exec.deleteWorkspace": "Delete workspace",
"exec.deleteWorkspaceConfirm": "Delete workspace \"{0}\"? This only removes it from the company registry; files on disk are not affected.",
"exec.inspect": "Inspect",
"exec.reveal": "Reveal",
"exec.noRemoteUrl": "No remote URL",
"exec.selectWorkspace": "Select a workspace",
"exec.selectWorkspaceDetail": "Select a workspace to view its details.",
"exec.selectWorkspaceDetail": "Choose a workspace from the list to view agents, tasks, and activity.",
"exec.agents": "Agents",
"exec.tasks": "Tasks",
"exec.runs": "Runs",
@@ -525,8 +531,10 @@ export const en: Record<string, string> = {
"dash.poor": "Poor",
"dash.critical": "Critical",
"dash.noAgentsYet": "No agents hired yet",
"dash.noAgentsDetail": "Your CEO is working on hiring. Check back shortly.",
"dash.createCeoAgent": "",
"dash.noActivityYet": "No activity yet",
"dash.noActivityYet": "Waiting for activity",
"dash.noActivityDetail": "Agent actions and events will stream here in real time.",
"dash.liveRuns": "Live Runs",
"dash.running": "running",
"dash.allQuiet": "All quiet",
@@ -577,11 +585,15 @@ export const en: Record<string, string> = {
"dash.points": "points",
// ── Organization ──
"org.noAgentsYet": "No agents yet",
"org.noAgentsYet": "No agents hired yet",
"org.noAgentsDetail": "Agents join your company through the CEO\u2019s hiring decisions or your direct approval.",
"org.noTitle": "No title",
"org.noGoalsYet": "No goals yet",
"org.noProjectsYet": "No projects yet",
"org.noAgentsToDisplay": "No agents to display",
"org.noGoalsYet": "No goals defined yet",
"org.noGoalsDetail": "Goals are set during company creation and drive how agents prioritize work.",
"org.noProjectsYet": "No projects created yet",
"org.noProjectsDetail": "The CEO organizes work into projects as the company scales.",
"org.noAgentsToDisplay": "Organization chart is empty",
"org.noAgentsToDisplayDetail": "Agents will appear here once hired, showing reporting structure and status.",
"org.saveChanges": "Save changes",
"org.submitHireRequest": "Submit hire request",
"org.createGoal": "Create goal",
@@ -611,8 +623,8 @@ export const en: Record<string, string> = {
"org.fit": "Fit",
// ── Runs ──
"runs.noRunsYet": "No runs yet",
"runs.noRunsDetail": "Runs appear here once tasks are executed.",
"runs.noRunsYet": "No agent runs yet",
"runs.noRunsDetail": "Runs are created when agents execute tasks. They\u2019ll appear here with logs and status.",
"runs.activeLabel": "Active",
"runs.queuedOrRunning": "Queued or running right now",
"runs.needsAttention": "Needs attention",
@@ -622,7 +634,7 @@ export const en: Record<string, string> = {
"runs.runQueue": "Run queue",
"runs.searchRuns": "Search by task, agent, connector, workspace, or error",
"runs.noRunsMatch": "No runs match this filter",
"runs.noRunsMatchDetail": "Try adjusting the filters.",
"runs.noRunsMatchDetail": "Try broadening your filter or clearing it to see all runs.",
"runs.runDetail": "Run detail",
"runs.openConsole": "Open console",
"runs.cancelRun": "Cancel run",
+30 -18
View File
@@ -35,7 +35,7 @@ export const zh: Record<string, string> = {
"nav.group.infrastructure": "基础设施",
// ── 应用级文本 ──
"app.loading": "正在加载本地配置...",
"app.loading": "正在准备您的工作区...",
"app.selectCompany": "选择公司",
"app.newCompany": "新建公司",
"app.commandPalette": "命令面板",
@@ -107,9 +107,9 @@ export const zh: Record<string, string> = {
"onboarding.enterCompanyName": "请输入公司名称。",
"onboarding.connectorRequired": "需要一个就绪的连接器才能继续。",
"onboarding.describeFirstGoal": "请描述公司的第一个目标。",
"onboarding.unableToBootstrap": "无法初始化公司。",
"onboarding.unableToBootstrap": "无法启动公司。请检查连接器认证后重试。",
"onboarding.unableToOpenControlCenter": "无法打开控制中心。",
"onboarding.connectorNotInstalled": "请先安装并认证一个 CLI 运行时。",
"onboarding.connectorNotInstalled": "未检测到 CLI 运行时。请安装并认证一个受支持的运行时以继续。",
"onboarding.connectorWarning": "请先安装并认证一个 CLI 运行时。",
"onboarding.languageSelection": "语言",
"onboarding.selectLanguage": "选择语言",
@@ -289,12 +289,13 @@ export const zh: Record<string, string> = {
// ── 通用面板标题 ──
"panel.choose": "选择",
"panel.noData": "暂无数据",
"panel.noData": "暂无内容",
"panel.empty": "空",
"panel.loading": "加载中...",
// ── 执行/任务检查器 ──
"exec.taskQueueEmpty": "任务队列为空",
"exec.taskQueueEmpty": "暂无待办任务",
"exec.taskQueueEmptyDetail": "智能体规划和分解工作时,新任务将出现在这里。",
"exec.deleteTask": "删除任务",
"exec.deleteTaskConfirm": "永久删除「{0}」?此操作无法撤消。",
"exec.budgetUsed": "已使用 {0}% 的月度预算",
@@ -348,10 +349,12 @@ export const zh: Record<string, string> = {
"exec.send": "发送",
"exec.createTask": "创建任务",
"exec.taskTitle": "任务标题",
"exec.noReviewsInProgress": "没有进行中的智能体审核",
"exec.noReviewsInProgress": "暂无待审核任务",
"exec.noReviewsDetail": "等待智能体审核的任务将出现在这里。",
"exec.awaitingReview": "等待审核",
"exec.reviewedBy": "由...审核",
"exec.noApprovals": "暂无审批",
"exec.noApprovals": "暂无待审批项",
"exec.noApprovalsDetail": "智能体需要您批准时,审批请求将出现在这里。",
"exec.deleteApproval": "删除审批",
"exec.deleteApprovalConfirm": "永久删除此审批?此操作无法撤消。",
"exec.requestedBy": "请求者:",
@@ -373,7 +376,8 @@ export const zh: Record<string, string> = {
"exec.requestRevision": "要求修改",
"exec.reject": "拒绝",
"exec.lastNote": "最近备注:",
"exec.noCostData": "暂无费用数据",
"exec.noCostData": "暂无费用记录",
"exec.noCostDataDetail": "智能体开始运行后,API 使用和令牌费用将在此处跟踪。",
"exec.allConnectors": "全部连接器",
"exec.runCost": "运行费用",
"exec.unknownAgent": "未知智能体",
@@ -381,19 +385,21 @@ export const zh: Record<string, string> = {
"exec.totalEntries": "合计({0} 条记录)",
"exec.knownSpend": "已知支出",
"exec.unattributedEntries": "未归因条目",
"exec.noActivityYet": "暂无活动",
"exec.noActivityYet": "暂无活动记录",
"exec.noActivityDetail": "智能体操作、任务更新和系统事件将在此处实时显示。",
"exec.searchActivity": "搜索活动...",
"exec.allTypes": "全部类型",
"exec.noMatchingActivity": "无匹配活动",
"exec.noMatchingActivity": "没有匹配此筛选条件的活动",
"exec.loadMore": "加载更多(还剩 {0} 条)",
"exec.noWorkspacesYet": "暂无工作区",
"exec.noWorkspacesDetail": "公司启动时将自动创建工作区。",
"exec.deleteWorkspace": "删除工作区",
"exec.deleteWorkspaceConfirm": "删除工作区「{0}」?这只会从公司注册表中移除,不会影响磁盘上的文件。",
"exec.inspect": "检查",
"exec.reveal": "在文件管理器中显示",
"exec.noRemoteUrl": "无远程地址",
"exec.selectWorkspace": "选择一个工作区",
"exec.selectWorkspaceDetail": "选择一个工作区以查看详情。",
"exec.selectWorkspaceDetail": "从列表中选择一个工作区,查看其智能体、任务和活动。",
"exec.agents": "智能体",
"exec.tasks": "任务",
"exec.runs": "运行",
@@ -525,8 +531,10 @@ export const zh: Record<string, string> = {
"dash.poor": "较差",
"dash.critical": "危急",
"dash.noAgentsYet": "尚未招聘智能体",
"dash.noAgentsDetail": "您的 CEO 正在招聘中,请稍后查看。",
"dash.createCeoAgent": "",
"dash.noActivityYet": "暂无活动",
"dash.noActivityYet": "等待活动",
"dash.noActivityDetail": "智能体的操作和事件将在此实时显示。",
"dash.liveRuns": "实时运行",
"dash.running": "运行中",
"dash.allQuiet": "一切平静",
@@ -577,11 +585,15 @@ export const zh: Record<string, string> = {
"dash.points": "点数",
// ── 组织 ──
"org.noAgentsYet": "暂无智能体",
"org.noAgentsYet": "暂无已招聘的智能体",
"org.noAgentsDetail": "智能体将通过 CEO 的招聘决策或您的直接批准加入公司。",
"org.noTitle": "无职位",
"org.noGoalsYet": "暂无目标",
"org.noGoalsYet": "暂未设定目标",
"org.noGoalsDetail": "目标在创建公司时设定,驱动智能体的工作优先级。",
"org.noProjectsYet": "暂无项目",
"org.noAgentsToDisplay": "没有可显示的智能体",
"org.noProjectsDetail": "CEO 将在公司扩展时组织项目。",
"org.noAgentsToDisplay": "组织架构图为空",
"org.noAgentsToDisplayDetail": "智能体招聘后将在此显示,展示汇报结构和状态。",
"org.saveChanges": "保存更改",
"org.submitHireRequest": "提交招聘请求",
"org.createGoal": "创建目标",
@@ -611,8 +623,8 @@ export const zh: Record<string, string> = {
"org.fit": "适配",
// ── 运行 ──
"runs.noRunsYet": "暂无运行",
"runs.noRunsDetail": "任务执行后运行记录将显示在这里。",
"runs.noRunsYet": "暂无智能体运行记录",
"runs.noRunsDetail": "智能体执行任务时将创建运行记录,包含日志和状态信息。",
"runs.activeLabel": "活跃",
"runs.queuedOrRunning": "正在排队或运行中",
"runs.needsAttention": "需要关注",
@@ -622,7 +634,7 @@ export const zh: Record<string, string> = {
"runs.runQueue": "运行队列",
"runs.searchRuns": "按任务、智能体、连接器、工作区或错误搜索",
"runs.noRunsMatch": "没有匹配此筛选条件的运行",
"runs.noRunsMatchDetail": "尝试调整筛选条件。",
"runs.noRunsMatchDetail": "尝试放宽筛选条件或清除筛选以查看所有运行。",
"runs.runDetail": "运行详情",
"runs.openConsole": "打开控制台",
"runs.cancelRun": "取消运行",
+21
View File
@@ -3,6 +3,14 @@ import type { AppState } from "../store";
import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
export interface Toast {
id: string;
message: string;
tone: "success" | "danger" | "warn";
}
function nextToastId() { return `t-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; }
export interface UiSlice {
locale: AppLocale;
showOnboarding: boolean;
@@ -10,12 +18,15 @@ export interface UiSlice {
confirmDeleteCompanyId: string | null;
taskViewMode: "list" | "kanban";
logRevision: number;
toasts: Toast[];
setLocale: (locale: AppLocale) => void;
setShowOnboarding: (show: boolean) => void;
setInitialOnboardingLocked: (locked: boolean) => void;
setConfirmDeleteCompanyId: (id: string | null) => void;
setTaskViewMode: (mode: "list" | "kanban") => void;
incrementLogRevision: () => void;
addToast: (message: string, tone?: Toast["tone"]) => void;
dismissToast: (id: string) => void;
}
export const createUiSlice: StateCreator<
@@ -30,6 +41,7 @@ export const createUiSlice: StateCreator<
confirmDeleteCompanyId: null,
taskViewMode: "list",
logRevision: 0,
toasts: [],
setLocale: (locale) =>
set((state) => {
state.locale = locale;
@@ -54,4 +66,13 @@ export const createUiSlice: StateCreator<
set((state) => {
state.logRevision += 1;
}),
addToast: (message, tone = "success") => {
const id = nextToastId();
set((state) => { state.toasts.push({ id, message, tone }); });
setTimeout(() => {
set((state) => { state.toasts = state.toasts.filter((t) => t.id !== id); });
}, 4000);
},
dismissToast: (id) =>
set((state) => { state.toasts = state.toasts.filter((t) => t.id !== id); }),
});
+70 -57
View File
@@ -10,24 +10,27 @@
:root {
color-scheme: light;
--bg: #fbfbfb;
--bg: #f7f8fa;
--bg-subtle: #ffffff;
--panel: #ffffff;
--panel-soft: #f4f5f5;
--panel-strong: #fafafa;
--line: rgba(15, 23, 42, 0.08);
--line-strong: rgba(15, 23, 42, 0.14);
--text: #1a202c;
--muted: #718096;
--muted-strong: #4a5568;
--accent: #c06a3a;
--accent-soft: #f7ece6;
--danger: #e5484d;
--danger-soft: #fdebec;
--warn: #b7791f;
--warn-soft: #fdf6e3;
--success: #2f855a;
--success-soft: #edf7f0;
--panel-soft: #f0f2f5;
--panel-strong: #f7f8fa;
--line: rgba(50, 65, 85, 0.08);
--line-strong: rgba(50, 65, 85, 0.15);
--text: #1a1d24;
--muted: #6b7280;
--muted-strong: #4b5563;
--accent: #4a5d7a;
--accent-text: #3d5070;
--accent-soft: #edf1f7;
--accent-hover: #3d4f6d;
--text-on-accent: #ffffff;
--danger: #c53030;
--danger-soft: #fef2f2;
--warn: #b45309;
--warn-soft: #fffbeb;
--success: #0f766e;
--success-soft: #ecfdf5;
}
:root[data-theme="light"] {
@@ -36,47 +39,53 @@
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #111315;
--bg-subtle: #171a1d;
--panel: #1b1f23;
--panel-soft: #232931;
--panel-strong: #20252c;
--line: rgba(226, 232, 240, 0.1);
--line-strong: rgba(226, 232, 240, 0.18);
--text: #f4f7fb;
--muted: #9aa7b5;
--muted-strong: #c2ccd8;
--accent: #ee8a58;
--accent-soft: rgba(238, 138, 88, 0.14);
--danger: #ff7b86;
--danger-soft: rgba(255, 123, 134, 0.14);
--warn: #f4bc5b;
--warn-soft: rgba(244, 188, 91, 0.14);
--success: #61d69d;
--success-soft: rgba(97, 214, 157, 0.14);
--bg: #0f1117;
--bg-subtle: #161820;
--panel: #1b1e26;
--panel-soft: #252830;
--panel-strong: #1e2129;
--line: rgba(148, 163, 190, 0.1);
--line-strong: rgba(148, 163, 190, 0.18);
--text: #edf0f7;
--muted: #8b95a5;
--muted-strong: #b0b8c8;
--accent: #7b9cc4;
--accent-text: #7b9cc4;
--accent-soft: rgba(123, 156, 196, 0.14);
--accent-hover: #93b0d4;
--text-on-accent: #ffffff;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.12);
--warn: #f59e0b;
--warn-soft: rgba(245, 158, 11, 0.12);
--success: #34d399;
--success-soft: rgba(52, 211, 153, 0.12);
}
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
color-scheme: dark;
--bg: #111315;
--bg-subtle: #171a1d;
--panel: #1b1f23;
--panel-soft: #232931;
--panel-strong: #20252c;
--line: rgba(226, 232, 240, 0.1);
--line-strong: rgba(226, 232, 240, 0.18);
--text: #f4f7fb;
--muted: #9aa7b5;
--muted-strong: #c2ccd8;
--accent: #ee8a58;
--accent-soft: rgba(238, 138, 88, 0.14);
--danger: #ff7b86;
--danger-soft: rgba(255, 123, 134, 0.14);
--warn: #f4bc5b;
--warn-soft: rgba(244, 188, 91, 0.14);
--success: #61d69d;
--success-soft: rgba(97, 214, 157, 0.14);
--bg: #0f1117;
--bg-subtle: #161820;
--panel: #1b1e26;
--panel-soft: #252830;
--panel-strong: #1e2129;
--line: rgba(148, 163, 190, 0.1);
--line-strong: rgba(148, 163, 190, 0.18);
--text: #edf0f7;
--muted: #8b95a5;
--muted-strong: #b0b8c8;
--accent: #7b9cc4;
--accent-text: #7b9cc4;
--accent-soft: rgba(123, 156, 196, 0.14);
--accent-hover: #93b0d4;
--text-on-accent: #ffffff;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.12);
--warn: #f59e0b;
--warn-soft: rgba(245, 158, 11, 0.12);
--success: #34d399;
--success-soft: rgba(52, 211, 153, 0.12);
}
}
@@ -124,7 +133,11 @@ select {
::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(113, 128, 150, 0.3);
background: rgba(60, 75, 100, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(60, 75, 100, 0.32);
}
.mono {
@@ -134,7 +147,7 @@ select {
.surface {
background: var(--panel);
border: 1px solid var(--line);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
box-shadow: 0 1px 3px rgba(26, 29, 36, 0.06), 0 0 0 0.5px rgba(26, 29, 36, 0.03);
}
.focus-ring {
@@ -147,9 +160,9 @@ select {
.focus-ring:focus-visible {
box-shadow:
0 0 0 1px rgba(192, 106, 58, 0.5),
0 0 0 4px rgba(192, 106, 58, 0.12);
border-color: rgba(192, 106, 58, 0.35);
0 0 0 1px rgba(74, 93, 122, 0.5),
0 0 0 4px rgba(74, 93, 122, 0.12);
border-color: rgba(74, 93, 122, 0.35);
}
@media (prefers-reduced-motion: reduce) {