diff --git a/.claude/skills/openbot-screen-layout/SKILL.md b/.claude/skills/openbot-screen-layout/SKILL.md index 958b5a1204..e27a721a4f 100644 --- a/.claude/skills/openbot-screen-layout/SKILL.md +++ b/.claude/skills/openbot-screen-layout/SKILL.md @@ -51,8 +51,9 @@ PageShell the page: header (title, description, action, backButton) then PageEmpty what a section says when it has nothing to list ``` -`PageShell` takes a `width`: `prose` (`max-w-2xl`) is the default because configuration is mostly -reading, and a row of label-and-control has no business being wider than the sentence explaining it. +`PageShell` takes a `width`: `prose` (`max-w-3xl`, 720px here) is the default, and every +configuration screen uses it so no screen is wider than another. Sentences inside it are held to +`max-w-prose`, so the column has room for a label beside its control without stretching the text. `wide` (`max-w-5xl`) exists for one screen — `admin/audit.tsx`, because an audit log is a table to be scanned and prose width would wrap every row. It is not a licence for anything else to be wide. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7582826b80..3baa354ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,31 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. request for an administrator to switch on a work app, grant it to a Bot, or connect a Shared app's team account is kept, with who answered it and when. It starts empty and nothing is backfilled. +### Every settings screen is the same, slightly wider column + +Settings, Admin, Connections, Capabilities and each Bot's pages share one column width, now 720px instead of 630px, so +no screen is wider than another and a row has room for its label beside its control. Sentences keep +their comfortable reading width inside it. The audit log stays wider, because it is a table. + +### Capabilities and Connections sit beside Bots in the sidebar + +The sidebar's Skills entry becomes two, below Bots: **Capabilities** (`/capabilities`), with Skills +(`/capabilities/skills`) and Memory (`/capabilities/memory`) as tabs under its title, Memory leaving +Settings; and **Connections** (`/connections`), the apps your Bots use as you, which leaves Settings +too. On the home page, Explore agents shows each Bot as a white card with its avatar on top, the same +card as elsewhere. The old +addresses — `/skills`, `/settings/connected-accounts` with any account beneath it, and +`/settings/memory` — redirect to the new ones, keeping `?connected=`, `?linked=` and a skill being +written or edited, so a consent begun before upgrading still lands on its page. OAuth, GitHub linking +and the Slack link confirmation now send people to `/connections`. + +Connections is one searchable list. Apps you connect, apps on your organisation's account (marked +"Team account") and your GitHub and Slack links are rows of the same kind, with those already in use +first, and each opens its own page. GitHub's page holds your link, the repositories whose events can +start a responsibility (formerly "GitHub events"), and a way to a Composio GitHub app where one is +enabled, so GitHub is listed once; linking GitHub now comes back to that page. The reset for what +you let shared Bots use moves to Settings → Approvals, as **Shared Bots**. + ### Each app on a Bot says whether you can use it, and you can ask an administrator for the rest A Bot's page now says, for every work app the Bot holds, what that app is for the person looking: diff --git a/README.md b/README.md index 7d5b936777..1d5891d817 100644 --- a/README.md +++ b/README.md @@ -193,14 +193,14 @@ Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you | `/bots/:id/reach` | Continue a conversation with the Bot in Slack, Microsoft Teams, or by text message. | | `/bots/:id/memory` | Which connected apps feed the Bot facts, and its background research. | | `/bots/:id/approvals` | What the Bot may do without asking you first. | -| `/skills` | Create and enable personal skills. | +| `/connections` | Connect your apps so your Bots can work with them. | +| `/capabilities/skills` | Create and enable personal skills. | +| `/capabilities/memory` | What your Bots remember about you, and facts you tell them. | | `/routines` | See the routines that are standing, and stop one. | | `/settings` | User preferences. | -| `/settings/connected-accounts` | Connect your apps so your Bots can work with them. | | `/settings/passwords` | Logins you saved when signing a Bot in to a website. | | `/settings/notifications` | Where updates go, your devices, and recent deliveries. | | `/settings/approvals` | Whether your Bots ask before acting, and rules for every Bot. | -| `/settings/memory` | What your Bots remember about you, and facts you tell them. | | `/admin/credentials` | Store write-only encrypted credentials. | | `/admin/approvals` | Team approval settings, team rules, and shared account requests. | | `/admin/computers` | View, stop, and reset Bot computers. | diff --git a/app/src/components/agents/agent-card.tsx b/app/src/components/agents/agent-card.tsx index 74f2a8216c..67cc270a06 100644 --- a/app/src/components/agents/agent-card.tsx +++ b/app/src/components/agents/agent-card.tsx @@ -1,6 +1,5 @@ import { IconArrowRight } from "@tabler/icons-react"; import { Link } from "@tanstack/react-router"; -import Avatar from "boring-avatars"; import { AbstractAvatar } from "@/components/agents/abstract-avatar"; import { Button } from "@/components/ui/button"; import type { AgentProfile } from "@/lib/agents/queries"; @@ -10,21 +9,23 @@ export function AgentCard({ appearance = "compact", }: { agent: AgentProfile; - appearance?: "compact" | "artwork"; + appearance?: "compact" | "tile"; }) { - if (appearance === "artwork") { + if (appearance === "tile") { return ( -
+ {notice.text} +
+ ) : null} +
{memories.error.message}
diff --git a/app/src/components/plugins/app-access-action.tsx b/app/src/components/plugins/app-access-action.tsx
index df9e2e9d37..2f1dcf4f5a 100644
--- a/app/src/components/plugins/app-access-action.tsx
+++ b/app/src/components/plugins/app-access-action.tsx
@@ -152,12 +152,7 @@ function ConnectAction({ access }: { access: AppAccess }) {
if (viaSettings)
return (
- }
+ render={}
aria-label={`${label} ${access.displayName}`}
size="sm"
variant="outline"
diff --git a/app/src/components/responsibilities/new-responsibility-dialog.tsx b/app/src/components/responsibilities/new-responsibility-dialog.tsx
index 757abe0b9c..abd5ea0b2a 100644
--- a/app/src/components/responsibilities/new-responsibility-dialog.tsx
+++ b/app/src/components/responsibilities/new-responsibility-dialog.tsx
@@ -251,7 +251,7 @@ export function NewResponsibilityDialog({
{source === "github" && (
diff --git a/app/src/components/settings/linked-accounts.test.tsx b/app/src/components/settings/linked-accounts.test.tsx
index a81f6f7759..8fd6151dde 100644
--- a/app/src/components/settings/linked-accounts.test.tsx
+++ b/app/src/components/settings/linked-accounts.test.tsx
@@ -347,3 +347,30 @@ test("the GitHub Connect button stays disabled from the request until the page l
true,
);
});
+
+test("one provider's page draws only that provider's link and offer", () => {
+ expect(
+ linkedAccountsSectionState({
+ data: [row, slackRow],
+ error: null,
+ only: "slack",
+ providers: { slack: true, github: true },
+ })?.entries,
+ ).toEqual([{ kind: "linked", account: slackRow }]);
+ expect(
+ linkedAccountsSectionState({
+ data: [slackRow],
+ error: null,
+ only: "github",
+ providers: { slack: true, github: true },
+ })?.entries,
+ ).toEqual([{ kind: "available", provider: "github", title: "GitHub" }]);
+ expect(
+ linkedAccountsSectionState({
+ data: [slackRow],
+ error: null,
+ only: "github",
+ providers: { slack: true, github: false },
+ }),
+ ).toBe(null);
+});
diff --git a/app/src/components/settings/linked-accounts.tsx b/app/src/components/settings/linked-accounts.tsx
index eb94374674..ecf2804ea2 100644
--- a/app/src/components/settings/linked-accounts.tsx
+++ b/app/src/components/settings/linked-accounts.tsx
@@ -1,7 +1,11 @@
import { IconBrandGithub, IconBrandSlack, IconLink } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
-import { PageRows, PageSection } from "@/components/layout/page-shell";
+import {
+ PageEmpty,
+ PageRows,
+ PageSection,
+} from "@/components/layout/page-shell";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -63,11 +67,21 @@ const linkableProviders = [
{ provider: "github", title: "GitHub" },
] as const;
+export type LinkableProvider = (typeof linkableProviders)[number]["provider"];
+
+/** The name a linkable provider is shown under. */
+export function linkableProviderTitle(provider: LinkableProvider): string {
+ return (
+ linkableProviders.find((entry) => entry.provider === provider)?.title ??
+ provider
+ );
+}
+
export type LinkedAccountsEntry =
| { kind: "linked"; account: LinkedAccount }
| {
kind: "available";
- provider: (typeof linkableProviders)[number]["provider"];
+ provider: LinkableProvider;
title: string;
};
@@ -85,11 +99,14 @@ export type LinkedAccountsEntry =
export function linkedAccountsSectionState({
data,
error,
+ only,
providers,
providersError = null,
}: {
data: LinkedAccount[] | undefined;
error: Error | null;
+ /** One provider's page draws only that provider's links and offer. */
+ only?: LinkableProvider;
providers?: IdentityProviders;
providersError?: Error | null;
}): {
@@ -100,13 +117,14 @@ export function linkedAccountsSectionState({
const links = data ?? [];
const entries: LinkedAccountsEntry[] = [];
for (const { provider, title } of linkableProviders) {
+ if (only && provider !== only) continue;
const linked = links.filter((account) => account.provider === provider);
for (const account of linked) entries.push({ kind: "linked", account });
if (linked.length === 0 && data !== undefined && providers?.[provider]) {
entries.push({ kind: "available", provider, title });
}
}
- for (const account of links) {
+ for (const account of only ? [] : links) {
if (
!linkableProviders.some(({ provider }) => provider === account.provider)
)
@@ -139,7 +157,15 @@ export function connectGithubPending(mutation: {
* Separate from the connector rows above it: those are services a Bot reads as you, these are how
* this deployment recognises you when a message or an event arrives from somewhere else.
*/
-export function LinkedAccountsSection() {
+/**
+ * The person's own account at one provider, on that provider's Connections page: the link with a
+ * Disconnect button, or Connect where this deployment can link it.
+ */
+export function LinkedAccountSection({
+ provider,
+}: {
+ provider: LinkableProvider;
+}) {
const links = useQuery(linkedAccountsQueryOptions());
const providers = useQuery(identityProvidersQueryOptions());
const issueCode = useMutation(issueSlackCodeMutationOptions());
@@ -169,10 +195,20 @@ export function LinkedAccountsSection() {
const state = linkedAccountsSectionState({
data: links.data,
error: links.error,
+ only: provider,
providers: providers.data,
providersError: providers.error,
});
- if (!state) return null;
+ if (!state) {
+ return (
+
+ {clear.error.message}
+
+ {error}
+
+ Your skills could not be loaded.
+
+ * {skill.summary}
+ *
+ * You do not own a Bot to put this on yet.
+ *
+ Whether you have connected this account could not be loaded, so
+ nothing about it is shown here rather than something that may be
+ wrong. Reload the page, and tell an administrator if it persists.
+
+ This connector could not be loaded, so nothing about it is shown here
+ rather than something that may be wrong. Reload the page, and tell an
+ administrator if it persists.
+
+ {landedActionNotice(landedAction)}
+
+ {notice}
+
+ {landedActionNotice(landedAction)}
+
+ {notice}
+
+ That account could not be connected. Nothing was saved — try again.
+
+ {notice.text}
+
+ {notice.text}
+
+ {arrival.authorizationNotice}
+
+ Your connections could not be loaded, so nothing is listed here
+ rather than a list that may be wrong. Reload the page, and tell an
+ administrator if it persists.
+
+ {inUse} in use · {rows.length} in all
+
+ Nothing matches “{search.trim()}”.
+
+ Whether you can link GitHub or Slack could not be loaded, so they
+ are not listed. Reload the page to try again.
+
- {error}
-
- Your skills could not be loaded.
-
- * {skill.summary}
- *
- * You do not own a Bot to put this on yet.
- *
- Whether you have connected this account could not be loaded, so
- nothing about it is shown here rather than something that may be
- wrong. Reload the page, and tell an administrator if it persists.
-
- This connector could not be loaded, so nothing about it is shown here
- rather than something that may be wrong. Reload the page, and tell an
- administrator if it persists.
-
- {landedActionNotice(landedAction)}
-
- {notice}
-
- {landedActionNotice(landedAction)}
-
- {notice}
-
- That account could not be connected. Nothing was saved — try again.
-
- {notice.text}
-
- {notice.text}
-
- {arrival.authorizationNotice}
-
- Your connected accounts could not be loaded, so nothing is listed here
- rather than a list that may be wrong. Reload the page, and tell an
- administrator if it persists.
-
- {connectedCount} connected · {accounts.length} available
-
- No apps match “{search.trim()}”.
-
- {clear.error.message}
- / and
+ a Bot follows. Yours are yours alone, and go on the Bots you own.
+ >
+ }
+ title="Your skills"
+ >
+ {/*
+ * Nothing while the two queries are still in flight. The alternative is the empty state
+ * standing there saying this person has written no skills, which is a claim the page has
+ * not yet earned.
+ *
+ * And not after a read that failed, for the same reason: `isPending` goes false on a
+ * failed fetch too, and no list is no evidence of no skills. A list already held stays on
+ * screen through a failed refetch, because `data` is still there.
+ */}
+ {loading ? null : !data ? (
+
+ /{skill.slug}
+
+ {skill.summary ? ` · ${skill.summary}` : null}
+
+ /{skill.slug}
+
+ {skill.summary ? ` · ${skill.summary}` : null}
+ /{skill.slug}
+ * {skill.title}
+ * / and a
- Bot follows. Yours are yours alone, and go on the Bots you own.
- >
- }
- title="Agent Skills"
- >
- {error ? (
-
- /{skill.slug}
-
- {skill.summary ? ` · ${skill.summary}` : null}
-
- /{skill.slug}
-
- {skill.summary ? ` · ${skill.summary}` : null}
- /{skill.slug}
- * {skill.title}
- *
- {section.title}
-
- `, and sends it to the
+**Connect** on Slack's page, which shows a message of the form `link `, and sends it to the
OpenBot app in a direct message within ten minutes. A code sent in a channel is refused and stays
valid until it expires. Teams cannot be linked this way.
@@ -530,16 +532,16 @@ GitHub uses a GitHub App. Create one under GitHub Settings, Developer settings,
- A client secret, generated on the App's page.
Then set `GITHUB_APP_CLIENT_ID` and `GITHUB_APP_CLIENT_SECRET`; set both or neither. People
-then use **Connect** on the GitHub card while signed in to OpenBot in the same browser. The GitHub
+then use **Connect** on GitHub's page while signed in to OpenBot in the same browser. The GitHub
user token is stored encrypted with the link.
-After GitHub, the browser is sent back to `OPENBOT_APP_URL`, else the first `TRUSTED_ORIGINS` entry,
+After GitHub, the browser is sent back to GitHub's page under Connections, at `OPENBOT_APP_URL`, else the first `TRUSTED_ORIGINS` entry,
else the public URL. Set `OPENBOT_APP_URL` when the app and the server are on different origins, as
in local development (app on `3010`, server on `3001`); one origin needs nothing.
-Reachability codes and Linked accounts codes look alike, and Reachability's are checked first. A
+Reachability codes and account-link codes look alike, and Reachability's are checked first. A
Reachability `link ` binds a conversation for delivery and is not recorded as a linked
-account; a `link ` from **Linked accounts** links the Slack user.
+account; a `link ` from Slack's page under Connections links the Slack user.
## Text messages and push notifications
diff --git a/docs/plugins/arcade.md b/docs/plugins/arcade.md
index 68c50f5af5..0b4ac3f25d 100644
--- a/docs/plugins/arcade.md
+++ b/docs/plugins/arcade.md
@@ -37,7 +37,7 @@ Setting it up takes three hands, and none of them can do another's:
| Whoever deploys | Sets `OPENBOT_BROKER_PROVIDER`, only where both keys are set | The deployment's environment |
| Whoever deploys | Registers an OAuth app per provider, and sets the verifier | Arcade's dashboard, below |
| An administrator | Enables an app and grants its actions to a Bot | `/admin/plugins/arcade`, then the app's own page, as for Composio |
-| Each person | Connects their own account to that app | `/settings/connected-accounts` |
+| Each person | Connects their own account to that app | `/connections` |
The key is an environment variable and nothing else, as Composio's is. No administrator can turn
Arcade on from a page; `/admin/plugins/arcade` is where a key that is already set gets used.
@@ -118,7 +118,7 @@ followed by the path, so paste:
```
It is the API's origin, not the browser app's, where the two differ. A browser that arrives there
-is sent on to the connected-accounts page, with a reason beside it if the consent was refused.
+is sent on to the Connections page, with a reason beside it if the consent was refused.
**Nothing checks this setting, so the deployment warns about it instead.** Arcade has no API that
reads which verifier a project uses, so this deployment cannot tell a correctly pointed project from
@@ -134,7 +134,7 @@ refuses a consent for any `user_id` that is not a member of the Arcade project
deployment sends are its own internal ids, not project members — so in practice **nobody can
connect**: the person is left on Arcade's
page with _"the authorization process was not completed"_. A consent that does complete at Arcade
-that way is bound to no attempt; the connected-accounts page may later record it when it asks Arcade
+that way is bound to no attempt; the Connections page may later record it when it asks Arcade
whether the person is connected, but without the identity check the verify leg exists to make.
> **Do not save the custom-verifier setting without a URL.** It switches off the path that currently
diff --git a/docs/plugins/composio.md b/docs/plugins/composio.md
index 0e5afe5144..97b9a29f10 100644
--- a/docs/plugins/composio.md
+++ b/docs/plugins/composio.md
@@ -15,7 +15,7 @@ Setting it up takes three hands, and none of them can do another's:
| Whoever deploys | Sets `COMPOSIO_API_KEY` | The deployment's environment, or `secrets.composioApiKey` in the Helm chart. There is no screen for it |
| An administrator | Enables an app | `/admin/plugins/composio` |
| An administrator | Grants its actions to a Bot | `/admin/plugins/composio-