Cut Settings from six tabs to four and stop drowning it in chrome

General (identity, mode, workspaces), Providers, Skills, and Advanced
(Composio key, the operator request console, billing, migration) replace
the six-tab sprawl. Card-in-card nesting and every decorative subtitle are
gone; row actions - provider Check/Manage/Revoke, workspace Add, skill
Import, migration Report - reveal on hover or focus instead of shouting all
the time. The Composio panel loses its hardcoded org-wide toolkit grid,
which the Connectors grants model had already made dead weight, and keeps
only the API key. Fixing the flow also surfaced a real bug: provider policy
rows remounted on a snapshot-derived key, so any refresh mid-edit wiped the
operator's unsaved choices - rows now keep stable identity and edits
survive. E2e follows the new layout.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-06 23:48:31 -04:00
1 parent 2f95fc2f2d
commit d64cf22feb
11 files changed
+186 -216

No files matched your search

@@ -2,13 +2,11 @@ import { useMemo, useState } from "react";
import { ProvidersPanel } from "./settings/ProvidersPanel"; import { ProvidersPanel } from "./settings/ProvidersPanel";
import { SkillsPanel } from "./settings/SkillsPanel"; import { SkillsPanel } from "./settings/SkillsPanel";
import { WorkspacePanel } from "./settings/WorkspacePanel"; import { WorkspacePanel } from "./settings/WorkspacePanel";
import { BillingPanel } from "./settings/BillingPanel"; import { AdvancedPanel } from "./settings/AdvancedPanel";
import { MigrationPanel } from "./settings/MigrationPanel";
import { ComposioPanel } from "./settings/ComposioPanel";
import type { PageProps } from "./types"; import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types"; import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types";
type SettingsTab = "workspace" | "billing" | "providers" | "tools" | "skills" | "migration"; type SettingsTab = "general" | "providers" | "skills" | "advanced";
interface NavSpec { interface NavSpec {
id: SettingsTab; id: SettingsTab;
@@ -16,19 +14,17 @@ interface NavSpec {
} }
const TABS: NavSpec[] = [ const TABS: NavSpec[] = [
{ id: "workspace", label: "Workspace" }, { id: "general", label: "General" },
{ id: "billing", label: "Billing" },
{ id: "providers", label: "Providers" }, { id: "providers", label: "Providers" },
{ id: "tools", label: "Tools" },
{ id: "skills", label: "Skills" }, { id: "skills", label: "Skills" },
{ id: "migration", label: "Migration" }, { id: "advanced", label: "Advanced" },
]; ];
export function SettingsPage({ snapshot, actions }: PageProps) { export function SettingsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot); const orgId = activeOrgId(snapshot);
const organization = requireActiveOrganization(snapshot); const organization = requireActiveOrganization(snapshot);
const employee = requireActiveEmployee(snapshot); const employee = requireActiveEmployee(snapshot);
const [tab, setTab] = useState<SettingsTab>("providers"); const [tab, setTab] = useState<SettingsTab>("general");
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]); const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
const providers = useMemo(() => snapshot.providers.filter((item) => item.orgId === orgId), [snapshot.providers, orgId]); const providers = useMemo(() => snapshot.providers.filter((item) => item.orgId === orgId), [snapshot.providers, orgId]);
@@ -60,8 +56,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
</aside> </aside>
<div className="main" style={{ flex: 1, minWidth: 0 }}> <div className="main" style={{ flex: 1, minWidth: 0 }}>
<div className="content"> <div className="content">
{tab === "workspace" && <WorkspacePanel organization={organization} workspaces={workspaces} projects={snapshot.projects.filter((p) => p.orgId === orgId)} orgId={orgId} actions={actions} />} {tab === "general" && <WorkspacePanel organization={organization} workspaces={workspaces} projects={snapshot.projects.filter((p) => p.orgId === orgId)} orgId={orgId} actions={actions} />}
{tab === "billing" && <BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} hiringPolicies={hiringPolicies} />}
{tab === "providers" && ( {tab === "providers" && (
<ProvidersPanel <ProvidersPanel
orgId={orgId} orgId={orgId}
@@ -70,13 +65,10 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
authProfiles={providerAuthProfiles} authProfiles={providerAuthProfiles}
catalog={snapshot.providerCatalog} catalog={snapshot.providerCatalog}
agents={agents} agents={agents}
workspaces={workspaces}
providerRateLimits={providerRateLimits} providerRateLimits={providerRateLimits}
providerRequests={providerRequests}
actions={actions} actions={actions}
/> />
)} )}
{tab === "tools" && <ComposioPanel orgId={orgId} settings={snapshot.composioSettings} actions={actions} />}
{tab === "skills" && ( {tab === "skills" && (
<SkillsPanel <SkillsPanel
orgId={orgId} orgId={orgId}
@@ -88,11 +80,18 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
actions={actions} actions={actions}
/> />
)} )}
{tab === "migration" && ( {tab === "advanced" && (
<MigrationPanel <AdvancedPanel
orgId={orgId} orgId={orgId}
employeeId={employee.id} employeeId={employee.id}
reports={migrationReports} agents={agents}
providers={providers}
catalog={snapshot.providerCatalog}
workspaces={workspaces}
providerRequests={providerRequests}
hiringPolicies={hiringPolicies}
composioSettings={snapshot.composioSettings}
migrationReports={migrationReports}
rollbackSnapshots={rollbackSnapshots} rollbackSnapshots={rollbackSnapshots}
reconciliationItems={snapshot.migrationReconciliationItems.filter((item) => item.orgId === orgId)} reconciliationItems={snapshot.migrationReconciliationItems.filter((item) => item.orgId === orgId)}
actions={actions} actions={actions}
@@ -0,0 +1,86 @@
import type {
AgentRecord,
HiringPolicyRecord,
MigrationReportRecord,
ProviderCatalogEntry,
ProviderRecord,
ProviderRequestRecord,
RollbackSnapshotRecord,
WorkspaceRecord,
} from "../../../../shared/domain";
import type { MigrationReconciliationItemRecord } from "../../../../shared/rollback-domain";
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
import type { SnapshotActions } from "../../app/useSnapshot";
import { BillingPanel } from "./BillingPanel";
import { ComposioPanel } from "./ComposioPanel";
import { MigrationPanel } from "./MigrationPanel";
import { ProviderRequestPanel } from "./ProviderRequestPanel";
/** The low-frequency operator surfaces, one section each. */
export function AdvancedPanel({
orgId,
employeeId,
agents,
providers,
catalog,
workspaces,
providerRequests,
hiringPolicies,
composioSettings,
migrationReports,
rollbackSnapshots,
reconciliationItems,
actions,
}: {
orgId: string;
employeeId: string;
agents: AgentRecord[];
providers: ProviderRecord[];
catalog: ProviderCatalogEntry[];
workspaces: WorkspaceRecord[];
providerRequests: ProviderRequestRecord[];
hiringPolicies: HiringPolicyRecord[];
composioSettings: ComposioSettingRecord[];
migrationReports: MigrationReportRecord[];
rollbackSnapshots: RollbackSnapshotRecord[];
reconciliationItems: MigrationReconciliationItemRecord[];
actions: SnapshotActions;
}) {
const connected = providers.filter((provider) => provider.enabled);
return (
<div className="page">
<h1 className="page-h1">Advanced</h1>
<div className="section-label" style={{ margin: "16px 0 10px" }}>Composio</div>
<ComposioPanel orgId={orgId} settings={composioSettings} actions={actions} />
{connected.length > 0 && (
<div style={{ marginTop: 28 }}>
<ProviderRequestPanel
orgId={orgId}
providers={connected}
catalog={catalog}
workspaces={workspaces}
providerRequests={providerRequests}
actions={actions}
/>
</div>
)}
<div style={{ marginTop: 28 }}>
<BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} hiringPolicies={hiringPolicies} />
</div>
<div style={{ marginTop: 28 }}>
<MigrationPanel
orgId={orgId}
employeeId={employeeId}
reports={migrationReports}
rollbackSnapshots={rollbackSnapshots}
reconciliationItems={reconciliationItems}
actions={actions}
/>
</div>
</div>
);
}
@@ -22,9 +22,8 @@ export function BillingPanel({
const agentSpend = buildAgentSpend(agents, providers, monthRequests); const agentSpend = buildAgentSpend(agents, providers, monthRequests);
return ( return (
<div className="page"> <div>
<h1 className="page-h1">Billing</h1> <div className="section-label" style={{ marginBottom: 10 }}>Billing</div>
<p className="page-sub">Allocations, spend, and cost breakdown.</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 12, marginBottom: 20 }}> <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 12, marginBottom: 20 }}>
<StatTile label="Monthly allocation" value={money(totalBudget)} /> <StatTile label="Monthly allocation" value={money(totalBudget)} />
@@ -3,44 +3,14 @@ import { Button } from "../../shared/ui";
import type { SnapshotActions } from "../../app/useSnapshot"; import type { SnapshotActions } from "../../app/useSnapshot";
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain"; import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
const AVAILABLE_TOOLKITS = [
{ id: "github", label: "GitHub" },
{ id: "gmail", label: "Gmail" },
{ id: "slack", label: "Slack" },
{ id: "notion", label: "Notion" },
{ id: "linear", label: "Linear" },
{ id: "jira", label: "Jira" },
{ id: "google_calendar", label: "Google Calendar" },
{ id: "google_drive", label: "Google Drive" },
{ id: "google_docs", label: "Google Docs" },
{ id: "google_sheets", label: "Google Sheets" },
{ id: "trello", label: "Trello" },
{ id: "asana", label: "Asana" },
{ id: "discord", label: "Discord" },
{ id: "twitter", label: "Twitter / X" },
{ id: "youtube", label: "YouTube" },
{ id: "hubspot", label: "HubSpot" },
{ id: "salesforce", label: "Salesforce" },
{ id: "zendesk", label: "Zendesk" },
{ id: "figma", label: "Figma" },
{ id: "airtable", label: "Airtable" },
];
function parseSavedToolkits(settings: ComposioSettingRecord[], orgId: string): Set<string> {
const row = settings.find((s) => s.orgId === orgId && s.key === "toolkits");
if (!row?.value) return new Set();
try {
const parsed = JSON.parse(row.value);
return new Set(Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : []);
} catch {
return new Set();
}
}
function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean { function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean {
return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0); return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0);
} }
/**
* The Composio project API key. Which toolkits agents may use is decided by
* connections and per-agent grants on the Connectors page, not here.
*/
export function ComposioPanel({ export function ComposioPanel({
orgId, orgId,
settings, settings,
@@ -50,98 +20,33 @@ export function ComposioPanel({
settings: ComposioSettingRecord[]; settings: ComposioSettingRecord[];
actions: SnapshotActions; actions: SnapshotActions;
}) { }) {
const savedToolkits = useMemo(() => parseSavedToolkits(settings, orgId), [settings, orgId]);
const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]); const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]);
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [selected, setSelected] = useState<Set<string>>(savedToolkits);
const [saved, setSaved] = useState(false);
const toggle = (id: string) => { const handleSave = (event: FormEvent) => {
setSelected((prev) => { event.preventDefault();
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const handleSave = async (e: FormEvent) => {
e.preventDefault();
const key = apiKey.trim(); const key = apiKey.trim();
if (!key && !keyConfigured) return; if (!key) return;
const row = settings.find((s) => s.orgId === orgId && s.key === "api_key"); void actions.run(() => window.agentCompany.saveComposioSettings({
const effectiveKey = key || row?.value || "";
if (!effectiveKey) return;
await actions.run(() => window.agentCompany.saveComposioSettings({
orgId, orgId,
apiKey: effectiveKey, apiKey: key,
toolkits: [...selected], toolkits: [],
})); })).then(() => setApiKey(""));
setApiKey("");
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}; };
return ( return (
<div style={{ maxWidth: 640 }}> <form onSubmit={handleSave} className="row" style={{ gap: 8, maxWidth: 480 }}>
<h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>External Tools</h2> <input
<p style={{ fontSize: 13, color: "var(--text-3)", marginBottom: 24 }}> type="password"
Connect your agents to external services via Composio. className="input"
</p> aria-label="Composio API key"
<form onSubmit={handleSave}> value={apiKey}
<label style={{ display: "block", marginBottom: 16 }}> onChange={(event) => setApiKey(event.target.value)}
<span style={{ fontSize: 13, fontWeight: 500, display: "block", marginBottom: 6 }}>API Key</span> placeholder={keyConfigured ? "••••••••" : "ak_..."}
<input autoComplete="off"
type="password" style={{ flex: 1 }}
className="input" />
value={apiKey} <Button tone="primary" type="submit" disabled={!apiKey.trim()}>Save</Button>
onChange={(e) => setApiKey(e.target.value)} </form>
placeholder={keyConfigured ? "••••••••" : undefined}
autoComplete="off"
style={{ width: "100%", maxWidth: 400 }}
/>
</label>
<div style={{ marginBottom: 20 }}>
<span style={{ fontSize: 13, fontWeight: 500, display: "block", marginBottom: 8 }}>Toolkits</span>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
gap: 6,
}}>
{AVAILABLE_TOOLKITS.map((toolkit) => (
<label
key={toolkit.id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 10px",
borderRadius: 8,
cursor: "pointer",
fontSize: 13,
background: selected.has(toolkit.id) ? "var(--accent-soft)" : "transparent",
transition: "background 0.12s",
}}
>
<input
type="checkbox"
checked={selected.has(toolkit.id)}
onChange={() => toggle(toolkit.id)}
style={{ accentColor: "var(--accent)" }}
/>
{toolkit.label}
</label>
))}
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Button tone="primary" type="submit" disabled={!apiKey.trim() && !keyConfigured}>
Save
</Button>
{saved && <span style={{ fontSize: 13, color: "var(--green)" }}>Saved</span>}
</div>
</form>
</div>
); );
} }
@@ -40,13 +40,10 @@ export function MigrationPanel({
}; };
return ( return (
<div className="page"> <div className="reveal-row">
<div className="row" style={{ marginBottom: 24 }}> <div className="row" style={{ marginBottom: 10 }}>
<div> <div className="section-label">Migration</div>
<h1 className="page-h1">Migration</h1> <div className="reveal" style={{ marginLeft: "auto" }}>
<p className="page-sub" style={{ marginBottom: 0 }}>{reports.length} migration {reports.length === 1 ? "report" : "reports"}.</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" size={16} /> Report</Button> <Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" size={16} /> Report</Button>
</div> </div>
</div> </div>
@@ -7,12 +7,9 @@ import type {
ProviderAuthProfileRecord, ProviderAuthProfileRecord,
ProviderRateLimitStateRecord, ProviderRateLimitStateRecord,
ProviderRecord, ProviderRecord,
ProviderRequestRecord,
WorkspaceRecord,
} from "../../../../shared/domain"; } from "../../../../shared/domain";
import { PROVIDER_DATA_POLICY } from "../../../../shared/domain"; import { PROVIDER_DATA_POLICY } from "../../../../shared/domain";
import { ProviderLogo } from "./ProviderLogo"; import { ProviderLogo } from "./ProviderLogo";
import { ProviderRequestPanel } from "./ProviderRequestPanel";
import { ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog"; import { ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog";
import { composeSecret } from "./provider-secret"; import { composeSecret } from "./provider-secret";
import type { SnapshotActions } from "../../app/useSnapshot"; import type { SnapshotActions } from "../../app/useSnapshot";
@@ -24,16 +21,14 @@ interface ProvidersPanelProps {
authProfiles: ProviderAuthProfileRecord[]; authProfiles: ProviderAuthProfileRecord[];
catalog: ProviderCatalogEntry[]; catalog: ProviderCatalogEntry[];
agents: AgentRecord[]; agents: AgentRecord[];
workspaces: WorkspaceRecord[];
providerRateLimits: ProviderRateLimitStateRecord[]; providerRateLimits: ProviderRateLimitStateRecord[];
providerRequests: ProviderRequestRecord[];
actions: SnapshotActions; actions: SnapshotActions;
} }
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 92px"; const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 92px";
type WorkspaceScope = "organization" | "workspace"; type WorkspaceScope = "organization" | "workspace";
export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, workspaces, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) { export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
const [active, setActive] = useState<ProviderRecord | null>(null); const [active, setActive] = useState<ProviderRecord | null>(null);
const connected = providers.filter((provider) => provider.enabled); const connected = providers.filter((provider) => provider.enabled);
const available = providers.filter((provider) => !provider.enabled); const available = providers.filter((provider) => !provider.enabled);
@@ -62,11 +57,10 @@ export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, cat
return ( return (
<div className="page"> <div className="page">
<h1 className="page-h1">Providers</h1> <h1 className="page-h1">Providers</h1>
<p className="page-sub">Connect model providers — keys, accounts, OAuth.</p>
{connected.length > 0 && ( {connected.length > 0 && (
<> <>
<div className="section-label" style={{ marginBottom: 10 }}>Connected</div> <div className="section-label" style={{ margin: "16px 0 10px" }}>Connected</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 28 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 28 }}>
{connected.map((provider) => ( {connected.map((provider) => (
<ProviderRow <ProviderRow
@@ -86,14 +80,6 @@ export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, cat
providerRateLimits={providerRateLimits} providerRateLimits={providerRateLimits}
actions={actions} actions={actions}
/> />
<ProviderRequestPanel
orgId={orgId}
providers={connected}
catalog={catalog}
workspaces={workspaces}
providerRequests={providerRequests}
actions={actions}
/>
</> </>
)} )}
@@ -159,7 +145,7 @@ function ProviderPolicyTable({
const rateLimit = requireProviderRateLimit(provider, providerRateLimits); const rateLimit = requireProviderRateLimit(provider, providerRateLimits);
return ( return (
<ProviderPolicyRow <ProviderPolicyRow
key={`${provider.id}:${provider.dataPolicy}:${provider.backgroundEnabled}:${provider.monthlyRequestLimit}`} key={provider.id}
orgId={orgId} orgId={orgId}
provider={provider} provider={provider}
rateLimit={rateLimit} rateLimit={rateLimit}
@@ -283,6 +269,7 @@ function ProviderRow({
const boundCount = agents.filter((agent) => agent.modelProviderId === provider.id).length; const boundCount = agents.filter((agent) => agent.modelProviderId === provider.id).length;
return ( return (
<div <div
className="reveal-row"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -301,13 +288,14 @@ function ProviderRow({
</div> </div>
</div> </div>
<ProviderHealth provider={provider} /> <ProviderHealth provider={provider} />
<div style={{ textAlign: "right", minWidth: 120 }}> <div className="num mono" style={{ fontSize: 13, minWidth: 72, textAlign: "right" }}>
<div className="num mono" style={{ fontSize: 14, fontWeight: 540 }}>{boundCount} {boundCount === 1 ? "agent" : "agents"}</div> {boundCount} {boundCount === 1 ? "agent" : "agents"}
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>bound</div>
</div> </div>
<Button onClick={onCheck}>Check</Button> <span className="reveal row" style={{ gap: 6 }}>
<Button onClick={onManage}>Manage</Button> <Button onClick={onCheck}>Check</Button>
<Button tone="ghost" onClick={onRevoke}>Revoke</Button> <Button onClick={onManage}>Manage</Button>
<Button tone="ghost" onClick={onRevoke}>Revoke</Button>
</span>
</div> </div>
); );
} }
@@ -44,15 +44,10 @@ export function SkillsPanel({ orgId, skills, revisions, bindings, tests, agents,
}; };
return ( return (
<div className="page"> <div className="page reveal-row">
<div className="row" style={{ marginBottom: 24 }}> <div className="row" style={{ marginBottom: 24 }}>
<div> <h1 className="page-h1" style={{ margin: 0 }}>Skills</h1>
<h1 className="page-h1">Skills</h1> <div className="reveal" style={{ marginLeft: "auto" }}>
<p className="page-sub" style={{ marginBottom: 0 }}>
{skills.length} {skills.length === 1 ? "definition" : "definitions"} · {revisions.length} revisions · {bindings.filter((b) => b.enabled).length} bindings
</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setImportOpen(true)}><Icon name="plus" size={16} /> Import</Button> <Button tone="primary" onClick={() => setImportOpen(true)}><Icon name="plus" size={16} /> Import</Button>
</div> </div>
</div> </div>
@@ -48,44 +48,38 @@ export function WorkspacePanel({
return ( return (
<div className="page"> <div className="page">
<h1 className="page-h1">Workspace</h1> <h1 className="page-h1">General</h1>
<p className="page-sub">Identity, mission, and execution mode for {organization.name}.</p>
<div className="card" style={{ padding: 18, marginBottom: 20 }}> <div className="row" style={{ gap: 8, margin: "16px 0 24px" }}>
<div className="section-label" style={{ marginBottom: 12 }}>Identity</div> <Pill tone="violet" dot>{organization.executionMode === "ai_ceo" ? "AI CEO" : "AI Co-Founder"}</Pill>
{organization.autoApproveHires ? <Pill tone="green" dot>Auto-approve hires</Pill> : <Pill>Hires require approval</Pill>}
{organization.reviewDeliverables ? <Pill tone="amber" dot>Reviews deliverables</Pill> : <Pill>Ships without review</Pill>}
</div>
<div style={{ marginBottom: 28 }}>
<KeyValue label="Name" value={organization.name} /> <KeyValue label="Name" value={organization.name} />
<KeyValue label="Mission" value={organization.mission || "—"} /> <KeyValue label="Mission" value={organization.mission || "—"} />
<KeyValue label="Goal" value={organization.goal || "—"} /> <KeyValue label="Goal" value={organization.goal || "—"} />
<KeyValue label="Description" value={organization.description || "—"} /> <KeyValue label="Description" value={organization.description || "—"} />
</div> </div>
<div className="card" style={{ padding: 18, marginBottom: 20 }}> <div className="row reveal-row" style={{ marginBottom: 8 }}>
<div className="section-label" style={{ marginBottom: 12 }}>Execution mode</div> <div className="section-label">Workspaces</div>
<div className="row" style={{ gap: 8 }}> <span className="reveal" style={{ marginLeft: "auto" }}>
<Pill tone="violet" dot>{organization.executionMode === "ai_ceo" ? "AI CEO" : "AI Co-Founder"}</Pill> <Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> Add</Button>
{organization.autoApproveHires ? <Pill tone="green" dot>Auto-approve hires</Pill> : <Pill>Hires require approval</Pill>} </span>
{organization.reviewDeliverables ? <Pill tone="amber" dot>Reviews deliverables</Pill> : <Pill>Deliverables ship without review</Pill>}
</div>
</div> </div>
{workspaces.map((workspace) => (
<div className="card" style={{ padding: 18 }}> <div key={workspace.id} className="row" style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div className="row" style={{ marginBottom: 12 }}> <div style={{ minWidth: 0 }}>
<div className="section-label">Workspaces</div> <div style={{ fontWeight: 540 }}>{workspace.name}</div>
<span style={{ marginLeft: "auto" }}><Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> Add</Button></span> <div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
</div>
{workspaces.map((workspace) => (
<div key={workspace.id} className="row" style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div style={{ minWidth: 0 }}>
<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>
<div style={{ marginLeft: "auto" }}>
{workspace.isPrimary ? <Pill tone="violet">Primary</Pill> : <Pill>Secondary</Pill>}
</div>
</div> </div>
))} <div style={{ marginLeft: "auto" }}>
{workspaces.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>No workspaces configured</div>} {workspace.isPrimary && <Pill tone="violet">Primary</Pill>}
</div> </div>
</div>
))}
<Dialog title="Add workspace" open={createOpen} submitLabel="Create" onClose={() => setCreateOpen(false)} onSubmit={submit}> <Dialog title="Add workspace" open={createOpen} submitLabel="Create" onClose={() => setCreateOpen(false)} onSubmit={submit}>
<Field label="Name"><input className="input" name="name" required /></Field> <Field label="Name"><input className="input" name="name" required /></Field>
@@ -226,6 +226,11 @@ a { color: inherit; }
.convo-item:hover { background: var(--hover); color: var(--text-1); } .convo-item:hover { background: var(--hover); color: var(--text-1); }
.convo-item.active { background: var(--panel-3); color: var(--text-1); } .convo-item.active { background: var(--panel-3); color: var(--text-1); }
/* Controls that appear on intent: hidden until the row is hovered or focused. */
.reveal-row .reveal { opacity: 0; transition: opacity var(--motion-fast) var(--ease-out); }
.reveal-row:hover .reveal,
.reveal-row:focus-within .reveal { opacity: 1; }
/* Selectable list row used by master-detail panes (Runs, etc.). width:100% /* Selectable list row used by master-detail panes (Runs, etc.). width:100%
+ min-width:0 lets the inner flex child ellipsize instead of pushing the + min-width:0 lets the inner flex child ellipsize instead of pushing the
row past the pane and forcing a horizontal scrollbar. */ row past the pane and forcing a horizontal scrollbar. */
+11 -9
View File
@@ -113,19 +113,27 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible(); await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible();
await page.getByRole("button", { name: "Settings" }).click(); await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByText("AI CEO", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Providers" }).click(); await page.getByRole("button", { name: "Providers" }).click();
await page.getByLabel("OpenAI background execution").check(); await page.getByLabel("OpenAI background execution").check();
await page.getByRole("button", { name: "Save OpenAI policy" }).click(); await page.getByRole("button", { name: "Save OpenAI policy" }).click();
await expect(page.getByText("Requests", { exact: true })).toBeVisible();
// The operator request console lives under Advanced.
await page.getByRole("button", { name: "Advanced" }).click();
await expect(page.getByText("Requests", { exact: true }).first()).toBeVisible();
await expect(page.getByLabel("Request provider")).toBeVisible(); await expect(page.getByLabel("Request provider")).toBeVisible();
await expect(page.getByRole("button", { name: "Background" })).toBeVisible(); await expect(page.getByRole("button", { name: "Background" })).toBeVisible();
await page.getByLabel("Request prompt").fill("Draft a provider control memo."); await page.getByLabel("Request prompt").fill("Draft a provider control memo.");
await page.getByLabel("Request instructions").fill("Use direct language."); await page.getByLabel("Request instructions").fill("Use direct language.");
await expect(page.getByLabel("Composio API key")).toBeVisible();
await page.getByRole("button", { name: "Providers" }).click();
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention"); await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked(); await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked();
await page.getByLabel("OpenAI monthly request limit").fill("25"); await page.getByLabel("OpenAI monthly request limit").fill("25");
await page.getByRole("button", { name: "Save OpenAI policy" }).click(); await page.getByRole("button", { name: "Save OpenAI policy" }).click();
await expect(page.getByText("0 / 25").first()).toBeVisible(); await expect(page.getByText("0 / 25").first()).toBeVisible();
await page.getByRole("button", { name: "Skills" }).click(); await page.getByRole("button", { name: "Skills" }).click();
await page.getByRole("button", { name: "Import" }).click(); await page.getByRole("button", { name: "Import" }).click();
await page.getByLabel("Directory path").fill(createSkillDirectory()); await page.getByLabel("Directory path").fill(createSkillDirectory());
@@ -135,7 +143,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Run tests" }).click(); await page.getByRole("button", { name: "Run tests" }).click();
await expect(page.getByText("1/1 passed").first()).toBeVisible(); await expect(page.getByText("1/1 passed").first()).toBeVisible();
const legacyPath = createLegacyDatabase(); const legacyPath = createLegacyDatabase();
await page.getByRole("button", { name: "Migration" }).click(); await page.getByRole("button", { name: "Advanced" }).click();
await page.getByRole("button", { name: "Report" }).click(); await page.getByRole("button", { name: "Report" }).click();
await page.getByLabel("Source database path").fill(legacyPath); await page.getByLabel("Source database path").fill(legacyPath);
await page.locator(".modal").getByRole("button", { name: "Create" }).click(); await page.locator(".modal").getByRole("button", { name: "Create" }).click();
@@ -150,12 +158,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Office" }).click(); await page.getByRole("button", { name: "Office" }).click();
await expect(page.locator("canvas")).toBeVisible(); await expect(page.locator("canvas")).toBeVisible();
// Composio settings panel
await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: "Tools", exact: true }).click();
await expect(page.getByText("External Tools")).toBeVisible();
await expect(page.getByText("GitHub")).toBeVisible();
// @mention with canonical handle in chat // @mention with canonical handle in chat
await page.getByRole("button", { name: "Messages" }).click(); await page.getByRole("button", { name: "Messages" }).click();
await page.getByText("general").first().click(); await page.getByText("general").first().click();
@@ -204,7 +206,7 @@ async function verifyPagesDoNotOverflow(page: Page): Promise<void> {
await page.getByRole("button", { name: nav, exact: true }).click(); await page.getByRole("button", { name: nav, exact: true }).click();
await expectPageToFit(page); await expectPageToFit(page);
} }
for (const tab of ["Workspace", "Billing", "Providers", "Tools", "Skills", "Migration"]) { for (const tab of ["General", "Providers", "Skills", "Advanced"]) {
await page.getByRole("button", { name: "Settings" }).click(); await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: tab, exact: true }).click(); await page.getByRole("button", { name: tab, exact: true }).click();
await expectPageToFit(page); await expectPageToFit(page);
@@ -50,7 +50,7 @@ test("captures every page for visual review", async () => {
await go(page, "Settings", outDir, "17-settings"); await go(page, "Settings", outDir, "17-settings");
await go(page, "Office", outDir, "18-office"); await go(page, "Office", outDir, "18-office");
for (const tab of ["Workspace", "Billing", "Skills", "Migration"]) { for (const tab of ["General", "Providers", "Skills", "Advanced"]) {
await page.getByRole("button", { name: "Settings" }).click(); await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: tab }).first().click(); await page.getByRole("button", { name: tab }).first().click();
await snap(page, outDir, `settings-${tab.toLowerCase()}`); await snap(page, outDir, `settings-${tab.toLowerCase()}`);