From d64cf22febae97d122c652377e5d1f0c50cda1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 6 Jul 2026 23:48:31 -0400 Subject: [PATCH] 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. --- .../src/renderer/src/pages/SettingsPage.tsx | 33 ++-- .../src/pages/settings/AdvancedPanel.tsx | 86 +++++++++++ .../src/pages/settings/BillingPanel.tsx | 5 +- .../src/pages/settings/ComposioPanel.tsx | 143 +++--------------- .../src/pages/settings/MigrationPanel.tsx | 11 +- .../src/pages/settings/ProvidersPanel.tsx | 34 ++--- .../src/pages/settings/SkillsPanel.tsx | 11 +- .../src/pages/settings/WorkspacePanel.tsx | 52 +++---- AgentCompany-New/src/renderer/src/styles.css | 5 + AgentCompany-New/tests/e2e/desktop.spec.ts | 20 +-- .../tests/e2e/screenshots.spec.ts | 2 +- 11 files changed, 186 insertions(+), 216 deletions(-) create mode 100644 AgentCompany-New/src/renderer/src/pages/settings/AdvancedPanel.tsx diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 69ed4f18..fb38abb2 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -2,13 +2,11 @@ import { useMemo, useState } from "react"; import { ProvidersPanel } from "./settings/ProvidersPanel"; import { SkillsPanel } from "./settings/SkillsPanel"; import { WorkspacePanel } from "./settings/WorkspacePanel"; -import { BillingPanel } from "./settings/BillingPanel"; -import { MigrationPanel } from "./settings/MigrationPanel"; -import { ComposioPanel } from "./settings/ComposioPanel"; +import { AdvancedPanel } from "./settings/AdvancedPanel"; import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types"; -type SettingsTab = "workspace" | "billing" | "providers" | "tools" | "skills" | "migration"; +type SettingsTab = "general" | "providers" | "skills" | "advanced"; interface NavSpec { id: SettingsTab; @@ -16,19 +14,17 @@ interface NavSpec { } const TABS: NavSpec[] = [ - { id: "workspace", label: "Workspace" }, - { id: "billing", label: "Billing" }, + { id: "general", label: "General" }, { id: "providers", label: "Providers" }, - { id: "tools", label: "Tools" }, { id: "skills", label: "Skills" }, - { id: "migration", label: "Migration" }, + { id: "advanced", label: "Advanced" }, ]; export function SettingsPage({ snapshot, actions }: PageProps) { const orgId = activeOrgId(snapshot); const organization = requireActiveOrganization(snapshot); const employee = requireActiveEmployee(snapshot); - const [tab, setTab] = useState("providers"); + const [tab, setTab] = useState("general"); 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]); @@ -60,8 +56,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
- {tab === "workspace" && p.orgId === orgId)} orgId={orgId} actions={actions} />} - {tab === "billing" && } + {tab === "general" && p.orgId === orgId)} orgId={orgId} actions={actions} />} {tab === "providers" && ( )} - {tab === "tools" && } {tab === "skills" && ( )} - {tab === "migration" && ( - item.orgId === orgId)} actions={actions} diff --git a/AgentCompany-New/src/renderer/src/pages/settings/AdvancedPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/AdvancedPanel.tsx new file mode 100644 index 00000000..901c8bcc --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/settings/AdvancedPanel.tsx @@ -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 ( +
+

Advanced

+ +
Composio
+ + + {connected.length > 0 && ( +
+ +
+ )} + +
+ +
+ +
+ +
+
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/settings/BillingPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/BillingPanel.tsx index 9f4e4006..50284b89 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/BillingPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/BillingPanel.tsx @@ -22,9 +22,8 @@ export function BillingPanel({ const agentSpend = buildAgentSpend(agents, providers, monthRequests); return ( -
-

Billing

-

Allocations, spend, and cost breakdown.

+
+
Billing
diff --git a/AgentCompany-New/src/renderer/src/pages/settings/ComposioPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/ComposioPanel.tsx index 7e71e113..756b6ca9 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/ComposioPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/ComposioPanel.tsx @@ -3,44 +3,14 @@ import { Button } from "../../shared/ui"; import type { SnapshotActions } from "../../app/useSnapshot"; 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 { - 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 { 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({ orgId, settings, @@ -50,98 +20,33 @@ export function ComposioPanel({ settings: ComposioSettingRecord[]; actions: SnapshotActions; }) { - const savedToolkits = useMemo(() => parseSavedToolkits(settings, orgId), [settings, orgId]); const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]); - const [apiKey, setApiKey] = useState(""); - const [selected, setSelected] = useState>(savedToolkits); - const [saved, setSaved] = useState(false); - const toggle = (id: string) => { - setSelected((prev) => { - 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 handleSave = (event: FormEvent) => { + event.preventDefault(); const key = apiKey.trim(); - if (!key && !keyConfigured) return; - const row = settings.find((s) => s.orgId === orgId && s.key === "api_key"); - const effectiveKey = key || row?.value || ""; - if (!effectiveKey) return; - await actions.run(() => window.agentCompany.saveComposioSettings({ + if (!key) return; + void actions.run(() => window.agentCompany.saveComposioSettings({ orgId, - apiKey: effectiveKey, - toolkits: [...selected], - })); - setApiKey(""); - setSaved(true); - setTimeout(() => setSaved(false), 2000); + apiKey: key, + toolkits: [], + })).then(() => setApiKey("")); }; return ( -
-

External Tools

-

- Connect your agents to external services via Composio. -

-
- -
- Toolkits -
- {AVAILABLE_TOOLKITS.map((toolkit) => ( - - ))} -
-
-
- - {saved && Saved} -
-
-
+
+ setApiKey(event.target.value)} + placeholder={keyConfigured ? "••••••••" : "ak_..."} + autoComplete="off" + style={{ flex: 1 }} + /> + +
); } diff --git a/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx index b92c9d6b..3ff57501 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx @@ -40,13 +40,10 @@ export function MigrationPanel({ }; return ( -
-
-
-

Migration

-

{reports.length} migration {reports.length === 1 ? "report" : "reports"}.

-
-
+
+
+
Migration
+
diff --git a/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx index be1dee1b..8d3a6c0c 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx @@ -7,12 +7,9 @@ import type { ProviderAuthProfileRecord, ProviderRateLimitStateRecord, ProviderRecord, - ProviderRequestRecord, - WorkspaceRecord, } from "../../../../shared/domain"; import { PROVIDER_DATA_POLICY } from "../../../../shared/domain"; import { ProviderLogo } from "./ProviderLogo"; -import { ProviderRequestPanel } from "./ProviderRequestPanel"; import { ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog"; import { composeSecret } from "./provider-secret"; import type { SnapshotActions } from "../../app/useSnapshot"; @@ -24,16 +21,14 @@ interface ProvidersPanelProps { authProfiles: ProviderAuthProfileRecord[]; catalog: ProviderCatalogEntry[]; agents: AgentRecord[]; - workspaces: WorkspaceRecord[]; providerRateLimits: ProviderRateLimitStateRecord[]; - providerRequests: ProviderRequestRecord[]; actions: SnapshotActions; } const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 92px"; 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(null); const connected = 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 (

Providers

-

Connect model providers — keys, accounts, OAuth.

{connected.length > 0 && ( <> -
Connected
+
Connected
{connected.map((provider) => ( - )} @@ -159,7 +145,7 @@ function ProviderPolicyTable({ const rateLimit = requireProviderRateLimit(provider, providerRateLimits); return ( agent.modelProviderId === provider.id).length; return (
-
-
{boundCount} {boundCount === 1 ? "agent" : "agents"}
-
bound
+
+ {boundCount} {boundCount === 1 ? "agent" : "agents"}
- - - + + + + +
); } diff --git a/AgentCompany-New/src/renderer/src/pages/settings/SkillsPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/SkillsPanel.tsx index d99b2f54..f42246be 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/SkillsPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/SkillsPanel.tsx @@ -44,15 +44,10 @@ export function SkillsPanel({ orgId, skills, revisions, bindings, tests, agents, }; return ( -
+
-
-

Skills

-

- {skills.length} {skills.length === 1 ? "definition" : "definitions"} · {revisions.length} revisions · {bindings.filter((b) => b.enabled).length} bindings -

-
-
+

Skills

+
diff --git a/AgentCompany-New/src/renderer/src/pages/settings/WorkspacePanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/WorkspacePanel.tsx index 8ce4b522..2b2ddf9a 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/WorkspacePanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/WorkspacePanel.tsx @@ -48,44 +48,38 @@ export function WorkspacePanel({ return (
-

Workspace

-

Identity, mission, and execution mode for {organization.name}.

+

General

-
-
Identity
+
+ {organization.executionMode === "ai_ceo" ? "AI CEO" : "AI Co-Founder"} + {organization.autoApproveHires ? Auto-approve hires : Hires require approval} + {organization.reviewDeliverables ? Reviews deliverables : Ships without review} +
+ +
-
-
Execution mode
-
- {organization.executionMode === "ai_ceo" ? "AI CEO" : "AI Co-Founder"} - {organization.autoApproveHires ? Auto-approve hires : Hires require approval} - {organization.reviewDeliverables ? Reviews deliverables : Deliverables ship without review} -
+
+
Workspaces
+ + +
- -
-
-
Workspaces
- -
- {workspaces.map((workspace) => ( -
-
-
{workspace.name}
-
{workspace.localPath}
-
-
- {workspace.isPrimary ? Primary : Secondary} -
+ {workspaces.map((workspace) => ( +
+
+
{workspace.name}
+
{workspace.localPath}
- ))} - {workspaces.length === 0 &&
No workspaces configured
} -
+
+ {workspace.isPrimary && Primary} +
+
+ ))} setCreateOpen(false)} onSubmit={submit}> diff --git a/AgentCompany-New/src/renderer/src/styles.css b/AgentCompany-New/src/renderer/src/styles.css index c4b91ff9..c904a7df 100644 --- a/AgentCompany-New/src/renderer/src/styles.css +++ b/AgentCompany-New/src/renderer/src/styles.css @@ -226,6 +226,11 @@ a { color: inherit; } .convo-item:hover { background: var(--hover); 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% + min-width:0 lets the inner flex child ellipsize instead of pushing the row past the pane and forcing a horizontal scrollbar. */ diff --git a/AgentCompany-New/tests/e2e/desktop.spec.ts b/AgentCompany-New/tests/e2e/desktop.spec.ts index 8c597308..7a897ec1 100644 --- a/AgentCompany-New/tests/e2e/desktop.spec.ts +++ b/AgentCompany-New/tests/e2e/desktop.spec.ts @@ -113,19 +113,27 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible(); 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.getByLabel("OpenAI background execution").check(); 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.getByRole("button", { name: "Background" })).toBeVisible(); await page.getByLabel("Request prompt").fill("Draft a provider control memo."); 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 expect(page.getByLabel("OpenAI background execution")).not.toBeChecked(); await page.getByLabel("OpenAI monthly request limit").fill("25"); await page.getByRole("button", { name: "Save OpenAI policy" }).click(); await expect(page.getByText("0 / 25").first()).toBeVisible(); + await page.getByRole("button", { name: "Skills" }).click(); await page.getByRole("button", { name: "Import" }).click(); 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 expect(page.getByText("1/1 passed").first()).toBeVisible(); 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.getByLabel("Source database path").fill(legacyPath); 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 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 await page.getByRole("button", { name: "Messages" }).click(); await page.getByText("general").first().click(); @@ -204,7 +206,7 @@ async function verifyPagesDoNotOverflow(page: Page): Promise { await page.getByRole("button", { name: nav, exact: true }).click(); 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: tab, exact: true }).click(); await expectPageToFit(page); diff --git a/AgentCompany-New/tests/e2e/screenshots.spec.ts b/AgentCompany-New/tests/e2e/screenshots.spec.ts index 1e242095..ace30509 100644 --- a/AgentCompany-New/tests/e2e/screenshots.spec.ts +++ b/AgentCompany-New/tests/e2e/screenshots.spec.ts @@ -50,7 +50,7 @@ test("captures every page for visual review", async () => { await go(page, "Settings", outDir, "17-settings"); 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: tab }).first().click(); await snap(page, outDir, `settings-${tab.toLowerCase()}`);