S3.5: every page on the pixel grid: tokenised type, square corners, pixel charts with HTML axis labels
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c97adc14cb
commit
5dfc2a6dc6
65 files changed
+362
-347
No files matched your search
@@ -8,7 +8,7 @@ const RISK_NAMES = { low: () => t("Low"), medium: () => t("Medium"), high: () =>
|
||||
export function AutonomySummary({ mode }: { mode: "ai_ceo" | "ai_cofounder" }) {
|
||||
const gated = { level: RISK_NAMES[COMPANY_PRESETS.taskRiskCeiling](), amount: dollars(COMPANY_PRESETS.hireReviewAboveCents) };
|
||||
return (
|
||||
<section aria-label={t("What this mode allows")} style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.6 }}>
|
||||
<section aria-label={t("What this mode allows")} style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", lineHeight: 1.6 }}>
|
||||
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
||||
<li>{mode === "ai_ceo"
|
||||
? t("Approved without you: tasks above {level} risk, tasks that run commands in a workspace, and hires over {amount} a month.", gated)
|
||||
|
||||
@@ -168,15 +168,15 @@ export function Onboarding({
|
||||
<div>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<div className="brand-mark">A</div>
|
||||
<div style={{ fontWeight: 540 }}>AgentCompany</div>
|
||||
<div>AgentCompany</div>
|
||||
{cancel && step !== "connectors" && (
|
||||
<button type="button" className="button ghost" style={{ marginLeft: "auto" }} onClick={cancel}>
|
||||
{t("Cancel")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="page-h1" style={{ fontSize: 24, marginTop: 32, letterSpacing: "-0.02em" }}>{t("Start your AI company.")}</h1>
|
||||
<p className="page-sub" style={{ maxWidth: 460, fontSize: 14, lineHeight: 1.55 }}>
|
||||
<h1 className="page-h1" style={{ fontSize: "var(--fs-display)", marginTop: 32 }}>{t("Start your AI company.")}</h1>
|
||||
<p className="page-sub" style={{ maxWidth: 460, fontSize: "var(--fs-body)", lineHeight: 1.55 }}>
|
||||
{t("Stand up the workspace, bind the provider, and queue the first operating loop.")}
|
||||
</p>
|
||||
<div className="onboarding-steps">
|
||||
@@ -189,10 +189,10 @@ export function Onboarding({
|
||||
<div className="card-label" style={{ marginBottom: 10 }}>{t("Runtime selected")}</div>
|
||||
<div className="row" style={{ gap: 10 }}>
|
||||
<Icon name="bot" />
|
||||
<strong style={{ fontSize: 14 }}>{selectedProvider.label}</strong>
|
||||
<strong style={{ fontSize: "var(--fs-body)" }}>{selectedProvider.label}</strong>
|
||||
{draft.providerModel && <Pill tone="violet">{draft.providerModel}</Pill>}
|
||||
</div>
|
||||
<div style={{ marginTop: 10, fontSize: 12, color: "var(--text-3)" }}>
|
||||
<div style={{ marginTop: 10, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
{t("We never embed the key in the build. Credentials encrypt locally via OS-protected storage.")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@ export function Onboarding({
|
||||
<form className="onboarding-panel" onSubmit={submit} noValidate>
|
||||
<div key={step} className="step-view">
|
||||
{restored && step !== "connectors" && (
|
||||
<p role="status" style={{ fontSize: 13, color: "var(--text-2)", margin: "0 0 12px" }}>
|
||||
<p role="status" style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", margin: "0 0 12px" }}>
|
||||
{t("Your progress was restored. Keys are never saved before launch, so enter yours again.")}
|
||||
</p>
|
||||
)}
|
||||
@@ -249,7 +249,7 @@ export function Onboarding({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{error && <div style={{ color: "var(--red)", marginTop: 16, fontSize: 14 }}>{error}</div>}
|
||||
{error && <div style={{ color: "var(--red)", marginTop: 16, fontSize: "var(--fs-body)" }}>{error}</div>}
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
@@ -284,10 +284,10 @@ function WelcomeStep({
|
||||
/>
|
||||
</div>
|
||||
<RestartForLanguage language={language} />
|
||||
<p style={{ color: "var(--text-2)", fontSize: 14, lineHeight: 1.55 }}>
|
||||
<p style={{ color: "var(--text-2)", fontSize: "var(--fs-body)", lineHeight: 1.55 }}>
|
||||
{t("AgentCompany sets up your operating loop in six steps. Bring an API key, pick a runtime for the founding CEO, and we'll wire up the first goal, project, and task.")}
|
||||
</p>
|
||||
<ul style={{ paddingLeft: 20, color: "var(--text-2)", lineHeight: 1.8, margin: 0, fontSize: 14 }}>
|
||||
<ul style={{ paddingLeft: 20, color: "var(--text-2)", lineHeight: 1.8, margin: 0, fontSize: "var(--fs-body)" }}>
|
||||
<li>{t("Connect one model provider (OpenAI, Anthropic, xAI, or Groq).")}</li>
|
||||
<li>{t("Name the company and the human board chair.")}</li>
|
||||
<li>{t("Pick execution mode and bind the workspace path.")}</li>
|
||||
@@ -333,7 +333,7 @@ function ModeStep({ draft, onChange, onBack, onNext }: StepProps) {
|
||||
return (
|
||||
<div className="form-grid">
|
||||
<h2 className="page-h1">{t("Mode")}</h2>
|
||||
<p style={{ color: "var(--text-3)", fontSize: 14 }}>{t("How autonomous should the CEO be?")}</p>
|
||||
<p style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{t("How autonomous should the CEO be?")}</p>
|
||||
<ModeCard
|
||||
title={t("AI CEO")}
|
||||
description={t("Runs on its own: approves its risky work and large hires, and writes deliverables without review.")}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ProviderStep({
|
||||
return (
|
||||
<div className="form-grid">
|
||||
<h2 className="page-h1">{t("Provider")}</h2>
|
||||
<p style={{ color: "var(--text-3)", fontSize: 14 }}>
|
||||
<p style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>
|
||||
{t("Pick the runtime for the founding CEO. You can attach more providers in Settings later.")}
|
||||
</p>
|
||||
<div className="option-grid">
|
||||
@@ -112,7 +112,7 @@ function ProviderCredentialFields({
|
||||
return (
|
||||
<div className="provider-inline-form">
|
||||
{selected.credentialHelp && (
|
||||
<div style={{ fontSize: 13, color: "var(--text-3)", lineHeight: 1.5 }}>{selected.credentialHelp}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.5 }}>{selected.credentialHelp}</div>
|
||||
)}
|
||||
{isOAuthLike ? (
|
||||
<ProviderOAuthConnect
|
||||
@@ -160,7 +160,7 @@ function ProviderCredentialFields({
|
||||
{loading ? t("Getting models...") : fetched ? t("Reload models") : t("Get models")}
|
||||
</Button>
|
||||
)}
|
||||
{loadError && <div style={{ color: "var(--red)", fontSize: 12 }}>{loadError}</div>}
|
||||
{loadError && <div style={{ color: "var(--red)", fontSize: "var(--fs-body)" }}>{loadError}</div>}
|
||||
{(hasCredential || isCustom) && (
|
||||
<Field label={t("Model")}>
|
||||
{fetched || showCuratedList ? (
|
||||
|
||||
@@ -9,7 +9,7 @@ export function StaleBanner({ failure, syncedAt, onRetry }: { failure: SyncFailu
|
||||
// Outside the app root, so it stays usable while a dialog makes the app inert.
|
||||
return createPortal(
|
||||
<div role="alert" className="card" style={{ position: "fixed", top: 8, left: "50%", transform: "translateX(-50%)", zIndex: 900, display: "flex", gap: 10, alignItems: "center", padding: "8px 12px", borderColor: "var(--amber, var(--red))", maxWidth: "min(720px, calc(100vw - 32px))" }}>
|
||||
<div style={{ fontSize: 13, minWidth: 0 }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", minWidth: 0 }}>
|
||||
<strong>{t("Not up to date.")}</strong>{" "}
|
||||
{syncedAt ? t("Showing data from {time}.", { time: formatTime(syncedAt) }) : null}{" "}
|
||||
<span style={{ color: "var(--text-2)", wordBreak: "break-word" }}>{t("Updating failed: {reason} (attempt {count}).", { reason: failure.message, count: failure.attempts })}</span>
|
||||
|
||||
@@ -727,7 +727,7 @@ export const zh: Record<string, string> = {
|
||||
"Copy failed": "复制失败",
|
||||
"Checking account status with Composio…": "正在向 Composio 核对账号状态…",
|
||||
"Account status checked with Composio at {time}.": "已于 {time} 向 Composio 核对账号状态。",
|
||||
"Account status could not be checked with Composio: {reason}. The statuses below may be out of date.": "无法向 Composio 核对账号状态:{reason}。下方状态可能已过时。",
|
||||
"Account status could not be checked with Composio, so the statuses below may be out of date. Reason: {reason}": "无法向 Composio 核对账号状态,下方状态可能已过时。原因:{reason}",
|
||||
"{name} ({label}) is connected.": "{name}({label})已连接。",
|
||||
"{name} ({label}) is still not connected. If you finished signing in, check again; otherwise connect again.": "{name}({label})仍未连接。如果已经完成登录,请再次检查;否则请重新连接。",
|
||||
"Check again": "再次检查",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
/* Dialog name plate, as a speaker's name above an RPG text box. */
|
||||
.modal-title { display: inline-block; margin: 0 0 var(--sp-4); padding: 2px 10px; font-size: var(--fs-title); font-weight: 400; background: var(--btn-primary-bg); color: var(--btn-primary-fg); box-shadow: 2px 2px 0 0 var(--border-strong); }
|
||||
.modal-title { display: inline-block; margin: 0 0 var(--sp-4); padding: 2px 10px; font-size: var(--fs-title); background: var(--btn-primary-bg); color: var(--btn-primary-fg); box-shadow: 2px 2px 0 0 var(--border-strong); }
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-5); }
|
||||
|
||||
.onboarding-window {
|
||||
@@ -104,7 +104,7 @@
|
||||
.option-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
|
||||
.option-card:active { transform: scale(0.99); }
|
||||
.option-card.active { border-color: var(--text-1); box-shadow: inset 0 0 0 1px var(--text-1); }
|
||||
.option-card .opt-title { font-size: var(--fs-body); font-weight: 540; }
|
||||
.option-card .opt-title { font-size: var(--fs-body); }
|
||||
.option-card .opt-sub { font-size: var(--fs-caption); color: var(--text-3); margin-top: var(--sp-1); }
|
||||
.provider-inline-form {
|
||||
display: grid;
|
||||
@@ -142,8 +142,6 @@
|
||||
.menu-label {
|
||||
padding: var(--sp-2) var(--sp-2) var(--sp-1);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.menu-row {
|
||||
@@ -161,7 +159,7 @@
|
||||
transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out);
|
||||
}
|
||||
.menu-row:hover { background: var(--hover); color: var(--text-1); }
|
||||
.menu-row.active { background: var(--panel-3); color: var(--text-1); font-weight: 540; }
|
||||
.menu-row.active { background: var(--panel-3); color: var(--text-1); }
|
||||
.menu-row.danger { color: var(--red); }
|
||||
.menu-row.danger:hover { background: rgba(210, 59, 59, 0.1); color: var(--red); }
|
||||
.menu-row .menu-check { margin-left: auto; color: var(--accent); display: inline-flex; }
|
||||
|
||||
@@ -158,7 +158,7 @@ export function ApprovalsPage({ snapshot, actions, navigate, focus }: PageProps)
|
||||
{decided.length > 0 && (
|
||||
<>
|
||||
<div className="section-label" style={{ margin: "32px 0 10px" }}>{t("Recently decided")}</div>
|
||||
{driftError && <div style={{ color: "var(--red)", fontSize: 13, marginBottom: 8 }}>{t("Could not check approved content: {message}", { message: driftError })}</div>}
|
||||
{driftError && <div style={{ color: "var(--red)", fontSize: "var(--fs-body)", marginBottom: 8 }}>{t("Could not check approved content: {message}", { message: driftError })}</div>}
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
{shownDecided.map((approval, index) => (
|
||||
<DecidedRow
|
||||
@@ -176,7 +176,7 @@ export function ApprovalsPage({ snapshot, actions, navigate, focus }: PageProps)
|
||||
</div>
|
||||
{decided.length > decidedLimit && (
|
||||
<div className="row" style={{ justifyContent: "center", marginTop: 12, gap: 10 }}>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Showing {shown} of {total}", { shown: Math.min(decidedLimit, decided.length), total: decided.length })}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Showing {shown} of {total}", { shown: Math.min(decidedLimit, decided.length), total: decided.length })}</span>
|
||||
<Button size="small" onClick={() => setDecidedLimit((limit) => limit + DECIDED_PAGE)}>{t("Show earlier decisions")}</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -232,7 +232,7 @@ function StatTile({ label, value, tone }: { label: string; value: string; tone?:
|
||||
<div className="card-label" style={{ marginBottom: 6 }}>{label}</div>
|
||||
<div
|
||||
className="num"
|
||||
style={{ fontSize: 18, fontWeight: 600, color: tone === "amber" ? "var(--amber)" : "var(--text-1)", letterSpacing: "-0.02em" }}
|
||||
style={{ fontSize: "var(--fs-display)", color: tone === "amber" ? "var(--amber)" : "var(--text-1)" }}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
@@ -266,7 +266,6 @@ function ApprovalRow({
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 12,
|
||||
background: "var(--panel)",
|
||||
padding: 18,
|
||||
marginBottom: 10,
|
||||
@@ -276,20 +275,20 @@ function ApprovalRow({
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 3, alignSelf: "stretch", background: tone.color, borderRadius: 2 }} />
|
||||
<div style={{ width: 3, alignSelf: "stretch", background: tone.color }} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div className="row" style={{ marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, color: tone.color, fontWeight: 540, letterSpacing: "0.02em" }}>{tone.label}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: tone.color }}>{tone.label}</span>
|
||||
<span style={{ color: "var(--text-4)" }}>·</span>
|
||||
<Pill>{approvalKindLabel(approval.kind)}</Pill>
|
||||
</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 540, letterSpacing: "-0.01em", marginBottom: 4 }}>{approval.title}</div>
|
||||
<div style={{ fontSize: 14, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12, whiteSpace: "pre-wrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", marginBottom: 4 }}>{approval.title}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12, whiteSpace: "pre-wrap" }}>
|
||||
{approval.details}
|
||||
</div>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<Avatar name={requester} size="sm" bot={isBot} />
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Requested by {requester}", { requester })} · {formatRelative(approval.createdAt)}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Requested by {requester}", { requester })} · {formatRelative(approval.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-end" }}>
|
||||
@@ -336,7 +335,7 @@ function DecidedRow({
|
||||
style={{
|
||||
gap: 12,
|
||||
padding: "12px 18px",
|
||||
fontSize: 14,
|
||||
fontSize: "var(--fs-body)",
|
||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||
background: focused ? "var(--accent-soft)" : undefined,
|
||||
}}
|
||||
@@ -347,16 +346,16 @@ function DecidedRow({
|
||||
className="link-button"
|
||||
aria-label={t("View record of {title}", { title: approval.title })}
|
||||
onClick={onView}
|
||||
style={{ flex: 1, fontWeight: 480, color: "var(--text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}
|
||||
style={{ flex: 1, color: "var(--text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}
|
||||
>
|
||||
{approval.title}
|
||||
</button>
|
||||
{approval.sourceEntityType === "task" && approval.sourceEntityId && (
|
||||
<button type="button" className="link-button" style={{ fontSize: 12 }} onClick={() => onOpenTask(approval.sourceEntityId!)}>{t("Open task")}</button>
|
||||
<button type="button" className="link-button" style={{ fontSize: "var(--fs-body)" }} onClick={() => onOpenTask(approval.sourceEntityId!)}>{t("Open task")}</button>
|
||||
)}
|
||||
{drift && <Pill tone="amber">{drift === "changed" ? t("Changed since approval") : t("Removed since approval")}</Pill>}
|
||||
<Avatar name={requester} size="sm" bot />
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", width: 180, textAlign: "right", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", width: 180, textAlign: "right", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{decidedLabel} · {formatRelative(approval.decidedAt ?? approval.createdAt)}{superseded ? "" : ` · ${decider.split(" ")[0]}`}
|
||||
</div>
|
||||
</div>
|
||||
@@ -412,7 +411,7 @@ function BatchSummary({ total, result, onDismiss }: { total: number; result: Bat
|
||||
return (
|
||||
<div role="status" className="card" style={{ padding: 14, marginBottom: 16 }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<span style={{ fontWeight: 600 }}>{t("Approved {n} of {total}.", { n: result.approved.length, total })}</span>
|
||||
<span>{t("Approved {n} of {total}.", { n: result.approved.length, total })}</span>
|
||||
<span style={{ marginLeft: "auto" }}><Button size="small" tone="ghost" onClick={onDismiss}>{t("Dismiss")}</Button></span>
|
||||
</div>
|
||||
{result.failed.map((failure) => (
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1060 }}>
|
||||
<h2 style={{ margin: "0 0 18px", fontSize: 16, fontWeight: 600 }}>{t("Connectors")}</h2>
|
||||
<h2 style={{ margin: "0 0 18px", fontSize: "var(--fs-body)" }}>{t("Connectors")}</h2>
|
||||
|
||||
{connectError && <ErrorText message={connectError} />}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export function FilesPage({ snapshot, actions, focus }: PageProps) {
|
||||
|
||||
<div className="row" style={{ marginBottom: 10 }}>
|
||||
<div className="section-label">{labelFor(section)}</div>
|
||||
<div style={{ marginLeft: "auto", fontSize: 12, color: "var(--text-3)" }}>{t("Modified")} ↓</div>
|
||||
<div style={{ marginLeft: "auto", fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Modified")} ↓</div>
|
||||
</div>
|
||||
|
||||
{view === "list" ? (
|
||||
|
||||
@@ -115,12 +115,12 @@ function GoalCard({
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card-label" style={{ marginBottom: 12 }}>{t("Quarter goal")}</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 540, lineHeight: 1.4, letterSpacing: "-0.01em", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden", minHeight: 44 }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", lineHeight: 1.4, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden", minHeight: 44 }}>
|
||||
{goal ? goal.title : t("Set the first goal for {organizationName}.", { organizationName })}
|
||||
</div>
|
||||
<div className="row" style={{ alignItems: "baseline", gap: 8, marginTop: 16 }}>
|
||||
<span className="num" style={{ fontSize: 18, fontWeight: 600 }}>{progress ? `${percent}%` : "—"}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{progress?.pace ? statusLabel(progress.pace) : t("no target date")}</span>
|
||||
<span className="num" style={{ fontSize: "var(--fs-display)" }}>{progress ? `${percent}%` : "—"}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{progress?.pace ? statusLabel(progress.pace) : t("no target date")}</span>
|
||||
{goal && <TargetDate goal={goal} actions={actions} />}
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}><XpBar percent={percent} label={t("Goal progress")} /></div>
|
||||
@@ -152,21 +152,21 @@ function SpendCard({
|
||||
<section className="card">
|
||||
<div className="card-label" style={{ marginBottom: 12 }}>{t("Monthly budget")}</div>
|
||||
<div className="row" style={{ alignItems: "baseline", gap: 6 }}>
|
||||
<span className="num" style={{ fontSize: 18, fontWeight: 600 }}>{money(totalBudgetCents)}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{capCents > 0 ? `/ ${money(capCents)}` : t("allocated")}</span>
|
||||
<span className="num" style={{ fontSize: "var(--fs-display)" }}>{money(totalBudgetCents)}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{capCents > 0 ? `/ ${money(capCents)}` : t("allocated")}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}><XpBar percent={fillPct} label={t("Budget allocated")} /></div>
|
||||
<div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{connected.slice(0, 3).map((provider) => {
|
||||
const allocatedCents = agents.filter((agent) => agent.modelProviderId === provider.id).reduce((sum, agent) => sum + agent.budgetCents, 0);
|
||||
return (
|
||||
<div key={provider.id} className="row" style={{ fontSize: 14 }}>
|
||||
<div key={provider.id} className="row" style={{ fontSize: "var(--fs-body)" }}>
|
||||
<span style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</span>
|
||||
<span className="num mono" style={{ marginLeft: "auto", color: "var(--text-1)", fontWeight: 500 }}>{money(allocatedCents)}</span>
|
||||
<span className="num mono" style={{ marginLeft: "auto", color: "var(--text-1)" }}>{money(allocatedCents)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{connected.length === 0 && <span style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No providers connected")}</span>}
|
||||
{connected.length === 0 && <span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No providers connected")}</span>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -188,7 +188,7 @@ function RunActivityCard({
|
||||
<section className="card">
|
||||
<div className="row" style={{ marginBottom: 12 }}>
|
||||
<span className="card-label">{t("Agent runs")}</span>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: 12 }} onClick={onViewAll}>{t("All runs")} →</button>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: "var(--fs-body)" }} onClick={onViewAll}>{t("All runs")} →</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{recent.map((run) => {
|
||||
@@ -198,15 +198,15 @@ function RunActivityCard({
|
||||
<div key={run.id} className="row" style={{ gap: 10 }}>
|
||||
{agent && <Avatar name={agent.name} size="sm" bot />}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{task?.title ?? run.taskId}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{task?.title ?? run.taskId}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}</div>
|
||||
</div>
|
||||
<Pill tone={runStateTone(run.state)}>{runStateLabel(run.state)}</Pill>
|
||||
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(run.createdAt)}</span>
|
||||
<span className="mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(run.createdAt)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{recent.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No runs yet")}</div>}
|
||||
{recent.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No runs yet")}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -230,7 +230,7 @@ function RecentMessagesCard({
|
||||
<section className="card">
|
||||
<div className="row" style={{ marginBottom: 12 }}>
|
||||
<span className="card-label">{t("Recent messages")}</span>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: 12 }} onClick={onViewAll}>{t("Open chat")} →</button>
|
||||
<button type="button" className="link-button" style={{ marginLeft: "auto", fontSize: "var(--fs-body)" }} onClick={onViewAll}>{t("Open chat")} →</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{recent.map((msg) => {
|
||||
@@ -244,16 +244,16 @@ function RecentMessagesCard({
|
||||
<Avatar name={name} size="sm" bot={msg.senderType === "agent"} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 540 }}>{name}</span>
|
||||
{space && <span style={{ fontSize: 12, color: "var(--text-3)" }}>#{space.name}</span>}
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatTime(msg.createdAt)}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{name}</span>
|
||||
{space && <span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>#{space.name}</span>}
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: "var(--fs-body)", color: "var(--text-3)", flexShrink: 0 }}>{formatTime(msg.createdAt)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.4 }}>{preview}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.4 }}>{preview}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{recent.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No messages yet")}</div>}
|
||||
{recent.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No messages yet")}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -313,7 +313,7 @@ function TargetDate({ goal, actions }: { goal: GoalRecord; actions: PageProps["a
|
||||
};
|
||||
return (
|
||||
<span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
<input className="input" type="date" aria-label={t("Target date")} value={value} onChange={(event) => change(event.target.value)} style={{ height: 26, padding: "0 6px", fontSize: 12 }} />
|
||||
<input className="input" type="date" aria-label={t("Target date")} value={value} onChange={(event) => change(event.target.value)} style={{ height: 26, padding: "0 6px", fontSize: "var(--fs-body)" }} />
|
||||
{error && <span className="form-error" role="alert">{error}</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ export function LibraryPage({ snapshot, actions, focus }: PageProps) {
|
||||
return (
|
||||
<div className="split-main">
|
||||
<aside className="secondary-pane">
|
||||
<div style={{ fontSize: 14, fontWeight: 600, padding: "4px 8px 14px" }}>{t("Library")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", padding: "4px 8px 14px" }}>{t("Library")}</div>
|
||||
<div className="nav" style={{ gap: 1 }}>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
|
||||
@@ -90,8 +90,8 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
<div className="split-main">
|
||||
<aside className="secondary-pane" style={{ width: 340 }}>
|
||||
<div style={{ padding: "12px 12px 8px" }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>{t("Runs")}</div>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 8, fontSize: 12, color: "var(--text-3)" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", marginBottom: 8 }}>{t("Runs")}</div>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 8, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
<span>{t("{n} queued", { n: queued })}</span><span>·</span><span>{t("{n} running", { n: running })}</span><span>·</span><span>{t("{n} failed", { n: failed })}</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -118,10 +118,10 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
onClick={() => setSelectedId(run.id)}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{task?.title ?? run.taskId}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,11 +129,11 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filtered.length === 0 && <div style={{ padding: 16, fontSize: 14, color: "var(--text-3)" }}>{t("No runs")}</div>}
|
||||
{filtered.length === 0 && <div style={{ padding: 16, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No runs")}</div>}
|
||||
{historyError && <div className="form-error" role="alert" style={{ padding: "0 16px" }}>{historyError}</div>}
|
||||
<div style={{ padding: 12, display: "flex", justifyContent: "center" }}>
|
||||
{olderDone
|
||||
? <span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("These are all the runs.")}</span>
|
||||
? <span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("These are all the runs.")}</span>
|
||||
: <Button size="small" onClick={loadOlder}>{t("Show older runs")}</Button>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
{selected ? (
|
||||
<RunDetail key={selected.id} run={selected} tasks={tasks} agents={agents} providers={providers} actions={actions} onOpenTask={(taskId) => navigate("work", { kind: "task", id: taskId })} />
|
||||
) : (
|
||||
<div style={{ display: "grid", placeItems: "center", height: "100%", color: "var(--text-3)", fontSize: 14 }}>
|
||||
<div style={{ display: "grid", placeItems: "center", height: "100%", color: "var(--text-3)", fontSize: "var(--fs-body)" }}>
|
||||
{t("Select a run")}
|
||||
</div>
|
||||
)}
|
||||
@@ -190,10 +190,10 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
<div className="row" style={{ gap: 10, marginBottom: 16 }}>
|
||||
{agent && <Avatar name={agent.name} size="sm" bot />}
|
||||
<div>
|
||||
<button type="button" className="link-button" style={{ fontSize: 16, fontWeight: 600 }} aria-label={t("Open task {title}", { title: task?.title ?? run.taskId })} onClick={() => onOpenTask(run.taskId)}>
|
||||
<button type="button" className="link-button" style={{ fontSize: "var(--fs-body)" }} aria-label={t("Open task {title}", { title: task?.title ?? run.taskId })} onClick={() => onOpenTask(run.taskId)}>
|
||||
{task?.title ?? run.taskId}
|
||||
</button>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{run.id}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{run.id}</div>
|
||||
</div>
|
||||
<Pill tone={runTone(run.state)}>{runLabel(run.state)}</Pill>
|
||||
{run.state === "running" && <Button size="small" onClick={stop} disabled={busy}>{t("Stop")}</Button>}
|
||||
@@ -201,13 +201,13 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
{run.state === "failed" && <Button size="small" onClick={again} disabled={busy}>{busy ? t("Running…") : t("Retry")}</Button>}
|
||||
{(run.state === "stopped" || run.state === "superseded") && <Button size="small" onClick={again} disabled={busy}>{busy ? t("Running…") : t("Resume")}</Button>}
|
||||
</div>
|
||||
{run.state === "queued" && <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 8 }}>{t("A cancelled session can start again on the next heartbeat; block the task to keep it stopped.")}</div>}
|
||||
{run.state === "queued" && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", marginBottom: 8 }}>{t("A cancelled session can start again on the next heartbeat; block the task to keep it stopped.")}</div>}
|
||||
{(run.state === "failed" || run.state === "stopped" || run.state === "superseded") && (
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 8 }}>{t("A new session on the task picks up from what earlier sessions did and where they stopped.")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", marginBottom: 8 }}>{t("A new session on the task picks up from what earlier sessions did and where they stopped.")}</div>
|
||||
)}
|
||||
{controlError && <div className="form-error" role="alert">{controlError}</div>}
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: 14, marginBottom: 20 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: "var(--fs-body)", marginBottom: 20 }}>
|
||||
<MetaLabel>{t("Agent")}</MetaLabel><div>{agent?.name ?? "—"} ({agent?.role ?? "—"})</div>
|
||||
<MetaLabel>{t("Provider")}</MetaLabel><div>{provider?.label ?? "—"}</div>
|
||||
<MetaLabel>{t("Model")}</MetaLabel><div>{run.runModel ?? t("Chosen when the session starts")}</div>
|
||||
@@ -216,7 +216,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
{run.startedAt && (<><MetaLabel>{t("Started")}</MetaLabel><div>{formatRelative(run.startedAt)}</div></>)}
|
||||
{run.completedAt && (<><MetaLabel>{t("Completed")}</MetaLabel><div>{formatRelative(run.completedAt)}</div></>)}
|
||||
{durationMs !== null && (<><MetaLabel>{t("Duration")}</MetaLabel><div>{formatDuration(durationMs)}</div></>)}
|
||||
<MetaLabel>{t("Context")}</MetaLabel><div className="mono" style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis" }}>{run.executionContextId}</div>
|
||||
<MetaLabel>{t("Context")}</MetaLabel><div className="mono" style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis" }}>{run.executionContextId}</div>
|
||||
</div>
|
||||
|
||||
<RunRequestList loaded={loadedRequests} />
|
||||
@@ -224,28 +224,28 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
{run.state === "superseded" && run.errorMessage && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<div className="section-label" style={{ marginBottom: 8 }}>{t("Why it did not run")}</div>
|
||||
<div style={{ fontSize: 14, color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.errorMessage}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.errorMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
{run.state === "failed" && run.errorMessage && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16, borderColor: "var(--red)" }}>
|
||||
<div className="section-label" style={{ color: "var(--red)", marginBottom: 8 }}>{t("Error")}</div>
|
||||
<div style={{ fontSize: 14, color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.errorMessage}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.errorMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{run.capabilityWarnings && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16, borderColor: "var(--amber)" }}>
|
||||
<div className="section-label" style={{ color: "var(--amber)", marginBottom: 8 }}>{t("Missing this run")}</div>
|
||||
<div style={{ fontSize: 14, color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.capabilityWarnings}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-1)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{run.capabilityWarnings}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<div className="section-label" style={{ marginBottom: 8 }}>{t("Provider request")}</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: 14 }}>
|
||||
<MetaLabel>{t("Request ID")}</MetaLabel><div className="mono" style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis" }}>{request.id}</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: "var(--fs-body)" }}>
|
||||
<MetaLabel>{t("Request ID")}</MetaLabel><div className="mono" style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis" }}>{request.id}</div>
|
||||
<MetaLabel>{t("Model")}</MetaLabel><div>{request.model}</div>
|
||||
<MetaLabel>{t("State")}</MetaLabel><div>{requestStateLabel(request.state)}</div>
|
||||
<MetaLabel>{t("Input chars")}</MetaLabel><div className="num">{request.inputCharCount.toLocaleString()}</div>
|
||||
@@ -257,7 +257,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div className="section-label" style={{ marginBottom: 6 }}>{t("Response")}</div>
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-2)",
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: 1.5,
|
||||
@@ -265,7 +265,6 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
overflow: "auto",
|
||||
padding: 10,
|
||||
background: "var(--panel-3)",
|
||||
borderRadius: 8,
|
||||
fontFamily: "var(--font-mono)",
|
||||
}}>
|
||||
{request.responseText}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SettingsPage({ snapshot, actions, focus }: PageProps) {
|
||||
return (
|
||||
<div className="split-main">
|
||||
<aside className="secondary-pane">
|
||||
<div style={{ fontSize: 14, fontWeight: 600, padding: "4px 8px 14px" }}>{t("Settings")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", padding: "4px 8px 14px" }}>{t("Settings")}</div>
|
||||
<div className="pane-scroll">
|
||||
<NavGroup items={TABS} active={tab} onChange={(next) => requestLeave(() => setTab(next))} />
|
||||
</div>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function TeamPage({ snapshot, actions, focus }: PageProps) {
|
||||
<input className="input" name="modelId" list="hire-model-suggestions" value={hireModelId} disabled={!hireProviderId} placeholder={providers.find((provider) => provider.id === hireProviderId)?.model ?? ""} onChange={(event) => setHireModelId(event.currentTarget.value)} />
|
||||
</Field>
|
||||
<ModelSuggestions id="hire-model-suggestions" providerId={hireProviderId} providers={providers} catalog={snapshot.providerCatalog} />
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
|
||||
<Field label={t("Manager")}>
|
||||
<select className="input" name="managerAgentId" defaultValue="">
|
||||
<option value="">{t("None")}</option>
|
||||
@@ -181,13 +181,13 @@ export function TeamPage({ snapshot, actions, focus }: PageProps) {
|
||||
<Field label={t("Department")}><input className="input" name="department" defaultValue={editingAgent.department} required /></Field>
|
||||
<Field label={t("Monthly budget")}><input className="input" name="budget" type="number" min="0" step="1" defaultValue={editingAgent.budgetCents / 100} /></Field>
|
||||
<EditModelFields agent={editingAgent} providers={providers} catalog={snapshot.providerCatalog} />
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
|
||||
<Field label={t("Seniority")}>
|
||||
{/* The CEO's rank carries company-wide powers, so it is shown, never picked. */}
|
||||
{editingAgent.roleLevel === "c_suite" ? (
|
||||
<>
|
||||
<input type="hidden" name="roleLevel" value="c_suite" />
|
||||
<div style={{ fontSize: 13, color: "var(--text-2)" }}>{t("CEO: hires officers, grants connectors and skills, and closes goals. This rank cannot be changed.")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-2)" }}>{t("CEO: hires officers, grants connectors and skills, and closes goals. This rank cannot be changed.")}</div>
|
||||
</>
|
||||
) : (
|
||||
<select className="input" name="roleLevel" defaultValue={editingAgent.roleLevel}>
|
||||
@@ -301,8 +301,8 @@ function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onE
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
<Avatar name={row.name} size="sm" bot={!row.human} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.role}</div>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.name}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.dept}</div>
|
||||
@@ -310,7 +310,7 @@ function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onE
|
||||
style={{
|
||||
color: "var(--text-2)",
|
||||
fontFamily: row.human ? "inherit" : "var(--font-mono)",
|
||||
fontSize: row.human ? 13 : 12,
|
||||
fontSize: "var(--fs-body)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -326,7 +326,7 @@ function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onE
|
||||
<div style={{ flex: 1, maxWidth: 100 }}>
|
||||
<XpBar percent={row.budgetCapCents && row.budgetCapCents > 0 ? (row.budgetCents / row.budgetCapCents) * 100 : 0} label={t("Budget compared with the largest agent budget")} />
|
||||
</div>
|
||||
<span className="num mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{money(row.budgetCents)}</span>
|
||||
<span className="num mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{money(row.budgetCents)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
? t("Goal: {title}", { title: goals.find((goal) => goal.id === scope.id)?.title ?? scope.id })
|
||||
: t("Project: {name}", { name: projects.find((project) => project.id === scope.id)?.name ?? scope.id })}
|
||||
</Pill>
|
||||
<button type="button" className="link-button" style={{ fontSize: 12 }} onClick={() => setScope(null)}>{t("Show all work")}</button>
|
||||
<button type="button" className="link-button" style={{ fontSize: "var(--fs-body)" }} onClick={() => setScope(null)}>{t("Show all work")}</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="page-sub">
|
||||
@@ -287,9 +287,9 @@ function BoardView({
|
||||
return (
|
||||
<div key={column.id} style={{ display: "flex", flexDirection: "column", minWidth: 0, minHeight: 0 }}>
|
||||
<div className="row" style={{ padding: "4px 6px 8px", flexShrink: 0 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: column.tone, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 540 }}>{column.label}</span>
|
||||
<span className="num" style={{ fontSize: 12, color: "var(--text-3)" }}>{columnTasks.length}</span>
|
||||
<span style={{ width: 8, height: 8, background: column.tone, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{column.label}</span>
|
||||
<span className="num" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{columnTasks.length}</span>
|
||||
<IconButton icon="plus" label={t("Add task")} onClick={onCreate} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 8, paddingBottom: 24 }}>
|
||||
@@ -342,7 +342,6 @@ function TaskCard({
|
||||
style={{
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -352,15 +351,13 @@ function TaskCard({
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 6 }}>
|
||||
{task.priority === "high" && <span style={{ width: 3, height: 14, background: "var(--red)", borderRadius: 2, marginTop: 2, flexShrink: 0 }} />}
|
||||
{task.priority === "high" && <span style={{ width: 3, height: 14, background: "var(--red)", marginTop: 2, flexShrink: 0 }} />}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: 14,
|
||||
fontWeight: 480,
|
||||
fontSize: "var(--fs-body)",
|
||||
lineHeight: 1.35,
|
||||
letterSpacing: "-0.005em",
|
||||
color: task.status === "done" ? "var(--text-3)" : "var(--text-1)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
|
||||
@@ -45,14 +45,14 @@ export function ApprovalDetailDialog({
|
||||
<IconButton icon="x" label={t("Close")} onClick={onClose} />
|
||||
</div>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 6, marginBottom: 14 }}>
|
||||
<span style={{ fontSize: 12, color: tone.color, fontWeight: 540, letterSpacing: "0.02em" }}>{tone.label}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: tone.color }}>{tone.label}</span>
|
||||
<span style={{ color: "var(--text-4)" }}>·</span>
|
||||
<Pill>{approvalKindLabel(approval.kind)}</Pill>
|
||||
</div>
|
||||
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: 14, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>
|
||||
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: "var(--fs-body)", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>
|
||||
{approval.details}
|
||||
</p>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "10px 14px", fontSize: 12 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "10px 14px", fontSize: "var(--fs-body)" }}>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Requested by")}</span>
|
||||
<span>{requester}</span>
|
||||
{approval.sourceEntityType && (
|
||||
@@ -117,8 +117,8 @@ export function ApprovalRecordDialog({ approval, requester, decider, drift, onCl
|
||||
<Pill>{approvalKindLabel(approval.kind)}</Pill>
|
||||
{drift && <Pill tone="amber">{drift === "changed" ? t("Changed since approval") : t("Removed since approval")}</Pill>}
|
||||
</div>
|
||||
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: 14, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{approval.details}</p>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "110px 1fr", gap: "10px 14px", fontSize: 12 }}>
|
||||
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: "var(--fs-body)", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{approval.details}</p>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "110px 1fr", gap: "10px 14px", fontSize: "var(--fs-body)" }}>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Requested by")}</span>
|
||||
<span>{requester} · {formatRelative(approval.createdAt)}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Decided by")}</span>
|
||||
|
||||
@@ -80,7 +80,7 @@ export function ChatComposer({
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} onInput={() => { if (spaceId && ref.current) noteTyping(spaceId, ref.current.value); }} data-space-id={spaceId ?? undefined} style={{ padding: 14, paddingTop: 4, flexShrink: 0 }}>
|
||||
<div style={{ border: "1px solid var(--border-strong)", borderRadius: 12, background: "var(--panel)", padding: "10px 12px" }}>
|
||||
<div style={{ border: "1px solid var(--border-strong)", background: "var(--panel)", padding: "10px 12px" }}>
|
||||
<MentionTextarea
|
||||
name="message"
|
||||
defaultValue={draft.body}
|
||||
@@ -111,7 +111,7 @@ export function ChatComposer({
|
||||
</span>
|
||||
))}
|
||||
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{shortcutLabel("Enter")}</span>
|
||||
<span className="mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{shortcutLabel("Enter")}</span>
|
||||
<Button tone="primary" size="small" type="submit" disabled={locked}><Icon name="send" /> {t("Send")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,7 +239,7 @@ export function ChatConversation({
|
||||
</div>
|
||||
{!atBottom && (
|
||||
<button type="button" className="jump-latest" onClick={() => jumpToLatest("smooth")}>
|
||||
<span aria-hidden style={{ fontSize: 13, lineHeight: 1 }}>↓</span>
|
||||
<span aria-hidden style={{ fontSize: "var(--fs-body)", lineHeight: 1 }}>↓</span>
|
||||
{t("Jump to latest")}
|
||||
</button>
|
||||
)}
|
||||
@@ -298,10 +298,10 @@ function ConversationHeader({
|
||||
return (
|
||||
<header className="row" style={{ height: 52, padding: "0 24px", borderBottom: "1px solid var(--border-soft)", flexShrink: 0 }}>
|
||||
<Icon name={space.visibility === "restricted" ? "lock" : "hash"} color="var(--text-3)" />
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{space.name}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{space.name}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-3)",
|
||||
borderLeft: "1px solid var(--border)",
|
||||
paddingLeft: 10,
|
||||
@@ -341,20 +341,20 @@ function MembersButton({ space, members }: { space: ChatSpaceRecord; members: Sp
|
||||
trigger={({ toggle }) => (
|
||||
<button type="button" onClick={toggle} aria-label={t("Members ({count})", { count: String(members.length) })} style={{ display: "flex", alignItems: "center", gap: 6, border: 0, background: "transparent", cursor: "pointer", padding: 0 }}>
|
||||
<AvatarStack names={members.slice(0, 5).map((member) => member.name)} />
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{members.length}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{members.length}</span>
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
{() => (
|
||||
// A large roster scrolls inside the window instead of running past it.
|
||||
<div className="members-roster" style={{ padding: 12, width: 320, maxHeight: "min(480px, calc(100vh - 120px))", overflowY: "auto", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{spaceReaders(space)}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{spaceReaders(space)}</div>
|
||||
{members.map((member) => (
|
||||
<div key={member.id} className="row" style={{ gap: 8, alignItems: "flex-start" }}>
|
||||
<Avatar name={member.name} size="sm" bot={member.bot} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 540 }}>{member.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{member.duty}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)" }}>{member.name}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{member.duty}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -84,9 +84,9 @@ export function ChatMessageRow({
|
||||
<Avatar name={name} bot={bot} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row" style={{ alignItems: "baseline", gap: 8, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 540 }}>{name}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{formatTime(message.createdAt)}</span>
|
||||
{message.editedAt && !deleted && <span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("(edited)")}</span>}
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{name}</span>
|
||||
<span className="mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{formatTime(message.createdAt)}</span>
|
||||
{message.editedAt && !deleted && <span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("(edited)")}</span>}
|
||||
{!editing && (canModify || (onOpenThread && !deleted)) && (
|
||||
<span style={{ marginLeft: "auto", display: "flex", gap: 2 }}>
|
||||
{onOpenThread && !deleted && <IconButton icon="chat" label={t("Reply in thread")} onClick={onOpenThread} />}
|
||||
@@ -102,7 +102,7 @@ export function ChatMessageRow({
|
||||
? t("Replying in thread: {snippet}", { snippet: thread.replyTo.snippet })
|
||||
: t("Replying to {name}: {snippet}", { name: thread.replyTo.name, snippet: thread.replyTo.snippet })}
|
||||
onClick={onOpenThread}
|
||||
style={{ display: "block", maxWidth: "100%", margin: "0 0 4px", padding: 0, border: 0, background: "transparent", color: "var(--text-3)", fontSize: 12, textAlign: "left", cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
style={{ display: "block", maxWidth: "100%", margin: "0 0 4px", padding: 0, border: 0, background: "transparent", color: "var(--text-3)", fontSize: "var(--fs-body)", textAlign: "left", cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
↳ {thread.replyTo.name === null ? thread.replyTo.snippet : `${thread.replyTo.name}: ${thread.replyTo.snippet}`}
|
||||
</button>
|
||||
@@ -113,7 +113,7 @@ export function ChatMessageRow({
|
||||
</div>
|
||||
)}
|
||||
{deleted ? (
|
||||
<div style={{ fontSize: 14, color: "var(--text-3)", fontStyle: "italic" }}>{t("Message deleted")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", fontStyle: "italic" }}>{t("Message deleted")}</div>
|
||||
) : editing ? (
|
||||
<form onSubmit={submitEdit} onInput={() => setChanged(true)}>
|
||||
<textarea
|
||||
@@ -132,7 +132,7 @@ export function ChatMessageRow({
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ fontSize: 14, color: "var(--text-1)", lineHeight: 1.5, wordBreak: "break-word" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-1)", lineHeight: 1.5, wordBreak: "break-word" }}>
|
||||
{renderBody(message.bodyMd, messageMentions, context)}
|
||||
</div>
|
||||
)}
|
||||
@@ -146,17 +146,17 @@ export function ChatMessageRow({
|
||||
</div>
|
||||
)}
|
||||
{thread && thread.replyCount > 0 && (
|
||||
<button type="button" onClick={onOpenThread} style={{ marginTop: 4, padding: 0, border: 0, background: "transparent", color: "var(--accent)", fontSize: 12, cursor: "pointer" }}>
|
||||
<button type="button" onClick={onOpenThread} style={{ marginTop: 4, padding: 0, border: 0, background: "transparent", color: "var(--accent)", fontSize: "var(--fs-body)", cursor: "pointer" }}>
|
||||
{thread.replyCount === 1 ? t("1 reply") : t("{count} replies", { count: String(thread.replyCount) })}
|
||||
</button>
|
||||
)}
|
||||
{replyStatuses.filter((status) => status.state !== "replied").map((status) => (
|
||||
<div key={`${status.agentId}-${status.state}`} role="status" style={{ fontSize: 12, color: status.state === "failed" || status.state === "expired" ? "var(--red)" : "var(--text-3)", marginTop: 4 }}>
|
||||
<div key={`${status.agentId}-${status.state}`} role="status" style={{ fontSize: "var(--fs-body)", color: status.state === "failed" || status.state === "expired" ? "var(--red)" : "var(--text-3)", marginTop: 4 }}>
|
||||
{replyStatusText(status, context)}
|
||||
</div>
|
||||
))}
|
||||
{readers.length > 0 && (
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 4 }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", marginTop: 4 }}>
|
||||
{t("Read by {names}", { names: readers.join(", ") })}
|
||||
</div>
|
||||
)}
|
||||
@@ -200,8 +200,6 @@ function renderBody(body: string, mentions: ChatMentionRecord[], context: Messag
|
||||
background: failed ? "rgba(255,100,80,0.1)" : "var(--accent-soft)",
|
||||
color: failed ? "var(--text-3)" : "var(--accent)",
|
||||
padding: "1px 5px",
|
||||
borderRadius: 4,
|
||||
fontWeight: 500,
|
||||
textDecoration: failed ? "line-through" : undefined,
|
||||
}}
|
||||
>
|
||||
@@ -235,7 +233,6 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
|
||||
style={{
|
||||
maxWidth: 360,
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -247,7 +244,6 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
background: "var(--panel-3)",
|
||||
color: "var(--text-2)",
|
||||
display: "grid",
|
||||
@@ -258,14 +254,14 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row" style={{ gap: 6, minWidth: 0 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
|
||||
{handoff && (
|
||||
<span className="pill violet" style={{ flexShrink: 0 }}>
|
||||
<Icon name="bot" /> {t("AI → AI")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{fileKindLabel(file.kind)}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{fileKindLabel(file.kind)}</div>
|
||||
</div>
|
||||
{canOpen && <IconButton icon="arrowUpRight" label={t("Open {name}", { name: file.name })} onClick={onOpen ?? undefined} />}
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ export function ChatSidebar({
|
||||
return (
|
||||
<aside className="secondary-pane" style={{ width: paneWidth, position: "relative" }}>
|
||||
<div className="row" style={{ padding: "4px 8px 12px" }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>{t("Messages")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)" }}>{t("Messages")}</div>
|
||||
<IconButton icon="plus" label={t("New space")} onClick={onCreateSpace} />
|
||||
</div>
|
||||
<div className="pane-scroll">
|
||||
@@ -147,7 +147,6 @@ function ConversationRow({
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 6,
|
||||
background: "var(--panel-3)",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
@@ -163,7 +162,6 @@ function ConversationRow({
|
||||
<span className="row" style={{ gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: conversation.unread > 0 ? 600 : 520,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -174,14 +172,14 @@ function ConversationRow({
|
||||
{conversation.title}
|
||||
</span>
|
||||
{conversation.isCeo && <span className="convo-ceo-badge">{t("CEO")}</span>}
|
||||
<span style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", flexShrink: 0 }}>
|
||||
{conversation.preview ? formatRelative(conversation.lastActivityAt) : ""}
|
||||
</span>
|
||||
</span>
|
||||
<span className="row" style={{ gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-3)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
|
||||
@@ -97,7 +97,7 @@ export function ChatTimeline({
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
elements.push(<div key="history-start" style={{ textAlign: "center", fontSize: 12, color: "var(--text-3)", marginBottom: 16 }}>{t("This is the start of the conversation.")}</div>);
|
||||
elements.push(<div key="history-start" style={{ textAlign: "center", fontSize: "var(--fs-body)", color: "var(--text-3)", marginBottom: 16 }}>{t("This is the start of the conversation.")}</div>);
|
||||
}
|
||||
let lastDay = "";
|
||||
for (const message of visible) {
|
||||
@@ -138,7 +138,7 @@ function DayDivider({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="row" style={{ gap: 12, margin: "8px 0 16px" }}>
|
||||
<div style={{ flex: 1, height: 1, background: "var(--border-soft)" }} />
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", fontWeight: 540 }}>{label}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{label}</div>
|
||||
<div style={{ flex: 1, height: 1, background: "var(--border-soft)" }} />
|
||||
</div>
|
||||
);
|
||||
@@ -146,14 +146,14 @@ function DayDivider({ label }: { label: string }) {
|
||||
|
||||
function ApprovalInline({ approval, onApprove, onDecline }: { approval: ApprovalRecord; onApprove: () => void; onDecline: () => void }) {
|
||||
return (
|
||||
<div style={{ margin: "14px 0 18px 44px", maxWidth: 480, border: "1px solid var(--border-strong)", borderRadius: 12, background: "var(--panel)", padding: 14 }}>
|
||||
<div style={{ margin: "14px 0 18px 44px", maxWidth: 480, border: "1px solid var(--border-strong)", background: "var(--panel)", padding: 14 }}>
|
||||
<div className="row" style={{ marginBottom: 8 }}>
|
||||
<Icon name="approve" color="var(--amber)" />
|
||||
<span style={{ fontSize: 12, color: "var(--amber)", fontWeight: 540 }}>{t("Approval needed")}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--amber)" }}>{t("Approval needed")}</span>
|
||||
<Pill tone={approval.risk === "high" ? "red" : approval.risk === "medium" ? "amber" : undefined}>{riskLabel(approval.risk)}</Pill>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 540, marginBottom: 4 }}>{approval.title}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 12, lineHeight: 1.5 }}>{approval.details}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", marginBottom: 4 }}>{approval.title}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", marginBottom: 12, lineHeight: 1.5 }}>{approval.details}</div>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<Button tone="primary" size="small" onClick={onApprove}>{t("Approve")}</Button>
|
||||
<Button size="small" onClick={onDecline}>{t("Decline")}</Button>
|
||||
|
||||
@@ -125,7 +125,6 @@ export function MentionTextarea({
|
||||
marginBottom: 4,
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
|
||||
maxHeight: 200,
|
||||
overflow: "auto",
|
||||
@@ -149,13 +148,13 @@ export function MentionTextarea({
|
||||
border: 0,
|
||||
background: i === index ? "var(--accent-soft)" : "transparent",
|
||||
cursor: "pointer",
|
||||
fontSize: 14,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-1)",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Avatar name={c.displayName} size="sm" bot={c.bot} />
|
||||
<span style={{ fontWeight: 500 }}>{c.label}</span>
|
||||
<span>{c.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -175,7 +174,7 @@ export function MentionTextarea({
|
||||
border: 0,
|
||||
resize: "none",
|
||||
outline: "none",
|
||||
fontSize: 14,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-1)",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { t } from "../../i18n/i18n";
|
||||
export type SpaceType = "company" | "division" | "department" | "project" | "pod" | "ad_hoc" | "direct";
|
||||
export type Visibility = "workspace" | "restricted";
|
||||
|
||||
const hint = { fontSize: 12, color: "var(--text-3)", margin: 0, lineHeight: 1.5 } as const;
|
||||
const hint = { fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0, lineHeight: 1.5 } as const;
|
||||
|
||||
/** Each choice says what it changes: who is a member, who can read it, and which agents a message wakes. */
|
||||
export function NewSpaceDialog({ open, employee, employees, agents, onClose, onSubmit }: {
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ThreadPanel({
|
||||
return (
|
||||
<aside role="complementary" aria-label={t("Thread")} style={{ width: 360, flexShrink: 0, display: "flex", flexDirection: "column", borderLeft: "1px solid var(--border-soft)", background: "var(--panel)" }}>
|
||||
<header className="row" style={{ height: 52, padding: "0 16px", borderBottom: "1px solid var(--border-soft)", flexShrink: 0 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{t("Thread")}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{t("Thread")}</span>
|
||||
<span style={{ marginLeft: "auto" }}><IconButton icon="x" label={t("Close thread")} onClick={onClose} /></span>
|
||||
</header>
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "16px 16px 8px" }}>
|
||||
|
||||
@@ -7,14 +7,14 @@ import type { AccountSync } from "./useConnectorCatalog";
|
||||
export function AccountSyncNote({ sync, onRetry }: { sync: AccountSync; onRetry: () => void }) {
|
||||
if (sync.phase === "failed") {
|
||||
return (
|
||||
<div role="alert" className="row" style={{ gap: 10, fontSize: 13, marginBottom: 14, color: "var(--red)" }}>
|
||||
<span style={{ flex: 1, minWidth: 0, wordBreak: "break-word" }}>{t("Account status could not be checked with Composio: {reason}. The statuses below may be out of date.", { reason: sync.message })}</span>
|
||||
<div role="alert" className="row" style={{ gap: 10, fontSize: "var(--fs-body)", marginBottom: 14, color: "var(--red)" }}>
|
||||
<span style={{ flex: 1, minWidth: 0, wordBreak: "break-word" }}>{t("Account status could not be checked with Composio, so the statuses below may be out of date. Reason: {reason}", { reason: sync.message })}</span>
|
||||
<Button size="small" onClick={onRetry}>{t("Retry")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div role="status" style={{ fontSize: 12, marginBottom: 14, color: "var(--text-3)" }}>
|
||||
<div role="status" style={{ fontSize: "var(--fs-body)", marginBottom: 14, color: "var(--text-3)" }}>
|
||||
{sync.phase === "syncing" ? t("Checking account status with Composio…") : t("Account status checked with Composio at {time}.", { time: formatTime(sync.at) })}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function ConnectedList({
|
||||
const grantable = agents.filter((agent) => agent.roleLevel !== "c_suite" && !grants.some((grant) => grant.agentId === agent.id && grant.toolkitSlug === account.toolkitSlug));
|
||||
const busy = busySlug !== null;
|
||||
return (
|
||||
<div key={account.id} style={{ border: "1px solid var(--border)", borderRadius: 12, padding: 14, background: "var(--panel)" }}>
|
||||
<div key={account.id} style={{ border: "1px solid var(--border)", padding: 14, background: "var(--panel)" }}>
|
||||
<div className="row" style={{ gap: 10 }}>
|
||||
<ToolkitTile
|
||||
name={account.toolkitName}
|
||||
@@ -76,8 +76,8 @@ export function ConnectedList({
|
||||
logoUrl={logoBySlug.get(account.toolkitSlug) ?? null}
|
||||
size={30}
|
||||
/>
|
||||
<span style={{ fontWeight: 600, fontSize: 13.5 }}>{account.toolkitName}</span>
|
||||
<span style={{ color: "var(--text-2)", fontSize: 12.5 }}>{account.label}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{account.toolkitName}</span>
|
||||
<span style={{ color: "var(--text-2)", fontSize: "var(--fs-body)" }}>{account.label}</span>
|
||||
<Pill tone={STATUS_TONE[account.status]} dot>{account.status.charAt(0) + account.status.slice(1).toLowerCase()}</Pill>
|
||||
{account.pendingAccountId && <Pill tone="amber" dot>{t("Reconnect waiting for authorization")}</Pill>}
|
||||
<span className="row" style={{ gap: 6, marginLeft: "auto" }}>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function SignInStatus({ signIn, onStop, onCheckAgain }: { signIn: SignIn;
|
||||
const name = signIn.slug;
|
||||
const failed = signIn.phase === "failed" || signIn.phase === "timed_out";
|
||||
return (
|
||||
<div role={failed ? "alert" : "status"} className="row" style={{ gap: 10, fontSize: 13, marginBottom: 14, color: failed ? "var(--red)" : "var(--text-2)" }}>
|
||||
<div role={failed ? "alert" : "status"} className="row" style={{ gap: 10, fontSize: "var(--fs-body)", marginBottom: 14, color: failed ? "var(--red)" : "var(--text-2)" }}>
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
{signIn.phase === "waiting" && t("Waiting for the {name} sign-in in your browser (check {check} of {checks}).", { name, check: signIn.check, checks: signIn.checks })}
|
||||
{signIn.phase === "connected" && t("{name} ({label}) is connected.", { name, label: signIn.label })}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ToolkitTile({ name, slug, logoUrl, size = 34 }: { name: string;
|
||||
alt=""
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ width: size, height: size, borderRadius: 9, objectFit: "cover", flexShrink: 0 }}
|
||||
style={{ width: size, height: size, objectFit: "cover", flexShrink: 0 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -29,14 +29,12 @@ export function ToolkitTile({ name, slug, logoUrl, size = 34 }: { name: string;
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: 9,
|
||||
background: tileColor(slug),
|
||||
color: "#fff",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: size * 0.44,
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--fs-body)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -67,14 +65,14 @@ export function ToolkitCatalog({
|
||||
<section>
|
||||
<div className="row" style={{ marginBottom: 12, gap: 10 }}>
|
||||
<div className="section-label" style={{ flex: 1 }}>{t("Catalog")}</div>
|
||||
<div className="row" style={{ gap: 6, background: "var(--panel-2)", borderRadius: 8, padding: "5px 10px", width: 240 }}>
|
||||
<div className="row" style={{ gap: 6, background: "var(--panel-2)", padding: "5px 10px", width: 240 }}>
|
||||
<Icon name="search" color="var(--text-3)" />
|
||||
<input
|
||||
aria-label={t("Search connectors")}
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("Search connectors")}
|
||||
style={{ border: 0, outline: 0, background: "transparent", fontSize: 13, flex: 1, minWidth: 0, color: "var(--text-1)" }}
|
||||
style={{ border: 0, outline: 0, background: "transparent", fontSize: "var(--fs-body)", flex: 1, minWidth: 0, color: "var(--text-1)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +84,6 @@ export function ToolkitCatalog({
|
||||
key={entry.slug}
|
||||
style={{
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
background: "var(--panel)",
|
||||
display: "flex",
|
||||
@@ -95,7 +92,7 @@ export function ToolkitCatalog({
|
||||
}}
|
||||
>
|
||||
<ToolkitTile name={entry.name} slug={entry.slug} logoUrl={entry.logoUrl} />
|
||||
<span style={{ fontWeight: 600, fontSize: 13.5, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<span style={{ fontSize: "var(--fs-body)", flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{entry.name}
|
||||
</span>
|
||||
{connected ? (
|
||||
|
||||
@@ -20,14 +20,11 @@ export function FileIcon({ kind, big = false }: { kind: string; big?: boolean })
|
||||
style={{
|
||||
width: big ? 28 : 22,
|
||||
height: big ? 28 : 22,
|
||||
borderRadius: big ? 6 : 4,
|
||||
background: `${map.color}14`,
|
||||
color: map.color,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontSize: big ? 9 : 8,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: "var(--fs-body)",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -76,20 +76,20 @@ export function PinnedCard({
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 540, letterSpacing: "-0.005em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{file.name}
|
||||
</div>
|
||||
<div className="mono" style={{ color: "var(--text-3)", fontSize: 10, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div className="mono" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
|
||||
</div>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<ScanPill file={file} />
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: 12 }}>{grantCount}</span>
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{grantCount}</span>
|
||||
</div>
|
||||
<ScanLabelStrip file={file} />
|
||||
<div className="row" style={{ gap: 8, marginTop: "auto", minWidth: 0 }}>
|
||||
<Avatar name={ownerName} size="sm" bot={isAgentOwner(file)} />
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{formatRelative(file.updatedAt ?? file.createdAt)} · {ownerName.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -77,23 +77,23 @@ function FileRow({
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
<FileIcon kind={file.kind} />
|
||||
<div style={{ display: "grid", gap: 3, minWidth: 0 }}>
|
||||
<span style={{ fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: 10, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
|
||||
</span>
|
||||
</div>
|
||||
{file.pinned && <Pill tone="violet" dot>{t("Pinned")}</Pill>}
|
||||
</div>
|
||||
<div className="num mono" style={{ color: "var(--text-3)", fontSize: 12 }}>{formatBytes(file.sizeBytes)}</div>
|
||||
<div className="num mono" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{formatBytes(file.sizeBytes)}</div>
|
||||
<div className="row" style={{ gap: 8, minWidth: 0 }}>
|
||||
<Avatar name={owner} size="sm" bot={isAgentOwner(file)} />
|
||||
<span style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{owner}</span>
|
||||
</div>
|
||||
<div className="row" style={{ gap: 6, minWidth: 0 }}>
|
||||
<ScanPill file={file} />
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: 12 }}>{grantCount}</span>
|
||||
<span className="mono" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{grantCount}</span>
|
||||
</div>
|
||||
<div style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(file.updatedAt ?? file.createdAt)}</div>
|
||||
<div style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{formatRelative(file.updatedAt ?? file.createdAt)}</div>
|
||||
<div style={{ textAlign: "right" }} onClick={(event) => event.stopPropagation()}>
|
||||
<Popover trigger={({ toggle }) => <IconButton icon="more" label={t("Actions for {name}", { name: file.name })} onClick={toggle} />}>
|
||||
{(close) => (
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useState, type ReactNode, type KeyboardEvent } from "react";
|
||||
import type { AgentRecord, TaskRecord } from "../../../../shared/domain";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
const CHART_H = 116;
|
||||
/** Plot height; axis labels sit below it as HTML text. */
|
||||
const PLOT_H = 100;
|
||||
const CHART_W = 260;
|
||||
const BAR_GAP = 4;
|
||||
|
||||
@@ -30,7 +31,7 @@ function ChartCard({ title, readout, values, point, onPoint, children }: {
|
||||
};
|
||||
return (
|
||||
<section className="card metric-card" aria-label={title}>
|
||||
<div className="row" style={{ marginBottom: 12, gap: 8 }}>
|
||||
<div style={{ display: "grid", gap: 4, marginBottom: 12 }}>
|
||||
<span className="card-label">{title}</span>
|
||||
<span className="metric-readout num" aria-live="polite">{readout}</span>
|
||||
</div>
|
||||
@@ -60,28 +61,27 @@ export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; t
|
||||
|
||||
return (
|
||||
<ChartCard title={t("Agent workload")} readout={readout} values={data.map((d) => `${d.label}: ${d.count}`)} point={hover} onPoint={setHover}>
|
||||
<svg viewBox={`0 0 ${CHART_W} ${CHART_H}`} style={{ width: "100%", height: CHART_H }}>
|
||||
<svg viewBox={`0 0 ${CHART_W} ${PLOT_H}`} preserveAspectRatio="none" shapeRendering="crispEdges" style={{ width: "100%", height: PLOT_H }}>
|
||||
{data.map((d, i) => {
|
||||
const h = (d.count / max) * (CHART_H - 22);
|
||||
const h = (d.count / max) * (PLOT_H - 6);
|
||||
const x = i * (barW + BAR_GAP);
|
||||
return (
|
||||
<g key={d.key} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<rect x={x} y={0} width={barW} height={CHART_H - 16} fill="transparent" />
|
||||
<rect x={x} y={0} width={barW} height={PLOT_H} fill="transparent" />
|
||||
<rect
|
||||
x={x}
|
||||
y={CHART_H - h - 16}
|
||||
y={PLOT_H - h}
|
||||
width={barW}
|
||||
height={Math.max(2, h)}
|
||||
rx={4}
|
||||
fill="var(--accent)"
|
||||
opacity={hover === null || hover === i ? 0.85 : 0.32}
|
||||
style={{ transition: "opacity var(--motion-fast) var(--ease-out)" }}
|
||||
/>
|
||||
<text x={x + barW / 2} y={CHART_H - 2} textAnchor="middle" fontSize="9" fill="var(--text-3)">{d.label}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<ChartAxis labels={data.map((d) => d.label)} />
|
||||
</ChartCard>
|
||||
);
|
||||
}
|
||||
@@ -96,28 +96,27 @@ export function RunActivityChart({ days }: { days: UsageDays }) {
|
||||
|
||||
return (
|
||||
<ChartCard title={t("Run activity")} readout={readout} values={buckets.map((b) => `${b.label}: ${b.count}`)} point={hover} onPoint={setHover}>
|
||||
<svg viewBox={`0 0 ${CHART_W} ${CHART_H}`} style={{ width: "100%", height: CHART_H }}>
|
||||
<svg viewBox={`0 0 ${CHART_W} ${PLOT_H}`} preserveAspectRatio="none" shapeRendering="crispEdges" style={{ width: "100%", height: PLOT_H }}>
|
||||
{buckets.map((b, i) => {
|
||||
const h = (b.count / max) * (CHART_H - 24);
|
||||
const h = (b.count / max) * (PLOT_H - 6);
|
||||
const x = i * (barW + BAR_GAP);
|
||||
return (
|
||||
<g key={`${b.label}-${i}`} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<rect x={x} y={0} width={barW} height={CHART_H - 16} fill="transparent" />
|
||||
<rect x={x} y={0} width={barW} height={PLOT_H} fill="transparent" />
|
||||
<rect
|
||||
x={x}
|
||||
y={CHART_H - h - 16}
|
||||
y={PLOT_H - h}
|
||||
width={barW}
|
||||
height={Math.max(2, h)}
|
||||
rx={4}
|
||||
fill="var(--text-1)"
|
||||
opacity={hover === null || hover === i ? 0.82 : 0.28}
|
||||
style={{ transition: "opacity var(--motion-fast) var(--ease-out)" }}
|
||||
/>
|
||||
<text x={x + barW / 2} y={CHART_H - 2} textAnchor="middle" fontSize="9" fill="var(--text-3)">{b.label}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<ChartAxis labels={buckets.map((b) => b.label)} />
|
||||
</ChartCard>
|
||||
);
|
||||
}
|
||||
@@ -139,16 +138,18 @@ export function AcceptanceChart({ days }: { days: UsageDays }) {
|
||||
readout={t("{accepted} done · {returned} sent back · sessions: {ok} ended, {failed} failed", { accepted, returned, ok: completed, failed })}
|
||||
values={[judged > 0 ? t("{rate}% of judged work accepted", { rate }) : t("No work judged yet")]}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style={{ width: 96, height: 96, margin: "4px auto", display: "block" }}>
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--panel-3)" strokeWidth="8" />
|
||||
<circle
|
||||
cx="50" cy="50" r="42" fill="none" stroke="var(--green)" strokeWidth="8"
|
||||
strokeDasharray={circumference} strokeDashoffset={offset}
|
||||
strokeLinecap="round" transform="rotate(-90 50 50)"
|
||||
style={{ transition: "stroke-dashoffset var(--motion-slow) var(--ease-out)" }}
|
||||
/>
|
||||
<text x="50" y="50" textAnchor="middle" dominantBaseline="central" fontSize="20" fontWeight="600" fill="var(--text-1)">{judged > 0 ? `${rate}%` : "—"}</text>
|
||||
</svg>
|
||||
<div style={{ position: "relative", width: 96, height: 96, margin: "4px auto" }}>
|
||||
<svg viewBox="0 0 100 100" shapeRendering="crispEdges" style={{ width: 96, height: 96, display: "block" }}>
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--panel-3)" strokeWidth="8" />
|
||||
<circle
|
||||
cx="50" cy="50" r="42" fill="none" stroke="var(--green)" strokeWidth="8"
|
||||
strokeDasharray={circumference} strokeDashoffset={offset}
|
||||
transform="rotate(-90 50 50)"
|
||||
style={{ transition: "stroke-dashoffset var(--motion-slow) var(--ease-out)" }}
|
||||
/>
|
||||
</svg>
|
||||
<span className="num" style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", fontSize: "var(--fs-display)" }}>{judged > 0 ? `${rate}%` : "—"}</span>
|
||||
</div>
|
||||
</ChartCard>
|
||||
);
|
||||
}
|
||||
@@ -161,7 +162,7 @@ export function CostTrendChart({ days }: { days: UsageDays }) {
|
||||
const max = Math.max(1, ...tokenBuckets.map((b) => b.tokens));
|
||||
const coords = tokenBuckets.map((b, i) => ({
|
||||
x: (i / 6) * (CHART_W - 10) + 5,
|
||||
y: CHART_H - 20 - (b.tokens / max) * (CHART_H - 30),
|
||||
y: PLOT_H - 4 - (b.tokens / max) * (PLOT_H - 8),
|
||||
b,
|
||||
}));
|
||||
const points = coords.map((c) => `${c.x},${c.y}`).join(" ");
|
||||
@@ -175,20 +176,32 @@ export function CostTrendChart({ days }: { days: UsageDays }) {
|
||||
|
||||
return (
|
||||
<ChartCard title={t("Token usage")} readout={readout} values={tokenBuckets.map((b) => `${b.label}: ${compact(b.tokens)}`)} point={hover} onPoint={setHover}>
|
||||
<svg viewBox={`0 0 ${CHART_W} ${CHART_H}`} style={{ width: "100%", height: CHART_H }}>
|
||||
<polyline points={points} fill="none" stroke="var(--accent)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
|
||||
{coords.map((c, i) => (
|
||||
<g key={`${c.b.label}-${i}`} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<circle cx={c.x} cy={c.y} r="9" fill="transparent" />
|
||||
<circle cx={c.x} cy={c.y} r={hover === i ? 4 : 3} fill="var(--accent)" style={{ transition: "r var(--motion-fast) var(--ease-out)" }} />
|
||||
<text x={c.x} y={CHART_H - 2} textAnchor="middle" fontSize="9" fill="var(--text-3)">{c.b.label}</text>
|
||||
</g>
|
||||
))}
|
||||
<svg viewBox={`0 0 ${CHART_W} ${PLOT_H}`} shapeRendering="crispEdges" style={{ width: "100%", height: PLOT_H }}>
|
||||
<polyline points={points} fill="none" stroke="var(--accent)" strokeWidth="2" />
|
||||
{coords.map((c, i) => {
|
||||
const side = hover === i ? 8 : 6;
|
||||
return (
|
||||
<g key={`${c.b.label}-${i}`} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
|
||||
<rect x={c.x - 9} y={c.y - 9} width={18} height={18} fill="transparent" />
|
||||
<rect x={c.x - side / 2} y={c.y - side / 2} width={side} height={side} fill="var(--accent)" />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<ChartAxis labels={tokenBuckets.map((b) => b.label)} />
|
||||
</ChartCard>
|
||||
);
|
||||
}
|
||||
|
||||
/** Axis labels as pixel text, one evenly spaced cell per datum under the plot. */
|
||||
function ChartAxis({ labels }: { labels: string[] }) {
|
||||
return (
|
||||
<div aria-hidden="true" style={{ display: "flex", marginTop: 2, fontSize: "var(--fs-caption)", color: "var(--text-3)" }}>
|
||||
{labels.map((label, index) => <span key={index} style={{ flex: 1, textAlign: "center", overflow: "hidden", whiteSpace: "nowrap" }}>{label}</span>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function compact(count: number): string {
|
||||
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Button, Icon, Pill } from "../../shared/ui";
|
||||
import type { NextUpItem } from "./next-up";
|
||||
|
||||
const SHOWN = 3;
|
||||
const TITLE = { fontSize: 14, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } as const;
|
||||
const META = { fontSize: 12, color: "var(--text-3)", margin: "2px 0 8px" } as const;
|
||||
const TITLE = { fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } as const;
|
||||
const META = { fontSize: "var(--fs-body)", color: "var(--text-3)", margin: "2px 0 8px" } as const;
|
||||
|
||||
/** Home's one place for what to look at next: decisions and stopped work in priority order, or else the next task in line. */
|
||||
export function NextUpCard({
|
||||
@@ -35,7 +35,7 @@ export function NextUpCard({
|
||||
</div>
|
||||
{needsYou.length === 0 ? (
|
||||
<>
|
||||
<div className="row" style={{ gap: 8, color: "var(--text-3)", fontSize: 14, marginBottom: 12 }}>
|
||||
<div className="row" style={{ gap: 8, color: "var(--text-3)", fontSize: "var(--fs-body)", marginBottom: 12 }}>
|
||||
<Icon name="check" /><span>{t("You're all caught up")}</span>
|
||||
</div>
|
||||
{next?.kind === "next" && (
|
||||
@@ -68,7 +68,7 @@ export function NextUpCard({
|
||||
<Button size="small" onClick={() => onOpenTask(item.task.id)}>{t("Open")}</Button>
|
||||
</li>
|
||||
) : null)}
|
||||
{more > 0 && <li style={{ fontSize: 12, color: "var(--text-3)" }}>{t("{n} more after these", { n: more })}</li>}
|
||||
{more > 0 && <li style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("{n} more after these", { n: more })}</li>}
|
||||
</ol>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -97,14 +97,14 @@ export function DocumentsPanel({
|
||||
style={{ display: "grid", justifyItems: "start", gap: 2 }}
|
||||
>
|
||||
<span>{doc.title}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{doc.docType} · v{doc.version} · {formatTime(doc.updatedAt)}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{doc.docType} · v{doc.version} · {formatTime(doc.updatedAt)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{selected && (
|
||||
<article aria-label={selected.title} style={{ minWidth: 0 }}>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18 }}>{shown?.title ?? selected.title}</h2>
|
||||
<h2 style={{ margin: 0, fontSize: "var(--fs-display)" }}>{shown?.title ?? selected.title}</h2>
|
||||
<Pill>{selected.docType}</Pill>
|
||||
<Pill tone={selected.status === "approved" ? "green" : undefined}>{documentStatusLabel(shown?.status ?? selected.status)}</Pill>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@ export function KnowledgePanel({
|
||||
style={{ display: "grid", justifyItems: "start", gap: 2 }}
|
||||
>
|
||||
<span>{entry.topic}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{entry.category} · {entry.scopeType} · {entry.importance}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{entry.category} · {entry.scopeType} · {entry.importance}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@ export function KnowledgePanel({
|
||||
{selected && (
|
||||
<article aria-label={selected.topic} style={{ minWidth: 0 }}>
|
||||
<div className="row" style={{ gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18 }}>{selected.topic}</h2>
|
||||
<h2 style={{ margin: 0, fontSize: "var(--fs-display)" }}>{selected.topic}</h2>
|
||||
<Pill>{selected.category}</Pill>
|
||||
<Pill>{selected.scopeType}</Pill>
|
||||
<Pill tone={entryState(selected, now) === "active" ? "green" : "amber"}>{entryState(selected, now)}</Pill>
|
||||
@@ -155,7 +155,7 @@ export function KnowledgePanel({
|
||||
<ol style={{ display: "grid", gap: 10, paddingLeft: 18 }}>
|
||||
{revisions.map((revision) => (
|
||||
<li key={revision.revision}>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
r{revision.revision} · {formatTime(revision.createdAt)} · {revision.changedByType} · {revision.reason}
|
||||
</div>
|
||||
<div style={{ whiteSpace: "pre-wrap" }}>{revision.content}</div>
|
||||
|
||||
@@ -24,7 +24,6 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
|
||||
width: 264,
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border-strong)",
|
||||
borderRadius: 12,
|
||||
boxShadow: "0 12px 32px rgba(16,16,24,0.18)",
|
||||
padding: 14,
|
||||
}}
|
||||
@@ -32,8 +31,8 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
|
||||
<div className="row" style={{ gap: 10, alignItems: "flex-start" }}>
|
||||
<Avatar name={agent.name} size="lg" bot />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.3, wordBreak: "break-word" }}>{agent.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-2)", marginTop: 1, wordBreak: "break-word" }}>{agent.title}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", lineHeight: 1.3, wordBreak: "break-word" }}>{agent.name}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", marginTop: 1, wordBreak: "break-word" }}>{agent.title}</div>
|
||||
</div>
|
||||
<IconButton icon="x" label={t("Close agent details")} onClick={onClose} />
|
||||
</div>
|
||||
@@ -47,7 +46,7 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
|
||||
<Icon name="work" color="var(--text-3)" />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-2)",
|
||||
lineHeight: 1.45,
|
||||
display: "-webkit-box",
|
||||
|
||||
@@ -19,9 +19,9 @@ export function RunRequestList({ loaded }: { loaded: LoadedRequests | null }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div className="section-label" style={{ marginBottom: 8 }}>{t("Model calls ({count})", { count: String(requests.length) })}</div>
|
||||
{requests.length === 0 && <div style={{ fontSize: 13, color: "var(--text-3)" }}>{t("This run made no model call.")}</div>}
|
||||
{requests.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("This run made no model call.")}</div>}
|
||||
{requests.map((request, index) => (
|
||||
<div key={request.id} className="row" style={{ gap: 10, fontSize: 13, padding: "6px 0", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<div key={request.id} className="row" style={{ gap: 10, fontSize: "var(--fs-body)", padding: "6px 0", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<span className="mono" style={{ color: "var(--text-3)", width: 24 }}>{index + 1}</span>
|
||||
<span style={{ width: 96 }}>{purposeLabel(request.purpose)}</span>
|
||||
<span className="mono" style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{request.model}</span>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function BillingPanel({
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 20 }}>
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
<div className="card-label" style={{ marginBottom: 8 }}>{t("Token usage (30d)")}</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "6px 14px", fontSize: 12 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "6px 14px", fontSize: "var(--fs-body)" }}>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Input")}</span><span className="num">{inputTokens.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("of which cached")}</span><span className="num">{cachedTokens.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Output")}</span><span className="num">{outputTokens.toLocaleString()}</span>
|
||||
@@ -54,11 +54,11 @@ export function BillingPanel({
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
<div className="card-label" style={{ marginBottom: 8 }}>{t("Spent this month")}</div>
|
||||
<div style={{ marginBottom: 8 }}><XpBar percent={totalBudget > 0 ? (spentCents / totalBudget) * 100 : 0} label={t("Spent this month")} /></div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
{t("${spent} of ${allocated} allocated to agents", { spent: (spentCents / 100).toFixed(2), allocated: (totalBudget / 100).toFixed(2) })}
|
||||
{unpricedCalls > 0 && <> · {t("{count} calls not priced", { count: unpricedCalls })}</>}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 4 }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", marginTop: 4 }}>
|
||||
{totalCap > 0 ? t("Allocations use {percent}% of department caps", { percent: Math.round((totalBudget / totalCap) * 100) }) : t("No budget cap set")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,12 +73,12 @@ export function BillingPanel({
|
||||
<div key={row.agent.id} className="table-row" style={{ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)", borderBottom: i === agentSpend.length - 1 ? "none" : "1px solid var(--border-soft)" }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<Avatar name={row.agent.name} size="sm" bot />
|
||||
<span style={{ fontWeight: 480 }}>{row.agent.name}</span>
|
||||
<span>{row.agent.name}</span>
|
||||
</div>
|
||||
<div className="num mono">{money(row.agent.budgetCents)}</div>
|
||||
<div className="num mono">{row.requestCount}</div>
|
||||
<div className="num mono" style={{ fontSize: 12 }}>{row.tokens.toLocaleString()}</div>
|
||||
<div className="num mono" style={{ fontSize: 12 }}>${(row.spentCents / 100).toFixed(2)}</div>
|
||||
<div className="num mono" style={{ fontSize: "var(--fs-body)" }}>{row.tokens.toLocaleString()}</div>
|
||||
<div className="num mono" style={{ fontSize: "var(--fs-body)" }}>${(row.spentCents / 100).toFixed(2)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -95,8 +95,8 @@ export function BillingPanel({
|
||||
return (
|
||||
<div key={provider.id} className="table-row" style={{ gridTemplateColumns: "minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)", borderBottom: index === providers.length - 1 ? "none" : "1px solid var(--border-soft)" }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540 }}>{provider.label}</div>
|
||||
{provider.model && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{provider.model}</div>}
|
||||
<div>{provider.label}</div>
|
||||
{provider.model && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{provider.model}</div>}
|
||||
</div>
|
||||
<div className="num mono">{activity?.requests ?? 0}</div>
|
||||
<div>{errors > 0 ? <Pill tone="amber">{errors}</Pill> : <span className="num mono" style={{ color: "var(--text-3)" }}>0</span>}</div>
|
||||
@@ -113,7 +113,7 @@ function StatTile({ label, value, tone }: { label: string; value: string; tone?:
|
||||
return (
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
<div className="card-label" style={{ marginBottom: 6 }}>{label}</div>
|
||||
<div className="num" style={{ fontSize: 18, fontWeight: 600, color: tone === "red" ? "var(--red)" : "var(--text-1)" }}>{value}</div>
|
||||
<div className="num" style={{ fontSize: "var(--fs-display)", color: tone === "red" ? "var(--red)" : "var(--text-1)" }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,10 +44,10 @@ export function CompanyDialog({ organization, open, actions, onClose }: { organi
|
||||
<option value="zh">{WORK_LANGUAGE_NAMES.zh}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Agents write messages, tasks, documents and deliverables in it. The display language does not change it, and a task can set its own.")}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>{t("Agents write messages, tasks, documents and deliverables in it. The display language does not change it, and a task can set its own.")}</p>
|
||||
<label className="checkbox-row"><input type="checkbox" name="autoApproveHires" defaultChecked={organization.autoApproveHires} /> {t("Auto-approve hires")}</label>
|
||||
<label className="checkbox-row"><input type="checkbox" name="reviewDeliverables" defaultChecked={organization.reviewDeliverables} /> {t("Review deliverables before they are written")}</label>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Changes apply to work from now on; requests already waiting keep their state.")}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Changes apply to work from now on; requests already waiting keep their state.")}</div>
|
||||
{error && <div className="form-error" role="alert">{error}</div>}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export function DisconnectProviderDialog({ orgId, provider, revokesRemotely, act
|
||||
) : impact === null ? (
|
||||
state.kind !== "error" && <p style={{ margin: 0, color: "var(--text-3)" }}>{t("Checking what this affects…")}</p>
|
||||
) : (
|
||||
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: 13 }}>
|
||||
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: "var(--fs-body)" }}>
|
||||
<li>{impact.agents.length
|
||||
? t("Agents on this provider stop working until it is reconnected or they move to another provider: {names}.", { names: impact.agents.join(", ") })
|
||||
: t("No agent uses this provider.")}</li>
|
||||
|
||||
@@ -20,11 +20,11 @@ export function GoalAuditPanel({ organization, providers, actions }: { organizat
|
||||
return (
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 6 }}>{t("Goal audit")}</div>
|
||||
<p style={{ color: "var(--text-2)", fontSize: 13, margin: "0 0 12px" }}>
|
||||
<p style={{ color: "var(--text-2)", fontSize: "var(--fs-body)", margin: "0 0 12px" }}>
|
||||
{t("An independent model checks every goal before it closes. Pick one your agents do not use, ideally from another provider.")}
|
||||
</p>
|
||||
{!organization.auditModelId && (
|
||||
<p style={{ color: "var(--red)", fontSize: 13, margin: "0 0 12px" }}>{t("No auditor is set, so goals cannot be closed yet.")}</p>
|
||||
<p style={{ color: "var(--red)", fontSize: "var(--fs-body)", margin: "0 0 12px" }}>{t("No auditor is set, so goals cannot be closed yet.")}</p>
|
||||
)}
|
||||
<form className="row" style={{ gap: 10, alignItems: "flex-end" }} onSubmit={save}>
|
||||
<Field label={t("Provider")}>
|
||||
@@ -38,7 +38,7 @@ export function GoalAuditPanel({ organization, providers, actions }: { organizat
|
||||
</Field>
|
||||
<Button type="submit">{t("Save")}</Button>
|
||||
</form>
|
||||
{error && <p style={{ color: "var(--red)", fontSize: 13, margin: "8px 0 0" }}>{error}</p>}
|
||||
{error && <p style={{ color: "var(--red)", fontSize: "var(--fs-body)", margin: "8px 0 0" }}>{error}</p>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function LanguageField({ uiPreferences, actions }: { uiPreferences: UiPre
|
||||
<option value="en">{NAMES.en}</option>
|
||||
<option value="zh">{NAMES.zh}</option>
|
||||
</select>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Changes only what you see; agents keep the company's working language.")}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Changes only what you see; agents keep the company's working language.")}</span>
|
||||
<RestartForLanguage language={uiPreferences.language} />
|
||||
</Field>
|
||||
);
|
||||
@@ -47,12 +47,12 @@ export function RestartForLanguage({ language }: { language: UiLanguage }) {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<span className="row" style={{ gap: 8, fontSize: 12 }} role="status">
|
||||
<span className="row" style={{ gap: 8, fontSize: "var(--fs-body)" }} role="status">
|
||||
{t("{language} applies after a restart.", { language: NAMES[language] })}
|
||||
<Button size="small" onClick={confirm}>{t("Restart now")}</Button>
|
||||
</span>
|
||||
<Dialog title={t("Restart to switch language?")} open={confirming} submitLabel={t("Restart now")} onClose={() => setConfirming(false)} onSubmit={(event) => { event.preventDefault(); restart(); }}>
|
||||
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: 13 }}>
|
||||
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: "var(--fs-body)" }}>
|
||||
<li>{t("Text you typed but have not sent or saved is lost.")}</li>
|
||||
<li>{running === null
|
||||
? t("Counting running agent sessions…")
|
||||
|
||||
@@ -44,7 +44,7 @@ export function ProviderConnectFields({
|
||||
if (entry.authType === "api_key") {
|
||||
return (
|
||||
<>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<Field label={t("API key")}><input className="input" name="secret" type="password" required autoFocus /></Field>
|
||||
<ModelPicker entry={entry} provider={provider} />
|
||||
</>
|
||||
@@ -56,7 +56,7 @@ export function ProviderConnectFields({
|
||||
function SavedEndpointForm({ provider, entry }: { provider: ProviderRecord; entry: ProviderCatalogEntry }) {
|
||||
const { endpoint, error } = useSavedEndpoint(provider, entry);
|
||||
if (error) return <div className="form-error" role="alert">{t("The saved settings could not be read: {error}", { error })}</div>;
|
||||
if (endpoint === undefined) return <p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Loading saved settings…")}</p>;
|
||||
if (endpoint === undefined) return <p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>{t("Loading saved settings…")}</p>;
|
||||
return entry.customEndpoint
|
||||
? <CompatFieldsForm entry={entry} provider={provider} saved={endpoint} />
|
||||
: <CloudFieldsForm entry={entry} provider={provider} saved={endpoint} />;
|
||||
@@ -78,7 +78,7 @@ export function ProviderOAuthFields({
|
||||
const [model, setModel] = useState(provider.model ?? entry.defaultModel ?? entry.models[0]?.id ?? "");
|
||||
return (
|
||||
<>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<Field label={t("Default model")}>
|
||||
{entry.models.length ? (
|
||||
<select className="input" value={model} onChange={(event) => setModel(event.target.value)}>
|
||||
@@ -105,7 +105,7 @@ function CompatFieldsForm({ entry, provider, saved }: { entry: ProviderCatalogEn
|
||||
const [baseUrl, setBaseUrl] = useState(saved?.baseUrl ?? "");
|
||||
return (
|
||||
<>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<Field label={t("Base URL")}>
|
||||
<input
|
||||
className="input"
|
||||
@@ -178,9 +178,9 @@ export function ModelPicker({ entry, provider }: { entry: ProviderCatalogEntry;
|
||||
{check === "running" ? t("Checking...") : t("Check for agent work")}
|
||||
</button>
|
||||
)}
|
||||
{typeof check === "object" && check.passed && <div style={{ color: "var(--green)", fontSize: 12, marginTop: 4 }}>{t("This model follows instructions and calls tools.")}</div>}
|
||||
{typeof check === "object" && !check.passed && <div style={{ color: "var(--red)", fontSize: 12, marginTop: 4 }}>{t("This model cannot do agent work here: {detail}", { detail: check.detail })}</div>}
|
||||
{error && <div style={{ color: "var(--red)", fontSize: 12, marginTop: 4 }}>{error}</div>}
|
||||
{typeof check === "object" && check.passed && <div style={{ color: "var(--green)", fontSize: "var(--fs-body)", marginTop: 4 }}>{t("This model follows instructions and calls tools.")}</div>}
|
||||
{typeof check === "object" && !check.passed && <div style={{ color: "var(--red)", fontSize: "var(--fs-body)", marginTop: 4 }}>{t("This model cannot do agent work here: {detail}", { detail: check.detail })}</div>}
|
||||
{error && <div style={{ color: "var(--red)", fontSize: "var(--fs-body)", marginTop: 4 }}>{error}</div>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -190,7 +190,7 @@ function CloudFieldsForm({ entry, provider, saved }: { entry: ProviderCatalogEnt
|
||||
const update = (patch: Partial<ProviderEndpoint>) => setState((current) => ({ ...current, ...patch }));
|
||||
return (
|
||||
<>
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
|
||||
<Field label={t("Base URL")}>
|
||||
<input className="input" name="cloudBaseUrl" value={state.baseUrl} onChange={(event) => update({ baseUrl: event.target.value })} />
|
||||
</Field>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ProviderPolicyTable({
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 6 }}>{t("Runtime policy")}</div>
|
||||
{/* What each column governs, so a change is made knowing what it reaches. */}
|
||||
<ul style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.6, margin: "0 0 10px", paddingLeft: 18 }}>
|
||||
<ul style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", lineHeight: 1.6, margin: "0 0 10px", paddingLeft: 18 }}>
|
||||
<li>{t("Scope: “Workspace” refuses every call that is not made for a workspace, including agent sessions on tasks without one.")}</li>
|
||||
<li>{t("Data policy: zero data retention refuses background requests, so it also switches background off.")}</li>
|
||||
<li>{t("Background: lets requests from the request console run in the provider's background mode.")}</li>
|
||||
@@ -123,8 +123,8 @@ function ProviderPolicyRow({
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
{linkedNote && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{linkedNote}</div>}
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
{linkedNote && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{linkedNote}</div>}
|
||||
</div>
|
||||
<select
|
||||
className="input"
|
||||
@@ -144,7 +144,7 @@ function ProviderPolicyRow({
|
||||
<option value={PROVIDER_DATA_POLICY.STANDARD}>{t("Standard retention")}</option>
|
||||
<option value={PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION}>{t("Zero data retention")}</option>
|
||||
</select>
|
||||
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
||||
<label className="row" style={{ gap: 8, fontSize: "var(--fs-body)", color: "var(--text-2)" }}>
|
||||
<input
|
||||
aria-label={t("{label} background execution", { label: provider.label })}
|
||||
type="checkbox"
|
||||
@@ -174,7 +174,7 @@ function ProviderPolicyRow({
|
||||
/>
|
||||
<div>
|
||||
<div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{t("Resets {date} (UTC)", { date: rateLimit.windowEnd.slice(0, 10) })}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Resets {date} (UTC)", { date: rateLimit.windowEnd.slice(0, 10) })}</div>
|
||||
<div className="row" style={{ gap: 6, marginTop: 4 }}>
|
||||
<button type="button" className="button small" aria-label={t("Save {label} policy", { label: provider.label })} onClick={savePolicy} disabled={!dirty || status?.kind === "saving"}>
|
||||
{t("Save")}
|
||||
|
||||
@@ -171,8 +171,8 @@ function ProviderRequestRow({
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{request.model}</div>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div className="mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{request.model}</div>
|
||||
</div>
|
||||
<div className="mono" style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace?.name ?? t("Organization")}</div>
|
||||
<div className="mono" style={{ color: "var(--text-2)" }}>{request.executionMode}</div>
|
||||
|
||||
@@ -140,7 +140,7 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
|
||||
)
|
||||
)}
|
||||
{active && (
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>
|
||||
{t("The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.", { following: defaultModelReach(agents, active.id).following, own: defaultModelReach(agents, active.id).ownModel })}
|
||||
</p>
|
||||
)}
|
||||
@@ -170,7 +170,7 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
|
||||
<>
|
||||
<ModelPicker entry={modelForEntry} provider={modelFor} />
|
||||
{modelError && <div className="form-error" role="alert">{modelError}</div>}
|
||||
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>
|
||||
<p style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: 0 }}>
|
||||
{t("The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.", { following: defaultModelReach(agents, modelFor.id).following, own: defaultModelReach(agents, modelFor.id).ownModel })}
|
||||
</p>
|
||||
</>
|
||||
@@ -215,7 +215,7 @@ function ProviderPricesRow({ orgId, provider, actions }: { orgId: string; provid
|
||||
);
|
||||
return (
|
||||
<div className="table-row" style={{ gridTemplateColumns: PRICE_GRID, minWidth: 640 }}>
|
||||
<div style={{ fontWeight: 540 }}>{provider.label}{provider.inputCentsPerMtok === null && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Unpriced: spend is not counted")}</div>}</div>
|
||||
<div>{provider.label}{provider.inputCentsPerMtok === null && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Unpriced: spend is not counted")}</div>}</div>
|
||||
{field(t("input"), input, setInput)}
|
||||
{field(t("cached input"), cached, setCached)}
|
||||
{field(t("output"), output, setOutput)}
|
||||
@@ -259,18 +259,17 @@ function ProviderRow({
|
||||
padding: 14,
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<ProviderLogo providerKey={provider.providerKey} label={provider.label} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{provider.model ? `${provider.model} · ` : ""}{readableAuth(provider.authType)}{authProfile ? ` · ${credentialSourceLabel(authProfile.sourceType)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<ProviderHealth provider={provider} />
|
||||
<div className="num mono" style={{ fontSize: 13, minWidth: 72, textAlign: "right" }}>
|
||||
<div className="num mono" style={{ fontSize: "var(--fs-body)", minWidth: 72, textAlign: "right" }}>
|
||||
{boundCount === 1 ? t("{count} agent", { count: boundCount }) : t("{count} agents", { count: boundCount })}
|
||||
</div>
|
||||
<span className="reveal row" style={{ gap: 6 }}>
|
||||
@@ -308,14 +307,13 @@ function ProviderCard({ provider, onConnect }: { provider: ProviderRecord; onCon
|
||||
padding: 14,
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 10,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<ProviderLogo providerKey={provider.providerKey} label={provider.label} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{readableAuth(provider.authType)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,5 +10,5 @@ export function ScopeNote({ scope, company }: { scope: SettingScope; company: st
|
||||
: scope === "company_folders"
|
||||
? t("Applies to {company} only; the folders are on this computer.", { company })
|
||||
: t("Applies to {company} only.", { company });
|
||||
return <div role="note" className="scope-note" style={{ fontSize: 12, color: "var(--text-3)", margin: "4px 0 10px" }}>{text}</div>;
|
||||
return <div role="note" className="scope-note" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", margin: "4px 0 10px" }}>{text}</div>;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export function WorkspacePanel({
|
||||
function KeyValue({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="row" style={{ padding: "6px 0", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<span style={{ color: "var(--text-3)", width: 120, flexShrink: 0, fontSize: 12 }}>{label}</span>
|
||||
<span style={{ color: "var(--text-3)", width: 120, flexShrink: 0, fontSize: "var(--fs-body)" }}>{label}</span>
|
||||
<span style={{ flex: 1, color: "var(--text-1)", overflow: "hidden", textOverflow: "ellipsis" }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,8 +33,8 @@ export function WorkspaceRow({ workspace, projects, focused, actions }: { worksp
|
||||
<div role="group" aria-label={workspace.name} aria-current={focused ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: focused ? "var(--accent-soft)" : undefined }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
|
||||
<div>{workspace.name}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
|
||||
</div>
|
||||
{workspace.isPrimary
|
||||
? <Pill tone="violet">{t("Primary")}</Pill>
|
||||
@@ -44,7 +44,7 @@ export function WorkspaceRow({ workspace, projects, focused, actions }: { worksp
|
||||
</div>
|
||||
{confirmRemove && (
|
||||
<div className="row" role="alertdialog" aria-label={t("Remove this workspace?")} style={{ gap: 8, marginTop: 6 }}>
|
||||
<span style={{ fontSize: 13 }}>{t("Remove this workspace? Its folder stays on disk.")}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)" }}>{t("Remove this workspace? Its folder stays on disk.")}</span>
|
||||
<Button size="small" tone="primary" onClick={() => act(() => actions.run(() => window.agentCompany.deleteWorkspace({ orgId, workspaceId: workspace.id })))}>{t("Remove")}</Button>
|
||||
<Button size="small" tone="ghost" onClick={() => { setConfirmRemove(false); setError(null); }}>{t("Cancel")}</Button>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,8 @@ export function ListView({
|
||||
style={{ gridTemplateColumns: GRID, borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
|
||||
>
|
||||
<div className="row" style={{ gap: 8, minWidth: 0, overflow: "hidden" }}>
|
||||
{task.risk === "high" && <span style={{ width: 3, height: 12, background: "var(--red)", borderRadius: 2, flexShrink: 0 }} />}
|
||||
<span style={{ fontWeight: 480, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{task.risk === "high" && <span style={{ width: 3, height: 12, background: "var(--red)", flexShrink: 0 }} />}
|
||||
<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{task.title}
|
||||
</span>
|
||||
{project && <span style={{ flexShrink: 0 }}><Pill>{truncate(project.name, 24)}</Pill></span>}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function ProgressView({ tasks, goals, projects }: { tasks: TaskRecord[];
|
||||
return (
|
||||
<section key={goal.id} className="card" style={{ padding: 18 }}>
|
||||
<div className="row" style={{ gap: 12 }}>
|
||||
<span style={{ fontWeight: 560, fontSize: 14, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>
|
||||
<span style={{ fontSize: "var(--fs-body)", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>
|
||||
{goal.title}
|
||||
</span>
|
||||
<CompletionReadout tasks={linked} />
|
||||
@@ -34,7 +34,7 @@ export function ProgressView({ tasks, goals, projects }: { tasks: TaskRecord[];
|
||||
<div key={project.id} className="row" style={{ gap: 12 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontSize: "var(--fs-body)",
|
||||
color: "var(--text-2)",
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
@@ -59,7 +59,7 @@ export function ProgressView({ tasks, goals, projects }: { tasks: TaskRecord[];
|
||||
{unlinked.length > 0 && (
|
||||
<section className="card" style={{ padding: 18 }}>
|
||||
<div className="row" style={{ gap: 12 }}>
|
||||
<span style={{ fontWeight: 560, fontSize: 14, color: "var(--text-2)", flex: 1 }}>{t("Unlinked tasks")}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-2)", flex: 1 }}>{t("Unlinked tasks")}</span>
|
||||
<CompletionReadout tasks={unlinked} />
|
||||
</div>
|
||||
<StatusBar tasks={unlinked} height={6} style={{ marginTop: 12 }} />
|
||||
@@ -80,7 +80,7 @@ function counts(tasks: TaskRecord[]): Map<TaskStatus, number> {
|
||||
function StatusBar({ tasks, height, style }: { tasks: TaskRecord[]; height: number; style?: React.CSSProperties }) {
|
||||
const byStatus = counts(tasks);
|
||||
if (tasks.length === 0) {
|
||||
return <div style={{ height, borderRadius: height, background: "var(--panel-3)", ...style }} />;
|
||||
return <div style={{ height, background: "var(--panel-3)", ...style }} />;
|
||||
}
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 2, height, ...style }}>
|
||||
@@ -93,7 +93,6 @@ function StatusBar({ tasks, height, style }: { tasks: TaskRecord[]; height: numb
|
||||
style={{
|
||||
flex: count,
|
||||
background: segment.tone,
|
||||
borderRadius: height,
|
||||
minWidth: height,
|
||||
}}
|
||||
/>
|
||||
@@ -108,8 +107,8 @@ function Legend({ tasks }: { tasks: TaskRecord[] }) {
|
||||
return (
|
||||
<div className="row" style={{ gap: 14, marginTop: 10, flexWrap: "wrap" }}>
|
||||
{SEGMENTS.map((segment) => (
|
||||
<span key={segment.id} className="row" style={{ gap: 6, fontSize: 12, color: "var(--text-3)" }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: segment.tone, flexShrink: 0 }} />
|
||||
<span key={segment.id} className="row" style={{ gap: 6, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
<span style={{ width: 8, height: 8, background: segment.tone, flexShrink: 0 }} />
|
||||
<span className="num">{byStatus.get(segment.id) ?? 0}</span>
|
||||
{segment.label}
|
||||
</span>
|
||||
@@ -121,7 +120,7 @@ function Legend({ tasks }: { tasks: TaskRecord[] }) {
|
||||
function CompletionReadout({ tasks }: { tasks: TaskRecord[] }) {
|
||||
const done = tasks.filter((task) => task.status === TASK_STATUS.DONE).length;
|
||||
return (
|
||||
<span className="num" style={{ color: "var(--text-3)", fontSize: 13, flexShrink: 0 }}>
|
||||
<span className="num" style={{ color: "var(--text-3)", fontSize: "var(--fs-body)", flexShrink: 0 }}>
|
||||
{done} / {tasks.length}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ function ProjectCard({
|
||||
<article className="card" style={{ padding: 14, minWidth: 0 }}>
|
||||
<div className="row" style={{ alignItems: "flex-start", gap: 10 }}>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ fontWeight: 560, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{project.name}</div>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{project.name}</div>
|
||||
<div className="row" style={{ marginTop: 8, gap: 6, flexWrap: "wrap" }}>
|
||||
<Pill>{project.status.replace(/_/g, " ")}</Pill>
|
||||
<Pill>{t("{done} / {total} tasks", { done, total: tasks.length })}</Pill>
|
||||
@@ -64,7 +64,7 @@ function ProjectCard({
|
||||
{lead && (
|
||||
<div className="row" style={{ gap: 6, flexShrink: 0 }}>
|
||||
<Avatar name={lead.name} size="sm" bot />
|
||||
<span style={{ fontSize: 12, color: "var(--text-2)" }}>{lead.name}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-2)" }}>{lead.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ export function TaskDetailDialog({
|
||||
<div className="modal" style={{ maxWidth: 560 }}>
|
||||
{confirmClose && <DiscardConfirm onKeep={() => setConfirmClose(false)} onDiscard={onClose} />}
|
||||
<div className="row" style={{ alignItems: "flex-start", gap: 10, marginBottom: 8 }}>
|
||||
{task.priority === "high" && <span style={{ width: 3, height: 18, background: "var(--red)", borderRadius: 2, marginTop: 2, flexShrink: 0 }} />}
|
||||
{task.priority === "high" && <span style={{ width: 3, height: 18, background: "var(--red)", marginTop: 2, flexShrink: 0 }} />}
|
||||
<h2 className="modal-title" style={{ margin: 0, flex: 1 }}>{task.title}</h2>
|
||||
<IconButton icon="edit" label={t("Edit")} onClick={() => { setEditError(null); setEditing(true); }} />
|
||||
<IconButton icon="x" label={t("Close")} onClick={requestClose} />
|
||||
@@ -244,9 +244,9 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs,
|
||||
<>
|
||||
{task.hold && <TaskHoldNotice hold={task.hold} />}
|
||||
{task.description && (
|
||||
<p style={{ margin: "0 0 14px", color: "var(--text-2)", fontSize: 14, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{task.description}</p>
|
||||
<p style={{ margin: "0 0 14px", color: "var(--text-2)", fontSize: "var(--fs-body)", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{task.description}</p>
|
||||
)}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "8px 14px", fontSize: 12 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "8px 14px", fontSize: "var(--fs-body)" }}>
|
||||
<MetaLabel>{t("Assignee")}</MetaLabel>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
{assignee ? (<><Avatar name={assignee.name} size="sm" bot /><span>{assignee.name}</span>{provider && <span style={{ color: "var(--text-3)" }}>({provider.label})</span>}</>) : <span style={{ color: "var(--text-4)" }}>{t("Unassigned")}</span>}
|
||||
@@ -266,14 +266,14 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs,
|
||||
<button type="button" className="link-button" aria-label={t("Open run {id}", { id: run.id })} onClick={() => navigate("runs", { kind: "run", id: run.id })}>
|
||||
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
|
||||
</button>
|
||||
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{runTriggerLabel(run.trigger)}</span>
|
||||
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: 12 }}>{formatRelative(run.createdAt)}</span>
|
||||
<span style={{ color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{runTriggerLabel(run.trigger)}</span>
|
||||
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: "var(--fs-body)" }}>{formatRelative(run.createdAt)}</span>
|
||||
</div>
|
||||
{run.state === "failed" && run.errorMessage && (
|
||||
<span style={{ color: "var(--red)", fontSize: 12, paddingLeft: 4 }}>{run.errorMessage}</span>
|
||||
<span style={{ color: "var(--red)", fontSize: "var(--fs-body)", paddingLeft: 4 }}>{run.errorMessage}</span>
|
||||
)}
|
||||
{run.capabilityWarnings && (
|
||||
<span style={{ color: "var(--amber)", fontSize: 12, paddingLeft: 4, whiteSpace: "pre-wrap" }}>{t("Missing this run")}: {run.capabilityWarnings}</span>
|
||||
<span style={{ color: "var(--amber)", fontSize: "var(--fs-body)", paddingLeft: 4, whiteSpace: "pre-wrap" }}>{t("Missing this run")}: {run.capabilityWarnings}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -287,13 +287,13 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs,
|
||||
function SubtasksTab({ subtasks, agents, onOpenTask }: { subtasks: TaskRecord[]; agents: AgentRecord[]; onOpenTask: (taskId: string) => void }) {
|
||||
return (
|
||||
<div style={{ minHeight: 60 }}>
|
||||
{subtasks.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No subtasks")}</div>}
|
||||
{subtasks.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No subtasks")}</div>}
|
||||
{subtasks.map((st) => {
|
||||
const owner = agents.find((a) => a.id === st.assigneeAgentId);
|
||||
return (
|
||||
<div key={st.id} className="row" style={{ gap: 10, padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<Icon name={st.status === TASK_STATUS.DONE ? "check" : "work"} color={st.status === TASK_STATUS.DONE ? "var(--green)" : "var(--text-3)"} />
|
||||
<button type="button" className="link-button" aria-label={t("Open task {title}", { title: st.title })} onClick={() => onOpenTask(st.id)} style={{ flex: 1, fontSize: 14, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}>{st.title}</button>
|
||||
<button type="button" className="link-button" aria-label={t("Open task {title}", { title: st.title })} onClick={() => onOpenTask(st.id)} style={{ flex: 1, fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}>{st.title}</button>
|
||||
<Pill>{prettyStatus(st.status)}</Pill>
|
||||
{owner && <Avatar name={owner.name} size="sm" bot />}
|
||||
</div>
|
||||
@@ -312,18 +312,18 @@ function ActivityTab({ audit, approvals, onOpenApproval }: { audit: AuditEventRe
|
||||
{approvals.map((a) => (
|
||||
<div key={a.id} className="row" style={{ gap: 8, padding: "6px 0" }}>
|
||||
<Pill tone={a.state === "approved" ? "green" : a.state === "pending" ? "amber" : "red"}>{approvalStateLabel(a.state)}</Pill>
|
||||
<button type="button" className="link-button" style={{ fontSize: 12 }} aria-label={t("Open approval {title}", { title: a.title })} onClick={() => onOpenApproval(a.id)}>{a.title}</button>
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: 12, color: "var(--text-3)" }}>{formatRelative(a.createdAt)}</span>
|
||||
<button type="button" className="link-button" style={{ fontSize: "var(--fs-body)" }} aria-label={t("Open approval {title}", { title: a.title })} onClick={() => onOpenApproval(a.id)}>{a.title}</button>
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{formatRelative(a.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="section-label">{t("Events")}</div>
|
||||
{audit.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No events")}</div>}
|
||||
{audit.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("No events")}</div>}
|
||||
{audit.map((e) => (
|
||||
<div key={e.id} className="row" style={{ gap: 8, padding: "5px 0" }}>
|
||||
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(e.createdAt)}</span>
|
||||
<span style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{taskEventLabel(e.action)}</span>
|
||||
<span className="mono" style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(e.createdAt)}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{taskEventLabel(e.action)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -339,9 +339,9 @@ function EditOverlay({ onEdited, task, companyLanguage, agents, allTasks, projec
|
||||
const beneath = descendantIds(task.id, allTasks);
|
||||
const otherTasks = allTasks.filter((t) => t.id !== task.id && t.orgId === task.orgId && !beneath.has(t.id));
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, background: "var(--bg)", borderRadius: 14, padding: 24, overflow: "auto", zIndex: 2 }}>
|
||||
<div style={{ position: "absolute", inset: 0, background: "var(--bg)", padding: 24, overflow: "auto", zIndex: 2 }}>
|
||||
<form onSubmit={submit} onInput={onEdited} onChange={onEdited} aria-busy={busy}>
|
||||
<h3 style={{ margin: "0 0 14px", fontSize: 15, fontWeight: 600 }}>{t("Edit task")}</h3>
|
||||
<h3 style={{ margin: "0 0 14px", fontSize: "var(--fs-body)" }}>{t("Edit task")}</h3>
|
||||
<Field label={t("Title")}><input className="input" name="title" defaultValue={task.title} required /></Field>
|
||||
<Field label={t("Description")}><textarea className="input" name="description" rows={4} defaultValue={task.description} /></Field>
|
||||
<Field label={t("Assignee")}>
|
||||
@@ -423,10 +423,10 @@ function DeleteConfirm({ impact, onConfirm, onCancel }: { impact: TaskDeleteImpa
|
||||
impact.pendingApprovals > 0 && t("{n} pending approvals for it stay pending.", { n: impact.pendingApprovals }),
|
||||
].filter((line): line is string => typeof line === "string");
|
||||
return (
|
||||
<div role="alertdialog" aria-label={t("Delete this task?")} style={{ marginTop: 12, padding: 12, border: "1px solid var(--red)", borderRadius: 10 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 6 }}>{t("Delete this task?")}</div>
|
||||
{lines.length === 0 && <div style={{ fontSize: 13, color: "var(--text-2)" }}>{t("Nothing else is affected.")}</div>}
|
||||
{lines.map((line) => <div key={line} style={{ fontSize: 13, color: "var(--text-2)" }}>{line}</div>)}
|
||||
<div role="alertdialog" aria-label={t("Delete this task?")} style={{ marginTop: 12, padding: 12, border: "1px solid var(--red)" }}>
|
||||
<div style={{ marginBottom: 6 }}>{t("Delete this task?")}</div>
|
||||
{lines.length === 0 && <div style={{ fontSize: "var(--fs-body)", color: "var(--text-2)" }}>{t("Nothing else is affected.")}</div>}
|
||||
{lines.map((line) => <div key={line} style={{ fontSize: "var(--fs-body)", color: "var(--text-2)" }}>{line}</div>)}
|
||||
{impact.running > 0 && <div className="form-error" role="alert">{t("A session is running; stop it or wait for it before deleting.")}</div>}
|
||||
<div className="row" style={{ gap: 8, marginTop: 10 }}>
|
||||
<Button tone="primary" size="small" onClick={onConfirm} disabled={impact.running > 0}>{t("Delete task")}</Button>
|
||||
|
||||
@@ -14,9 +14,9 @@ export function TaskHoldNotice({ hold }: { hold: TaskHold }) {
|
||||
: t("Waiting on you.");
|
||||
const color = hold.waitingOn === "operator" ? "var(--red)" : "var(--amber)";
|
||||
return (
|
||||
<div role="status" style={{ marginBottom: 12, fontSize: 13, color, lineHeight: 1.5 }}>
|
||||
<div role="status" style={{ marginBottom: 12, fontSize: "var(--fs-body)", color, lineHeight: 1.5 }}>
|
||||
<div>{hold.reason}</div>
|
||||
<div style={{ fontWeight: 600 }}>{next}</div>
|
||||
<div>{next}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export function DiscardConfirm({ onKeep, onDiscard }: { onKeep: () => void; onDi
|
||||
return (
|
||||
<Modal label={t("Discard your changes?")} role="alertdialog" onEscape={onKeep} onBackdrop={onKeep}>
|
||||
<div className="modal" style={{ maxWidth: 360 }}>
|
||||
<p style={{ margin: "0 0 12px", fontSize: 14 }}>{t("Discard your changes?")}</p>
|
||||
<p style={{ margin: "0 0 12px", fontSize: "var(--fs-body)" }}>{t("Discard your changes?")}</p>
|
||||
<div className="modal-actions">
|
||||
<Button tone="ghost" onClick={onKeep}>{t("Keep editing")}</Button>
|
||||
<Button tone="primary" onClick={onDiscard}>{t("Discard")}</Button>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ErrorText({ message, action }: { message: string; action?: { lab
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div role="alert" className="row" style={{ gap: 10, alignItems: "flex-start", fontSize: 13, color: "var(--red)", marginBottom: 14 }}>
|
||||
<div role="alert" className="row" style={{ gap: 10, alignItems: "flex-start", fontSize: "var(--fs-body)", color: "var(--red)", marginBottom: 14 }}>
|
||||
<span style={{ flex: 1, minWidth: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", userSelect: "text" }}>{message}</span>
|
||||
<Button size="small" onClick={copyMessage}>{copy === "copied" ? t("Copied") : copy === "failed" ? t("Copy failed") : t("Copy")}</Button>
|
||||
{action && <Button size="small" onClick={action.onClick}>{action.label}</Button>}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function FailureToasts() {
|
||||
<div style={{ position: "fixed", right: 16, bottom: 16, zIndex: 1000, display: "grid", gap: 8, maxWidth: 420 }}>
|
||||
{failures.map((failure) => (
|
||||
<div key={failure.id} role="alert" className="card" style={{ display: "flex", gap: 8, alignItems: "flex-start", borderColor: "var(--red)", padding: 12 }}>
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: 13 }}>
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: "var(--fs-body)" }}>
|
||||
<strong style={{ display: "block", marginBottom: 2 }}>{t("That did not work")}</strong>
|
||||
<span style={{ color: "var(--text-2)", wordBreak: "break-word" }}>{failure.message}</span>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ProviderLogo({ providerKey, label, size = 30 }: { providerKey: s
|
||||
alt=""
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ width: size, height: size, borderRadius: 7, objectFit: "cover", background: "#fff", flexShrink: 0 }}
|
||||
style={{ width: size, height: size, objectFit: "cover", background: "#fff", flexShrink: 0 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -60,13 +60,11 @@ export function ProviderLogo({ providerKey, label, size = 30 }: { providerKey: s
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: 7,
|
||||
background: "#a1a1aa",
|
||||
color: "#fff",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontSize: Math.round(size * 0.42),
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--fs-body)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -133,7 +133,7 @@ export function SearchDialog({
|
||||
className="input"
|
||||
value={filter}
|
||||
onChange={(event) => setFilter(event.target.value as SearchFilter)}
|
||||
style={{ width: 150, height: 28, padding: "2px 8px", fontSize: 12 }}
|
||||
style={{ width: 150, height: 28, padding: "2px 8px", fontSize: "var(--fs-body)" }}
|
||||
>
|
||||
<option value="all">{t("All")}</option>
|
||||
{SEARCH_ENTITY_TYPES.map((type) => <option key={type} value={type}>{LABELS[type]}</option>)}
|
||||
@@ -141,7 +141,7 @@ export function SearchDialog({
|
||||
<Button size="small" onClick={onClose}>{t("Close")}</Button>
|
||||
</div>
|
||||
<div className="search-results" aria-busy={searching}>
|
||||
{searching && <div role="status" style={{ padding: 8, fontSize: 13, color: "var(--text-3)" }}>{t("Searching…")}</div>}
|
||||
{searching && <div role="status" style={{ padding: 8, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Searching…")}</div>}
|
||||
{current?.error && <div className="form-error" role="alert">{t("Search failed: {reason}", { reason: current.error })}</div>}
|
||||
{visibleResults.map((result) => (
|
||||
<button
|
||||
@@ -152,17 +152,17 @@ export function SearchDialog({
|
||||
>
|
||||
<Pill>{LABELS[result.entityType]}</Pill>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{result.title}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{result.preview}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{result.title}</div>
|
||||
<div style={{ fontSize: "var(--fs-body)", color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{result.preview}</div>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{result.facets.type}</span>
|
||||
<span style={{ fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{result.facets.type}</span>
|
||||
</button>
|
||||
))}
|
||||
{current && !current.error && current.total === 0 && (
|
||||
<div role="status" style={{ padding: 8, fontSize: 13, color: "var(--text-3)" }}>{t("Nothing matches “{words}”.", { words: trimmed })}</div>
|
||||
<div role="status" style={{ padding: 8, fontSize: "var(--fs-body)", color: "var(--text-3)" }}>{t("Nothing matches “{words}”.", { words: trimmed })}</div>
|
||||
)}
|
||||
{current && current.total > 0 && (
|
||||
<div className="row" style={{ padding: "6px 8px", fontSize: 12, color: "var(--text-3)" }}>
|
||||
<div className="row" style={{ padding: "6px 8px", fontSize: "var(--fs-body)", color: "var(--text-3)" }}>
|
||||
<span role="status">{t("Showing {shown} of {total}", { shown: current.results.length, total: current.total })}</span>
|
||||
{current.results.length < current.total && (
|
||||
<span style={{ marginLeft: "auto" }}><Button size="small" onClick={showMore} disabled={loadingMore}>{loadingMore ? t("Loading…") : t("Show more")}</Button></span>
|
||||
|
||||
@@ -108,7 +108,6 @@ body {
|
||||
background: var(--bg);
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
font-synthesis: none;
|
||||
-webkit-font-smoothing: none;
|
||||
text-rendering: optimizeSpeed;
|
||||
@@ -181,11 +180,9 @@ a { color: inherit; }
|
||||
background: var(--btn-primary-bg);
|
||||
box-shadow: 2px 2px 0 0 var(--border-strong);
|
||||
font-size: var(--fs-body);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.brand-name {
|
||||
flex: 1;
|
||||
font-weight: 560;
|
||||
font-size: var(--fs-body);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -224,8 +221,6 @@ a { color: inherit; }
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
padding: var(--sp-3) var(--sp-2) var(--sp-2);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
@@ -236,7 +231,6 @@ a { color: inherit; }
|
||||
border-radius: var(--r-sm);
|
||||
background: transparent;
|
||||
color: var(--text-2);
|
||||
font-weight: 500;
|
||||
font-size: var(--fs-body);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
@@ -280,7 +274,6 @@ a { color: inherit; }
|
||||
.convo-item.active { background: var(--panel-3); color: var(--text-1); }
|
||||
.convo-ceo-badge {
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 560;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
padding: 1px 6px;
|
||||
@@ -332,8 +325,6 @@ a { color: inherit; }
|
||||
.org-menu-section-label {
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
padding: var(--sp-1) var(--sp-2);
|
||||
}
|
||||
.org-menu-row {
|
||||
@@ -351,7 +342,7 @@ a { color: inherit; }
|
||||
transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out);
|
||||
}
|
||||
.org-menu-row:hover { background: var(--hover); color: var(--text-1); }
|
||||
.org-menu-row.active { background: var(--panel-3); color: var(--text-1); font-weight: 540; }
|
||||
.org-menu-row.active { background: var(--panel-3); color: var(--text-1); }
|
||||
.org-menu-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.org-menu-delete {
|
||||
display: inline-flex;
|
||||
@@ -377,7 +368,6 @@ a { color: inherit; }
|
||||
}
|
||||
.org-menu-confirm-title {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
color: var(--text-1);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -398,7 +388,6 @@ a { color: inherit; }
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 560;
|
||||
cursor: pointer;
|
||||
}
|
||||
.org-menu-confirm-no {
|
||||
@@ -476,7 +465,7 @@ a { color: inherit; }
|
||||
.topbar select,
|
||||
.topbar input,
|
||||
.topbar a { -webkit-app-region: no-drag; }
|
||||
.crumb { font-size: var(--fs-body); font-weight: 560; color: var(--text-1); }
|
||||
.crumb { font-size: var(--fs-body); color: var(--text-1); }
|
||||
.crumb-sub { font-size: var(--fs-body); color: var(--text-3); }
|
||||
.crumb-sep { color: var(--text-4); }
|
||||
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
|
||||
@@ -558,9 +547,7 @@ a { color: inherit; }
|
||||
.page-h1 {
|
||||
margin: 0 0 var(--sp-1);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--fs-title);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
font-size: var(--fs-display);
|
||||
}
|
||||
.page-sub {
|
||||
margin: 0 0 var(--sp-6);
|
||||
@@ -571,8 +558,6 @@ a { color: inherit; }
|
||||
margin-bottom: var(--sp-2);
|
||||
color: var(--text-3);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.row { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
|
||||
.col { display: flex; flex-direction: column; min-width: 0; }
|
||||
@@ -654,8 +639,6 @@ a { color: inherit; }
|
||||
.org-node-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.org-node-name {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 560;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
.card-label {
|
||||
color: var(--text-3);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -121,8 +119,6 @@
|
||||
border-radius: 0;
|
||||
color: #fff;
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.avatar.bot { border-radius: var(--r-sm); }
|
||||
.avatar.sm { width: 24px; height: 24px; font-size: var(--fs-body); }
|
||||
@@ -132,7 +128,7 @@
|
||||
.avatar-stack > .avatar-ring { margin-left: -6px; border: 2px solid var(--panel); border-radius: 0; }
|
||||
.avatar-stack > .avatar-ring:first-child { margin-left: 0; }
|
||||
|
||||
.num { font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||||
.num { font-variant-numeric: tabular-nums; }
|
||||
.mono { font-family: var(--font-mono); }
|
||||
|
||||
.input {
|
||||
@@ -157,7 +153,7 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
color: var(--text-2);
|
||||
font-size: var(--fs-caption);
|
||||
}
|
||||
.field > span { color: var(--text-2); font-weight: 500; }
|
||||
.field > span { color: var(--text-2); }
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -180,8 +176,6 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
padding: var(--sp-2) var(--sp-4);
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0;
|
||||
background: var(--panel-2);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
@@ -198,7 +192,6 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
}
|
||||
.metric-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--border-strong); }
|
||||
.metric-readout {
|
||||
margin-left: auto;
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -285,7 +278,6 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
color: var(--text-1);
|
||||
font: inherit;
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 540;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(16, 16, 24, 0.18);
|
||||
}
|
||||
@@ -319,14 +311,14 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
.md-body p { margin: 0 0 6px; white-space: pre-wrap; }
|
||||
.md-body ul, .md-body ol { margin: 0 0 6px; padding-left: 22px; }
|
||||
.md-body li > ul, .md-body li > ol { margin: 2px 0; }
|
||||
.md-body h1, .md-body h2, .md-body h3 { font-size: var(--fs-body); font-weight: 600; margin: 8px 0 4px; }
|
||||
.md-body h1, .md-body h2, .md-body h3 { font-size: var(--fs-body); margin: 8px 0 4px; }
|
||||
.md-body blockquote { margin: 0 0 6px; padding-left: 10px; border-left: 3px solid var(--border-strong); color: var(--text-2); }
|
||||
.md-body code { font-family: var(--font-mono); background: var(--panel-3); padding: 1px 5px; border-radius: 0; font-size: 0.92em; }
|
||||
.md-body pre { margin: 0 0 6px; padding: 10px 12px; background: var(--panel-3); border-radius: 0; overflow-x: auto; }
|
||||
.md-body pre code { background: transparent; padding: 0; white-space: pre; }
|
||||
.md-body table { border-collapse: collapse; margin: 0 0 6px; display: block; overflow-x: auto; max-width: 100%; }
|
||||
.md-body th, .md-body td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
|
||||
.md-body th { background: var(--panel-2); font-weight: 600; }
|
||||
.md-body th { background: var(--panel-2); }
|
||||
|
||||
/* A link to another object, styled as text the reader can follow. */
|
||||
.link-button { border: 0; background: transparent; padding: 0; color: var(--accent); cursor: pointer; font: inherit; text-align: inherit; }
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
/* Game over: a framed panel with a blinking banner and Continue. */
|
||||
.game-over { display: grid; gap: 12px; max-width: 520px; text-align: center; justify-items: center; }
|
||||
.game-over-banner { font-size: var(--fs-display); color: var(--red); letter-spacing: 0.1em; animation: px-blink 1.2s steps(1, end) infinite; }
|
||||
.game-over-banner { font-size: var(--fs-display); color: var(--red); animation: px-blink 1.2s steps(1, end) infinite; }
|
||||
.game-over-error { font-size: var(--fs-caption); color: var(--text-2); word-break: break-word; }
|
||||
.game-over-note { color: var(--text-3); }
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
/** Visible text off the pixel font's grid: a size other than its 1x or 2x, or added letter spacing; code excepted. */
|
||||
export function offGridText(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const scale = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--px"));
|
||||
const sizes = [12 * scale, 24 * scale];
|
||||
const odd: string[] = [];
|
||||
for (const element of document.querySelectorAll<HTMLElement>("body *")) {
|
||||
const own = [...element.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && node.textContent!.trim() !== "");
|
||||
if (!own || element.closest("svg, canvas, .sr-only")) continue;
|
||||
const style = getComputedStyle(element);
|
||||
if (style.fontFamily.includes("Geist Mono") || element.getClientRects().length === 0) continue;
|
||||
const size = parseFloat(style.fontSize);
|
||||
const spaced = style.letterSpacing !== "normal" && parseFloat(style.letterSpacing) !== 0;
|
||||
if (spaced || !sizes.some((grid) => Math.abs(grid - size) < 0.01)) odd.push(`${element.tagName}.${element.className} ${size}px ${style.letterSpacing} "${element.textContent!.trim().slice(0, 30)}"`);
|
||||
}
|
||||
return odd;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { _electron as electron, expect, test } from "@playwright/test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture";
|
||||
import { closeApp, onboard } from "./onboard";
|
||||
import { offGridText } from "./pixel-grid";
|
||||
|
||||
test("every page and onboarding step draws its text on the pixel font's grid", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-e2e-pxpages-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-pxpages-builtin-${randomUUID()}`));
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir },
|
||||
});
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
|
||||
expect(await offGridText(page)).toEqual([]);
|
||||
await onboard(page, "Grid Co");
|
||||
for (const route of ["Home", "Work", "Team", "Office", "Messages", "Runs", "Files", "Library", "Approvals", "Connectors", "Settings"]) {
|
||||
await page.getByRole("button", { name: route, exact: true }).click();
|
||||
await page.waitForTimeout(400);
|
||||
expect({ route, odd: await offGridText(page) }).toEqual({ route, odd: [] });
|
||||
}
|
||||
} finally {
|
||||
await closeApp(app);
|
||||
}
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture";
|
||||
import { onboard } from "./onboard";
|
||||
import { offGridText } from "./pixel-grid";
|
||||
|
||||
test("a record that breaks one page leaves the rest of the app working", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-e2e-render-${randomUUID()}`);
|
||||
@@ -35,6 +36,7 @@ test("a record that breaks one page leaves the rest of the app working", async (
|
||||
await expect(failure).toContainText("Approval risk tone is missing.");
|
||||
// A failure reads as a game over, with Continue to try again; the same fault fails the same way.
|
||||
await expect(failure).toContainText("GAME OVER");
|
||||
expect(await offGridText(page)).toEqual([]);
|
||||
await failure.getByRole("button", { name: "Continue" }).click();
|
||||
await expect(failure).toContainText("Approval risk tone is missing.");
|
||||
await page.getByRole("button", { name: "Home" }).click();
|
||||
|
||||
Reference in new issue
Block a user