diff --git a/.claude/skills/openbot-copy/SKILL.md b/.claude/skills/openbot-copy/SKILL.md
new file mode 100644
index 0000000000..17afc18ddb
--- /dev/null
+++ b/.claude/skills/openbot-copy/SKILL.md
@@ -0,0 +1,77 @@
+---
+name: openbot-copy
+description: The words and the voice for every piece of text a person reads in OpenBot — screen headings and descriptions, buttons, empty states, notices, chat cards, and the server's error and refusal messages that reach the browser. Holds the glossary (Bot, app, admin, OpenBot) and the style rules. Use when writing or changing any text that is shown to a person, naming a screen, a section, a button or a state, writing an error or refusal message, or reviewing a diff that adds user-facing strings under app/src or server/src. Don't use for text written for the model (tool descriptions, standing instructions, system prompts), for code comments, or for the docs under docs/.
+---
+
+# OpenBot Copy
+
+## When To Use
+
+Any string a person reads: a heading, a description, a button, a placeholder, an empty state, a
+notice, a chat card, a server error that the browser shows. Server messages count: a 409 that says
+what to do next is read by a person as much as any screen is.
+
+Not text written for the model. Tool descriptions, standing instructions and prompts steer how a Bot
+behaves, and changing their wording is a behaviour change with its own review. Not code comments,
+and not `docs/`, which has its own register.
+
+## The Glossary
+
+One word for one thing, everywhere a person can see it.
+
+| Say | For | Not |
+|---|---|---|
+| **Bot**, **Bots** (capitalised) | What a person creates, talks to and gives work to. | agent, coworker, assistant, AI |
+| **agent** | Only an outside framework's program, in the places that are about connecting one: "an agent you already run", "AG-UI agent". | Bot, when the thing is a Bot |
+| **app** | A service a Bot uses: Gmail, Google Drive, Notion. | connector, integration, plugin (outside Admin) |
+| **Connections** | The apps and accounts a Bot uses as you, and the page that lists them. | connected accounts, linked accounts |
+| **connect / disconnect / reconnect** | What a person does to their own account. | link (except GitHub and Slack identity, where "link" is the vendor's own word) |
+| **Capabilities** | Your skills and your Bots' memory of you, and the page with both. | plugins |
+| **skill** | A named instruction invoked with `/`. | command, prompt |
+| **admin** | The person who runs OpenBot for everybody. | administrator, owner, operator |
+| **OpenBot** | The product the person is using. | this deployment, the server, the instance |
+| **turn on / turn off** | An admin switching an app or a feature. | enable / disable (in sentences; a switch may still read "Enabled") |
+| **conversation** | A thread with one or more Bots. | channel, chat (in sentences) |
+
+**Admin is allowed exact names.** An admin page may name `COMPOSIO_API_KEY`, OAuth and "deployment",
+because the admin is the one who sets them. A page anybody can open may not.
+
+## The Style
+
+1. **Say what happened, then what to do.** "Gmail isn't connected. Connect it to let Bots read your
+ mail." Not why the screen is built the way it is.
+2. **One idea per sentence. Short sentences.** A description is one or two sentences, about 20 words.
+3. **Plain words.** "use", not "hold a grant on". "can't", not "is unable to". Contractions are fine.
+4. **No "rather than", no em dashes, no "which is why", no "so that nothing … that may be wrong".**
+ These are the marks of text that is explaining itself. Cut the clause or make it its own sentence.
+5. **No internals outside Admin.** Composio, OAuth client, credential, token, grant, scope,
+ deployment, broker, endpoint: say what the person sees instead ("not set up yet", "your account",
+ "this Bot can use it").
+6. **Second person.** "Your apps", "you can". The Bot is "it".
+7. **Sentence case** for headings and buttons: "New Bot", "Your connections".
+8. **Buttons are verbs**: Connect, Request access, Create, Save. A button never says "Click here" or
+ "OK" when it does something specific.
+9. **Errors**: what failed, in the person's terms, and one next step. "Couldn't load your
+ connections. Reload the page." No stack of reasons, no blame.
+10. **Empty states** say what will appear and how: "No skills yet. Write one with New skill."
+
+## Before And After
+
+| Before | After |
+|---|---|
+| 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. | Couldn't load your connections. Reload the page, or ask an admin if it keeps happening. |
+| Sent to the coworker it is for. Type @ to choose one yourself. | OpenBot picks the right Bot. Type @ to choose one. |
+| Gmail is a Composio app, and this deployment has no Composio key. (to a non-admin) | Gmail isn't set up yet. An admin has to turn it on. |
+| The agent is still working on another request. Please wait or stop that task first. | This Bot is still working on something. Wait for it, or stop it first. |
+
+## Red Flags
+
+| Signal | Do instead |
+|---|---|
+| "agent" or "coworker" for a Bot | Bot |
+| "administrator" | admin |
+| "this deployment" on a page anybody can open | OpenBot, or say what is missing |
+| A sentence over ~25 words | Split it, or cut the clause that justifies the screen |
+| "rather than", an em dash, "which is" in UI text | Rewrite as two short sentences |
+| An env var, OAuth, Composio or "grant" outside Admin | Say what the person sees |
+| A test asserting long explanatory copy | Assert the short sentence, or the role and the key words |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3baa354ed8..da2b039241 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,17 @@ 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.
+### Plainer words, and one word for each thing
+
+OpenBot now says **Bot** everywhere a person reads it, where screens used to mix Bot, agent, coworker
+and assistant; "agent" is kept only for an outside program you connect. It says **admin** rather
+than administrator, **conversation** rather than channel, and **OpenBot** rather than "this
+deployment" on pages anybody can open. Headings, descriptions, empty states, errors and the
+messages the server shows are shorter and say what happened and what to do next, across the home
+page, a Bot's pages, Capabilities, Connections, Settings, conversations and Admin. Admin keeps the
+exact setting names an admin needs, such as `COMPOSIO_API_KEY`. Text written for the model, such
+as tool descriptions and instructions, is unchanged.
+
### 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
diff --git a/app/src/components/admin/admin-sidebar.tsx b/app/src/components/admin/admin-sidebar.tsx
index 0217dc3759..2b4a7d5d6c 100644
--- a/app/src/components/admin/admin-sidebar.tsx
+++ b/app/src/components/admin/admin-sidebar.tsx
@@ -83,12 +83,12 @@ const GROUPS: {
linkOptions: { to: "/admin/skills" },
},
{
- title: "Automatic Learning",
+ title: "Automatic learning",
icon: IconFileText,
linkOptions: { to: "/admin/learning" },
},
{
- title: "UI Components",
+ title: "UI components",
icon: IconLayoutGrid,
linkOptions: { to: "/admin/components" },
},
diff --git a/app/src/components/agents/agent-profile.tsx b/app/src/components/agents/agent-profile.tsx
index 6d77bf57d5..a7b87d77a6 100644
--- a/app/src/components/agents/agent-profile.tsx
+++ b/app/src/components/agents/agent-profile.tsx
@@ -57,7 +57,7 @@ export function AgentProfile({ agentId }: { agentId: string }) {
if (agent.error || !agent.data) {
return (
- Could not load this coworker.
+ Couldn't load this Bot.
);
}
@@ -103,7 +103,7 @@ export function AgentProfile({ agentId }: { agentId: string }) {
void navigate({ search: { agent: agentId }, to: "/channel/new" })
}
>
- Start new channel
+ New conversation
@@ -787,8 +785,8 @@ function EndpointStep({
/>
) : (
- Anything that speaks AG-UI works. This server dials your agent, so
- an agent on your own machine has to be reachable from here.
+ Any agent that speaks AG-UI works. OpenBot connects to it, so it
+ has to be reachable from OpenBot's server.
)}
@@ -806,8 +804,8 @@ function EndpointStep({
value={secret}
/>
- Sent as an Authorization header on every run, and kept
- in the credential vault.
+ Sent as an Authorization header with every request, and
+ stored encrypted.
diff --git a/app/src/components/agents/proposed-bot.tsx b/app/src/components/agents/proposed-bot.tsx
index 34786c6eb4..0b64b45ffc 100644
--- a/app/src/components/agents/proposed-bot.tsx
+++ b/app/src/components/agents/proposed-bot.tsx
@@ -95,7 +95,7 @@ export function ProposedBotCard({
if (!respond) {
return (
-
Writing the coworker…
+
Writing the Bot…
);
}
@@ -116,9 +116,7 @@ export function ProposedBotCard({
// Left on the card with the buttons still live. See `refusal` above.
setSending(null);
setRefusal(
- cause instanceof Error
- ? cause.message
- : "The coworker was not created.",
+ cause instanceof Error ? cause.message : "The Bot wasn't created.",
);
return;
}
@@ -139,8 +137,8 @@ export function ProposedBotCard({
action={Waiting on you}
caption={
clashes
- ? `There is already a ${clashes.title} here, which is ${clashes.ownership}. Names do not have to be unique, but two with the same name are hard to tell apart in a channel list.`
- : "Nothing is created until you press the button."
+ ? `There's already a ${clashes.title}, and it's ${clashes.ownership === "the deployment's" ? "built into OpenBot" : clashes.ownership}. Two Bots with the same name are hard to tell apart.`
+ : "Nothing is created until you press Create it."
}
title={titleFor(values.name, clashes)}
>
@@ -162,7 +160,7 @@ export function ProposedBotCard({
- Its standing instructions. This text is given to a model on every turn.
+ Its instructions. It follows these every time it replies.
{/*
* Scrolled rather than clamped, and the distinction is the whole point of the card. A clamp
@@ -174,8 +172,7 @@ export function ProposedBotCard({
- It arrives private, with no address, no connector and no tool. Anything
- it needs to reach is granted on its profile afterwards.
+ It starts private, with no apps. Apps are added on its page afterwards.
{refusal ? (
@@ -209,7 +206,7 @@ function titleFor(
name: string | undefined,
clashes: { title: string } | undefined,
): string {
- const called = name?.trim() ? name.trim() : "a new coworker";
+ const called = name?.trim() ? name.trim() : "a new Bot";
return clashes ? `Create ${called}, again` : `Create ${called}`;
}
@@ -233,7 +230,7 @@ function TalkToIt({
params={{ agentId }}
to="/bots/$agentId"
>
- Open {name?.trim() || "the coworker"}
+ Open {name?.trim() || "the Bot"}
);
}
@@ -269,11 +266,10 @@ function Unwritable({
return (
Not created}
- title="Create a coworker"
+ title="Create a Bot"
>
- The coworker could not be created as written, so nothing was asked of
- you. It is being redrafted.
+ This Bot couldn't be created as written. It's being rewritten.
);
diff --git a/app/src/components/app-sidebar/app-sidebar.tsx b/app/src/components/app-sidebar/app-sidebar.tsx
index 0189142f40..47e336b056 100644
--- a/app/src/components/app-sidebar/app-sidebar.tsx
+++ b/app/src/components/app-sidebar/app-sidebar.tsx
@@ -257,6 +257,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
)}
/>
(
@@ -280,9 +281,9 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
setSearch(event.target.value)}
- placeholder="Search..."
+ placeholder="Search…"
value={search}
/>
@@ -303,13 +304,13 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
{channels.hasNextPage
- ? "No loaded channels match your search"
- : "No channels match your search"}
+ ? "No loaded conversations match your search"
+ : "No conversations match your search"}
{channels.hasNextPage
? "Load older conversations to search more of your history."
- : `Nothing here is named “${search.trim()}”, and nobody has said it recently either.`}
+ : `No conversation has “${search.trim()}” in its name or latest message.`}
@@ -319,10 +320,9 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
- You don't have channels yet
+ No conversations yet
- Start talking to agents and your channels will appear
- here.
+ Start a conversation with a Bot and it shows up here.
@@ -452,7 +452,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
variant="destructive"
>
- Log out
+ Sign out
diff --git a/app/src/components/app-sidebar/channel-pagination.tsx b/app/src/components/app-sidebar/channel-pagination.tsx
index 6ff7cde40b..14bfaaf18a 100644
--- a/app/src/components/app-sidebar/channel-pagination.tsx
+++ b/app/src/components/app-sidebar/channel-pagination.tsx
@@ -65,7 +65,7 @@ export function ChannelPagination({ query, scrollRoot, searching }: Props) {
{isFetchNextPageError ? (
- Could not load older conversations.
+ Couldn't load older conversations.
) : null}
diff --git a/app/src/components/app-sidebar/channel.tsx b/app/src/components/app-sidebar/channel.tsx
index 782cd881ae..6532c90f7f 100644
--- a/app/src/components/app-sidebar/channel.tsx
+++ b/app/src/components/app-sidebar/channel.tsx
@@ -147,7 +147,7 @@ export const Channel = memo(function Channel({
}}
>
{pinned ? : }
- {pinned ? "Unpin channel" : "Pin channel"}
+ {pinned ? "Unpin conversation" : "Pin conversation"}
- Delete channel…
+ Delete conversation…
@@ -177,7 +177,7 @@ export const Channel = memo(function Channel({
Delete {name}?
- The conversation will no longer appear for anyone in it.
+ It disappears for everyone in the conversation.
{deleteChannel.error ? (
diff --git a/app/src/components/approvals/bot-rules.tsx b/app/src/components/approvals/bot-rules.tsx
index 09d12d895f..3dfae60572 100644
--- a/app/src/components/approvals/bot-rules.tsx
+++ b/app/src/components/approvals/bot-rules.tsx
@@ -46,8 +46,8 @@ export function BotApprovalRules({ agentId }: { agentId: string }) {
- The strictest matching rule wins, so an "ask" rule beats an "allow"
- one. Rules for every Bot are in{" "}
+ The strictest matching rule wins. An "ask" rule beats an "allow" rule.
+ Rules for every Bot are in{" "}
.
{rulesOff
- ? " Your team has switched personal rules off, so they are kept but do not apply."
+ ? " An admin has turned off personal rules. Yours are kept but don't apply."
: ""}
>
}
diff --git a/app/src/components/approvals/rules.tsx b/app/src/components/approvals/rules.tsx
index f7b290ea32..a3e844dd10 100644
--- a/app/src/components/approvals/rules.tsx
+++ b/app/src/components/approvals/rules.tsx
@@ -246,8 +246,8 @@ export function RuleForm({
{label}
- * matches anything, so mcp/gmail/* is every Gmail tool and a
- target of *.example.com is every page on that site.
+ Use * to match anything. mcp/gmail/* means every Gmail tool. A
+ target of *.example.com means every page on that site.
diff --git a/app/src/components/approvals/settings.tsx b/app/src/components/approvals/settings.tsx
index 8301c09c19..1f6a73033e 100644
--- a/app/src/components/approvals/settings.tsx
+++ b/app/src/components/approvals/settings.tsx
@@ -121,11 +121,10 @@ export function ApprovalSettings() {
className="line-clamp-none"
id="auto-review-description"
>
- {enforced ? "Required by your team. " : ""}
- Before an action that could affect your accounts or share
- information, a model checks it against what you asked for,
- your rules and the safety requirements. If it cannot decide,
- it asks you.
+ {enforced ? "An admin requires this. " : ""}
+ Before a Bot does something that could affect your accounts
+ or share information, OpenBot checks it against your request
+ and your rules. If it can't tell, it asks you.
@@ -153,10 +152,10 @@ export function ApprovalSettings() {
{inbox.data.hostCommands &&
inbox.data.hostCommands !==
inbox.data.preferences.hostCommands
- ? `Your team limits this to "${HOST_LABELS[inbox.data.hostCommands]}". `
+ ? `An admin limits this to "${HOST_LABELS[inbox.data.hostCommands]}". `
: ""}
- The OpenBot desktop app still shows each command before it
- runs on your computer.
+ The OpenBot desktop app still shows you each command before
+ it runs.
@@ -178,21 +177,21 @@ export function ApprovalSettings() {
- The strictest matching rule wins, so an "ask" rule beats an "allow"
- one. Team rules are locked. Changing a password, security settings
- and payments are always handed to you. Rules for a single Bot are on{" "}
+ The strictest matching rule wins. An "ask" rule beats an "allow"
+ rule. Team rules are locked. Passwords, security settings and
+ payments are always handed to you. Rules for one Bot are on{" "}
that Bot's page
.
{rulesOff
- ? " Your team has switched personal rules off, so they are kept but do not apply."
+ ? " An admin has turned off personal rules. Yours are kept but don't apply."
: ""}
>
}
title="Rules for every Bot"
>
- {saved === 0 ? No rules saved. : null}
+ {saved === 0 ? No rules yet. : null}
{saved > 0 || canAdd ? (
{teamRules.map((rule, index) => (
@@ -233,7 +232,7 @@ export function ApprovalSettings() {
{agents.data !== undefined && otherBots.length > 0 ? (
diff --git a/app/src/components/approvals/shared-use-requests.tsx b/app/src/components/approvals/shared-use-requests.tsx
index b0e85d2648..acba22727f 100644
--- a/app/src/components/approvals/shared-use-requests.tsx
+++ b/app/src/components/approvals/shared-use-requests.tsx
@@ -22,10 +22,10 @@ import {
const WHY: Record = {
refused_call:
- "A call was refused because this Bot reaches more people than approved.",
+ "An action was blocked. This Bot reaches more people than approved.",
publish: "Its owner widened who can reach it.",
- trigger: "Its owner added a trigger that lets outside input steer it.",
- grant: "It was granted the app's actions.",
+ trigger: "Its owner added a trigger that lets outside messages steer it.",
+ grant: "It was given the app's actions.",
};
/**
diff --git a/app/src/components/approvals/team-settings.tsx b/app/src/components/approvals/team-settings.tsx
index c4eff933b5..0095531d83 100644
--- a/app/src/components/approvals/team-settings.tsx
+++ b/app/src/components/approvals/team-settings.tsx
@@ -128,7 +128,7 @@ export function TeamApprovalSettings() {
) : null}
{teamRules.length === 0 ? No team rules. : null}
diff --git a/app/src/components/bot-profile/access.tsx b/app/src/components/bot-profile/access.tsx
index 61d994d59e..2154222568 100644
--- a/app/src/components/bot-profile/access.tsx
+++ b/app/src/components/bot-profile/access.tsx
@@ -101,7 +101,7 @@ function YourSkills({ agentId }: { agentId: string }) {
);
return (
{grant.error ? (
@@ -111,11 +111,11 @@ function YourSkills({ agentId }: { agentId: string }) {
) : null}
{page.isPending ? null : page.error ? (
- Could not load your skills.
+ Couldn't load your skills. Reload the page.
) : skills.length === 0 ? (
- You have not written a skill yet.{" "}
+ No skills yet.{" "}
- What this Bot may reach could not be loaded.
+ Couldn't load what this Bot can use. Reload the page.
);
}
@@ -203,18 +203,18 @@ function Granted({ agentId }: { agentId: string }) {
const count = states.length + rest.length + skills.length;
return (
{apps.error ? (
- Whether each app is ready for you could not be loaded.
+ Couldn't check whether each app is ready for you. Reload the page.
) : null}
{count === 0 ? (
- Nothing granted yet. An administrator grants apps from the Plugins
- screens; until then this Bot can converse, and nothing more.
+ No apps yet. An admin has to add them. Until then this Bot can only
+ talk with you.
) : (
diff --git a/app/src/components/bot-profile/activity.tsx b/app/src/components/bot-profile/activity.tsx
index cf50d94b3a..b390effebc 100644
--- a/app/src/components/bot-profile/activity.tsx
+++ b/app/src/components/bot-profile/activity.tsx
@@ -184,7 +184,7 @@ export function BotActivitySections({ agentId }: { agentId: string }) {
return (
- Could not load this Bot's activity.
+ Couldn't load this Bot's activity. Reload the page.
- This Bot is not one you can reach.
+ Couldn't open this Bot. It may not be shared with you.
) : (
children(agent.data)
diff --git a/app/src/components/bot-profile/manage.tsx b/app/src/components/bot-profile/manage.tsx
index b033d88cd5..76ea9c9662 100644
--- a/app/src/components/bot-profile/manage.tsx
+++ b/app/src/components/bot-profile/manage.tsx
@@ -82,22 +82,21 @@ function ResetDialog({
Reset {agent.name}?
- This deletes what {agent.name} has with you, and only with you.
- Nobody else's conversations or data are touched. It cannot be
- undone.
+ This deletes what {agent.name} has with you. Other people's
+ conversations and data stay. You can't undo this.
{plan.isPending ? null : plan.error ? (
- Could not count what a reset would delete.
+ Couldn't check what a reset would delete. Try again.
) : reset.isSuccess ? (
Done. {agent.name} starts fresh with you.
) : (
{lines.length === 0 ? (
-
There is nothing of yours to delete.
+
There's nothing of yours to delete.
) : (
<>
This will delete:
@@ -116,7 +115,7 @@ function ResetDialog({
) : null}
- Your own memories, the ones you told a Bot directly, are kept.
+ Memories you told a Bot yourself are kept.
)}
@@ -193,10 +192,10 @@ export function ManageSection({
Pinned
{assigned
- ? "Assigned to you by an administrator, so it stays at the top of your Bots list."
+ ? "An admin assigned it to you. It stays at the top of your Bots list."
: agent.pinned
- ? "At the top of your Bots list. Nobody else's list changes."
- : "Keep it at the top of your Bots list. Nobody else's list changes."}
+ ? "At the top of your Bots list. Only your list changes."
+ : "Keep it at the top of your Bots list. Only your list changes."}
@@ -219,10 +218,10 @@ export function ManageSection({
Hidden
{assigned
- ? "Assigned to you by an administrator, so it cannot be hidden."
+ ? "An admin assigned it to you. You can't hide it."
: agent.hidden
- ? "Folded under Hidden on your Bots list. Nobody else's list changes."
- : "Take it off your Bots list. Nobody else's list changes."}
+ ? "Under Hidden on your Bots list. Only your list changes."
+ : "Take it off your Bots list. Only your list changes."}
@@ -256,7 +255,7 @@ export function ManageSection({
Duplicate
- A copy of your own, with no key and no conversations.
+ Make your own copy. Its conversations and keys aren't copied.
@@ -272,7 +271,7 @@ export function ManageSection({
Reset
Delete your conversations with it, what it remembers for you, and
- its scheduled work. You see what will go first.
+ its scheduled work. You'll see the list first.
@@ -294,7 +293,7 @@ export function ManageSection({
Delete
- For everyone. This cannot be undone.
+ Deletes it for everyone. You can't undo this.
@@ -309,7 +308,9 @@ export function ManageSection({
Delete {agent.name}?
- This cannot be undone.
+
+ It's deleted for everyone. You can't undo this.
+
{remove.error ? (
diff --git a/app/src/components/bot-profile/pause-banner.tsx b/app/src/components/bot-profile/pause-banner.tsx
index e844a9d9e5..3575be2f52 100644
--- a/app/src/components/bot-profile/pause-banner.tsx
+++ b/app/src/components/bot-profile/pause-banner.tsx
@@ -23,7 +23,7 @@ export function BotPausedBanner({ agentId }: { agentId: string }) {
variant="secondary"
>
- {resume.isPending ? "Resuming…" : "Paused, tap to resume"}
+ {resume.isPending ? "Resuming…" : "Paused. Resume"}
);
}
diff --git a/app/src/components/bot-profile/profile.tsx b/app/src/components/bot-profile/profile.tsx
index 2fc72bd96a..dace529344 100644
--- a/app/src/components/bot-profile/profile.tsx
+++ b/app/src/components/bot-profile/profile.tsx
@@ -102,7 +102,7 @@ export function BotProfile({
) : null}
{lifecycle.isPending ? null : lifecycle.error ? (
- Could not load this Bot's state.
+ Couldn't load this Bot's settings. Reload the page.
) : (
@@ -116,7 +116,7 @@ export function BotProfile({
Active
{lifecycle.data.paused
- ? "Paused. No routine, responsibility, hand-off or follow-up starts for you, and what was running has stopped."
+ ? "Paused. Nothing runs for you until you turn it back on."
: "Runs its routines, responsibilities, hand-offs and follow-ups for you."}
diff --git a/app/src/components/bot-profile/setup.tsx b/app/src/components/bot-profile/setup.tsx
index 70cb8d05b0..0a4ad5097f 100644
--- a/app/src/components/bot-profile/setup.tsx
+++ b/app/src/components/bot-profile/setup.tsx
@@ -125,8 +125,7 @@ export function SetupSections({ agent }: { agent: AgentProfile }) {
System owned
- Ships with this deployment rather than belonging to a
- person.
+ Comes with OpenBot. It doesn't belong to a person.
@@ -170,7 +169,7 @@ function VisibilityRow({
{error ??
(agent.visibility === "private"
? "Only you can see it and start conversations with it."
- : "Everyone in the deployment can find and use it.")}
+ : "Everyone on OpenBot can find and use it.")}
@@ -189,7 +188,7 @@ function VisibilityRow({
setError(
failure instanceof Error
? failure.message
- : "Could not save.",
+ : "Couldn't save. Try again.",
);
} finally {
setSaving(false);
@@ -241,7 +240,9 @@ function EditTextDialog({
const submit = async () => {
const parsed = schema.safeParse(draft);
if (!parsed.success) {
- setError(parsed.error.issues[0]?.message ?? "That value does not fit.");
+ setError(
+ parsed.error.issues[0]?.message ?? "Check this field and try again.",
+ );
return;
}
setSaving(true);
@@ -249,7 +250,11 @@ function EditTextDialog({
await onSave(parsed.data);
onClose();
} catch (failure) {
- setError(failure instanceof Error ? failure.message : "Could not save.");
+ setError(
+ failure instanceof Error
+ ? failure.message
+ : "Couldn't save. Try again.",
+ );
} finally {
setSaving(false);
}
@@ -321,14 +326,14 @@ function ConnectionSection({ agent }: { agent: AgentProfile }) {
- {builtIn ? "Built in" : "Endpoint"}
+ {builtIn ? "Built in" : "Agent URL"}
{builtIn
- ? "Runs on this deployment's own Bot. Nothing to connect and nothing to authenticate."
+ ? "Runs inside OpenBot. There's nothing to connect."
: agent.endpoint}
diff --git a/app/src/components/bot-profile/sharing.tsx b/app/src/components/bot-profile/sharing.tsx
index 5a2e7d38a0..16a69214f4 100644
--- a/app/src/components/bot-profile/sharing.tsx
+++ b/app/src/components/bot-profile/sharing.tsx
@@ -104,8 +104,8 @@ export function SharingSections({ agent }: { agent: AgentProfile }) {
{published.visibleToTeam
- ? "Each chat with it is private to the person having it."
- : "Hidden from teammates until it has a real name and a description."}
+ ? "Each person's conversations with it are private."
+ : "Teammates can't see it until it has a real name and a description."}
{agent.mine ? (
@@ -128,7 +128,9 @@ export function SharingSections({ agent }: { agent: AgentProfile }) {
Link
- Opens a private chat with it.
+
+ Opens a private conversation with it.
+
{agent.visibility === "public"
- ? "Not published to the team. It is public, so everyone in the deployment can already use it."
+ ? "Not published to the team. It's public, so everyone on OpenBot can already use it."
: agent.mine
? "Not published. Only you can use it."
: "Not published."}
@@ -157,7 +159,7 @@ export function SharingSections({ agent }: { agent: AgentProfile }) {
{agent.mine && (published || publishable) ? (
{/* More than one value — an audience, then people and groups — so a summary row and a
@@ -192,7 +194,7 @@ export function SharingSections({ agent }: { agent: AgentProfile }) {
) : null}
{me?.role === "admin" && published ? (
@@ -246,7 +248,8 @@ function PublishDialog({
{current ? "Who it is published to" : "Publish"}
- Publishing does not change its visibility, and it can be undone.
+ Publishing doesn't change its visibility. You can unpublish at any
+ time.
@@ -424,7 +427,7 @@ function AssignDialog({ bot, onClose }: { bot: TeamBot; onClose: () => void }) {
Assign {bot.name} to a group
- It is pinned at the top of every member's Bots list.
+ It's pinned at the top of every member's Bots list.
diff --git a/app/src/components/bots/bot-roster.tsx b/app/src/components/bots/bot-roster.tsx
index cb595dd55d..c13668f378 100644
--- a/app/src/components/bots/bot-roster.tsx
+++ b/app/src/components/bots/bot-roster.tsx
@@ -46,7 +46,7 @@ export function BotRoster() {
if (agents.data === undefined) {
return (
- Could not load your Bots.
+ Couldn't load your Bots. Reload the page.
);
}
@@ -81,7 +81,7 @@ export function BotRoster() {
? "Your Bots are all pinned above."
: yoursWaiting
? "Your Bots are all under Needs you."
- : "You have no Bots of your own yet."}
+ : "You don't have any Bots yet. Create one with New Bot."}
)}
@@ -118,7 +118,7 @@ function HiddenSection({
{open ? "Hide" : "Show"}
}
- description={`${agents.length} ${agents.length === 1 ? "Bot" : "Bots"} you took off this list. Nobody else's list changed.`}
+ description={`${agents.length} ${agents.length === 1 ? "Bot" : "Bots"} you hid. Only your list changes.`}
title="Hidden"
>
{open ? : null}
diff --git a/app/src/components/channels/app-access-card.tsx b/app/src/components/channels/app-access-card.tsx
index d58164cdf9..c41443819d 100644
--- a/app/src/components/channels/app-access-card.tsx
+++ b/app/src/components/channels/app-access-card.tsx
@@ -144,6 +144,6 @@ function refusedSentence(access: AppAccess, prompt: ConnectPrompt): string {
prompt.reason === "shared" &&
!access.requestKind
)
- return `Your organisation's ${access.displayName} account needs reconnecting. Only an administrator can reconnect it.`;
+ return `Your organisation's ${access.displayName} account needs reconnecting. Only an admin can reconnect it.`;
return appAccessSentence(access);
}
diff --git a/app/src/components/channels/channel-chat.tsx b/app/src/components/channels/channel-chat.tsx
index 27f1047200..3bd769d2e2 100644
--- a/app/src/components/channels/channel-chat.tsx
+++ b/app/src/components/channels/channel-chat.tsx
@@ -107,13 +107,13 @@ export function channelHistoryNotice({
historyAvailability === "unavailable" &&
(historyReadFailed || (messageCount === 0 && lastMessageAt !== null))
) {
- return "Earlier messages are temporarily unavailable. You can keep using this conversation.";
+ return "Couldn't load earlier messages. You can keep using this conversation.";
}
if (unreadable > 0) {
return unreadable === 1
- ? "One earlier message could not be read and is not shown. The rest of this conversation is complete."
- : `${unreadable} earlier messages could not be read and are not shown. The rest of this conversation is complete.`;
+ ? "One earlier message couldn't be read, so it isn't shown. Everything else is here."
+ : `${unreadable} earlier messages couldn't be read, so they aren't shown. Everything else is here.`;
}
return null;
@@ -651,7 +651,7 @@ export function ChannelChat({
])
.catch(() => {
setVoiceContextNotice(
- "Earlier voice context wasn’t available for this message. The agent may need you to repeat details from the call.",
+ "Couldn’t bring in what was said on the call. You may need to repeat it.",
);
return cachedVoiceArchive(channel.id);
})
@@ -779,7 +779,7 @@ export function ChannelChat({
// Voice and typed requests share this turn boundary, including gaps between frontend tools.
if (turnsRef.current > 0 || agentRef.current.isRunning) {
throw new Error(
- "The agent is still working on another request. Please wait or stop that task first.",
+ "This Bot is still working on something. Wait for it, or stop it first.",
);
}
@@ -968,7 +968,7 @@ export function ChannelChat({
/>
{voiceArchive.error && (
- Voice chats couldn’t be loaded. Refresh to try again.
+ Couldn’t load voice chats. Reload the page.
)}
{voiceContextNotice && (
@@ -992,8 +992,8 @@ export function ChannelChat({
className="pb-2 text-sm text-muted-foreground"
role="status"
>
- This coworker has been deleted. The conversation stays
- readable, but it can no longer reply.
+ This Bot was deleted. You can still read this conversation,
+ but the Bot can't reply.
)}
>
diff --git a/app/src/components/channels/composer/attachment-strip.tsx b/app/src/components/channels/composer/attachment-strip.tsx
index 6885b960e2..c14f769370 100644
--- a/app/src/components/channels/composer/attachment-strip.tsx
+++ b/app/src/components/channels/composer/attachment-strip.tsx
@@ -157,7 +157,7 @@ export function AttachmentStrip({
className="truncate text-muted-foreground text-xs"
title={
file.mayTruncate === true
- ? "The model reads the first 120,000 characters of this file."
+ ? "The Bot reads only the first 120,000 characters of this file."
: undefined
}
>
diff --git a/app/src/components/channels/composer/attachments.ts b/app/src/components/channels/composer/attachments.ts
index 81b933acba..df2429a967 100644
--- a/app/src/components/channels/composer/attachments.ts
+++ b/app/src/components/channels/composer/attachments.ts
@@ -81,7 +81,9 @@ export function uploadToChannel(
const body: unknown = await response.json().catch(() => undefined);
// Named after the file rather than left empty, for every way this can go wrong. It is the only
// sentence anybody gets: the composer keeps the string verbatim and shows it as the reason.
- const couldNotUpload = new Error(`Could not upload "${file.name}".`);
+ const couldNotUpload = new Error(
+ `Couldn't upload "${file.name}". Try again.`,
+ );
if (!response.ok) {
// The server phrases refusals in the product's voice — naming the mime type or the limit
diff --git a/app/src/components/channels/composer/composer.tsx b/app/src/components/channels/composer/composer.tsx
index f92acd582b..22d12e02bd 100644
--- a/app/src/components/channels/composer/composer.tsx
+++ b/app/src/components/channels/composer/composer.tsx
@@ -259,9 +259,8 @@ function overCap(draft: ComposerDraft): boolean {
/** What the person is told when Send is held shut by the cap, and what to do about it. */
function tooManyStagedReason(count: number): string {
return (
- `${count} attachments are staged and a message can carry at most ` +
- `${MAX_ATTACHMENTS_PER_MESSAGE}. Remove ` +
- `${count - MAX_ATTACHMENTS_PER_MESSAGE} to send the rest.`
+ `A message can take at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments. ` +
+ `Remove ${count - MAX_ATTACHMENTS_PER_MESSAGE} to send.`
);
}
@@ -274,11 +273,9 @@ function tooManyStagedReason(count: number): string {
*/
const DROPPED_REASON: Record = {
"merged-over-cap":
- "dropped when queued messages were merged into one: a " +
- `message can carry at most ${MAX_ATTACHMENTS_PER_MESSAGE} ` +
- "attachments.",
- "queued-message-removed":
- "removed along with the queued message it was attached to.",
+ "dropped when your queued messages were merged. A message can take " +
+ `at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments.`,
+ "queued-message-removed": "removed with its queued message.",
};
/**
@@ -306,11 +303,10 @@ const DROPPED_REASON: Record = {
* second time.
*/
const UNACCEPTED_DROP_REASON = {
- "cannot-send":
- "was not attached: this conversation can no longer take messages.",
+ "cannot-send": "not attached. This conversation can no longer take messages.",
"no-conversation":
- "was not attached: there is no conversation here yet. Send this " +
- "message first, then attach to the one it opens.",
+ "not attached. There is no conversation here yet. Send your message " +
+ "first, then attach files in the conversation it starts.",
} as const;
export function Composer({
@@ -1363,8 +1359,8 @@ export function Composer({
callAction.supported
? callAction.active
? "Show voice call"
- : "Call this agent"
- : "Voice calls need HTTPS or localhost and microphone support"
+ : "Call this Bot"
+ : "Voice calls need a microphone and a secure (HTTPS) connection"
}
className={actionClassName}
disabled={disabled || dictation.busy || !callAction.supported}
diff --git a/app/src/components/channels/composer/dictation-controls.tsx b/app/src/components/channels/composer/dictation-controls.tsx
index d4e373119b..a1de0b6b86 100644
--- a/app/src/components/channels/composer/dictation-controls.tsx
+++ b/app/src/components/channels/composer/dictation-controls.tsx
@@ -29,7 +29,7 @@ export function DictationButton({
title={
dictation.supported
? "Dictate a message"
- : "Dictation requires a supported browser and HTTPS (or localhost)"
+ : "Dictation needs a supported browser and a secure (HTTPS) connection"
}
className="size-8 shrink-0 self-end rounded-full"
disabled={disabled || !dictation.supported || dictation.busy}
diff --git a/app/src/components/channels/composer/picked-files.ts b/app/src/components/channels/composer/picked-files.ts
index 0a50c15750..cb3654a71a 100644
--- a/app/src/components/channels/composer/picked-files.ts
+++ b/app/src/components/channels/composer/picked-files.ts
@@ -50,8 +50,8 @@ export function screenPickedFiles(
reject(
file,
mediaType === "image/svg+xml"
- ? `'${file.name}' is an SVG, which can carry scripts and is not accepted.`
- : `'${file.name}' is an image format that is not supported.`,
+ ? `'${file.name}' can't be attached. SVG files aren't accepted.`
+ : `'${file.name}' isn't a supported image format.`,
),
);
continue;
@@ -76,7 +76,7 @@ export function screenPickedFiles(
if (kind === "unsupported" && !unnamed) {
rejected.push(
- reject(file, `'${file.name}' is not a file type this chat accepts.`),
+ reject(file, `'${file.name}' isn't a file type you can attach.`),
);
continue;
}
@@ -85,7 +85,7 @@ export function screenPickedFiles(
rejected.push(
reject(
file,
- `'${file.name}' is too large for an image attachment (limit ${formatBytes(MAX_IMAGE_BYTES)}).`,
+ `'${file.name}' is too large for an image. The limit is ${formatBytes(MAX_IMAGE_BYTES)}.`,
),
);
continue;
@@ -95,7 +95,7 @@ export function screenPickedFiles(
rejected.push(
reject(
file,
- `'${file.name}' is too large for a text attachment (limit ${formatBytes(MAX_FILE_BYTES)}).`,
+ `'${file.name}' is too large for a text file. The limit is ${formatBytes(MAX_FILE_BYTES)}.`,
),
);
continue;
@@ -108,7 +108,7 @@ export function screenPickedFiles(
rejected.push(
reject(
file,
- `'${file.name}' is larger than the ${formatBytes(MAX_IMAGE_BYTES)} limit for an attachment.`,
+ `'${file.name}' is too large. The limit is ${formatBytes(MAX_IMAGE_BYTES)}.`,
),
);
continue;
@@ -121,7 +121,7 @@ export function screenPickedFiles(
rejected.push(
reject(
file,
- `'${file.name}' was not added: a message can carry at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments.`,
+ `'${file.name}' wasn't added. A message can take at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments.`,
),
);
continue;
diff --git a/app/src/components/channels/composer/sources.ts b/app/src/components/channels/composer/sources.ts
index 548ab5073d..5d3070a974 100644
--- a/app/src/components/channels/composer/sources.ts
+++ b/app/src/components/channels/composer/sources.ts
@@ -16,6 +16,6 @@ export const PLACEHOLDER_COMMANDS: CommandOption[] = [
name: "summarize",
description: "Summarize this conversation",
kind: "prompt",
- prompt: "Summarize what we covered in this channel so far.",
+ prompt: "Summarize what we covered in this conversation so far.",
},
];
diff --git a/app/src/components/channels/composer/triggers.ts b/app/src/components/channels/composer/triggers.ts
index 7a0a9d7a4c..f2cbef110c 100644
--- a/app/src/components/channels/composer/triggers.ts
+++ b/app/src/components/channels/composer/triggers.ts
@@ -54,9 +54,9 @@ function matches(query: string, ...fields: (string | undefined)[]): boolean {
export function agentTrigger(agents: readonly AgentOption[]): TriggerConfig {
return mentionTrigger({
char: AGENT_TRIGGER,
- accessibilityLabel: "agent",
+ accessibilityLabel: "Bot",
reopenOnChipClick: true,
- emptyMessage: "No agents in this channel",
+ emptyMessage: "No Bots in this conversation",
onSearch: (query): TriggerSuggestion[] =>
agents
.filter((agent) => matches(query, agent.name, agent.description))
@@ -80,8 +80,8 @@ export function slashCommandTrigger(
return commandTrigger({
char: COMMAND_TRIGGER,
position: "start",
- accessibilityLabel: "command",
- emptyMessage: "No matching commands",
+ accessibilityLabel: "skill",
+ emptyMessage: "No matching skills",
onSearch: (query): TriggerSuggestion[] =>
commands
.filter((command) => matches(query, command.name, command.description))
diff --git a/app/src/components/channels/connect-app-card.tsx b/app/src/components/channels/connect-app-card.tsx
index 8a8910c9db..d89372fa4b 100644
--- a/app/src/components/channels/connect-app-card.tsx
+++ b/app/src/components/channels/connect-app-card.tsx
@@ -90,8 +90,7 @@ export function ConnectAppCard({
) : needsKey ? (
- {title} is connected with a key you hold rather than by visiting a
- page.{" "}
+ {title} needs a key from you.{" "}
- {message.text || "This Bot could not answer."}
+ {message.text || "This Bot couldn't answer."}
) : (
People in this group
- Everyone here sees the whole conversation, and every Bot in it
- answers them.
+ Everyone here sees the whole conversation. Every Bot in it answers
+ them.
diff --git a/app/src/components/channels/recipient-field.tsx b/app/src/components/channels/recipient-field.tsx
index bb33f113e5..e97f7ae39f 100644
--- a/app/src/components/channels/recipient-field.tsx
+++ b/app/src/components/channels/recipient-field.tsx
@@ -61,9 +61,9 @@ export function RecipientField({
{isFull ? null : (
setSearch(event.target.value)}
- placeholder="Choose a coworker…"
+ placeholder="Choose a Bot…"
value={search}
/>
@@ -96,7 +96,7 @@ export function RecipientField({
))}
{matches.length === 0 ? (
- No coworker by that name.
+ No Bot by that name.
- {step.reason ?? "This step could not be completed."}
+ {step.reason ?? "Couldn't finish this step."}
) : null;
// A browser step says everything it has in its line. Anything else opens to what it was given
diff --git a/app/src/components/channels/tool-boundary.tsx b/app/src/components/channels/tool-boundary.tsx
index c53d6082d4..73a9637c01 100644
--- a/app/src/components/channels/tool-boundary.tsx
+++ b/app/src/components/channels/tool-boundary.tsx
@@ -30,8 +30,8 @@ export class ToolRenderBoundary extends Component<
if (this.state.failed) {
return (
- {this.props.name} could not be
- drawn. The rest of this conversation is unaffected.
+ Couldn't show {this.props.name}.
+ The rest of the conversation is fine.
);
}
diff --git a/app/src/components/channels/voice-call-widget.tsx b/app/src/components/channels/voice-call-widget.tsx
index 292e33cbce..8d7389f980 100644
--- a/app/src/components/channels/voice-call-widget.tsx
+++ b/app/src/components/channels/voice-call-widget.tsx
@@ -223,7 +223,7 @@ export function VoiceCallWidget({
title={
state.muted
? "Unmute your microphone"
- : "Mute your microphone · you’ll still hear the agent"
+ : "Mute your microphone · you’ll still hear the Bot"
}
variant="secondary"
size="icon"
@@ -264,7 +264,7 @@ export function VoiceCallWidget({
{state.reply ||
state.transcript ||
- "Speak when you’re ready. This call uses the current chat."}
+ "Speak when you’re ready. This call is part of this conversation."}
{entry.text}
diff --git a/app/src/components/computer/activity-log.tsx b/app/src/components/computer/activity-log.tsx
index 3a7a771919..417dc47b41 100644
--- a/app/src/components/computer/activity-log.tsx
+++ b/app/src/components/computer/activity-log.tsx
@@ -106,8 +106,7 @@ export function ActivityLog({ computerId }: { computerId: string }) {
- Nothing yet. Commands the Bot runs, and files it reads, appear here
- as they happen.
+ Nothing yet. Commands the Bot runs and files it reads appear here.
diff --git a/app/src/components/computer/command-output.tsx b/app/src/components/computer/command-output.tsx
index 502c61a5b4..741752b5a5 100644
--- a/app/src/components/computer/command-output.tsx
+++ b/app/src/components/computer/command-output.tsx
@@ -28,7 +28,7 @@ export function CommandOutput({
{truncated ? (
- Output was cut short at the start. What follows is the end of it.
+ The output was too long. This is the end of it.
) : null}
{output ? (
diff --git a/app/src/components/computer/computer-view.tsx b/app/src/components/computer/computer-view.tsx
index bd5efa58e9..4fc93e71b6 100644
--- a/app/src/components/computer/computer-view.tsx
+++ b/app/src/components/computer/computer-view.tsx
@@ -150,19 +150,17 @@ function NothingToSee({
>
) : problem ? (
<>
-
- You cannot see the screen right now
-
+ Can't show the screen right now{problem}
- The assistant may still be working. An administrator can check
- whether its computer is running.
+ The Bot may still be working. If this keeps happening, ask an admin
+ to check its Computer.
>
) : blankBrowser ? (
- The assistant has not opened a page yet.
+ The Bot hasn't opened a page yet.
) : (
- Waiting for the assistant's screen…
+ Waiting for the Bot's screen…
)}
);
@@ -345,7 +343,7 @@ export function ComputerView({
if (generation.current !== mine) return;
if (!frame) {
- setProblem(error ?? "The screen is not available right now.");
+ setProblem(error ?? "The screen isn't available right now.");
} else {
// Exact byte comparison is the settling signal.
unchanged = frame.base64 === lastFrame ? unchanged + 1 : 0;
@@ -430,7 +428,7 @@ export function ComputerView({
const polledScreen = showScreen ? (
@@ -452,7 +450,7 @@ export function ComputerView({
*/
className="relative block w-full cursor-pointer bg-muted"
style={frameStyle}
- aria-label="Open the assistant's screen full size"
+ aria-label="Open the Bot's screen full size"
>
{polledScreen}
@@ -487,7 +485,7 @@ export function ComputerView({
{!driving && !settled && control?.requested ? (
- The assistant needs you.{" "}
+ The Bot needs you.{" "}
{control.reason}
- The Computer stopped while you had control, so the assistant can't
- use the browser yet. Take control to check the page, then hand it
- back.
+ The Computer stopped while you had control. Take control to check
+ the page, then hand it back to the Bot.
- The Computer stopped before anyone took control. The assistant will
- ask again if it still needs you.
+ The Computer stopped before anyone took control. The Bot will ask
+ again if it still needs you.
@@ -559,7 +556,7 @@ export function ComputerView({
autoComplete="off"
autoCorrect="off"
spellCheck={false}
- placeholder="Typed here, never shown to the assistant"
+ placeholder="Never shown to the Bot"
className="min-w-0 flex-1 rounded-md border bg-background px-2 py-1 text-sm"
/>
- This goes straight to the page. It is not shown in the
- conversation and the assistant never receives it.
+ This goes straight to the page. It isn't shown in the
+ conversation, and the Bot never sees it.
{secretProblem ? (
{secretProblem}
@@ -589,7 +586,7 @@ export function ComputerView({
? "Finishing the current action…"
: driving
? "You have control. Open the screen to click and type."
- : "The assistant has control."}
+ : "The Bot has control."}
{/*
@@ -617,7 +614,7 @@ export function ComputerView({
!wheelHere && setExpanded(false)}
- aria-label="Close the assistant's screen"
+ aria-label="Close the Bot's screen"
aria-hidden={wheelHere}
tabIndex={wheelHere ? -1 : 0}
className={`absolute inset-0 bg-black/80 ${wheelHere ? "cursor-default" : "cursor-zoom-out"}`}
@@ -714,13 +711,13 @@ export function ComputerView({
) : null}
{driving ? (
- You have control — click and type on the page.
+ You have control. Click and type on the page.
{control?.reason ? ` ${control.reason}` : null}
) : control?.requested ? (
- The assistant needs you.
+ The Bot needs you.
{" "}
{control.reason}
diff --git a/app/src/components/computer/demonstration-recorder.tsx b/app/src/components/computer/demonstration-recorder.tsx
index 6b124e916a..50b2cba1c2 100644
--- a/app/src/components/computer/demonstration-recorder.tsx
+++ b/app/src/components/computer/demonstration-recorder.tsx
@@ -253,9 +253,8 @@ export function DemonstrationRecorder({
Teach a browser workflow
- Take control, record the steps, then review a skill draft. Typed
- values and images are omitted. Sensitive fields stay in your hands.
- Recording stops automatically after ten minutes.
+ Take control, record the steps, then review the skill it drafts. Typed
+ values and images aren't recorded. Recording stops after ten minutes.
{active ? (
@@ -402,8 +401,7 @@ export function DemonstrationRecorder({
Review and edit the skill
- Replace parameter names and clarify the expected outcome. Saving
- uses the same skill permissions as Skills.
+ Rename the inputs and say what the result should be.
- /{savedSlug} is ready on this Bot. Invoke it in the conversation with
- the inputs you want to use, or choose Run on a schedule to rerun it as
- a routine.
+ /{savedSlug} is ready on this Bot. Type it in the conversation with
+ your inputs, or choose Run on a schedule to make it a routine.
)}
diff --git a/app/src/components/computer/handoff-resume-notice.tsx b/app/src/components/computer/handoff-resume-notice.tsx
index 95b17675b3..ea83d54d2f 100644
--- a/app/src/components/computer/handoff-resume-notice.tsx
+++ b/app/src/components/computer/handoff-resume-notice.tsx
@@ -81,7 +81,7 @@ export function HandoffResumeNotice({
await resumeHandoffTask(botId, pending, agent, run, abort.signal);
} catch (error) {
setProblem(
- error instanceof Error ? error.message : "The task could not resume.",
+ error instanceof Error ? error.message : "Couldn't resume the task.",
);
} finally {
setBusy(false);
diff --git a/app/src/components/computer/live-screen.tsx b/app/src/components/computer/live-screen.tsx
index 0cd4195075..b4e7a38228 100644
--- a/app/src/components/computer/live-screen.tsx
+++ b/app/src/components/computer/live-screen.tsx
@@ -200,7 +200,7 @@ export function LiveScreen({
};
if (typeof message.type !== "string") return;
if (message.type === "error") {
- onProblem?.(message.error ?? "The screen could not be shown.");
+ onProblem?.(message.error ?? "Couldn't show the screen.");
return;
}
if (message.type !== "frame" || !message.data) return;
@@ -496,8 +496,8 @@ export function LiveScreen({
: {})}
aria-label={
driving
- ? "The assistant's screen. You have control: click and type here."
- : "The assistant's screen, live"
+ ? "The Bot's screen. You have control. Click and type here."
+ : "The Bot's screen, live"
}
data-connected={connected}
/>
diff --git a/app/src/components/connections/identity-connection-page.tsx b/app/src/components/connections/identity-connection-page.tsx
index 2a7d2dce6b..a280445201 100644
--- a/app/src/components/connections/identity-connection-page.tsx
+++ b/app/src/components/connections/identity-connection-page.tsx
@@ -117,7 +117,7 @@ function GithubTools() {
).some((row) => row.serverId === app.id);
return (
diff --git a/app/src/components/delivery/reaching-you.tsx b/app/src/components/delivery/reaching-you.tsx
index 189275fcac..4a6a7f00c7 100644
--- a/app/src/components/delivery/reaching-you.tsx
+++ b/app/src/components/delivery/reaching-you.tsx
@@ -159,12 +159,12 @@ export function ReachingYou({ agentId: fixedBot }: { agentId?: string }) {
return "A verification code is on its way. Enter it to finish.";
}
if (heldLink(transport)) {
- return "A link code is waiting to be sent. Open this to see it again.";
+ return "Your link code is ready to send. Open this to see it again.";
}
if (open(transport)) return TRANSPORT[transport].summary;
return transport === "sms"
- ? "Text messages are not set up on this deployment."
- : "An administrator needs to pair OpenBot with OpenTag before Slack or Teams can be linked.";
+ ? "Text messages aren't set up yet. An admin has to turn them on first."
+ : "Not set up yet. An admin has to turn on Slack and Teams first.";
};
return (
@@ -212,7 +212,7 @@ export function ReachingYou({ agentId: fixedBot }: { agentId?: string }) {
{botName(binding.agentId)}
{binding.optedOutAt
- ? " · Replied STOP; text START to resume"
+ ? " · You texted STOP. Text START to resume."
: null}
@@ -234,7 +234,7 @@ export function ReachingYou({ agentId: fixedBot }: { agentId?: string }) {
)}
{reach.data ? (
@@ -364,8 +364,8 @@ function ConnectDialog({
{TRANSPORT[transport].link}
- An account or phone number reaches one conversation at a time.
- Linking it here moves it from wherever it is linked now.
+ Each account or phone number works with one conversation at a time.
+ Linking it here moves it from the one it's linked to now.
diff --git a/app/src/components/gallery/activity.tsx b/app/src/components/gallery/activity.tsx
index 1b10598e75..e2ace10b85 100644
--- a/app/src/components/gallery/activity.tsx
+++ b/app/src/components/gallery/activity.tsx
@@ -90,7 +90,7 @@ export function ActivityReportCard({
: {
status: "refused",
reason:
- result.reason ?? result.error ?? "That data could not be read.",
+ result.reason ?? result.error ?? "Couldn't read that data.",
},
);
});
@@ -110,10 +110,7 @@ export function ActivityReportCard({
if (state.status === "reading") {
return (
-
+
Reading…
);
@@ -183,7 +180,7 @@ function ActivityChart({
) : undefined
}
- caption={`Counted from this deployment's audit trail, last ${data.days} days`}
+ caption={`Counted from OpenBot's audit trail, last ${data.days} days`}
title={title ?? "Bot activity"}
>
@@ -226,7 +223,7 @@ function RefusalList({
return (
- This deployment has refused nothing.
+ OpenBot hasn't refused anything.
);
@@ -249,7 +246,7 @@ function RefusalList({
) : undefined
}
- caption="Read from this deployment's audit trail"
+ caption="Read from OpenBot's audit trail"
title={title ?? "Recent refusals"}
>
- The assistant is putting the questions together.
+ The Bot is putting the questions together.
);
@@ -138,7 +138,7 @@ function InvalidForm({
{returned
? "The form had invalid questions. The Bot can send a corrected form."
- : "The form has invalid questions and could not be displayed."}
+ : "The form has invalid questions, so it can't be shown."}
- Your answers could not be sent. Try again.
+ Couldn't send your answers. Try again.
)}
diff --git a/app/src/components/gallery/preview.tsx b/app/src/components/gallery/preview.tsx
index ce613665ef..8b55af7c89 100644
--- a/app/src/components/gallery/preview.tsx
+++ b/app/src/components/gallery/preview.tsx
@@ -62,7 +62,7 @@ export function GalleryPreview({
if (shown.length === 0) {
return (
- This Bot has not been granted any components, so it answers in prose.
+ This Bot can't use any components yet. It answers in text.
);
}
@@ -117,8 +117,8 @@ function PreviewOf({ name }: { name: string }) {
*/
return (
- Draws this deployment's own records, so it has nothing to show until a
- Bot asks for it.
+ Shows OpenBot's own records. There's nothing to show until a Bot asks
+ for it.
);
}
diff --git a/app/src/components/memory/memories.tsx b/app/src/components/memory/memories.tsx
index 4d49df8670..f19d725852 100644
--- a/app/src/components/memory/memories.tsx
+++ b/app/src/components/memory/memories.tsx
@@ -69,9 +69,7 @@ export function RememberFact() {
Remember a fact
-
- It reaches every one of your Bots.
-
+ All your Bots will know it.
{/* The submit button is in the footer and reaches this form by id, so DialogBody stays a
@@ -165,7 +163,7 @@ export function MemoryList({
))}
) : (
- You have no memories yet.
+ No memories yet. Add one with Remember a fact.
)}
);
@@ -196,11 +194,11 @@ function MemoryRow({
const status = heldByIntelligence
? "Kept in Intelligence"
: !memory.enabled
- ? "Disabled"
+ ? "Turned off"
: memory.formedBy === "bot"
- ? `Formed by ${botName}${memory.reviewState === "unreviewed" ? " · awaiting review" : ""}`
+ ? `Formed by ${botName}${memory.reviewState === "unreviewed" ? " · needs review" : ""}`
: memory.reviewState === "unreviewed"
- ? "Imported · awaiting review"
+ ? "Imported · needs review"
: "Reviewed";
return (
diff --git a/app/src/components/memory/sources.tsx b/app/src/components/memory/sources.tsx
index 5df3214f87..a789a508b2 100644
--- a/app/src/components/memory/sources.tsx
+++ b/app/src/components/memory/sources.tsx
@@ -114,15 +114,15 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
Add source
}
- description="Each app you connected in your own account that this Bot can read becomes a source on its own, and you can add more. Enabled sources refresh every 15 minutes, and their facts are available to this Bot only. Turn one off or remove it and it stays that way."
- title="Connected app sources"
+ description="Apps you connected that this Bot can read show up here, and you can add more. Sources refresh every 15 minutes. Only this Bot sees what they hold."
+ title="App sources"
>
{sources.isPending ? null : sources.error ? (
{sources.error.message}
) : shown.length === 0 ? (
- No connected app sources yet.
+ No sources yet. Add one with Add source.
) : (
{shown.map((source, index) => (
@@ -139,7 +139,7 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
? source.syncStatus === "succeeded"
? "Up to date"
: source.syncStatus
- : "Disabled"}
+ : "Turned off"}
{source.lastSyncAt
? ` · Last read ${new Date(source.lastSyncAt).toLocaleString()}`
: ""}
@@ -203,7 +203,7 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
Add a source
- It is read now, and then every 15 minutes while it is enabled.
+ OpenBot reads it now, then every 15 minutes while it's on.
@@ -235,7 +235,7 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
.filter(Boolean);
else if (setting.schema.type === "object")
throw new Error(
- "Choose an action with simple search settings.",
+ "This one has settings OpenBot can't show. Choose another.",
);
else values[setting.name] = value;
}
@@ -293,9 +293,7 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
) : null}
-
- Connected app action
-
+ What to read
- Changes apply the next time /{skill.slug} is used. Agents
- already carrying it keep it.
+ Changes apply the next time someone uses /{skill.slug}.
+ Bots that have it keep it.
diff --git a/app/src/components/skills/new-skill.tsx b/app/src/components/skills/new-skill.tsx
index 48b8ca9090..2f1674bb39 100644
--- a/app/src/components/skills/new-skill.tsx
+++ b/app/src/components/skills/new-skill.tsx
@@ -21,8 +21,8 @@ export function NewSkill() {
New skill
- A named instruction you invoke with /. It goes on the
- Bots you own, and nobody else sees it.
+ An instruction you start with /. Only you see it, and it
+ works on Bots you own.
diff --git a/app/src/components/skills/proposed-skill.tsx b/app/src/components/skills/proposed-skill.tsx
index 1f914f15ee..9a84265e57 100644
--- a/app/src/components/skills/proposed-skill.tsx
+++ b/app/src/components/skills/proposed-skill.tsx
@@ -94,7 +94,7 @@ export function ProposedSkillCard({
// Left on the card with the buttons still live. See `refusal` above.
setSending(null);
setRefusal(
- cause instanceof Error ? cause.message : "The skill was not saved.",
+ cause instanceof Error ? cause.message : "Couldn't save the skill.",
);
return;
}
@@ -112,7 +112,7 @@ export function ProposedSkillCard({
caption={
replaces
? `This replaces ${replaces.title}, which is ${replaces.ownership}.`
- : "Nothing is saved until you press the button."
+ : "Nothing is saved until you say so."
}
title={titleFor(values.slug, replaces)}
>
@@ -145,8 +145,8 @@ export function ProposedSkillCard({
{values.tools.length > 0 ? (
- Naming a tool grants nothing. A Bot is offered these only if an
- administrator has already granted them to it.
+ Naming a tool doesn't give access. A Bot can only use these if it
+ already has them.
- The Bot's draft was not a valid skill, so nothing was saved. It has been
- told what to fix.
+ The Bot's draft wasn't a valid skill. Nothing was saved, and the Bot
+ knows what to fix.
{slugLocked ? (
- "A command cannot be changed. To rename a skill, write a new one and delete this."
+ "You can't change a command. To rename this skill, write a new one and delete this one."
) : (
<>
What you type after a slash.{" "}
@@ -174,15 +174,15 @@ export function SkillFields({
name={field.name}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
- placeholder="Summarise what I did yesterday from the channel, then list what is left."
+ placeholder="Summarise what I did yesterday in this conversation, then list what is left."
value={field.state.value}
/>
{isInvalid ? (
) : (
- Added to the run when the command is used. Write it as
- instructions to the Bot, not as a description of them.
+ What the Bot is told when you use the command. Write it as
+ instructions to the Bot.
- Could not load the tools this deployment has.
+ Couldn't load the tools. Reload the page.
) : servers.length === 0 ? (
- No connected server offers a tool yet. A skill can still be written —
- most are instructions rather than tool use.
+ No app offers tools yet. You can still write the skill. Most skills
+ are only instructions.
) : (
@@ -143,7 +143,7 @@ export function SkillTools({
key={role.ref}
onClick={() => toggle(role.ref)}
size="sm"
- title={`${role.ref} — on whichever research connector the Bot holds`}
+ title={`${role.ref}: uses whichever research app the Bot has`}
type="button"
variant={held.has(role.ref) ? "default" : "outline"}
>
@@ -156,7 +156,7 @@ export function SkillTools({
{elsewhere.length > 0 ? (
- This skill names these, and no server connected here offers them —
- because the connector has not been added, or was removed. They cost
- nothing and load nothing until it exists. Click one to stop naming
- it.
+ This skill asks for these, but no app here offers them. They do
+ nothing until an admin adds the app. Click one to remove it.
) : null}
- Picking this skill is what loads these tools for a turn. It does not
- grant them — a Bot still only calls what it was granted, so naming a
- tool here gives nobody access to it.
+ Using this skill loads these tools for that reply. It doesn't give
+ access. A Bot can only use tools it already has.
);
diff --git a/app/src/lib/agents/mutations.ts b/app/src/lib/agents/mutations.ts
index 639f6961e8..760f833c17 100644
--- a/app/src/lib/agents/mutations.ts
+++ b/app/src/lib/agents/mutations.ts
@@ -19,7 +19,7 @@ export type AgentInput = {
};
/** The sentence for every write here, since they all fail the same way to a reader. */
-const FALLBACK = "Coworker operation failed";
+const FALLBACK = "Couldn't save that change to the Bot.";
/** Server-derived fields are invalidated instead of patched by hand. */
function invalidateAgents(queryClient: QueryClient) {
diff --git a/app/src/lib/agents/queries.ts b/app/src/lib/agents/queries.ts
index 326d006485..182ef08541 100644
--- a/app/src/lib/agents/queries.ts
+++ b/app/src/lib/agents/queries.ts
@@ -118,7 +118,7 @@ export function agentListQueryOptions(hidden = false) {
queryKey: agentKeys.list(hidden),
queryFn: (): Promise =>
client(`/api/agents${hidden ? "?hidden=true" : ""}`, "agents", {
- fallback: "Could not load coworkers",
+ fallback: "Couldn't load your Bots.",
}),
});
}
@@ -133,7 +133,7 @@ export function agentQueryOptions(agentId: string) {
queryKey: agentKeys.detail(agentId),
queryFn: (): Promise =>
client(agentApiPath(agentId), "agent", {
- fallback: "Could not load this coworker",
+ fallback: "Couldn't load this Bot.",
}),
});
}
diff --git a/app/src/lib/channels/mutations.ts b/app/src/lib/channels/mutations.ts
index 7665c9b823..f837bb7faa 100644
--- a/app/src/lib/channels/mutations.ts
+++ b/app/src/lib/channels/mutations.ts
@@ -18,7 +18,7 @@ export function createChannelMutationOptions(queryClient: QueryClient) {
const response = await client("/api/channels", {
method: "POST",
body: { agentIds },
- fallback: "Could not start a channel",
+ fallback: "Couldn't start a conversation.",
});
return ((await response.json()) as { channel: AgentChannel }).channel;
},
@@ -84,7 +84,7 @@ export function setChannelPinnedMutationOptions(queryClient: QueryClient) {
await client(`/api/channels/${variables.channelId}/pin`, {
method: "PUT",
body: { pinned: variables.pinned },
- fallback: "Could not pin this channel",
+ fallback: "Couldn't pin this conversation.",
});
},
onSuccess: () =>
@@ -107,7 +107,7 @@ export function markChannelReadMutationOptions(queryClient: QueryClient) {
mutationFn: async (channelId: string) => {
await client(`/api/channels/${channelId}/read`, {
method: "PUT",
- fallback: "Could not mark this channel read",
+ fallback: "Couldn't mark this conversation as read.",
});
},
onSuccess: () =>
@@ -150,7 +150,7 @@ export function deleteChannelMutationOptions(queryClient: QueryClient) {
mutationFn: async (channelId: string) => {
await client(`/api/channels/${channelId}`, {
method: "DELETE",
- fallback: "Could not delete this channel",
+ fallback: "Couldn't delete this conversation.",
});
},
// The roster only. The open channel's detail query would refetch into the fresh 404 and
diff --git a/app/src/lib/channels/queries.ts b/app/src/lib/channels/queries.ts
index bcd2f6e925..e8ef1a2f08 100644
--- a/app/src/lib/channels/queries.ts
+++ b/app/src/lib/channels/queries.ts
@@ -77,7 +77,7 @@ export function channelListQueryOptions() {
? `?cursor=${encodeURIComponent(pageParam as string)}`
: "";
const response = await client(`/api/channels${suffix}`, {
- fallback: "Could not load channels",
+ fallback: "Couldn't load your conversations.",
});
return (await response.json()) as ChannelPage;
},
@@ -92,7 +92,7 @@ export function channelQueryOptions(channelId: string) {
queryKey: channelKeys.detail(channelId),
queryFn: async (): Promise => {
return client(`/api/channels/${channelId}`, "channel", {
- fallback: "Could not load this channel",
+ fallback: "Couldn't load this conversation.",
});
},
});
diff --git a/app/src/lib/channels/route.ts b/app/src/lib/channels/route.ts
index a214429a0c..59ef14f690 100644
--- a/app/src/lib/channels/route.ts
+++ b/app/src/lib/channels/route.ts
@@ -62,9 +62,7 @@ export async function routeMessage(
names?: unknown;
} | null;
const message =
- typeof body?.error === "string"
- ? body.error
- : "Could not choose a coworker.";
+ typeof body?.error === "string" ? body.error : "Couldn't pick a Bot.";
if (
response.status === 409 &&
Array.isArray(body?.names) &&
diff --git a/app/src/lib/client.ts b/app/src/lib/client.ts
index 2e92b282b4..14a5f1b945 100644
--- a/app/src/lib/client.ts
+++ b/app/src/lib/client.ts
@@ -20,7 +20,7 @@ export type ClientOptions = {
/**
* What a person reads when the server sent no message of its own.
*
- * Name the entity in it — "Could not load coworkers" rather than "Request failed" — because this
+ * Name the entity in it — "Couldn't load your Bots." rather than "Request failed" — because this
* is the sentence that reaches the screen when the server is the one that broke.
*/
fallback?: string;
diff --git a/app/src/lib/computers/control.ts b/app/src/lib/computers/control.ts
index 9b1a5325a5..f7ee332f7d 100644
--- a/app/src/lib/computers/control.ts
+++ b/app/src/lib/computers/control.ts
@@ -84,11 +84,14 @@ export async function supplySecret(
const body = (await response.json().catch(() => null)) as {
error?: string;
} | null;
- return { ok: false, error: body?.error ?? "That could not be entered." };
+ return {
+ ok: false,
+ error: body?.error ?? "Couldn't type that into the page. Try again.",
+ };
} catch {
return {
ok: false,
- error: "The assistant's computer could not be reached.",
+ error: "Couldn't reach the Bot's computer. Try again.",
};
}
}
diff --git a/app/src/lib/plugins/grants.ts b/app/src/lib/plugins/grants.ts
index da331695c8..890a570fe1 100644
--- a/app/src/lib/plugins/grants.ts
+++ b/app/src/lib/plugins/grants.ts
@@ -35,7 +35,7 @@ export function grantsQueryOptions(serverId: string, enabled: boolean) {
queryFn: async (): Promise => {
const response = await client(
`/api/plugins/servers/${encodeURIComponent(serverId)}/grants`,
- { fallback: "What this account holds could not be read." },
+ { fallback: "Couldn't read what this account allows." },
);
const body: { actions?: unknown } = await response.json();
return Array.isArray(body.actions) ? (body.actions as GrantState[]) : [];
@@ -62,7 +62,7 @@ export function authorizeActionMutationOptions(
{
method: "POST",
body: { action: variables.action },
- fallback: "That action could not be authorized.",
+ fallback: "Couldn't authorize that action.",
},
);
return authorizationUrl ?? null;
diff --git a/app/src/lib/plugins/mutations.ts b/app/src/lib/plugins/mutations.ts
index f933d8f954..c448077e42 100644
--- a/app/src/lib/plugins/mutations.ts
+++ b/app/src/lib/plugins/mutations.ts
@@ -52,7 +52,7 @@ export type CustomServerInput = {
/** Which kinds of plugin a grant can be about. */
export type PluginKind = "mcp" | "skill";
-const FALLBACK = "That did not work.";
+const FALLBACK = "That didn't work. Try again.";
/**
* Refetch everything the plugin screens read.
@@ -123,7 +123,7 @@ export function grantPlugin(variables: {
agentId: variables.agentId,
...(variables.approval ? { approval: variables.approval } : {}),
},
- fallback: "That Agent could not be changed.",
+ fallback: "Couldn't change that Bot.",
});
}
@@ -214,7 +214,7 @@ export function setPluginGrantMutationOptions(queryClient: QueryClient) {
}
await client(
`/api/plugins/grants?kind=${variables.kind}&ref=${encodeURIComponent(variables.ref)}&agentId=${encodeURIComponent(variables.agentId)}`,
- { method: "DELETE", fallback: "That Agent could not be changed." },
+ { method: "DELETE", fallback: "Couldn't change that Bot." },
);
},
onSettled: () => invalidatePlugins(queryClient),
@@ -318,7 +318,7 @@ export function saveSkillMutationOptions(queryClient: QueryClient) {
* The server refuses for reasons a form cannot check — a slug somebody else already owns is
* the common one — and paraphrasing that would throw away the only part worth reading.
*/
- fallback: "The skill could not be saved.",
+ fallback: "Couldn't save the skill.",
}),
onSettled: () => invalidatePlugins(queryClient),
});
@@ -407,7 +407,7 @@ export function connectAccountMutationOptions(
const authorizationUrl = await client(
`/api/plugins/servers/${encodeURIComponent(serverId)}/connect?returnTo=${returnTo}`,
"authorizationUrl",
- { method: "POST", fallback: "That account could not be connected." },
+ { method: "POST", fallback: "Couldn't connect that account." },
);
return authorizationUrl ?? null;
},
@@ -450,7 +450,7 @@ export function confirmBrokeredConnectionMutationOptions(
): Promise => {
const response = await client(
`/api/plugins/servers/${encodeURIComponent(serverId)}/connection/confirm`,
- { method: "POST", fallback: "That connection could not be confirmed." },
+ { method: "POST", fallback: "Couldn't confirm that connection." },
);
return response.json();
},
@@ -472,7 +472,7 @@ export function disconnectBrokeredMutationOptions(queryClient: QueryClient) {
`/api/plugins/servers/${encodeURIComponent(serverId)}/connection`,
{
method: "DELETE",
- fallback: "That account could not be disconnected.",
+ fallback: "Couldn't disconnect that account.",
},
);
},
@@ -534,7 +534,7 @@ export function brokeredConnectionFieldsMutationOptions() {
"fields",
{
method: "POST",
- fallback: "That app could not be asked what it needs.",
+ fallback: "Couldn't find out what this app needs.",
},
);
return fields ?? null;
@@ -597,7 +597,7 @@ export function connectBrokeredWithFieldsMutationOptions(
{
method: "POST",
body: { values: variables.values },
- fallback: "That account could not be connected.",
+ fallback: "Couldn't connect that account.",
},
);
return response.json();
@@ -649,7 +649,7 @@ export function recheckBrokeredConnectionMutationOptions(
mutationFn: async (serverId: string): Promise => {
const response = await client(
`/api/plugins/servers/${encodeURIComponent(serverId)}/connection/recheck`,
- { method: "POST", fallback: "That connection could not be checked." },
+ { method: "POST", fallback: "Couldn't check that connection." },
);
return response.json();
},
diff --git a/app/src/lib/plugins/provider-vocabulary.ts b/app/src/lib/plugins/provider-vocabulary.ts
index 9a0e2a9147..deecac7020 100644
--- a/app/src/lib/plugins/provider-vocabulary.ts
+++ b/app/src/lib/plugins/provider-vocabulary.ts
@@ -52,7 +52,7 @@ const VOCABULARY: Record = {
envVar: "COMPOSIO_API_KEY",
directoryPath: "/admin/plugins/composio",
directoryDescription:
- "A few hundred apps, reached as whoever is asking. Each person connects their own account.",
+ "A few hundred apps. Each person connects their own account.",
},
arcade: {
name: "Arcade",
@@ -60,7 +60,7 @@ const VOCABULARY: Record = {
envVar: "ARCADE_API_KEY",
directoryPath: "/admin/plugins/arcade",
directoryDescription:
- "A directory this deployment has not reviewed, reached as whoever is asking. Each person connects their own account.",
+ "Apps nobody here has reviewed. Each person connects their own account.",
},
};
@@ -77,12 +77,12 @@ export function providerVocabulary(
* fallback once spelled a vendor on its own and printed Composio's name on an Arcade deployment.
*/
export function directoryReadFailure(provider: PluginsProvider): string {
- return `${VOCABULARY[provider].name}'s app directory could not be read.`;
+ return `Couldn't load ${VOCABULARY[provider].name}'s app directory. Reload the page.`;
}
/** The "More apps" section's own sentence. */
export function moreAppsSectionDescription(provider: PluginsProvider): string {
- return `A directory this deployment has not reviewed, reached through ${VOCABULARY[provider].name}.`;
+ return `Apps from ${VOCABULARY[provider].name}. Nobody here has reviewed them.`;
}
/**
@@ -108,7 +108,7 @@ export function moreAppsRow(
if (!configured) {
return {
title: name,
- description: `Add your ${name} key to enable a catalogue of tools. Set ${envVar} on this deployment.`,
+ description: `Not set up. Set ${envVar} on this deployment to browse its apps.`,
linked: false,
};
}
@@ -133,42 +133,27 @@ export function brokeredReach(provider: PluginsProvider): {
} {
const { name } = VOCABULARY[provider];
return {
- description: `This app is reached through ${name}, which holds the account. This deployment sends one key and the name of whoever is asking, so each person connects their own and a Bot sees only what that person can see. There is no token to paste.`,
+ description: `${name} holds the account. This deployment sends one key and who is asking. Each person connects their own, and a Bot sees only what that person can see. There is no token to paste.`,
badge: `Through ${name}`,
};
}
/** What disconnecting a brokered account does, said where the button is. */
export function brokeredDisconnectNote(provider: PluginsProvider): string {
- return `Disconnecting ends the account at ${VOCABULARY[provider].name}, not just here.`;
+ return `Disconnecting also ends the account at ${VOCABULARY[provider].name}.`;
}
/**
- * The line under a brokered app's own page title on a person's connected-accounts screen.
+ * What an admin's own brokered row says where the broker authorizes PER ACTION, in place of a
+ * consent trip.
*
- * NOT {@link brokeredConnectionDescription}, which is the admin Connection card's sentence and says
- * something different ("each person's own account"). This one speaks to the person reading their
- * own page: the broker holds the account, so a Bot sees only what they can see.
- */
-export function brokeredAccountPageDescription(
- provider: PluginsProvider,
-): string {
- return `Reached through ${VOCABULARY[provider].name}, which holds the account, so a Bot sees only what you can see.`;
-}
-
-/** What Connect does on a consent app, said before the press: where it goes first. */
-export function brokeredConsentTrip(provider: PluginsProvider): string {
- return `Connecting takes you to ${VOCABULARY[provider].name} and then to the vendor to consent.`;
-}
-
-/**
- * What stands in for {@link brokeredConsentTrip} where the broker authorizes PER ACTION.
+ * NOT A CONSENT TRIP WITH A WORD CHANGED, because it is not the same offer. Under a per-action broker
+ * there is no whole-app press to name — the row withholds Connect entirely — so this says where the
+ * act actually is instead. A person reading "Connecting takes you to…" beside no Connect button is
+ * being told about a button that is not there.
*
- * NOT THE SAME SENTENCE WITH A WORD CHANGED, because it is not the same offer. The consent trip
- * names a press and says where it goes; under a per-action broker there is no whole-app press to
- * name — the row withholds Connect entirely — so this says where the act actually is instead. A
- * person reading "Connecting takes you to…" beside no Connect button is being told about a button
- * that is not there.
+ * The person's own page under Connections names no vendor, so it says this in its own words; the
+ * admin setup page may name the broker, and reads it from here.
*
* `where` IS NOT DECORATION AND HAS NO DEFAULT. The two screens that draw this row do not both
* draw the action list: the Connections page has it directly underneath, and the admin setup page
@@ -181,7 +166,7 @@ export function brokeredPerActionTrip(
provider: PluginsProvider,
where: "below" | "settings",
): string {
- const stem = `${VOCABULARY[provider].name} approves one action at a time, so there is nothing to connect for the whole app.`;
+ const stem = `${VOCABULARY[provider].name} approves one action at a time, so there's nothing to connect for the whole app.`;
return where === "below"
? `${stem} Authorize the actions you want below.`
: `${stem} Authorize them from Connections.`;
@@ -189,7 +174,7 @@ export function brokeredPerActionTrip(
/** What a person is told where the deployment has no key for the active broker. */
export function brokeredKeyMissing(provider: PluginsProvider): string {
- return `Set ${VOCABULARY[provider].envVar} on this deployment. Without it there is no broker to reach, so this account can be neither connected nor ended from here. The app stays enabled and every grant on its tools still stands.`;
+ return `Set ${VOCABULARY[provider].envVar} on this deployment. Until then, this account can't be connected or disconnected here. The app stays turned on, and its tool grants still stand.`;
}
/**
@@ -218,8 +203,8 @@ export function disconnectSiblingsNotice(answer: {
if (unanswered > 0) {
parts.push(
siblings.length > 0
- ? `${unanswered} other ${unanswered === 1 ? "app" : "apps"} could not be checked and may be affected too.`
- : `${unanswered} ${unanswered === 1 ? "app" : "apps"} on this connection could not be checked, so disconnecting may end ${unanswered === 1 ? "it" : "them"} too.`,
+ ? `${unanswered} other ${unanswered === 1 ? "app" : "apps"} couldn't be checked and may be affected too.`
+ : `${unanswered} ${unanswered === 1 ? "app" : "apps"} on this connection couldn't be checked, so disconnecting may end ${unanswered === 1 ? "it" : "them"} too.`,
);
}
return parts.length > 0 ? parts.join(" ") : null;
diff --git a/app/src/lib/plugins/queries.ts b/app/src/lib/plugins/queries.ts
index 28e08e5609..c3a42ccbaf 100644
--- a/app/src/lib/plugins/queries.ts
+++ b/app/src/lib/plugins/queries.ts
@@ -442,7 +442,7 @@ export function connectionsQueryOptions() {
queryKey: pluginKeys.connections(),
queryFn: async (): Promise => {
const response = await client("/api/plugins/connections", {
- fallback: "Your connected accounts could not be loaded.",
+ fallback: "Couldn't load your connections.",
});
return response.json();
},
@@ -465,7 +465,7 @@ export function connectionSiblingsQueryOptions(serverId: string) {
const response = await client(
`/api/plugins/servers/${encodeURIComponent(serverId)}/connection/siblings`,
{
- fallback: "Which other apps share this connection could not be read.",
+ fallback: "Couldn't read which other apps share this connection.",
},
);
const body: Partial = await response.json();
@@ -529,7 +529,7 @@ export function agentPluginsQueryOptions(agentId: string) {
queryFn: async (): Promise => {
const response = await client(
`/api/plugins/for/${encodeURIComponent(agentId)}`,
- { fallback: "This Bot's plugins could not be read." },
+ { fallback: "Couldn't load this Bot's apps." },
);
return response.json();
},
diff --git a/app/src/lib/plugins/shared-use.ts b/app/src/lib/plugins/shared-use.ts
index 358646caa1..8a2010e197 100644
--- a/app/src/lib/plugins/shared-use.ts
+++ b/app/src/lib/plugins/shared-use.ts
@@ -53,7 +53,7 @@ export function sharedUseRequestsQueryOptions(enabled: boolean) {
queryKey: sharedUseKeys.requests(),
queryFn: () =>
client("/api/approvals/shared-use", "requests", {
- fallback: "Shared account requests could not be loaded.",
+ fallback: "Couldn't load shared account requests.",
}),
enabled,
refetchInterval: 10_000,
@@ -66,7 +66,7 @@ export function botSharedAppsQueryOptions(botId: string) {
queryFn: async (): Promise => {
const response = await client(
`/api/approvals/shared-use/bot/${encodeURIComponent(botId)}`,
- { fallback: "This Bot's shared apps could not be loaded." },
+ { fallback: "Couldn't load this Bot's shared apps." },
);
return response.json();
},
@@ -84,7 +84,7 @@ export function decideSharedUseMutationOptions(queryClient: QueryClient) {
}) =>
client(
`/api/approvals/shared-use/${encodeURIComponent(id)}/${decision}`,
- { method: "POST", fallback: "That decision could not be saved." },
+ { method: "POST", fallback: "Couldn't save that decision." },
),
onSettled: () => queryClient.invalidateQueries({ queryKey: ["approvals"] }),
});
@@ -100,7 +100,7 @@ export function requestSharedUseMutationOptions(queryClient: QueryClient) {
client("/api/approvals/shared-use", {
method: "POST",
body,
- fallback: "The request could not be sent.",
+ fallback: "Couldn't send the request.",
}),
onSettled: (_data, _error, body) =>
queryClient.invalidateQueries({
diff --git a/app/src/lib/skills/form.ts b/app/src/lib/skills/form.ts
index 2346f722df..b570056cf8 100644
--- a/app/src/lib/skills/form.ts
+++ b/app/src/lib/skills/form.ts
@@ -34,7 +34,7 @@ export const skillFormSchema = z.object({
instructions: z
.string()
.trim()
- .min(1, "Instructions are required — this is what the Bot follows."),
+ .min(1, "Instructions are required. They are what the Bot follows."),
/**
* The tools this skill says it needs, as `/` refs.
*
diff --git a/app/src/routes/_authed/_app/bot.tsx b/app/src/routes/_authed/_app/bot.tsx
index c64ff8263c..f5c41b5ab9 100644
--- a/app/src/routes/_authed/_app/bot.tsx
+++ b/app/src/routes/_authed/_app/bot.tsx
@@ -67,10 +67,10 @@ async function loadExplicitBot(agentId: string): Promise {
`/api/agents/${encodeURIComponent(agentId)}`,
);
if (response.status === 404) return { status: "missing" };
- if (!response.ok) throw new Error("Bot couldn't be loaded.");
+ if (!response.ok) throw new Error("Couldn't load this Bot.");
const body: unknown = await response.json().catch(() => null);
- if (!isAgentEnvelope(body)) throw new Error("Bot couldn't be loaded.");
+ if (!isAgentEnvelope(body)) throw new Error("Couldn't load this Bot.");
return { bot: body.agent, status: "found" };
}
@@ -106,7 +106,7 @@ function RouteComponent() {
return (
- Bots couldn't be loaded.
+ Couldn't load your Bots. Reload the page.
);
@@ -115,7 +115,7 @@ function RouteComponent() {
return (
- Bot couldn't be loaded.
+ Couldn't load this Bot. Reload the page.
);
@@ -125,8 +125,8 @@ function RouteComponent() {
{agent
- ? `This deployment has no Bot called "${agent}".`
- : "This deployment has no Bots yet."}
+ ? `There's no Bot called "${agent}".`
+ : "There are no Bots yet."}
);
@@ -216,8 +216,7 @@ function BotChat({ agentId, name }: { agentId: string; name: string }) {
data-testid="bot-chat-history-unavailable"
role="alert"
>
- Earlier messages in this conversation could not be loaded, and the
- Bot is answering without them.
+ Couldn't load earlier messages. The Bot is answering without them.
) : null}
{/*
diff --git a/app/src/routes/_authed/_app/bots.$agentId.access.tsx b/app/src/routes/_authed/_app/bots.$agentId.access.tsx
index 1c8591197d..c4d80db8e8 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.access.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.access.tsx
@@ -11,7 +11,7 @@ function AccessPage() {
return (
{(agent) => }
diff --git a/app/src/routes/_authed/_app/bots.$agentId.approvals.tsx b/app/src/routes/_authed/_app/bots.$agentId.approvals.tsx
index 94f2fa87c9..a8906fa7eb 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.approvals.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.approvals.tsx
@@ -11,7 +11,7 @@ function ApprovalRulesPage() {
return (
{(agent) => }
diff --git a/app/src/routes/_authed/_app/bots.$agentId.index.tsx b/app/src/routes/_authed/_app/bots.$agentId.index.tsx
index 410eb5d106..e3b1fd7a97 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.index.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.index.tsx
@@ -18,12 +18,12 @@ function BotProfilePage() {
return (
{agent.isPending ? null : agent.error ? (
- This Bot is not one you can reach.
+ Couldn't open this Bot. It may not be shared with you.
) : (
{(agent) => (
@@ -23,7 +23,7 @@ function MemoryPage() {
className="underline underline-offset-4"
to="/capabilities/memory"
>
- Settings → Memory
+ Capabilities
.
diff --git a/app/src/routes/_authed/_app/bots.$agentId.responsibilities.tsx b/app/src/routes/_authed/_app/bots.$agentId.responsibilities.tsx
index ec612945bd..cc21b2c7d8 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.responsibilities.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.responsibilities.tsx
@@ -13,7 +13,7 @@ function ResponsibilitiesPage() {
return (
{(agent) => }
diff --git a/app/src/routes/_authed/_app/bots.$agentId.routines.tsx b/app/src/routes/_authed/_app/bots.$agentId.routines.tsx
index b2cc658497..71cac12735 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.routines.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.routines.tsx
@@ -11,7 +11,7 @@ function RoutinesPage() {
return (
{(agent) => }
diff --git a/app/src/routes/_authed/_app/bots.$agentId.sharing.tsx b/app/src/routes/_authed/_app/bots.$agentId.sharing.tsx
index 69243d320b..dbacc40ec1 100644
--- a/app/src/routes/_authed/_app/bots.$agentId.sharing.tsx
+++ b/app/src/routes/_authed/_app/bots.$agentId.sharing.tsx
@@ -11,7 +11,7 @@ function SharingPage() {
return (
{(agent) => }
diff --git a/app/src/routes/_authed/_app/bots.index.tsx b/app/src/routes/_authed/_app/bots.index.tsx
index e27fd9f506..be6dfe60f2 100644
--- a/app/src/routes/_authed/_app/bots.index.tsx
+++ b/app/src/routes/_authed/_app/bots.index.tsx
@@ -32,7 +32,7 @@ function BotsPage() {
New Bot
}
- description="Every Bot you can reach: what each is doing, what it needs from you, and whether it is paused."
+ description="Your Bots and the ones shared with you. See what each is doing and what it needs from you."
title="Bots"
>
diff --git a/app/src/routes/_authed/_app/capabilities.memory.tsx b/app/src/routes/_authed/_app/capabilities.memory.tsx
index 37647af15c..00393f5ae3 100644
--- a/app/src/routes/_authed/_app/capabilities.memory.tsx
+++ b/app/src/routes/_authed/_app/capabilities.memory.tsx
@@ -11,7 +11,7 @@ function MemoryPage() {
}
- description="What your Bots know about you. Facts you tell them reach every Bot; what a Bot learns stays with that Bot."
+ description="What your Bots know about you. Facts you add reach every Bot. What a Bot learns on its own stays with that Bot."
/>
);
diff --git a/app/src/routes/_authed/_app/capabilities.skills.tsx b/app/src/routes/_authed/_app/capabilities.skills.tsx
index 321122fd08..6cf3b47ce3 100644
--- a/app/src/routes/_authed/_app/capabilities.skills.tsx
+++ b/app/src/routes/_authed/_app/capabilities.skills.tsx
@@ -129,8 +129,8 @@ function SkillsPage() {
}
description={
<>
- A skill is a named instruction you invoke with / and
- a Bot follows. Yours are yours alone, and go on the Bots you own.
+ Type / and a skill's name to give a Bot that
+ instruction. Only you see yours, and they work on Bots you own.
>
}
title="Your skills"
@@ -146,13 +146,13 @@ function SkillsPage() {
*/}
{loading ? null : !data ? (
- Your skills could not be loaded.
+ Couldn't load your skills. Reload the page.
) : mine.length === 0 ? (
- You don't have any skills yet.
+ No skills yet. Write one with New skill.
@@ -235,7 +235,7 @@ function SkillsPage() {
*/}
{deployment.length > 0 ? (
diff --git a/app/src/routes/_authed/_app/channel/$channelId.tsx b/app/src/routes/_authed/_app/channel/$channelId.tsx
index 4e60ec43a7..727c1b8871 100644
--- a/app/src/routes/_authed/_app/channel/$channelId.tsx
+++ b/app/src/routes/_authed/_app/channel/$channelId.tsx
@@ -141,7 +141,7 @@ function RouteComponent() {
onOpenChange={(open) => show(open ? "watch" : null)}
/>
- Could not load this channel.
+ Couldn't load this conversation. Reload the page.
);
}
diff --git a/app/src/routes/_authed/_app/channel/new.tsx b/app/src/routes/_authed/_app/channel/new.tsx
index ed9bb14b43..cc49cdb752 100644
--- a/app/src/routes/_authed/_app/channel/new.tsx
+++ b/app/src/routes/_authed/_app/channel/new.tsx
@@ -39,7 +39,7 @@ import { newId } from "../../../../lib/new-id";
* profile links and reloads preserve the pending recipient without creating an empty channel.
*/
/** What `GET /api/agents/:id` answers for a Bot this person cannot see. */
-const AGENT_NOT_FOUND = "Agent not found.";
+const AGENT_NOT_FOUND = "Couldn't find that Bot.";
export const Route = createFileRoute("/_authed/_app/channel/new")({
/*
@@ -90,7 +90,7 @@ function RouteComponent() {
const urlAgentDetailFailed = needsUrlAgentDetail && detailError && !fetched;
const loadError =
rosterError && profiles === undefined
- ? "Coworkers couldn't be loaded."
+ ? "Couldn't load your Bots. Reload the page."
: urlAgentDetailFailed
? /*
* The server's 404 sentence (agents/routes.ts `mapStoreError`) means this person cannot see
@@ -98,8 +98,8 @@ function RouteComponent() {
* other failure is a load that can be retried, and says so.
*/
detailFailure?.message === AGENT_NOT_FOUND
- ? "This Bot isn't available to you. It may be unpublished, not shared with you, or not described yet."
- : "Coworker couldn't be loaded."
+ ? "This Bot isn't available to you. It may be unpublished or not shared with you."
+ : "Couldn't load that Bot. Reload the page."
: null;
/** Bots picked in the To: field, in the order they will answer; the URL's Bot until one is picked. */
const [picked, setPicked] = useState(null);
@@ -223,7 +223,7 @@ function RouteComponent() {
setError(
caught instanceof Error
? caught.message
- : "Could not start the group.",
+ : "Couldn't start the group. Try again.",
);
throw caught;
}
@@ -243,7 +243,7 @@ function RouteComponent() {
setError(
caught instanceof Error
? caught.message
- : "Could not start the conversation.",
+ : "Couldn't start the conversation. Try again.",
);
throw caught;
}
diff --git a/app/src/routes/_authed/_app/connections.$key.tsx b/app/src/routes/_authed/_app/connections.$key.tsx
index 444fb6e6ad..c8d03089b5 100644
--- a/app/src/routes/_authed/_app/connections.$key.tsx
+++ b/app/src/routes/_authed/_app/connections.$key.tsx
@@ -32,10 +32,6 @@ import { Separator } from "@/components/ui/separator";
import { currentUserQueryOptions } from "@/lib/auth/queries";
import { connectAccountMutationOptions } from "@/lib/plugins/mutations";
import {
- brokeredAccountPageDescription,
- brokeredConsentTrip,
- brokeredDisconnectNote,
- brokeredPerActionTrip,
brokeredProvenance,
disconnectSiblingsNotice,
} from "@/lib/plugins/provider-vocabulary";
@@ -196,7 +192,7 @@ function AppConnectionPage() {
*/
if (authorizationUrl === null) {
setNotice(
- "This deployment answered without a consent link, so there was nowhere to send you and nothing was connected. Try again, and tell an administrator if it persists.",
+ "Couldn't start connecting, so nothing was connected. Try again, or ask an admin if it keeps happening.",
);
return;
}
@@ -334,9 +330,8 @@ function AppConnectionPage() {
return (
- 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.
+ Couldn't load this connection. Reload the page, or ask an admin if it
+ keeps happening.
);
@@ -351,9 +346,8 @@ function AppConnectionPage() {
return (
- 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.
+ Couldn't load this app. Reload the page, or ask an admin if it keeps
+ happening.
);
@@ -370,8 +364,8 @@ function AppConnectionPage() {
return (
- {server.title} is not available on this OpenBot yet. An administrator
- has to finish setting it up before anybody can connect it.
+ {server.title} isn't set up yet. An admin has to turn it on before
+ anyone can connect it.
);
@@ -445,12 +439,17 @@ function AppConnectionPage() {
if (brokered && server) {
/* Bound once because the row below is told it twice — once inside the whole disconnected
sentence and once on its own — and two copies of it would drift. */
- const reassurance = "No Bot can read this as you.";
+ const reassurance = "No Bot can use this as you.";
+ /* Where the next step is. A broker that approves one action at a time has no whole-app Connect,
+ so the sentence points at the action list underneath instead of a button that is not there. */
+ const trip = perActionGrants
+ ? "Authorize the actions you want below."
+ : `Connecting takes you to ${server.title} to approve access.`;
return (
{notice ? (
@@ -477,8 +476,8 @@ function AppConnectionPage() {
/* Said beside the button rather than after it: disconnecting ends the account at
Composio, so what it undoes is not the row here but the grant on your own mailbox,
and connecting again is a fresh consent. */
- connectedDescription={`A Bot granted its tools reads your ${server.title} as you. ${brokeredDisconnectNote(provider)}`}
- disconnectedDescription={`${reassurance} ${perActionGrants ? brokeredPerActionTrip(provider, "below") : brokeredConsentTrip(provider)}`}
+ connectedDescription={`Bots you allow use your ${server.title} as you. Disconnecting removes their access everywhere.`}
+ disconnectedDescription={`${reassurance} ${trip}`}
/* What is true either way. The trip to Composio is not — an app whose key somebody
types never leaves this page — but which Bots can read this person's mail is the
whole point of the screen and holds for both kinds. */
@@ -512,13 +511,13 @@ function AppConnectionPage() {
return (
{entry
- ? "A Bot reaches this one with a credential the deployment holds, the same for everybody."
- : "This deployment has no connector by that name."}
+ ? "Bots use one shared account for this app, the same for everybody."
+ : "OpenBot has no app by that name."}
);
@@ -546,10 +545,10 @@ function AppConnectionPage() {
Your account
{!enabled
- ? "An administrator has not enabled this connector, so there is nothing to connect to yet."
+ ? "Not available yet. An admin has to turn this on first."
: connection
- ? "A Bot granted its tools reads this as you, and sees only what you can see."
- : "No Bot can read this as you. Connecting takes you to the vendor to consent."}
+ ? "Bots you allow use this as you, and see only what you can see."
+ : `No Bot can use this as you. Connecting takes you to ${entry.vendor} to approve access.`}
@@ -591,7 +590,7 @@ function AppConnectionPage() {
* had not, which is the one outcome worse than not offering it.
*/
setNotice(
- `Disconnecting is not built yet. Until it is, revoke it in your ${entry.vendor} account's third-party access settings — that stops this deployment reading anything immediately.`,
+ `You can't disconnect here yet. To stop access now, remove OpenBot in your ${entry.vendor} account's third-party access settings.`,
)
}
className="whitespace-nowrap"
@@ -627,15 +626,15 @@ function AppConnectionPage() {
{connection ? (
- Granted
+ Allowed
- {connection.scope || "The vendor named no scope."}
+ {connection.scope || `${entry.vendor} didn't say.`}
diff --git a/app/src/routes/_authed/_app/connections.index.tsx b/app/src/routes/_authed/_app/connections.index.tsx
index 5fa96884a0..4dcc9de07a 100644
--- a/app/src/routes/_authed/_app/connections.index.tsx
+++ b/app/src/routes/_authed/_app/connections.index.tsx
@@ -113,55 +113,55 @@ export const Route = createFileRoute("/_authed/_app/connections/")({
export const AUTHORIZATION_NOTICES: ReadonlyMap = new Map([
[
"no-broker",
- "This deployment is not set up to connect apps, so nothing was recorded. Ask an administrator to set it up.",
+ "Connecting apps isn't set up yet, so nothing was recorded. Ask an admin to set it up.",
],
[
"not-supported",
- "This deployment's apps are connected once for the whole app, with the Connect button on the app's page; they are not authorized one action at a time.",
+ "Apps here connect once, with Connect on the app's page. They aren't authorized one action at a time.",
],
[
"missing-handle",
- "That link is missing the part that says which authorization it finishes, so nothing was done. Start the authorization again from the app's page.",
+ "That link doesn't say which authorization it finishes, so nothing was done. Start again from the app's page.",
],
[
"no-attempt",
- "There is no authorization in progress for you to finish — it may have expired, or it may have been started by somebody else or in another session. Start it again from the app's page.",
+ "There's no authorization in progress for you to finish. It may have expired, or started in another session. Start again from the app's page.",
],
[
"vendor-unavailable",
- "The app's vendor did not answer when this deployment tried to finish that authorization, so nothing was recorded. Try the link again in a moment.",
+ "The app didn't answer when OpenBot tried to finish that authorization, so nothing was recorded. Try the link again in a moment.",
],
[
"not-your-authorization",
- "That link does not finish the authorization you have in progress, so nothing was recorded and it is still waiting. Finish it from the tab it was started in, or start it again from the app's page.",
+ "That link doesn't match the authorization you have in progress, so nothing was recorded. Finish it in the tab you started it in, or start again from the app's page.",
],
[
"not-finished",
- "That authorization has not finished yet, so nothing was recorded. Finish it at the app's vendor, then open the link again — it is still waiting.",
+ "That authorization hasn't finished yet, so nothing was recorded. Finish it at the app, then open the link again.",
],
[
"authorization-failed",
- "The app's vendor did not complete that authorization, so nothing was recorded. Start it again from the app's page.",
+ "The app didn't complete that authorization, so nothing was recorded. Start again from the app's page.",
],
[
"context-changed",
- "That app's account changed while the authorization was open, so nothing was recorded and anything granted for the old account was withdrawn. Start it again from the app's page.",
+ "That app's account changed while the authorization was open, so nothing was recorded. Access given to the old account was removed. Start again from the app's page.",
],
[
"not-permitted",
- "You can no longer connect that app's account — your access or the app's set-up changed while the authorization was open — so nothing was recorded. Ask an administrator if you expected to finish it.",
+ "You can't connect that app's account anymore, so nothing was recorded. Your access or the app's setup changed while the authorization was open. Ask an admin if you expected to finish it.",
],
[
"cancelled",
- "That authorization was cancelled while it was being finished, so it was not recorded, and what it granted at the app's vendor was withdrawn.",
+ "That authorization was cancelled while it was being finished, so nothing was recorded. The access it gave was removed.",
],
[
"cancelled-left-live",
- "That authorization was cancelled while it was being finished, so it was not recorded — but what it granted could not be withdrawn at the app's vendor, so the vendor may still allow the action you cancelled. To remove it, disconnect the app from its page, which ends its whole connection there, then connect again and authorize only what you want.",
+ "That authorization was cancelled while it was being finished, so nothing was recorded. The access it gave couldn't be removed, so the app may still allow the action you cancelled. To remove it, disconnect the app from its page, then connect again and authorize only what you want.",
],
[
"not-recorded",
- "This deployment could not record that authorization, so none of it was saved. Start it again from the app's page, and tell an administrator if it persists.",
+ "Couldn't record that authorization, so nothing was saved. Start again from the app's page, or ask an admin if it keeps happening.",
],
]);
@@ -369,7 +369,7 @@ function RouteComponent() {
*/}
{arrival.outcome === "failed" ? (
- That account could not be connected. Nothing was saved — try again.
+ Couldn't connect that account. Nothing was saved. Try again.
- 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.
+ Couldn't load your connections. Reload the page, or ask an admin if
+ it keeps happening.
) : rows.length === 0 ? (
- Nothing to connect yet. Apps appear here once an administrator
- enables one that reads as the person asking.
+ Nothing to connect yet. Apps show up here once an admin turns them
+ on.
) : (
<>
@@ -445,8 +444,8 @@ function RouteComponent() {
)}
{links.error || providers.error ? (
- Whether you can link GitHub or Slack could not be loaded, so they
- are not listed. Reload the page to try again.
+ Couldn't load GitHub and Slack, so they aren't listed. Reload the
+ page.
@@ -86,7 +86,7 @@ function RouteComponent() {
setError(
caught instanceof Error
? caught.message
- : "Could not start the conversation.",
+ : "Couldn't start the conversation. Try again.",
);
throw caught;
}
@@ -97,8 +97,7 @@ function RouteComponent() {
// Said out loud: a message that silently reaches somebody you did not choose is the
// kind of surprise that costs trust the first time it happens.
- Sent to the coworker it is for. Type @ to choose one
- yourself.
+ OpenBot picks the right Bot. Type @ to choose one.
) : null}
{error ? (
@@ -120,8 +119,8 @@ function RouteComponent() {
className="mt-2 w-full max-w-2xl text-sm text-destructive"
role="alert"
>
- Your coworkers couldn't be loaded, so there's no one to send this
- to yet.
+ Couldn't load your Bots, so there's no one to send this to. Reload
+ the page.
) : null}
{/*
* `static` undoes the primitive's own absolute placement, which parks these either
@@ -194,11 +193,11 @@ function RouteComponent() {
// card there would say the opposite of what a populated composer beside it (still
// working off that same, successfully loaded `agents`) proves.
<>
-
Explore agents
+
Explore Bots
- Agents shared with you couldn't be loaded.
+ Couldn't load the Bots shared with you.
@@ -208,11 +207,11 @@ function RouteComponent() {
// when it failed but `agents` is defined — a loaded, empty slice either way. Same
// plain copy for both: an empty roster is a fact, not an error.
<>
-
Explore agents
+
Explore Bots
- Nobody has shared an agent with you yet.
+ Nobody has shared a Bot with you yet.
diff --git a/app/src/routes/_authed/admin/approvals.tsx b/app/src/routes/_authed/admin/approvals.tsx
index af067d45f8..e0cdf94f18 100644
--- a/app/src/routes/_authed/admin/approvals.tsx
+++ b/app/src/routes/_authed/admin/approvals.tsx
@@ -9,7 +9,7 @@ export const Route = createFileRoute("/_authed/admin/approvals")({
function AdminApprovalsPage() {
return (
diff --git a/app/src/routes/_authed/admin/audit.tsx b/app/src/routes/_authed/admin/audit.tsx
index 9d661a1da1..ec27d4faed 100644
--- a/app/src/routes/_authed/admin/audit.tsx
+++ b/app/src/routes/_authed/admin/audit.tsx
@@ -83,7 +83,7 @@ function AuditPage() {
setExportCapped(null);
try {
const response = await client(auditExportUrl(search), {
- fallback: "Could not export audit events",
+ fallback: "Couldn't export audit events. Try again.",
});
// A file that stopped early must say so, or it reads as the whole trail.
const capped = Number(response.headers.get("x-audit-export-truncated"));
@@ -101,7 +101,7 @@ function AuditPage() {
setExportError(
error instanceof Error
? error.message
- : "Could not export audit events",
+ : "Couldn't export audit events. Try again.",
);
} finally {
setExporting(false);
@@ -160,13 +160,13 @@ function AuditPage() {
) : null}
{exportCapped ? (
- The file holds the newest {exportCapped.toLocaleString()} events,
- not all of them. Narrow the filter or the dates to export the rest.
+ The file has the newest {exportCapped.toLocaleString()} events, not
+ all of them. Narrow the filter or dates to export the rest.
- The audit trail could not be loaded.
+ Couldn't load the audit log. Reload the page.
) : rows.length === 0 ? (
No events match this filter yet.
@@ -278,10 +278,10 @@ function Row({
const routed =
event.eventType === "channel.routed"
? payload.viaMention === true
- ? "The person chose this coworker"
+ ? "The person chose this Bot"
: payload.fallback === true
- ? "Sent to the default coworker"
- : "Sent to the coworker it is for"
+ ? "Sent to the default Bot"
+ : "Sent to the Bot it was meant for"
: null;
// Allowed by policy but not carried out. A stalled turn belongs in the same family: the Bot was
// asked and the answer never arrived. Colour is how this table is read, and a row left in the
@@ -485,7 +485,7 @@ function Row({
const DISCOVERY_REASONS: Record = {
"under-floor": "Few enough tools to offer them all",
"nothing-declared": "No skill declares any of these tools",
- unavailable: "Could not choose, so all were offered",
+ unavailable: "Couldn't choose, so all were offered",
"nothing-chosen": "No skill applied, so all were offered",
selected: "Chosen by skill",
};
@@ -505,8 +505,8 @@ const DECISIONS: Record = {
"agent.stream_stalled": "The Bot stopped responding",
"computer.policy_loaded": "Boundary at start-up",
"computer.isolation_loaded": "Isolation at start-up",
- "computer.control_taken": "A person took the wheel",
- "computer.control_released": "The wheel was handed back",
+ "computer.control_taken": "A person took control",
+ "computer.control_released": "Control went back to the Bot",
"computer.help_requested": "The Bot asked for help",
"computer.secret_requested": "The Bot asked for a secret",
"computer.secret_supplied": "A person supplied a secret",
@@ -516,25 +516,25 @@ const DECISIONS: Record = {
"component.granted": "Granted to this Bot",
"component.revoked": "Taken away from this Bot",
- "component.published": "Published, so every Bot may use it",
- "component.unpublished": "Unpublished, so no Bot may use it",
+ "component.published": "Published. Every Bot can use it",
+ "component.unpublished": "Unpublished. No Bot can use it",
"component.draft_saved": "Draft saved, not yet published",
"component.refused": "Refused",
- "component.function_granted": "May read this",
- "component.function_revoked": "May no longer read this",
+ "component.function_granted": "Can read this",
+ "component.function_revoked": "Can no longer read this",
"component.function_called": "Read real data",
"component.function_refused": "Refused",
// A function failure is execution failure, not a policy refusal.
- "component.function_failed": "Could not be read",
+ "component.function_failed": "Couldn't read it",
// Not a call and not a decision: the tools this run was allowed to see. Worded so nobody reads it
// as permission, which it is not — everything named was already granted.
"mcp.tools_discovered": "Tools offered for one run",
"mcp.call_succeeded": "Called on this Bot's behalf",
"mcp.call_rejected": "Blocked",
- "mcp.call_failed": "The server did not answer",
+ "mcp.call_failed": "The server didn't answer",
// Not "Blocked": nothing about the Bot was judged, because nothing proved which Bot it was.
- "mcp.callback_refused": "Could not prove which Bot it was",
+ "mcp.callback_refused": "Couldn't confirm which Bot it was",
"configuration.changed": "Configuration changed",
"credential.created": "Credential saved",
diff --git a/app/src/routes/_authed/admin/boundaries.tsx b/app/src/routes/_authed/admin/boundaries.tsx
index 46d5f7351c..8906302fde 100644
--- a/app/src/routes/_authed/admin/boundaries.tsx
+++ b/app/src/routes/_authed/admin/boundaries.tsx
@@ -31,17 +31,17 @@ const PRESETS: { label: string; rule: string; cost?: string }[] = [
// flag that presses it once the text is in, and a rule naming only `computer_key` left that door
// open.
rule: '(intent == "activate" && contains(element.name, "submit")) || ((tool.name == "computer_key" || tool.name == "computer_type") && key == "Enter")',
- cost: "Also stops the Bot pressing Enter for anything else, because a form submits from Enter in any of its fields.",
+ cost: "Also stops the Bot pressing Enter anywhere else. A form can submit from Enter in any field.",
},
{
label: "Never type into a password field",
rule: 'intent == "type" && contains(element.name, "password")',
- cost: "A password box the page labels something else is not covered, the rule matches the label.",
+ cost: "A password box with a different label isn't covered. The rule matches the label.",
},
{
label: "Stay off social media",
rule: 'intent == "navigate" && (contains(page.host, "facebook.com") || contains(page.host, "x.com"))',
- cost: "Only the two hosts named. A link that redirects there from somewhere else is allowed.",
+ cost: "Only these two hosts. A link that redirects there from elsewhere is allowed.",
},
];
@@ -136,18 +136,18 @@ function BoundariesPage() {
- What every Bot may and may not do with its computer. Rules are checked
- on every action before it happens, and a refusal is recorded in{" "}
+ What a Bot can and can't do with its computer. Every action is checked
+ before it happens. Refusals show in{" "}
Audit
{" "}
- with the rule that refused it.
+ with the rule that refused them.
>
}
title="Boundaries"
>
@@ -170,31 +170,28 @@ function BoundariesPage() {
{policy.mode === "enforce"
? "The Bot is stopped and told which rule refused it."
- : "Nothing is stopped. Every action a rule matches is recorded as it would have been refused, which is how a rule is tried out before it is switched on."}
+ : "Nothing is stopped. Each match is recorded as if it had been refused."}
- Checked first, and a match ends it: nothing below is consulted and
- the Bot is told which rule refused it. Rules are CEL, and can ask
- about tool.name, intent,{" "}
- bot.id, actor.id, page.url{" "}
- and page.host, the element being acted on, the{" "}
- key being pressed, the file being touched, the{" "}
- command being run, and mcp.server,{" "}
- mcp.tool and mcp.effect for a call to
- somebody else’s tools. initiator.kind says what
- started the run — person, routine or{" "}
- handoff — which actor.id cannot,
- because a scheduled run carries its owner’s authority while
- nobody is watching. A rule that cannot be evaluated counts as a
- match, so a mistyped deny refuses rather than quietly permitting
- what it was meant to forbid.
+ Checked first. A match stops the action, and the Bot is told which
+ rule refused it. Rules are CEL. They can check{" "}
+ tool.name, intent, bot.id,{" "}
+ actor.id, page.url, page.host
+ , the element, the key pressed, the file, the{" "}
+ command run, and mcp.server,{" "}
+ mcp.tool and mcp.effect for outside tools.{" "}
+ initiator.kind says what started the run:{" "}
+ person, routine or handoff. A
+ scheduled run acts as its owner, so actor.id alone
+ can't tell you that. A rule that can't be evaluated counts as a
+ match. A typo never lets an action through.
>
}
- title="It may never"
+ title="Bots may never"
>
{policy.deny.length === 0 ? (
@@ -290,8 +287,8 @@ function BoundariesPage() {
{policy.allow.map((rule) => (
@@ -310,7 +307,7 @@ function BoundariesPage() {
) : saved ? (
"Saved. It applies to the next action any Bot takes."
) : (
- "Changes apply to the next action any Bot takes, and are kept: a restart comes back up enforcing what is here."
+ "Changes apply to the next action any Bot takes. They stay after a restart."
)}
@@ -327,8 +324,8 @@ function DryRunResult({ report }: { report: DryRunReport }) {
if (report.scanned === 0) {
return (
- No recorded computer actions to test against yet. The rule is valid;
- what it matches will only be known once Bots have acted.
+ No recorded computer actions to test against yet. The rule is valid.
+ You'll see what it matches once Bots have acted.
{report.wouldRefuse === 0
- ? `Tested against the last ${report.scanned} recorded actions: this rule would have refused none of them. It may still match future actions.`
- : `Tested against the last ${report.scanned} recorded actions: this rule would have refused ${report.wouldRefuse}.`}
+ ? `Tested on the last ${report.scanned} recorded actions. This rule would have refused none. It may still match future ones.`
+ : `Tested on the last ${report.scanned} recorded actions. This rule would have refused ${report.wouldRefuse}.`}
- Showing the first {report.changes.length}; the count above covers
- everything scanned.
+ Showing the first {report.changes.length}. The count above covers
+ everything checked.
) : null}
diff --git a/app/src/routes/_authed/admin/components/$name.tsx b/app/src/routes/_authed/admin/components/$name.tsx
index 86ab48129c..5651291600 100644
--- a/app/src/routes/_authed/admin/components/$name.tsx
+++ b/app/src/routes/_authed/admin/components/$name.tsx
@@ -68,7 +68,7 @@ import { queryClient } from "@/query-client";
*/
/** The same way back from every state this route can be in. */
const BACK = {
- label: "UI Components",
+ label: "UI components",
linkProps: { to: "/admin/components" },
} as const;
@@ -107,7 +107,7 @@ function RouteComponent() {
return (
- Could not load components.
+ Couldn't load components. Reload the page.
);
@@ -121,15 +121,14 @@ function RouteComponent() {
return (
{name}
- It may have been renamed, or this deployment may no longer ship
- it.
+ It may have been renamed, or this deployment no longer ships it.
fn.name).join(", ");
return (
@@ -311,7 +309,7 @@ function ComponentDetail({
@@ -328,7 +326,7 @@ function ComponentDetail({
Not in this build
- Nothing here can draw it, whatever else is set.
+ OpenBot can't draw it. The settings below have no effect.
@@ -344,10 +342,10 @@ function ComponentDetail({
Published
{component.published
- ? "Bots may answer with it."
- : "No Bot may use it."}
+ ? "Bots can answer with it."
+ : "No Bot can use it."}
{component.hasUnpublishedChanges
- ? " The description has changes that are not published."
+ ? " The description has unpublished changes."
: null}
@@ -385,7 +383,7 @@ function ComponentDetail({
}
- label="May read"
+ label="Can read"
onOpen={() => setSheet("functions")}
summary={functionSummary}
testId={`functions-${component.name}`}
@@ -430,8 +428,8 @@ function ComponentDetail({
Description
- What the model reads when deciding to call this. It changes
- nothing until the component is published.
+ What the model reads when it decides to use this. It has no effect
+ until the component is published.
@@ -475,16 +473,13 @@ function ComponentDetail({
Available to
- Switch a Bot off and it is never told this component exists, so it
- cannot ask for it and does not apologise for not having it. Each
- change takes effect immediately.
+ Turn a Bot off and it never learns this component exists. Changes
+ apply right away.
{bots.length === 0 ? (
-
- There are no Bots yet.
-
+
No Bots yet.
) : (
{bots.map((bot, index) => {
@@ -525,19 +520,18 @@ function ComponentDetail({
>
- May read
+ Can read
- A separate grant from Available to, and not implied by it: that
- one decides who may draw this, and this decides what it may go and
- fetch in order to draw itself. Until one of these is on it shows
- only what the Bot passes it, and every read it does make is a row
- in Audit. Each change takes effect immediately.
+ The data this component can fetch to draw itself. This is separate
+ from Available to. With all of these off, it shows only what the
+ Bot passes it. Every read shows in Audit. Changes apply right
+ away.
{dataFunctions.length === 0 ? (
- This deployment grants no data functions.
+ This deployment has no data functions.
) : (
diff --git a/app/src/routes/_authed/admin/components/index.tsx b/app/src/routes/_authed/admin/components/index.tsx
index 8abd0a4e1e..49382e7443 100644
--- a/app/src/routes/_authed/admin/components/index.tsx
+++ b/app/src/routes/_authed/admin/components/index.tsx
@@ -26,8 +26,8 @@ function RouteComponent() {
return (
{/*
* Pending, error, empty, rows — in that order. Pending draws nothing rather than a
@@ -36,14 +36,14 @@ function RouteComponent() {
*/}
{components.isPending ? null : components.error ? (
- Could not load components.
+ Couldn't load components. Reload the page.
) : components.data?.length === 0 ? (
No components yet
- Components that your Agents can use will be shown here.
+ Components your Bots can use will show here.
diff --git a/app/src/routes/_authed/admin/computers.tsx b/app/src/routes/_authed/admin/computers.tsx
index e2bc9e424e..fe314eee70 100644
--- a/app/src/routes/_authed/admin/computers.tsx
+++ b/app/src/routes/_authed/admin/computers.tsx
@@ -72,7 +72,9 @@ function ComputersPage() {
* A list that could not be read. A refused stop, reset or update is said beside the buttons
* instead, because the top of this page is a long scroll away from them.
*/
- const problem = fleet.error ? "The computers could not be listed." : null;
+ const problem = fleet.error
+ ? "Couldn't load the computers. Reload the page."
+ : null;
const hostProblem = hostAccess.error
? hostAccess.error.message
: requestGrant.error
@@ -92,7 +94,7 @@ function ComputersPage() {
return (
{problem ? (
@@ -109,14 +111,14 @@ function ComputersPage() {
Every Bot is sharing one computer.
{" "}
- They share its logins, its files and its session, so a Bot can reach
- what another signed into. Set COMPUTER_SUPERVISOR_URL to
- give each Bot its own.
+ They share its logins, files and session. A Bot can reach what another
+ signed into. Set COMPUTER_SUPERVISOR_URL to give each Bot
+ its own.
) : isolation === "per-bot" ? (
- Each Bot has a computer of its own: its own container, its own files
- and its own browser profile.
+ Each Bot has its own computer, with its own container, files and
+ browser profile.
) : null}
@@ -154,10 +156,10 @@ function ComputersPage() {
) : null}
{computers === null && problem ? (
- The list could not be loaded.
+ Couldn't load the list.
) : computers === null ? null : computers.length === 0 ? (
- No computers yet. One appears the first time a Bot opens a page.
+ No computers yet. One shows up the first time a Bot opens a page.
) : (
@@ -235,8 +237,8 @@ function ComputersPage() {
Reset {confirming ? nameFor(confirming) : ""}'s computer?
- Its profile is deleted, so the Bot is signed out of every service
- it had logged into and starts clean. This cannot be undone.
+ Its profile is deleted. The Bot is signed out of every service and
+ starts clean. You can't undo this.
@@ -274,9 +276,9 @@ function ComputersPage() {
computer?
- It moves onto the computer image this deployment now runs. Its
- files and sign-ins are kept. If it is running it restarts, which
- interrupts anything it is doing right now.
+ It moves to the computer image this deployment now runs. Its files
+ and sign-ins are kept. If it's running, it restarts and stops what
+ it's doing.
@@ -301,13 +303,12 @@ function ComputersPage() {
- Stop closes the browser and keeps its logins: the next
- thing the Bot does starts it again where it left off.{" "}
- Reset deletes the profile, so the Bot is signed out of
- everything and starts clean. Update computer appears
- when a computer runs an older image than this deployment, and moves it
- onto the new one with its files and sign-ins kept. All three are
- recorded in{" "}
+ Stop closes the browser and keeps its logins. It starts
+ again the next time the Bot needs it. Reset deletes the
+ profile and signs the Bot out of everything.{" "}
+ Update computer shows when a computer runs an older
+ image. It moves the computer to the new one and keeps its files and
+ sign-ins. All three show in{" "}
Audit
@@ -372,15 +373,13 @@ function HostFoldersSection({
{!connected && !loading ? (
- The desktop app is offline. Folders can only be approved from the
- desktop app, and command access stays unavailable while it is offline.
+ The desktop app is offline. Open it to approve folders or run
+ commands.
) : null}
{loading ? null : botIds.length === 0 ? (
-
- No Bots yet. Create a Bot before choosing folders.
-
+ No Bots yet. Create one, then choose its folders.
) : (
{botIds.map((botId, index) => (
@@ -418,8 +417,8 @@ function HostFoldersSection({
- Grants are read-only by default. Writes are requested from the native
- desktop prompt when a Bot needs them.
+ Folders are read-only. When a Bot needs to write, the desktop app asks
+ you first.
}
- description="Credentials are write-only. OpenBot never displays their secret values."
+ description="Keys and tokens for this deployment. OpenBot never shows a secret after it is saved."
title="Credentials"
>
{/*
@@ -108,7 +108,7 @@ function CredentialsPage() {
Add credential
- Held for this deployment and never shown again once saved.
+ Saved for this deployment. You won't see the secret again.
@@ -255,15 +255,15 @@ function CredentialsPage() {
{([kind, provider, keyId]) =>
liveCredentialFor(credentials.data, kind, provider, keyId) ? (
- This key already holds a live credential. Saving replaces
- it, and the one it replaces is revoked.
+ This key already has an active credential. Saving replaces
+ it and revokes the old one.
) : null
}
{createCredential.error ? (
- Could not save the credential. Try again.
+ Couldn't save the credential. Try again.
- Could not load credentials.
+ Couldn't load credentials. Reload the page.
) : credentials.data?.length === 0 ? (
- No credentials are configured.
+
+ No credentials yet. Add one with Add credential.
+
) : (
{credentials.data?.map((credential, index) => (
diff --git a/app/src/routes/_authed/admin/enterprise.tsx b/app/src/routes/_authed/admin/enterprise.tsx
index 2ac2408350..b17daeb225 100644
--- a/app/src/routes/_authed/admin/enterprise.tsx
+++ b/app/src/routes/_authed/admin/enterprise.tsx
@@ -108,7 +108,7 @@ function rulesToText(rules: EgressRule[]): string {
function summarizePolicy(policy: NetworkPolicy | undefined): string {
if (!policy)
- return "No policy: Bot computers may reach anything the cluster allows";
+ return "No policy. Bot computers can reach anything the cluster allows";
const count = policy.rules.length;
return `${MODE_NAMES[policy.mode]} · ${count} ${count === 1 ? "rule" : "rules"}${policy.locked ? " · locked" : ""}`;
}
@@ -123,7 +123,7 @@ function EnterprisePage() {
return (
{failure ? (
@@ -133,7 +133,7 @@ function EnterprisePage() {
) : null}
{overview.isPending ? null : overview.error ? (
- Could not load enterprise controls.
+ Couldn't load enterprise controls. Reload the page.
) : (
@@ -205,8 +205,8 @@ function EnterpriseSections({
Require SSO
{settings.ssoRequired
- ? "Only a registered SAML or OpenID Connect provider may sign anybody in."
- : "People may also sign in with Google, Microsoft or Okta."}
+ ? "People can only sign in through a registered SAML or OpenID Connect provider."
+ : "People can also sign in with Google, Microsoft or Okta."}
@@ -227,7 +227,7 @@ function EnterpriseSections({
SCIM provisioning
{data.scim.configured
- ? `Directory base URL ${data.scim.baseUrl ?? "(set BETTER_AUTH_URL)"} with the SCIM_BEARER_TOKEN bearer token. Deprovisioning ends sessions, deny-lists the address, retires connector credentials and stops their computers.`
+ ? `Directory base URL ${data.scim.baseUrl ?? "(set BETTER_AUTH_URL)"} with the SCIM_BEARER_TOKEN bearer token. Deprovisioning ends sessions, blocks the address, retires connector credentials and stops their computers.`
: "Off. Set SCIM_BEARER_TOKEN to accept provisioning from Okta, Entra or another directory."}
@@ -241,7 +241,7 @@ function EnterpriseSections({
@@ -258,7 +258,7 @@ function EnterpriseSections({
{effective
? capability.description
- : "Nobody may, unless a role or group grants it."}
+ : "Off for everybody, unless a role or group turns it on."}
@@ -289,7 +289,7 @@ function EnterpriseSections({
Role and group overrides
{overrideCount === 0
- ? "None: everybody gets the switches above"
+ ? "None. Everybody gets the switches above"
: `${overrideCount} set`}
@@ -301,7 +301,7 @@ function EnterpriseSections({
@@ -373,7 +373,7 @@ function EnterpriseSections({
@@ -386,7 +386,7 @@ function EnterpriseSections({
{settings.mcpAllowlist.enabled
? `${settings.mcpAllowlist.servers.length} approved`
- : "Off: every registered server may be used as its grants allow."}
+ : "Off. Bots can use any registered server they have access to."}
@@ -441,7 +441,7 @@ function EnterpriseSections({
@@ -453,8 +453,8 @@ function EnterpriseSections({
Model allowlist
{settings.modelAllowlist.enabled
- ? "A built-in Bot on a model not listed is refused before it runs."
- : "Off: any model may serve a run."}
+ ? "A built-in Bot on an unlisted model is refused before it runs."
+ : "Off. Any model can be used."}
@@ -498,7 +498,7 @@ function EnterpriseSections({
@@ -510,8 +510,8 @@ function EnterpriseSections({
Action Recording
{settings.actionRecording
- ? `Shell commands Bots run are recorded with secrets scrubbed and kept ${data.actionRecordRetentionDays} days.`
- : "Off: commands are on the audit trail only."}
+ ? `Shell commands Bots run are recorded with secrets removed, and kept ${data.actionRecordRetentionDays} days.`
+ : "Off. Commands show in Audit only."}
@@ -535,7 +535,7 @@ function EnterpriseSections({
{data.otel.configured
? "Exporting over OTLP/HTTP."
- : "Not configured. Set OTEL_EXPORTER_OTLP_ENDPOINT on the server."}
+ : "Not set up. Set OTEL_EXPORTER_OTLP_ENDPOINT on the server."}
@@ -549,7 +549,7 @@ function EnterpriseSections({
@@ -614,13 +614,11 @@ function ModelUsage() {
if (usage.error)
return (
- Could not load model usage.
+ Couldn't load model usage. Reload the page.
);
if (usage.data.summary.length === 0)
- return (
- No Bot runs have been recorded in the last 30 days.
- );
+ return No Bot runs in the last 30 days.;
return (
{usage.data.summary.map((row, index) => (
@@ -657,7 +655,7 @@ function RecordedActions() {
if (actions.error)
return (
- Could not load recorded actions.
+ Couldn't load recorded actions. Reload the page.
{scope?.kind === "group"
- ? "A group can only widen: Grant gives its members the capability even where the organization has it off."
- : "A role's answer replaces the organization switch for everybody holding that role."}
+ ? "A group can only add. Grant gives its members the capability even when the organization has it off."
+ : "A role's setting replaces the organization switch for everybody with that role."}
- One per line. A domain covers its subdomains; *.name covers only
- subdomains. An IP range may name ports. Defaults are{" "}
+ One per line. A domain covers its subdomains. *.name covers only
+ subdomains. An IP range can list ports. Defaults are{" "}
{data.defaultDestinations.slice(0, 4).join(", ")} and{" "}
{data.defaultDestinations.length - 4} more package and source
hosts.
@@ -951,7 +949,7 @@ function NetworkDialog({
{initial.scopeKind === "organization" ? (
}
- description="A company's own identity provider, by SAML or OpenID Connect. Somebody types their email address and the domain decides which one they are sent to."
+ description="Your company's own SAML or OpenID Connect provider. People sign in with their email, and its domain picks the provider."
title="Identity providers"
>
{failure ? (
@@ -119,12 +119,12 @@ function IdentityProvidersPage() {
) : null}
{providers.isPending ? null : providers.error ? (
- Could not load identity providers.
+ Couldn't load identity providers. Reload the page.
) : providers.data?.length === 0 ? (
- No identity providers are registered. Add one with the metadata your
- identity team supplied.
+ No identity providers yet. Add one with the metadata from your
+ identity team.
) : (
@@ -211,8 +211,8 @@ function IdentityProvidersPage() {
/>
{/* Said out loud because it is the field that decides who this applies to. */}
- Anybody signing in with an address at this domain is sent
- here. Separate several with commas.
+ Anybody with an address at this domain signs in here. Separate
+ several with commas.
@@ -259,8 +259,8 @@ function IdentityProvidersPage() {
value={draft.metadata}
/>
- The document your identity team supplied. It carries the
- signing certificate.
+ The file from your identity team. It includes the signing
+ certificate.
>
diff --git a/app/src/routes/_authed/admin/index.tsx b/app/src/routes/_authed/admin/index.tsx
index d80c2831c3..440fbecfac 100644
--- a/app/src/routes/_authed/admin/index.tsx
+++ b/app/src/routes/_authed/admin/index.tsx
@@ -54,30 +54,29 @@ const SECTIONS: {
}[] = [
{
title: "What Bots can reach",
- description:
- "Everything a Bot can touch outside this app, and the limits on it.",
+ description: "What a Bot can touch outside OpenBot, and the limits on it.",
items: [
{
title: "Credentials",
- description: "Keys and tokens held for this deployment.",
+ description: "Keys and tokens for this deployment.",
icon: IconKey,
linkOptions: { to: "/admin/credentials" },
},
{
title: "Boundaries",
- description: "Rules that decide what a Bot may never do.",
+ description: "Things no Bot may ever do.",
icon: IconShieldCheck,
linkOptions: { to: "/admin/boundaries" },
},
{
title: "Approvals",
- description: "What every member's Bots must ask before acting.",
+ description: "What members' Bots must ask before they act.",
icon: IconChecks,
linkOptions: { to: "/admin/approvals" },
},
{
title: "Computers",
- description: "The machines Bots run their tools on.",
+ description: "The machines Bots run tools on.",
icon: IconDeviceDesktop,
linkOptions: { to: "/admin/computers" },
},
@@ -85,30 +84,30 @@ const SECTIONS: {
},
{
title: "What Bots can do",
- description: "Capabilities and interface pieces available across Bots.",
+ description: "Apps, skills and interface pieces every Bot can use.",
items: [
{
title: "Plugins",
description:
- "The services this deployment can reach, and which Bots may.",
+ "The services OpenBot can reach, and which Bots can use them.",
icon: IconPuzzle,
linkOptions: { to: "/admin/plugins" },
},
{
title: "Skills",
- description: "Named instructions anybody can invoke with a slash.",
+ description: "Named instructions anybody can run with /.",
icon: IconFileText,
linkOptions: { to: "/admin/skills" },
},
{
- title: "Automatic Learning",
+ title: "Automatic learning",
description:
- "Learn from completed conversations and review proposed skills.",
+ "Learn from finished conversations and review the skills it proposes.",
icon: IconFileText,
linkOptions: { to: "/admin/learning" },
},
{
- title: "UI Components",
+ title: "UI components",
description: "Custom pieces a Bot can draw in a conversation.",
icon: IconLayoutGrid,
linkOptions: { to: "/admin/components" },
@@ -128,21 +127,21 @@ const SECTIONS: {
{
title: "People",
description:
- "Everybody who has signed in, who administers this deployment, and whose access has been removed.",
+ "Everybody who has signed in, who the admins are, and whose access was removed.",
icon: IconUsers,
linkOptions: { to: "/admin/people" },
},
{
title: "Enterprise controls",
description:
- "Capabilities by role and group, SSO required, SCIM, network policy, MCP and model allowlists, Action Recording.",
+ "Roles and groups, required SSO, SCIM, network policy, MCP and model allowlists, Action Recording.",
icon: IconShieldCheck,
linkOptions: { to: "/admin/enterprise" },
},
{
title: "Identity providers",
description:
- "A company's own SAML or OpenID Connect provider, routed by email domain.",
+ "Your company's SAML or OpenID Connect provider, matched by email domain.",
icon: IconBuildingBank,
linkOptions: { to: "/admin/identity-providers" },
},
@@ -154,7 +153,7 @@ const SECTIONS: {
items: [
{
title: "Audit",
- description: "Every action taken in this deployment, and by whom.",
+ description: "Every action taken in OpenBot, and who took it.",
icon: IconListDetails,
linkOptions: { to: "/admin/audit" },
},
@@ -165,7 +164,7 @@ const SECTIONS: {
function RouteComponent() {
return (
{SECTIONS.map((section) => (
diff --git a/app/src/routes/_authed/admin/learning.tsx b/app/src/routes/_authed/admin/learning.tsx
index 19a82e05bd..c5094d6342 100644
--- a/app/src/routes/_authed/admin/learning.tsx
+++ b/app/src/routes/_authed/admin/learning.tsx
@@ -35,13 +35,13 @@ function automaticMessage(
automatic.state === "assigned" &&
settings.defaultTarget?.containerId === automatic.containerId
)
- return `OpenBot assigned this Intelligence project's "${automatic.containerId}" container automatically. Change it here at any time.`;
+ return `OpenBot assigned this Intelligence project's "${automatic.containerId}" container automatically. You can change it here.`;
if (automatic.state === "absent")
- return `This Intelligence project has no "${automatic.containerId}" Learning container yet, so nothing is collected.`;
+ return `This Intelligence project has no "${automatic.containerId}" Learning container yet. Nothing is collected.`;
if (automatic.state === "not_entitled")
- return "This Intelligence project's plan does not include Learning, so no container was assigned.";
+ return "This Intelligence project's plan doesn't include Learning. No container was assigned.";
if (automatic.state === "unavailable")
- return "OpenBot could not ask Intelligence for this project's Learning container. It tries again.";
+ return "Couldn't reach Intelligence to find this project's Learning container. OpenBot will try again.";
return undefined;
}
@@ -127,8 +127,7 @@ function ContainerSetup({
: `Create the ${automatic.containerId} container`}
- Uses the credential this deployment was given, never the runtime
- key.
+ Uses this deployment's credential, not the runtime key.
@@ -224,7 +223,7 @@ export function LearningPage() {
change({ ...settings, enabled: event.target.checked })
}
/>
- Enable Automatic Learning and skill delivery
+ Turn on automatic learning and skill delivery
)}
- An empty default leaves unmapped Bots out. IDs use 1–64 lowercase
- letters, numbers, and single hyphens. Leave revision empty to
- follow the latest published skills.
+ With no default, Bots without their own container are left out.
+ IDs are 1 to 64 lowercase letters, numbers and single hyphens.
+ Leave revision empty to use the latest published skills.
diff --git a/app/src/routes/_authed/admin/people.tsx b/app/src/routes/_authed/admin/people.tsx
index 6b008633aa..ea00dfccb9 100644
--- a/app/src/routes/_authed/admin/people.tsx
+++ b/app/src/routes/_authed/admin/people.tsx
@@ -58,7 +58,7 @@ function describe(person: Person): string {
if (person.revoked)
return `Access removed · ${providers || "no provider"} · ${when}`;
if (person.configuredAdmin) {
- return `Administrator by configuration · ${when}`;
+ return `Admin by configuration · ${when}`;
}
return `${providers || "no provider"} · ${when}`;
}
@@ -89,11 +89,11 @@ function PeoplePage() {
return (
{failure ? (
@@ -106,7 +106,7 @@ function PeoplePage() {
Stopped {terminate.data.stopped.length}{" "}
{terminate.data.stopped.length === 1 ? "computer" : "computers"}
{terminate.data.failed.length > 0
- ? `; ${terminate.data.failed.length} could not be stopped`
+ ? `. ${terminate.data.failed.length} couldn't be stopped`
: ""}
. Their disks are kept.
@@ -125,13 +125,13 @@ function PeoplePage() {
{people.isPending ? null : people.error ? (
- Could not load people.
+ Couldn't load people. Reload the page.
) : rows.length === 0 ? (
{query
- ? `Nobody here matches "${query}".`
- : "Nobody has signed in yet. People appear here once they do."}
+ ? `Nobody matches "${query}".`
+ : "Nobody has signed in yet. People show here once they do."}
) : (
@@ -186,7 +186,7 @@ function PeoplePage() {
{person.revoked ? "Restore" : "Remove"}
diff --git a/app/src/routes/_authed/admin/playground.tsx b/app/src/routes/_authed/admin/playground.tsx
index dbfeebb01e..878bc2a9c2 100644
--- a/app/src/routes/_authed/admin/playground.tsx
+++ b/app/src/routes/_authed/admin/playground.tsx
@@ -207,9 +207,8 @@ function PlaygroundPage() {
Playground
- Write a component here and publish it without a deployment. What
- you edit is a draft; a conversation only ever draws what is
- published.
+ Write a component and publish it without redeploying. Your edits
+ are a draft. Conversations only show what is published.
{validation.ok
- ? "The sample arguments are not available."
+ ? "The sample arguments aren't available."
: (validation.fields.sampleArguments ??
"Fix the JSON fields before previewing.")}
@@ -335,7 +334,7 @@ function PlaygroundPage() {
{(components ?? []).length === 0 ? (
- Nothing yet.
+ Nothing saved yet. Save a draft to see it here.
) : (
@@ -378,9 +377,8 @@ function PlaygroundPage() {
)}
- Publishing makes it available to every Bot. Switch it off for a
- particular Bot on the Components page, the same as for a component
- this build ships.
+ Publishing makes it available to every Bot. Turn it off for one
+ Bot on the UI components page.
@@ -400,8 +398,8 @@ function PlaygroundPage() {
Delete {deleting}?
- It is removed from this deployment. Any Bot that could draw it no
- longer can, and this cannot be undone.
+ It's removed from this deployment, and no Bot can draw it. You
+ can't undo this.
diff --git a/app/src/routes/_authed/admin/plugins/$key.tsx b/app/src/routes/_authed/admin/plugins/$key.tsx
index 56a4ebbda2..4d780e42e0 100644
--- a/app/src/routes/_authed/admin/plugins/$key.tsx
+++ b/app/src/routes/_authed/admin/plugins/$key.tsx
@@ -198,10 +198,8 @@ function ConnectionStateUnreadable() {
Your account
- Whether you have connected an account here could not be loaded, so
- nothing about it is shown rather than something that may be wrong.
- Reload the page, and check this deployment’s logs if it
- persists.
+ Couldn't load whether you've connected an account here. Reload the
+ page, or check this deployment's logs if it keeps happening.
@@ -323,7 +321,7 @@ function RouteComponent() {
*/
if (authorizationUrl === null) {
setError(
- "This deployment answered without a consent link, so there was nowhere to send you and nothing was connected. Try again, and check this connector's setup if it persists.",
+ "OpenBot got no consent link back, so nothing was connected. Try again, or check this connector's setup if it keeps happening.",
);
return;
}
@@ -514,7 +512,7 @@ function RouteComponent() {
title="Plugins"
>
- Plugins could not be loaded.
+ Couldn't load plugins. Reload the page.
);
@@ -523,7 +521,7 @@ function RouteComponent() {
return (
Nothing to configure.
@@ -574,8 +572,8 @@ function RouteComponent() {
Enable for this deployment
{server
- ? "Bots may be granted its tools. Switching this off removes it and every grant on its tools."
- : "No Bot can reach this vendor. Switch it on to configure it and grant its tools."}
+ ? "Bots can be given its tools. Turning this off removes it and every Bot's access to its tools."
+ : "No Bot can reach this vendor. Turn it on to set it up and give Bots its tools."}
@@ -652,13 +650,13 @@ function RouteComponent() {
@@ -695,8 +693,8 @@ function RouteComponent() {
Connection
- Nothing to configure. These tools run inside this
- deployment, on the tables it already owns.
+ Nothing to set up. These tools run inside this deployment,
+ on its own data.
@@ -743,7 +741,7 @@ function RouteComponent() {
- {server?.hasCredential ? "Held" : "Not set"}
+ {server?.hasCredential ? "Saved" : "Not set"}
@@ -782,8 +780,8 @@ function RouteComponent() {
OAuth client
- Identifies this deployment to the vendor. It reaches
- nobody's documents on its own.
+ Identifies this deployment to the vendor. On its own, it
+ can't reach anyone's documents.
@@ -831,8 +829,8 @@ function RouteComponent() {
) : shared ? (
Your account
{youConnected
- ? `Connected, so a Bot granted these tools uses your ${title} as you. Everybody else connects their own.`
+ ? `Connected. A Bot with these tools uses your ${title} as you. Everybody else connects their own.`
: "Connect your own account to try this connector. Setup is complete without it, and it reaches your documents only."}
@@ -928,8 +926,8 @@ function RouteComponent() {
Instance host
- This vendor gives every customer their own hostname,
- checked against its pattern before anything is stored.
+ This vendor gives every customer their own hostname. It is
+ checked against the vendor's pattern before it is saved.
@@ -975,20 +973,20 @@ function RouteComponent() {
{server?.dynamicClient ? (
- The deployment registers its redirect URI itself, so there is
+ The deployment registers its redirect URI itself. There is
nothing to add at the vendor.
) : (
Add this to the client's authorised redirect URIs at the
- vendor, exactly as written. A single wrong character fails
- there, with a message that does not mention OpenBot.
+ vendor, exactly as written. One wrong character fails there,
+ with an error that doesn't mention OpenBot.
)}
{!plugins.data?.redirectUri ? (
- This deployment has no public URL, so nobody can complete a
- consent flow. Set OPENBOT_PUBLIC_URL.
+ This deployment has no public URL, so nobody can finish
+ connecting. Set OPENBOT_PUBLIC_URL.
) : server?.dynamicClient ? null : (
/* Selectable and monospaced: it is copied by hand into somebody else's console. */
@@ -1040,14 +1038,14 @@ function RouteComponent() {
) : null}
}
- description="A Bot is told about a tool only when it holds it. Every call is decided again when it happens, so removing a grant takes effect on the next one."
+ description="A Bot only sees the tools it has been given. Each call is checked when it happens, so removing access applies from the next call."
title="Tools"
>
{server.tools.length === 0 ? (
{server.lastError ??
- "No tools listed. Refresh to ask the vendor again."}
+ "No tools listed. Refresh tools to check again."}
) : (
@@ -1132,8 +1130,8 @@ function RouteComponent() {
{bot.name}
{bot.hidden
- ? "Hidden from your roster. Every action this app offers, switched one at a time."
- : "Every action this app offers, switched one at a time."}
+ ? "Hidden from your roster. Turn this app's actions on or off one at a time."
+ : "Turn this app's actions on or off one at a time."}
@@ -1174,8 +1172,8 @@ function RouteComponent() {
*/}
{server && server.withdrawn.length > 0 ? (
{server.withdrawn.map((held, index) => (
@@ -1219,10 +1217,10 @@ function RouteComponent() {
{dialog === "client"
- ? "From the vendor's console. The secret is stored in this deployment's vault and never read back."
+ ? "From the vendor's console. The secret is saved in this deployment's vault and never shown again."
: dialog === "instance"
- ? "Your own hostname with this vendor. It is checked against their pattern before anything is stored."
- : "Stored in this deployment's vault and never read back."}
+ ? "Your own hostname with this vendor. It is checked against the vendor's pattern before it is saved."
+ : "Saved in this deployment's vault and never shown again."}
@@ -1319,8 +1317,8 @@ function RouteComponent() {
Grant tools
- Each grant is its own entry on the audit trail, and a granted
- write is still checked against the boundaries on every call.
+ Each grant is its own row in Audit. Tools that change things are
+ still checked against the boundaries on every call.
@@ -1470,13 +1468,13 @@ function RouteComponent() {
) : null}
setDialog(null)} size="sm" variant="ghost">
diff --git a/app/src/routes/_authed/admin/plugins/$key_.bots.$agentId.tsx b/app/src/routes/_authed/admin/plugins/$key_.bots.$agentId.tsx
index e61b099d6b..b2a0ba6036 100644
--- a/app/src/routes/_authed/admin/plugins/$key_.bots.$agentId.tsx
+++ b/app/src/routes/_authed/admin/plugins/$key_.bots.$agentId.tsx
@@ -205,10 +205,10 @@ function RouteComponent() {
return (
- There is nothing here to grant.
+ Nothing to give a Bot here.
);
}
@@ -227,7 +227,7 @@ function RouteComponent() {
title="Plugins"
>
- Plugins could not be loaded.
+ Couldn't load plugins. Reload the page.
);
@@ -249,11 +249,11 @@ function RouteComponent() {
return (
- It may have been deleted since this page was opened.
+ It may have been deleted after you opened this page.
);
@@ -335,7 +335,7 @@ function RouteComponent() {
{tool.name}
- {tool.description || "This action came with no description."}
+ {tool.description || "This action has no description."}
@@ -409,7 +409,7 @@ function RouteComponent() {
return (
{error ? (
@@ -423,7 +423,7 @@ function RouteComponent() {
{server.tools.length === 0 ? (
{server.lastError ??
- `${appTitle} lists no actions. Refresh its tools to ask again.`}
+ `${appTitle} lists no actions. Refresh its tools to check again.`}
) : (
<>
@@ -446,7 +446,7 @@ function RouteComponent() {
{ungranted.length > 0 ? (
- {`This Bot would then hold ${wouldHold} ${
+ {`This Bot would then have ${wouldHold} ${
wouldHold === 1 ? "tool" : "tools"
} from this app.`}
@@ -468,7 +468,7 @@ function RouteComponent() {
@@ -476,7 +476,7 @@ function RouteComponent() {
{needle
? `Nothing that only reads matches "${search.trim()}".`
- : `${appTitle} offers nothing that only reads.`}
+ : `${appTitle} has no read-only actions.`}
) : (
rows(reads)
@@ -486,7 +486,7 @@ function RouteComponent() {
@@ -494,7 +494,7 @@ function RouteComponent() {
{needle
? `Nothing that changes things matches "${search.trim()}".`
- : `${appTitle} offers nothing that changes anything.`}
+ : `${appTitle} has no actions that change things.`}
) : (
rows(writes)
@@ -544,7 +544,7 @@ function RouteComponent() {
onClick={() => void confirmSharedGrant()}
size="sm"
>
- {grantingShared ? "Granting…" : "Let it in"}
+ {grantingShared ? "Allowing…" : "Allow"}
diff --git a/app/src/routes/_authed/admin/plugins/$key_.tools.$tool.tsx b/app/src/routes/_authed/admin/plugins/$key_.tools.$tool.tsx
index 9dcb0e203f..952462b175 100644
--- a/app/src/routes/_authed/admin/plugins/$key_.tools.$tool.tsx
+++ b/app/src/routes/_authed/admin/plugins/$key_.tools.$tool.tsx
@@ -93,7 +93,7 @@ function RouteComponent() {
return (
- Plugins could not be loaded.
+ Couldn't load plugins. Reload the page.
);
@@ -103,7 +103,7 @@ function RouteComponent() {
return (
{/*
@@ -112,8 +112,8 @@ function RouteComponent() {
*/}
{server
- ? "It may have been withdrawn since the tool list was last refreshed."
- : "This deployment has not enabled that connector."}
+ ? "It may have been removed since the tool list was last refreshed."
+ : "This connector isn't turned on."}
);
@@ -137,7 +137,7 @@ function RouteComponent() {
return (
{error ? (
@@ -149,8 +149,8 @@ function RouteComponent() {
@@ -164,8 +164,8 @@ function RouteComponent() {
Effect
- Decided by the connector, not by the tool's name. Anything
- unrecognised counts as a write.
+ Set by the connector, not the tool's name. Anything unknown
+ counts as a write.
@@ -184,14 +184,11 @@ function RouteComponent() {
{bots.length === 0 ? (
-
- This deployment has no Bots yet, so there is nobody to grant this
- to.
-
+ No Bots yet. Create one to give it this tool.
) : (
{bots.map((bot, index) => {
@@ -205,9 +202,9 @@ function RouteComponent() {
{bot.hidden ? "Hidden from your roster. " : ""}
{held
? canCallBack(bot)
- ? "May call this tool. Every call is still checked against the boundaries and written to the audit trail."
- : "Granted, but this Bot has no credential for calling tools back, so every call is refused before it reaches the boundary. Issue one on its own page, or set AGENT_TOOL_TOKEN for the deployment."
- : "Cannot call this tool. It is not offered to the model at all, so it has nothing to refuse."}
+ ? "Can call this tool. Every call is still checked against boundaries and logged in Audit."
+ : "Allowed, but this Bot has no callback token. Every call is refused. Issue one on its page, or set AGENT_TOOL_TOKEN."
+ : "Can't call this tool. The Bot never sees it."}
diff --git a/app/src/routes/_authed/admin/plugins/arcade.tsx b/app/src/routes/_authed/admin/plugins/arcade.tsx
index 4fd37bbf3f..75bd361faa 100644
--- a/app/src/routes/_authed/admin/plugins/arcade.tsx
+++ b/app/src/routes/_authed/admin/plugins/arcade.tsx
@@ -37,8 +37,7 @@ function RouteComponent() {
if (plugins.isError) {
return (
- This deployment's plugin settings could not be read, so nothing was
- shown.
+ Couldn't load the plugin settings. Reload the page.
);
}
diff --git a/app/src/routes/_authed/admin/plugins/composio.tsx b/app/src/routes/_authed/admin/plugins/composio.tsx
index 4a738499eb..a6832fcf3c 100644
--- a/app/src/routes/_authed/admin/plugins/composio.tsx
+++ b/app/src/routes/_authed/admin/plugins/composio.tsx
@@ -37,8 +37,7 @@ function RouteComponent() {
if (plugins.isError) {
return (
- This deployment's plugin settings could not be read, so nothing was
- shown.
+ Couldn't load the plugin settings. Reload the page.
- Plugins could not be loaded.
+ Couldn't load plugins. Reload the page.
) : (
<>
@@ -156,12 +156,12 @@ function RouteComponent() {
{plugins.data?.servers.length === 0 ? (
- Nothing connected yet. Everything available is below.
+ Nothing connected yet. Pick one from the list below.
) : (
@@ -227,11 +227,11 @@ function RouteComponent() {
{explore.length === 0 ? (
- Everything in the catalogue is connected.
+ Every plugin in the catalogue is connected.
) : (
{explore.map((entry: CatalogueItem, index) => {
diff --git a/app/src/routes/_authed/admin/skills.tsx b/app/src/routes/_authed/admin/skills.tsx
index b02cf55a1d..34ca6d93fc 100644
--- a/app/src/routes/_authed/admin/skills.tsx
+++ b/app/src/routes/_authed/admin/skills.tsx
@@ -94,7 +94,7 @@ function RouteComponent() {
Write a skill
}
- description="Named instructions anybody here can invoke with a slash. A skill adds no capability: it can only ask a Bot to use what that Bot already holds, and every one of those calls is still decided and recorded."
+ description="Named instructions anybody can run with /. A skill adds no access. It can only ask a Bot to use what it already has."
title="Skills"
>
{error ? (
@@ -104,16 +104,16 @@ function RouteComponent() {
) : null}
{/* A read that failed says so; `isPending` goes false on it too, and it is not "none". */}
{plugins.isPending ? null : !plugins.data ? (
- Skills could not be loaded.
+ Couldn't load skills. Reload the page.
) : skills.length === 0 ? (
- No skills yet.
+ No skills yet. Write one with Write a skill.
) : (
{skills.map((skill, index) => (
@@ -178,11 +178,11 @@ function RouteComponent() {
@@ -81,7 +81,7 @@ function ComputerUseStep() {
return (
- Each agent has its own computer
+ Each Bot has its own computer
- Choose from a variety of agents or create your own
+ Pick a Bot or make your own
- Your own agent
+ Your own Bot
@@ -210,8 +210,7 @@ function AgentsStep() {
Bring the agents you already have
- Any agent that speaks AG-UI joins as a Bot. Add one any time from Bots,
- then New Bot.
+ Any AG-UI agent can join as a Bot. Add one from Bots, then New Bot.
- One container image, or a Helm chart for EKS, GKE and AKS, with sign-in
- through Google, Microsoft or Okta.
+ Run one container image, or a Helm chart for EKS, GKE or AKS. Your
+ people sign in with Google, Microsoft or Okta.
diff --git a/app/src/routes/_authed/settings/components-gallery/$name.tsx b/app/src/routes/_authed/settings/components-gallery/$name.tsx
index 6c2c7b1e52..ab9297530a 100644
--- a/app/src/routes/_authed/settings/components-gallery/$name.tsx
+++ b/app/src/routes/_authed/settings/components-gallery/$name.tsx
@@ -78,7 +78,7 @@ function RouteComponent() {
return (
- Could not load components.
+ Couldn't load components. Reload the page.
);
@@ -96,15 +96,14 @@ function RouteComponent() {
return (
{name}
- It may have been withdrawn, or this deployment may no longer ship
- it.
+ It may have been removed or unpublished.
{components.isPending ? null : components.error ? (
- Could not load components.
+ Couldn't load components. Reload the page.
) : published?.length === 0 ? (
Nothing published yet
- When an administrator publishes a component, it will show up here.
+ When an admin publishes a component, it will show up here.
diff --git a/app/src/routes/_authed/settings/index.tsx b/app/src/routes/_authed/settings/index.tsx
index f506b44e9e..288ed9b779 100644
--- a/app/src/routes/_authed/settings/index.tsx
+++ b/app/src/routes/_authed/settings/index.tsx
@@ -36,7 +36,7 @@ function RouteComponent() {
*/
return (
diff --git a/app/src/routes/_authed/settings/notifications.tsx b/app/src/routes/_authed/settings/notifications.tsx
index 6a7a8de51c..70b1ece24f 100644
--- a/app/src/routes/_authed/settings/notifications.tsx
+++ b/app/src/routes/_authed/settings/notifications.tsx
@@ -11,7 +11,7 @@ export const Route = createFileRoute("/_authed/settings/notifications")({
function NotificationsPage() {
return (
diff --git a/app/src/routes/_authed/settings/passwords.tsx b/app/src/routes/_authed/settings/passwords.tsx
index 151070fc9e..44b744afe6 100644
--- a/app/src/routes/_authed/settings/passwords.tsx
+++ b/app/src/routes/_authed/settings/passwords.tsx
@@ -37,17 +37,17 @@ function PasswordsPage() {
return (
{logins.isPending ? null : logins.error ? (
- Could not load your passwords.
+ Couldn't load your passwords. Reload the page.
) : !logins.data.passwordManager ? (
- Saved passwords are turned off for this workspace. Existing logins
- are kept but not offered.
+ An admin has turned off saved passwords. Your logins are kept but
+ won't be offered.
) : logins.data.logins.length === 0 ? (
@@ -74,7 +74,7 @@ function PasswordsPage() {
diff --git a/app/src/routes/sign.tsx b/app/src/routes/sign.tsx
index b83b2ec06b..92697d02f2 100644
--- a/app/src/routes/sign.tsx
+++ b/app/src/routes/sign.tsx
@@ -104,7 +104,7 @@ function SignScreen() {
setError(
caughtError instanceof Error
? caughtError.message
- : "No identity provider is registered for that address.",
+ : "No company sign-in is set up for that address.",
);
setOpening(null);
}
@@ -124,7 +124,7 @@ function SignScreen() {
setError(
caughtError instanceof Error
? caughtError.message
- : `Could not start ${providerName(provider)} sign-in.`,
+ : `Couldn't open ${providerName(provider)} sign-in. Try again.`,
);
setOpening(null);
}
@@ -205,14 +205,15 @@ function SignScreen() {
>
{options?.ssoRequired ? (
- Your organization requires single sign-on.
+ Your organization requires you to sign in with your company
+ account.
diff --git a/app/tests/account-mode-dialog.test.tsx b/app/tests/account-mode-dialog.test.tsx
index 6941fc23d1..3f8cdb9e53 100644
--- a/app/tests/account-mode-dialog.test.tsx
+++ b/app/tests/account-mode-dialog.test.tsx
@@ -87,7 +87,7 @@ describe("switching an app to Shared", () => {
).toBeTruthy();
expect(
view.getByText(
- "Everyone who can use a Bot granted this app will act as this one account.",
+ "Everyone using a Bot with this app will act as this one account.",
),
).toBeTruthy();
expect(view.getByText(/triage/)).toBeTruthy();
diff --git a/app/tests/admin-brokered-row-provider.test.tsx b/app/tests/admin-brokered-row-provider.test.tsx
index ab8bc2f302..acc104d3b7 100644
--- a/app/tests/admin-brokered-row-provider.test.tsx
+++ b/app/tests/admin-brokered-row-provider.test.tsx
@@ -167,9 +167,9 @@ function everythingRendered(view: ReturnType): string {
const WAIT_FOR_ROW = /^Set [A-Z]+_API_KEY on this deployment\./;
const COMPOSIO_KEY_MISSING =
- "Set COMPOSIO_API_KEY on this deployment. Without it there is no broker to reach, so this account can be neither connected nor ended from here. The app stays enabled and every grant on its tools still stands.";
+ "Set COMPOSIO_API_KEY on this deployment. Until then, this account can't be connected or disconnected here. The app stays turned on, and its tool grants still stand.";
const ARCADE_KEY_MISSING =
- "Set ARCADE_API_KEY on this deployment. Without it there is no broker to reach, so this account can be neither connected nor ended from here. The app stays enabled and every grant on its tools still stands.";
+ "Set ARCADE_API_KEY on this deployment. Until then, this account can't be connected or disconnected here. The app stays turned on, and its tool grants still stand.";
for (const shared of [false, true]) {
const row = shared ? "team row" : "personal row";
diff --git a/app/tests/agent-roster-error.test.tsx b/app/tests/agent-roster-error.test.tsx
index cf0ff05be6..169a5b6123 100644
--- a/app/tests/agent-roster-error.test.tsx
+++ b/app/tests/agent-roster-error.test.tsx
@@ -142,20 +142,18 @@ test("a failed roster on / reports the failure and explains the disabled compose
expect(
await view.findByText(
- "Agents shared with you couldn't be loaded.",
+ "Couldn't load the Bots shared with you.",
{},
HOME_FIND_TIMEOUT,
),
).toBeTruthy();
- expect(
- view.queryByText("Nobody has shared an agent with you yet."),
- ).toBeNull();
+ expect(view.queryByText("Nobody has shared a Bot with you yet.")).toBeNull();
// The composer goes `disabled={!fallback}` on the very same failure, with nothing on screen
// saying why unless this alert renders.
expect(
await view.findByText(
- "Your coworkers couldn't be loaded, so there's no one to send this to yet.",
+ "Couldn't load your Bots, so there's no one to send this to. Reload the page.",
{},
HOME_FIND_TIMEOUT,
),
@@ -181,18 +179,18 @@ test("a failed REFETCH on / keeps the roster and does not disclaim the composer"
// evidence that the composer is not the disabled, nothing-to-send-to state its alert describes.
expect(
await view.findByText(
- "Sent to the coworker it is for.",
+ "OpenBot picks the right Bot.",
{ exact: false },
HOME_FIND_TIMEOUT,
),
).toBeTruthy();
expect(
view.queryByText(
- "Your coworkers couldn't be loaded, so there's no one to send this to yet.",
+ "Couldn't load your Bots, so there's no one to send this to. Reload the page.",
),
).toBeNull();
expect(
- view.queryByText("Agents shared with you couldn't be loaded."),
+ view.queryByText("Couldn't load the Bots shared with you."),
).toBeNull();
});
@@ -213,19 +211,19 @@ test("a failed REFETCH on / with explore empty shows it as empty, not broken", a
expect(
await view.findByText(
- "Nobody has shared an agent with you yet.",
+ "Nobody has shared a Bot with you yet.",
{},
HOME_FIND_TIMEOUT,
),
).toBeTruthy();
expect(
- view.queryByText("Agents shared with you couldn't be loaded."),
+ view.queryByText("Couldn't load the Bots shared with you."),
).toBeNull();
// `agents` loaded (it holds "Mine Agent"), so `fallback` falls back to it and the composer is
// enabled — the alert claiming a load failure must not appear beside that working composer.
expect(
view.queryByText(
- "Your coworkers couldn't be loaded, so there's no one to send this to yet.",
+ "Couldn't load your Bots, so there's no one to send this to. Reload the page.",
),
).toBeNull();
});
diff --git a/app/tests/app-access-action.test.tsx b/app/tests/app-access-action.test.tsx
index 67a6d20a4d..1eb3e7a327 100644
--- a/app/tests/app-access-action.test.tsx
+++ b/app/tests/app-access-action.test.tsx
@@ -160,39 +160,39 @@ const COPY: [string, Partial, RegExp][] = [
[
"shared_not_connected",
{ state: "shared_not_connected" },
- /Your organisation's Gmail account isn't connected yet\. Only an administrator can connect it\./,
+ /Your organisation's Gmail account isn't connected yet\. Only an admin can connect it\./,
],
[
"shared_not_approved",
{ state: "shared_not_approved" },
- /^This Bot reaches more people than an administrator has approved for your organisation's Gmail account\. Only an administrator can approve it\.$/,
+ /^This Bot reaches more people than an admin approved for your organisation's Gmail account\. Only an admin can approve it\.$/,
],
[
"disabled, never switched on",
{ state: "disabled", unavailable: "absent" },
- /^Gmail isn't switched on for this OpenBot\. Only an administrator can switch it on\.$/,
+ /^Gmail isn't turned on yet\. Only an admin can turn it on\.$/,
],
[
"disabled, with no reason given",
{ state: "disabled", unavailable: null },
- /^Gmail isn't switched on for this OpenBot\./,
+ /^Gmail isn't turned on yet\./,
],
[
"disabled, with no Composio key",
{ state: "disabled", unavailable: "composio_unconfigured" },
- /^Gmail is not available on this OpenBot yet\. An administrator has to finish setting it up\.$/,
+ /^Gmail isn't set up yet\. An admin has to turn it on\.$/,
],
...(
["scheme_unreadable", "oauth_client_missing", "token_missing"] as const
).map((unavailable): [string, Partial, RegExp] => [
`disabled, ${unavailable}`,
{ state: "disabled", unavailable },
- /^Gmail isn't fully set up yet\. Only an administrator can finish setting it up\.$/,
+ /^Gmail isn't fully set up yet\. Only an admin can finish setting it up\.$/,
]),
[
"not_granted",
{ state: "not_granted" },
- /This Bot hasn't been given Gmail\. Only an administrator can give it\./,
+ /This Bot hasn't been given Gmail\. Only an admin can give it\./,
],
];
@@ -200,7 +200,7 @@ test.each(COPY)("says whose move it is: %s", (_, overrides, sentence) => {
expect(appAccessSentence(appAccess(overrides))).toMatch(sentence);
});
-test("no sentence about a switched-off app says an administrator turned it off", () => {
+test("no sentence about a switched-off app says an admin turned it off", () => {
for (const [, overrides] of COPY.filter(([, o]) => o.state === "disabled"))
expect(appAccessSentence(appAccess(overrides))).not.toMatch(/Turned off/);
});
@@ -312,7 +312,7 @@ test("a Reconnect on the owner's account offers nothing: only the owner can reco
expect(view.queryByRole("link")).toBeNull();
});
-test("a Shared app waiting on an administrator's approval offers nothing: the refused call already asked", async () => {
+test("a Shared app waiting on an admin's approval offers nothing: the refused call already asked", async () => {
const { view } = mounted(
appAccess({
state: "shared_not_approved",
@@ -320,7 +320,7 @@ test("a Shared app waiting on an administrator's approval offers nothing: the re
accountMode: "shared",
}),
);
- await view.findByText(/Only an administrator can approve it/);
+ await view.findByText(/Only an admin can approve it/);
expect(view.queryByRole("button")).toBeNull();
});
diff --git a/app/tests/app-access-card.test.tsx b/app/tests/app-access-card.test.tsx
index d76f4fd1c5..5530127fac 100644
--- a/app/tests/app-access-card.test.tsx
+++ b/app/tests/app-access-card.test.tsx
@@ -149,7 +149,7 @@ test("a Shared app with no team account: Request access, not a sentence", async
prompt={prompt({
reason: "shared",
message:
- "Gmail is shared across this deployment and no account is connected to it yet. Ask an administrator to connect it.",
+ "Gmail is shared across this deployment and no account is connected to it yet. Ask an admin to connect it.",
})}
/>,
);
@@ -195,18 +195,18 @@ test("an account lost and then connected again: the newer connection wins", asyn
expect(view.queryByText("Reconnect")).toBeNull();
});
-test("a team account the vendor lost: says an administrator has to reconnect it, with nothing to press", async () => {
+test("a team account the vendor lost: says an admin has to reconnect it, with nothing to press", async () => {
serving(appAccess({ state: "ready", account: "shared" }));
const view = mount();
const drawn = await card(view);
expect(drawn.textContent).toContain(
- "Your organisation's Gmail account needs reconnecting. Only an administrator can reconnect it.",
+ "Your organisation's Gmail account needs reconnecting. Only an admin can reconnect it.",
);
expect(view.queryByRole("button")).toBeNull();
});
test.each([
- ["disabled", "enable", /Gmail isn't switched on for this OpenBot/],
+ ["disabled", "enable", /Gmail isn't turned on yet/],
["not_granted", "grant", /This Bot hasn't been given Gmail/],
] as const)(
"the Bot asked about an app that is %s: Request access",
@@ -262,7 +262,7 @@ test("a Shared app whose audience is not approved: says so, with nothing to pres
);
const view = mount();
const drawn = await card(view);
- expect(drawn.textContent).toContain("Only an administrator can approve it.");
+ expect(drawn.textContent).toContain("Only an admin can approve it.");
expect(view.queryByRole("button")).toBeNull();
});
diff --git a/app/tests/approvals-rules.test.tsx b/app/tests/approvals-rules.test.tsx
index a4f3833c9c..61e22820af 100644
--- a/app/tests/approvals-rules.test.tsx
+++ b/app/tests/approvals-rules.test.tsx
@@ -164,9 +164,9 @@ test("team rules are locked for members, enforced auto-review is shown as requir
expect(view.getByText("Locked")).toBeTruthy();
const review = view.getByRole("switch", { name: "Auto-review" });
expect(review.getAttribute("aria-checked")).toBe("true");
- expect(view.getByText(/Required by your team/)).toBeTruthy();
+ expect(view.getByText(/An admin requires this/)).toBeTruthy();
expect(
- view.getByText(/Your team limits this to "Ask every time"/),
+ view.getByText(/An admin limits this to "Ask every time"/),
).toBeTruthy();
expect(view.queryByText("Team settings")).toBeNull();
});
@@ -289,7 +289,11 @@ test("with personal rules switched off by the team, they are shown as not applyi
team: { ...inbox.team, customRulesEnabled: false },
};
const view = draw();
- expect(await view.findByText(/kept but do not apply/)).toBeTruthy();
+ expect(
+ await view.findByText(
+ /An admin has turned off personal rules\. Yours are kept but don't apply\./,
+ ),
+ ).toBeTruthy();
expect(view.queryByText("Add a rule")).toBeNull();
});
diff --git a/app/tests/arcade-admin-plugins.test.tsx b/app/tests/arcade-admin-plugins.test.tsx
index 8915e1a4e5..3dfe10b467 100644
--- a/app/tests/arcade-admin-plugins.test.tsx
+++ b/app/tests/arcade-admin-plugins.test.tsx
@@ -47,21 +47,20 @@ test("a Composio deployment keeps today's exact wording", () => {
// `provider === "composio"` branch in `moreAppsRow`, and moving it here is what let that
// branch go.
directoryDescription:
- "A few hundred apps, reached as whoever is asking. Each person connects their own account.",
+ "A few hundred apps. Each person connects their own account.",
});
expect(moreAppsSectionDescription("composio")).toBe(
- "A directory this deployment has not reviewed, reached through Composio.",
+ "Apps from Composio. Nobody here has reviewed them.",
);
expect(moreAppsRow("composio", true)).toEqual({
title: "Browse Composio",
- description:
- "A few hundred apps, reached as whoever is asking. Each person connects their own account.",
+ description: "A few hundred apps. Each person connects their own account.",
linked: true,
});
expect(moreAppsRow("composio", false)).toEqual({
title: "Composio",
description:
- "Add your Composio key to enable a catalogue of tools. Set COMPOSIO_API_KEY on this deployment.",
+ "Not set up. Set COMPOSIO_API_KEY on this deployment to browse its apps.",
linked: false,
});
expect(brokeredConnectionDescription("composio")).toBe(
@@ -69,11 +68,11 @@ test("a Composio deployment keeps today's exact wording", () => {
);
expect(brokeredReach("composio")).toEqual({
description:
- "This app is reached through Composio, which holds the account. This deployment sends one key and the name of whoever is asking, so each person connects their own and a Bot sees only what that person can see. There is no token to paste.",
+ "Composio holds the account. This deployment sends one key and who is asking. Each person connects their own, and a Bot sees only what that person can see. There is no token to paste.",
badge: "Through Composio",
});
expect(brokeredDisconnectNote("composio")).toBe(
- "Disconnecting ends the account at Composio, not just here.",
+ "Disconnecting also ends the account at Composio.",
);
});
@@ -95,7 +94,7 @@ test("a configured deployment links to its own directory, whichever provider it
title: "Browse Arcade",
// Arcade's own sentence: it claims no app count, because nobody here has counted Arcade's.
description:
- "A directory this deployment has not reviewed, reached as whoever is asking. Each person connects their own account.",
+ "Apps nobody here has reviewed. Each person connects their own account.",
linked: true,
});
expect(moreAppsRow("composio", true).linked).toBe(true);
diff --git a/app/tests/authoring-tool-cards.test.tsx b/app/tests/authoring-tool-cards.test.tsx
index 8559bcc4e7..48f3bbbbbc 100644
--- a/app/tests/authoring-tool-cards.test.tsx
+++ b/app/tests/authoring-tool-cards.test.tsx
@@ -182,7 +182,7 @@ test("a proposed coworker named like an existing one says so", async () => {
,
);
- await tree.findByText("Writing the coworker…");
+ await tree.findByText("Writing the Bot…");
// Whoever asks for it, the roster has loaded by now.
await waitFor(() =>
expect(
diff --git a/app/tests/bot-access-apps.test.tsx b/app/tests/bot-access-apps.test.tsx
index 23050ab8ea..5ec8ba7953 100644
--- a/app/tests/bot-access-apps.test.tsx
+++ b/app/tests/bot-access-apps.test.tsx
@@ -21,7 +21,7 @@ import { settleReactWork } from "./settle-react-work";
* whether the person reading could use any of it.
*
* Each app is named by its own name — never "Composio Gmail" — and says what it is for the person
- * looking, with the one move they have. An administrator keeps the way to the Bot's grants for each
+ * looking, with the one move they have. An admin keeps the way to the Bot's grants for each
* app; nobody gets anybody else's connection state, because the read answers for the asker alone.
*/
@@ -124,7 +124,7 @@ function draw() {
}
const row = async (view: ReturnType, key: string) => {
- await view.findByText("Granted");
+ await view.findByText("What it can use");
const found = await view.findByText((_, element) =>
Boolean(element?.getAttribute("data-app") === key),
);
@@ -163,7 +163,7 @@ test("an app this person has not connected says so and offers Connect", async ()
expect(gmail.getByRole("button", { name: "Connect Gmail" })).toBeTruthy();
});
-test("an app only an administrator can fix offers Request access, and a pending one reads Requested", async () => {
+test("an app only an admin can fix offers Request access, and a pending one reads Requested", async () => {
serving({
apps: [
appAccess({
@@ -192,7 +192,7 @@ test("an app only an administrator can fix offers Request access, and a pending
const slack = await row(view, "composio-slack");
expect(
slack.getByText(
- "Your organisation's Slack account isn't connected yet. Only an administrator can connect it.",
+ "Your organisation's Slack account isn't connected yet. Only an admin can connect it.",
),
).toBeTruthy();
expect(
@@ -201,7 +201,7 @@ test("an app only an administrator can fix offers Request access, and a pending
const notion = await row(view, "notion");
expect(
notion.getByText(
- "Notion isn't switched on for this OpenBot. Only an administrator can switch it on.",
+ "Notion isn't turned on yet. Only an admin can turn it on.",
),
).toBeTruthy();
expect(notion.getByText("Requested")).toBeTruthy();
@@ -210,7 +210,7 @@ test("an app only an administrator can fix offers Request access, and a pending
).toBeTruthy();
});
-test("an administrator keeps the way to this Bot's grants on each app, beside the action", async () => {
+test("an admin keeps the way to this Bot's grants on each app, beside the action", async () => {
serving({ role: "admin", apps: [appAccess({ state: "not_connected" })] });
const gmail = await row(draw(), "composio-gmail");
const manage = gmail.getByRole("button", {
@@ -234,7 +234,7 @@ test("an app's sentence is never cut short, so who can fix it stays on screen at
});
const gmail = await row(draw(), "composio-gmail");
const sentence = gmail.getByText(
- "Gmail is not available on this OpenBot yet. An administrator has to finish setting it up.",
+ "Gmail isn't set up yet. An admin has to turn it on.",
);
expect(sentence.className).toContain("line-clamp-none");
expect(sentence.className).not.toContain("line-clamp-2");
@@ -270,7 +270,7 @@ test("on somebody else's Team Bot, an app on its owner's account says so, and an
expect(notion.queryByRole("button")).toBeNull();
});
-test("a person who is not an administrator gets no way to the grants", async () => {
+test("a person who is not an admin gets no way to the grants", async () => {
serving({ apps: [appAccess()] });
const gmail = await row(draw(), "composio-gmail");
expect(gmail.queryByRole("button", { name: /Manage/ })).toBeNull();
@@ -285,7 +285,7 @@ test("when the states cannot be read, the apps are still listed and the failure
});
const view = draw();
expect((await view.findByRole("alert")).textContent).toContain(
- "Whether each app is ready for you could not be loaded.",
+ "Couldn't check whether each app is ready for you. Reload the page.",
);
expect(await view.findByText("Google Drive")).toBeTruthy();
});
diff --git a/app/tests/bot-access-skills.test.tsx b/app/tests/bot-access-skills.test.tsx
index fc7d4eee84..4ef63c680a 100644
--- a/app/tests/bot-access-skills.test.tsx
+++ b/app/tests/bot-access-skills.test.tsx
@@ -150,7 +150,9 @@ test("switching a skill on grants it to this Bot", async () => {
test("on a Bot that is not yours there are no skill switches", async () => {
serving();
const view = draw({ ...BOT, mine: false, canManage: false });
- expect(await view.findByText(/^Nothing granted yet\./)).toBeTruthy();
+ expect(
+ await view.findByText(/^No apps yet\. An admin has to add them\./),
+ ).toBeTruthy();
expect(view.queryByRole("switch")).toBeNull();
});
@@ -162,6 +164,8 @@ test("your skills failing to load says so, not that you have none", async () =>
? new Response(null, { status: 500 })
: served(input, init)) as unknown as typeof fetch;
const view = draw(BOT);
- expect(await view.findByText("Could not load your skills.")).toBeTruthy();
- expect(view.queryByText(/You have not written a skill yet/)).toBeNull();
+ expect(
+ await view.findByText("Couldn't load your skills. Reload the page."),
+ ).toBeTruthy();
+ expect(view.queryByText(/No skills yet/)).toBeNull();
});
diff --git a/app/tests/bot-app-grants-screen.test.tsx b/app/tests/bot-app-grants-screen.test.tsx
index 56f6b22b88..8c707b7827 100644
--- a/app/tests/bot-app-grants-screen.test.tsx
+++ b/app/tests/bot-app-grants-screen.test.tsx
@@ -260,20 +260,20 @@ function renderScreen(queryClient: QueryClient) {
test("a failed plugin read reports the failure instead of claiming the app is not enabled", async () => {
const view = renderScreen(failingQueryClient());
- expect(await view.findByText("Plugins could not be loaded.")).toBeTruthy();
+ expect(
+ await view.findByText("Couldn't load plugins. Reload the page."),
+ ).toBeTruthy();
// The whole point: an app that may be enabled, with grants on it right now, must never be
// reported as absent on the evidence of a request that never came back.
- expect(
- view.queryByText("This deployment has not enabled an app by that name."),
- ).toBeNull();
- expect(view.queryByText("There is nothing here to grant.")).toBeNull();
+ expect(view.queryByText("No app with that name is turned on.")).toBeNull();
+ expect(view.queryByText("Nothing to give a Bot here.")).toBeNull();
});
test("a failed plugin read draws no switches, so nothing reads as a Bot holding nothing", async () => {
const view = renderScreen(failingQueryClient());
- await view.findByText("Plugins could not be loaded.");
+ await view.findByText("Couldn't load plugins. Reload the page.");
expect(view.container.querySelectorAll('[role="switch"]').length).toBe(0);
expect(view.queryByText("Turn on every read-only action")).toBeNull();
@@ -292,11 +292,11 @@ test("an answer that genuinely lacks the app still says the app is not enabled",
await waitForFailedRefetch(queryClient);
expect(
- await view.findByText(
- "This deployment has not enabled an app by that name.",
- ),
+ await view.findByText("No app with that name is turned on."),
).toBeTruthy();
- expect(view.queryByText("Plugins could not be loaded.")).toBeNull();
+ expect(
+ view.queryByText("Couldn't load plugins. Reload the page."),
+ ).toBeNull();
});
test("a failed REFETCH keeps the grant list it already had, not the error", async () => {
@@ -315,10 +315,10 @@ test("a failed REFETCH keeps the grant list it already had, not the error", asyn
await waitForFailedRefetch(queryClient);
expect(await view.findByText("list_channels")).toBeTruthy();
- expect(view.queryByText("Plugins could not be loaded.")).toBeNull();
expect(
- view.queryByText("This deployment has not enabled an app by that name."),
+ view.queryByText("Couldn't load plugins. Reload the page."),
).toBeNull();
+ expect(view.queryByText("No app with that name is turned on.")).toBeNull();
});
/*
diff --git a/app/tests/bot-manage.test.tsx b/app/tests/bot-manage.test.tsx
index c2b7323db3..c79a925731 100644
--- a/app/tests/bot-manage.test.tsx
+++ b/app/tests/bot-manage.test.tsx
@@ -113,7 +113,7 @@ test("duplicating opens the copy", async () => {
);
});
-test("a Bot an administrator assigned to you reads as pinned and cannot be hidden or unpinned", () => {
+test("a Bot an admin assigned to you reads as pinned and cannot be hidden or unpinned", () => {
const view = draw({
...BOT,
mine: false,
@@ -132,7 +132,7 @@ test("a Bot an administrator assigned to you reads as pinned and cannot be hidde
hidden.getAttribute("aria-disabled") === "true",
).toBe(true);
expect(
- view.getAllByText(/Assigned to you by an administrator/).length,
+ view.getAllByText(/An admin assigned it to you/).length,
).toBeGreaterThan(0);
});
diff --git a/app/tests/bot-memory.test.tsx b/app/tests/bot-memory.test.tsx
index 57f891c9f3..cff7f25472 100644
--- a/app/tests/bot-memory.test.tsx
+++ b/app/tests/bot-memory.test.tsx
@@ -139,9 +139,7 @@ async function openAddSource(view: ReturnType) {
const user = userEvent.setup({ document });
await view.findByText("Receipts folder");
await user.click(view.getByRole("button", { name: "Add source" }));
- await user.click(
- await view.findByRole("combobox", { name: "Connected app action" }),
- );
+ await user.click(await view.findByRole("combobox", { name: "What to read" }));
await user.click(await view.findByRole("option", { name: "Search Drive" }));
return user;
}
diff --git a/app/tests/bot-profile-active.test.tsx b/app/tests/bot-profile-active.test.tsx
index c38385479d..6e33aa9597 100644
--- a/app/tests/bot-profile-active.test.tsx
+++ b/app/tests/bot-profile-active.test.tsx
@@ -118,7 +118,11 @@ test("a paused Bot's Active switch is off, and the row says it is paused", async
const view = draw(BOT);
const control = await view.findByRole("switch", { name: "Active" });
expect(checked(control)).toBe(false);
- expect(await view.findByText(/^Paused\. No routine/)).toBeTruthy();
+ expect(
+ await view.findByText(
+ /^Paused\. Nothing runs for you until you turn it back on\./,
+ ),
+ ).toBeTruthy();
});
test("switching a running Bot off pauses it", async () => {
diff --git a/app/tests/bot-profile-notice.test.tsx b/app/tests/bot-profile-notice.test.tsx
index e08a8317b7..ec21a10e70 100644
--- a/app/tests/bot-profile-notice.test.tsx
+++ b/app/tests/bot-profile-notice.test.tsx
@@ -96,7 +96,7 @@ test("whoever manages the Bot is told its Shared-app calls are refused", async (
serving();
const view = draw(BOT);
expect(
- await view.findByText(/Gmail calls from this Bot are refused/),
+ await view.findByText(/can't use the shared Gmail account/),
).toBeTruthy();
});
@@ -104,7 +104,7 @@ test("someone who cannot manage the Bot is not shown the warning", async () => {
serving();
const view = draw({ ...BOT, canManage: false, mine: false });
await new Promise((resolve) => setTimeout(resolve, 100));
- expect(view.queryByText(/calls from this Bot are refused/)).toBeNull();
+ expect(view.queryByText(/can't use the shared .* account until/)).toBeNull();
});
test("the Message row shows the chevron every row that goes somewhere carries", async () => {
diff --git a/app/tests/bot-reaching-you.test.tsx b/app/tests/bot-reaching-you.test.tsx
index 3494013cc5..6760db3372 100644
--- a/app/tests/bot-reaching-you.test.tsx
+++ b/app/tests/bot-reaching-you.test.tsx
@@ -162,7 +162,7 @@ test("closing the dialog keeps the link code, so reopening the row shows it agai
await user.keyboard("{Escape}");
await waitFor(() => expect(view.queryByRole("dialog")).toBeNull());
// The row says a code is waiting, and opening it shows that code without asking for another.
- expect(view.getByText(/A link code is waiting/)).toBeTruthy();
+ expect(view.getByText(/Your link code is ready to send/)).toBeTruthy();
await user.click(view.getByRole("button", { name: /^Link Slack/ }));
expect(await view.findByText("link abc")).toBeTruthy();
// With the conversation it links, so "this conversation" still names one.
@@ -198,7 +198,11 @@ test("a transport the deployment cannot reach says why instead of opening", asyn
expect(
view.queryByRole("button", { name: /^Link Microsoft Teams/ }),
).toBeNull();
- expect(view.getByText(/needs to pair OpenBot with OpenTag/)).toBeTruthy();
+ expect(
+ view.getByText(
+ "Not set up yet. An admin has to turn on Slack and Teams first.",
+ ),
+ ).toBeTruthy();
});
test("a phone is verified for the chosen conversation, then asks for its code", async () => {
diff --git a/app/tests/bot-roster.test.tsx b/app/tests/bot-roster.test.tsx
index 765eb7eb2a..2a07fb9d2c 100644
--- a/app/tests/bot-roster.test.tsx
+++ b/app/tests/bot-roster.test.tsx
@@ -135,7 +135,9 @@ test("with every Bot of yours pinned, Yours says so instead of claiming none", a
serving([bot({ id: "favourite", name: "Favourite", pinned: true })], []);
const view = draw();
expect(await view.findByText("Your Bots are all pinned above.")).toBeTruthy();
- expect(view.queryByText("You have no Bots of your own yet.")).toBeNull();
+ expect(
+ view.queryByText("You don't have any Bots yet. Create one with New Bot."),
+ ).toBeNull();
});
test("a hidden Bot is under Hidden, folded until asked for, and its row opens its page", async () => {
@@ -168,7 +170,9 @@ test("a roster that never loaded says so, not that you have no Bots", async () =
new Response(null, { status: 500 })) as unknown as typeof fetch;
const view = draw();
expect(await view.findByRole("alert")).toBeTruthy();
- expect(view.queryByText("You have no Bots of your own yet.")).toBeNull();
+ expect(
+ view.queryByText("You don't have any Bots yet. Create one with New Bot."),
+ ).toBeNull();
});
test("a failed refetch keeps the roster it already had, not the error", async () => {
diff --git a/app/tests/bot-route-default-agent.test.tsx b/app/tests/bot-route-default-agent.test.tsx
index 1046115084..76f977e264 100644
--- a/app/tests/bot-route-default-agent.test.tsx
+++ b/app/tests/bot-route-default-agent.test.tsx
@@ -182,15 +182,17 @@ test("/bot with an empty agent query uses the normal default Bot", async () => {
expect(view.getByTestId("copilot-chat").dataset.agentId).toBe(
"picked-harness",
);
- expect(view.queryByText('This deployment has no Bot called "".')).toBeNull();
- expect(view.queryByText("This deployment has no Bots yet.")).toBeNull();
+ expect(view.queryByText('There\'s no Bot called "".')).toBeNull();
+ expect(view.queryByText("There are no Bots yet.")).toBeNull();
});
test("/bot reports a failed initial roster load instead of claiming there are no Bots", async () => {
const view = renderBot(queryClientWithFailingAgents());
- expect(await view.findByText("Bots couldn't be loaded.")).toBeTruthy();
- expect(view.queryByText("This deployment has no Bots yet.")).toBeNull();
+ expect(
+ await view.findByText("Couldn't load your Bots. Reload the page."),
+ ).toBeTruthy();
+ expect(view.queryByText("There are no Bots yet.")).toBeNull();
});
test("/bot preserves an explicit agent, including the built-in first agent", async () => {
@@ -218,7 +220,7 @@ test("/bot preserves an explicit unknown agent as a clear missing-bot state", as
);
expect(
- await view.findByText('This deployment has no Bot called "missing-agent".'),
+ await view.findByText('There\'s no Bot called "missing-agent".'),
).toBeTruthy();
expect(view.queryByTestId("copilot-chat")).toBeNull();
});
@@ -241,9 +243,7 @@ test("/bot loads a hidden explicit agent from the detail endpoint", async () =>
expect(await view.findByRole("heading", { name: "Hidden Bot" })).toBeTruthy();
expect(view.getByTestId("copilot-chat").dataset.agentId).toBe("hidden-bot");
- expect(
- view.queryByText('This deployment has no Bot called "hidden-bot".'),
- ).toBeNull();
+ expect(view.queryByText('There\'s no Bot called "hidden-bot".')).toBeNull();
});
test("/bot hidden lookup does not collide with the shared agent detail cache", async () => {
@@ -263,9 +263,7 @@ test("/bot hidden lookup does not collide with the shared agent detail cache", a
expect(await view.findByRole("heading", { name: "Hidden Bot" })).toBeTruthy();
expect(view.getByTestId("copilot-chat").dataset.agentId).toBe("hidden-bot");
- expect(
- view.queryByText('This deployment has no Bot called "hidden-bot".'),
- ).toBeNull();
+ expect(view.queryByText('There\'s no Bot called "hidden-bot".')).toBeNull();
});
test("/bot reports an explicit agent detail load failure", async () => {
@@ -279,7 +277,7 @@ test("/bot reports an explicit agent detail load failure", async () => {
);
expect((await view.findByRole("alert")).textContent).toBe(
- "Bot couldn't be loaded.",
+ "Couldn't load this Bot. Reload the page.",
);
expect(view.queryByTestId("copilot-chat")).toBeNull();
});
@@ -341,7 +339,7 @@ test("/bot opens a live Computer sidebar with ownership controls and preserves t
).toBe(false);
expect(
sidebar.getByRole("button", {
- name: "Open the assistant's screen full size",
+ name: "Open the Bot's screen full size",
}),
).toBeTruthy();
expect(sidebar.getByRole("heading", { name: "Activity" })).toBeTruthy();
diff --git a/app/tests/bot-sharing.test.tsx b/app/tests/bot-sharing.test.tsx
index a0ff6315c3..77c1d89fb0 100644
--- a/app/tests/bot-sharing.test.tsx
+++ b/app/tests/bot-sharing.test.tsx
@@ -224,7 +224,7 @@ test("a public Bot that is not published says everyone can already use it", asyn
);
const view = draw({ ...BOT, visibility: "public" });
expect(
- await view.findByText(/everyone in the deployment can already use it/),
+ await view.findByText(/everyone on OpenBot can already use it/),
).toBeTruthy();
expect(view.queryByText(/Only you can use it/)).toBeNull();
});
diff --git a/app/tests/brokered-account-row.test.tsx b/app/tests/brokered-account-row.test.tsx
index a3bbdea3a8..2258404b20 100644
--- a/app/tests/brokered-account-row.test.tsx
+++ b/app/tests/brokered-account-row.test.tsx
@@ -831,9 +831,7 @@ test("a deployment with no Composio key says the app is not available yet and of
const view = renderAccountScreen(queryClient());
- expect(
- await view.findByText(/is not available on this OpenBot yet/),
- ).toBeTruthy();
+ expect(await view.findByText(/isn't set up yet/)).toBeTruthy();
expect(view.queryByText(NAMES_THE_SETTING)).toBeNull();
expect(view.queryByText("Connected")).toBeNull();
expect(view.queryByRole("button", { name: "Disconnect" })).toBeNull();
@@ -946,18 +944,14 @@ test("a key app nobody has connected says it will ask for a key, not send you of
const view = renderAccountScreen(queryClient());
expect(
- await view.findByText(
- /connected with a key you already hold, not a trip to Gmail's consent screen/,
- ),
+ await view.findByText(/Gmail connects with a key you already have/),
).toBeTruthy();
/*
* The screen's own not-connected sentence is written for the kind that leaves, and pressing
* Connect on this app opens a form instead. Promising a trip to the vendor here is not a vaguer
* sentence than the truth; it is a different act from the one about to happen.
*/
- expect(
- view.queryByText(/takes you to Composio and then to the vendor to consent/),
- ).toBeNull();
+ expect(view.queryByText(/to approve access/)).toBeNull();
});
test("a key app nobody has connected keeps the screen's own reassurance", async () => {
@@ -1079,9 +1073,7 @@ test("an app that could not be asked what it needs says why, inside the dialog",
// Reported to the screen as well, not instead, exactly as a refused submission is.
expect(view.getAllByText(UNASKABLE).length).toBe(2);
// And never the line that stands in for a sentence nobody sent.
- expect(
- view.queryByText(/That app could not be asked what it needs/),
- ).toBeNull();
+ expect(view.queryByText(/Couldn't find out what this app needs/)).toBeNull();
});
test("a consent app's Connect leaves for the vendor's own page", async () => {
@@ -1135,7 +1127,9 @@ test("a typed scheme this screen does not know is refused, not navigated to unde
* this deployment's own origin, having been told nothing.
*/
await waitFor(() =>
- expect(view.queryByText(/no page to send you to/)).toBeTruthy(),
+ expect(
+ view.queryByText(/connects with a key, not a sign-in page/),
+ ).toBeTruthy(),
);
// The defect in its plainest form: before the fix this array held the one string "undefined".
expect(watched.navigations).toEqual([]);
@@ -1161,12 +1155,8 @@ test("the row names the app rather than calling it the app", async () => {
* drew this row without handing over the title left them all saying "the app", which names
* nowhere at all.
*/
- expect(
- await view.findByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
- expect(
- view.queryByText(/accepted without being checked against the app/),
- ).toBeNull();
+ expect(await view.findByText(/hasn't been checked with Gmail/)).toBeTruthy();
+ expect(view.queryByText(/hasn't been checked with the app/)).toBeNull();
});
/**
@@ -1240,7 +1230,7 @@ test("both kinds say Connected, and the line beneath says how", () => {
const consent = renderRow(accountState({ connected: true, kind: "consent" }));
expect(consent.getByText("Connected")).toBeTruthy();
- expect(consent.getByText(/through Gmail's consent screen/)).toBeTruthy();
+ expect(consent.getByText(/Signed in with Gmail\./)).toBeTruthy();
cleanup();
@@ -1259,7 +1249,7 @@ test("both kinds say Connected, and the line beneath says how", () => {
expect(key.getByText("Connected")).toBeTruthy();
expect(
key.getByText(
- `Connected with a key you provided, last checked ${asDay(CHECKED_AT)}.`,
+ `Connected with your key. Last checked ${asDay(CHECKED_AT)}.`,
),
).toBeTruthy();
});
@@ -1318,7 +1308,7 @@ test("an app needing no account offers nothing to press", () => {
// Not a disabled Connect, and not a Connect that would make an account nobody needs: there is no
// account here to make, so there is no control.
expect(view.queryByRole("button")).toBeNull();
- expect(view.getByText(/Gmail needs no account/)).toBeTruthy();
+ expect(view.getByText(/Gmail doesn't need an account/)).toBeTruthy();
});
test("Re-check appears only where a check is possible, and asks when pressed", async () => {
@@ -1360,9 +1350,7 @@ test("Re-check appears only where a check is possible, and asks when pressed", a
);
expect(unchecked.queryByRole("button", { name: "Re-check" })).toBeNull();
- expect(
- unchecked.getByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
+ expect(unchecked.getByText(/hasn't been checked with Gmail/)).toBeTruthy();
});
test("a connected consent app offers no Re-check at all", () => {
@@ -1395,7 +1383,7 @@ test("a connected consent app offers no Re-check at all", () => {
expect(view.queryByRole("button", { name: "Re-check" })).toBeNull();
// The row is otherwise itself: a live account somebody can still end.
expect(view.getByRole("button", { name: "Disconnect" })).toBeTruthy();
- expect(view.getByText(/through Gmail's consent screen/)).toBeTruthy();
+ expect(view.getByText(/Signed in with Gmail\./)).toBeTruthy();
});
/**
@@ -1419,13 +1407,11 @@ test("an app with nothing to check a key against says that about the app", () =>
accountState({ connected: true, kind: "fields", probe: null }),
);
+ expect(view.getByText(/hasn't been checked with Gmail/)).toBeTruthy();
expect(
- view.getByText(/accepted without being checked against Gmail/),
+ view.getByText(/At the time, Gmail had no safe way to check a key/),
).toBeTruthy();
- expect(
- view.getByText(/published nothing safe to try a key on at the time/),
- ).toBeTruthy();
- expect(view.getByText(/about the app, not about your key/)).toBeTruthy();
+ expect(view.getByText(/about the app, not your key/)).toBeTruthy();
// The one thing this state must never read as: a verdict on the key.
expect(view.queryByText(/rejected/)).toBeNull();
});
@@ -1448,12 +1434,10 @@ test("a key that passed its check says when it passed", () => {
expect(
view.getByText(
- `Connected with a key you provided, last checked ${asDay(CHECKED_AT)}.`,
+ `Connected with your key. Last checked ${asDay(CHECKED_AT)}.`,
),
).toBeTruthy();
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeNull();
});
test("a check that did not come back clean says so, without ruling on the key", () => {
@@ -1475,12 +1459,8 @@ test("a check that did not come back clean says so, without ruling on the key",
}),
);
- expect(
- view.getByText(
- /did not come back clean — that may be the key, and it may be Gmail itself/,
- ),
- ).toBeTruthy();
- expect(view.getByText(/The account still stands at Composio/)).toBeTruthy();
+ expect(view.getByText(/That may be the key, or Gmail itself/)).toBeTruthy();
+ expect(view.getByText(/Your account is still connected/)).toBeTruthy();
/*
* AND NEVER A VERDICT ABOUT THE CREDENTIAL. The pair behind this sentence is written off
* Composio's `{ data, error, successful }` envelope, which carries no status and no error code —
@@ -1497,9 +1477,7 @@ test("a check that did not come back clean says so, without ruling on the key",
* AND NOT THE OTHER SENTENCE. This is the state that sentence was false in: saying nothing had
* been checked, to the one person whose key has definitely been checked.
*/
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeNull();
});
/**
@@ -1538,21 +1516,21 @@ const SERVER_PROBE_OUTCOMES = [
outcome: "nothing",
/** The app published nothing safe to call, so no name and no verdict earned. */
row: { probe: null, verified: false, verifiedAt: null },
- says: /published nothing safe to try a key on at the time/,
+ says: /At the time, Gmail had no safe way to check a key/,
sharesItsSentenceWith: null,
},
{
outcome: "answered",
/** It ran in this person's account and the vendor took the key. */
row: { probe: PROBE, verified: true, verifiedAt: CHECKED_AT },
- says: /last checked/,
+ says: /Last checked/,
sharesItsSentenceWith: null,
},
{
outcome: "complained",
/** It ran and the app answered with a failure, and the account it ran in still stands. */
row: { probe: PROBE, verified: false, verifiedAt: null },
- says: /was made with your key and did not come back clean/,
+ says: /Your key didn't pass a check with/,
sharesItsSentenceWith: null,
},
{
@@ -1574,7 +1552,7 @@ const SERVER_PROBE_OUTCOMES = [
*/
outcome: "unreachable",
row: { probe: null, verified: false, verifiedAt: null },
- says: /published nothing safe to try a key on at the time/,
+ says: /At the time, Gmail had no safe way to check a key/,
sharesItsSentenceWith: "nothing",
},
] as const;
@@ -1655,7 +1633,7 @@ test("the probe roster covers every outcome the server has, and names the one th
);
expect(outageSentence).toBe(descriptionOf(nothingTried));
- expect(outageSentence).toMatch(/published nothing safe to try a key on/);
+ expect(outageSentence).toMatch(/had no safe way to check a key/);
});
test("Re-check is offered where the key is bad and withheld where there is nothing to check", async () => {
@@ -1714,9 +1692,9 @@ test("disconnecting a key names the step this deployment cannot take", () => {
// The account ends at Composio and the key does not end anywhere. Saying "disconnected" and
// stopping would leave somebody believing they had ended access they still have live.
- expect(view.getByText(/Removed from Composio/)).toBeTruthy();
+ expect(view.getByText(/Disconnected\. Your key still works/)).toBeTruthy();
expect(
- view.getByText(/Your key still works at Gmail — rotate it there/),
+ view.getByText(/Your key still works at Gmail\. Change it there/),
).toBeTruthy();
});
@@ -1737,7 +1715,7 @@ test("a key re-checked and then disconnected stops claiming it was checked", asy
expect(
await view.findByText(
- new RegExp(`last checked ${asDay(CHECKED_AT)}`.replace(/\//g, "\\/")),
+ new RegExp(`Last checked ${asDay(CHECKED_AT)}`.replace(/\//g, "\\/")),
),
).toBeTruthy();
@@ -1745,7 +1723,7 @@ test("a key re-checked and then disconnected stops claiming it was checked", asy
await waitFor(() =>
expect(
view.queryByText(
- new RegExp(`last checked ${asDay(RECHECKED_AT)}`.replace(/\//g, "\\/")),
+ new RegExp(`Last checked ${asDay(RECHECKED_AT)}`.replace(/\//g, "\\/")),
),
).toBeTruthy(),
);
@@ -1753,10 +1731,12 @@ test("a key re-checked and then disconnected stops claiming it was checked", asy
await userEvent.click(view.getByRole("button", { name: "Disconnect" }));
await waitFor(() =>
- expect(view.queryByText(/Removed from Composio/)).toBeTruthy(),
+ expect(
+ view.queryByText(/Disconnected\. Your key still works/),
+ ).toBeTruthy(),
);
// The account is gone; the answer the re-check gave was about it and must go with it.
- expect(view.queryByText(/last checked/)).toBeNull();
+ expect(view.queryByText(/Last checked/)).toBeNull();
expect(view.queryByRole("button", { name: "Re-check" })).toBeNull();
/*
@@ -1775,10 +1755,8 @@ test("a key re-checked and then disconnected stops claiming it was checked", asy
);
await waitFor(() => expect(view.queryByText("Connected")).toBeTruthy());
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
- expect(view.queryByText(/last checked/)).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeTruthy();
+ expect(view.queryByText(/Last checked/)).toBeNull();
});
test("a re-check's verdict is still there after a reload, because the server wrote down what it spent", async () => {
@@ -1824,15 +1802,11 @@ test("a re-check's verdict is still there after a reload, because the server wro
const view = renderAccountScreen(queryClient());
- expect(
- await view.findByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
+ expect(await view.findByText(/hasn't been checked with Gmail/)).toBeTruthy();
await userEvent.click(view.getByRole("button", { name: "Re-check" }));
await waitFor(() =>
- expect(
- view.queryByText(/was made with your key and did not come back clean/),
- ).toBeTruthy(),
+ expect(view.queryByText(/Your key didn't pass a check with/)).toBeTruthy(),
);
/*
@@ -1844,15 +1818,11 @@ test("a re-check's verdict is still there after a reload, because the server wro
const reloaded = renderAccountScreen(queryClient());
expect(
- await reloaded.findByText(
- /was made with your key and did not come back clean/,
- ),
+ await reloaded.findByText(/Your key didn't pass a check with/),
).toBeTruthy();
// And never either sentence that would be false about a key the vendor has just refused.
- expect(
- reloaded.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
- expect(reloaded.queryByText(/last checked/)).toBeNull();
+ expect(reloaded.queryByText(/hasn't been checked with Gmail/)).toBeNull();
+ expect(reloaded.queryByText(/Last checked/)).toBeNull();
// The way back out, which is withheld on exactly the render somebody would look for it.
expect(
await reloaded.findByRole("button", { name: "Re-check" }),
@@ -1895,7 +1865,7 @@ test("a re-check the vendor refused stops saying when the key was last checked",
expect(
await view.findByText(
- new RegExp(`last checked ${asDay(CHECKED_AT)}`.replace(/\//g, "\\/")),
+ new RegExp(`Last checked ${asDay(CHECKED_AT)}`.replace(/\//g, "\\/")),
),
).toBeTruthy();
@@ -1906,11 +1876,9 @@ test("a re-check the vendor refused stops saying when the key was last checked",
// And the row, which is the half that was.
await waitFor(() =>
- expect(
- view.queryByText(/was made with your key and did not come back clean/),
- ).toBeTruthy(),
+ expect(view.queryByText(/Your key didn't pass a check with/)).toBeTruthy(),
);
- expect(view.queryByText(/last checked/)).toBeNull();
+ expect(view.queryByText(/Last checked/)).toBeNull();
// Still theirs to press again, because the key is what is wrong and it is fixable at the vendor.
expect(view.getByRole("button", { name: "Re-check" })).toBeTruthy();
});
@@ -1965,9 +1933,7 @@ test("a key refused with its account left standing stops offering Connect", asyn
// And never the press that would only be refused again for an account that already exists.
expect(view.queryByRole("button", { name: "Connect" })).toBeNull();
// The row says which of the three states it is in, too: the key was tried and refused.
- expect(
- view.queryByText(/was made with your key and did not come back clean/),
- ).toBeTruthy();
+ expect(view.queryByText(/Your key didn't pass a check with/)).toBeTruthy();
});
/**
@@ -2016,7 +1982,9 @@ for (const [screen, renderScreen] of [
const view = renderScreen(queryClient());
expect(await view.findByRole("alert")).toBeTruthy();
- expect(view.getByRole("alert").textContent).toMatch(/could not be loaded/);
+ expect(view.getByRole("alert").textContent).toMatch(
+ /[Cc]ould(n't| not) (load|be loaded)/,
+ );
// AND NO ROW, WHICH IS THE HALF THAT MATTERS. Connect is the press the route would refuse; the
// other two are the only ways out of the state, and drawing none of them beside a silent page
@@ -2025,12 +1993,8 @@ for (const [screen, renderScreen] of [
expect(view.queryByRole("button", { name: "Re-check" })).toBeNull();
expect(view.queryByRole("button", { name: "Disconnect" })).toBeNull();
// And never a sentence about the account, in either direction.
- expect(
- view.queryByText(/was made with your key and did not come back clean/),
- ).toBeNull();
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
+ expect(view.queryByText(/Your key didn't pass a check with/)).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeNull();
});
}
@@ -2069,7 +2033,9 @@ test("the administrator's connector screen keeps the rest of the page when the c
// The row is still withheld and still says why, which is the fix this one must not undo.
expect(await view.findByRole("alert")).toBeTruthy();
- expect(view.getByRole("alert").textContent).toMatch(/could not be loaded/);
+ expect(view.getByRole("alert").textContent).toMatch(
+ /[Cc]ould(n't| not) (load|be loaded)/,
+ );
// AND THE PAGE IS STILL THERE. The switch is how a server is removed from this deployment, and
// its absence was the whole cost of collapsing the page.
@@ -2107,13 +2073,11 @@ test("a rejected key still says so on a page that has only read, and still offer
const view = renderAccountScreen(queryClient());
expect(
- await view.findByText(/was made with your key and did not come back clean/),
+ await view.findByText(/Your key didn't pass a check with/),
).toBeTruthy();
- expect(view.getByText(/still stands at Composio/)).toBeTruthy();
+ expect(view.getByText(/Your account is still connected/)).toBeTruthy();
// And never the sentence that is false here.
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeNull();
// The way back: a key corrected at the vendor is worth a second check, not a second connection.
expect(await view.findByRole("button", { name: "Re-check" })).toBeTruthy();
@@ -2140,9 +2104,7 @@ test("a rejected key still says so on a page that has only read, and still offer
const admin = renderAdminScreen(queryClient());
expect(
- await admin.findByText(
- /was made with your key and did not come back clean/,
- ),
+ await admin.findByText(/Your key didn't pass a check with/),
).toBeTruthy();
expect(await admin.findByRole("button", { name: "Re-check" })).toBeTruthy();
});
@@ -2174,9 +2136,7 @@ test("a key nothing was tried on offers Re-check once the app has something to t
);
expect(row.getByRole("button", { name: "Re-check" })).toBeTruthy();
- expect(
- row.getByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
+ expect(row.getByText(/hasn't been checked with Gmail/)).toBeTruthy();
/*
* AND IN THE PAST TENSE, which is what keeps the line and the button from contradicting each
* other. The app publishes something NOW — that is why the button is there — so a clause claiming
@@ -2184,12 +2144,10 @@ test("a key nothing was tried on offers Re-check once the app has something to t
* would have to be wrong. The clause is about the moment of the check, and says so.
*/
expect(
- row.getByText(/published nothing safe to try a key on at the time/),
+ row.getByText(/At the time, Gmail had no safe way to check a key/),
).toBeTruthy();
// And never the sentence written for a key the vendor refused: nothing was refused here.
- expect(
- row.queryByText(/was made with your key and did not come back clean/),
- ).toBeNull();
+ expect(row.queryByText(/Your key didn't pass a check with/)).toBeNull();
cleanup();
@@ -2212,9 +2170,7 @@ test("a key nothing was tried on offers Re-check once the app has something to t
const view = renderAccountScreen(queryClient());
- expect(
- await view.findByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
+ expect(await view.findByText(/hasn't been checked with Gmail/)).toBeTruthy();
expect(await view.findByRole("button", { name: "Re-check" })).toBeTruthy();
cleanup();
@@ -2233,9 +2189,7 @@ test("a key nothing was tried on offers Re-check once the app has something to t
const admin = renderAdminScreen(queryClient());
- expect(
- await admin.findByText(/accepted without being checked against Gmail/),
- ).toBeTruthy();
+ expect(await admin.findByText(/hasn't been checked with Gmail/)).toBeTruthy();
expect(await admin.findByRole("button", { name: "Re-check" })).toBeTruthy();
});
@@ -2376,7 +2330,7 @@ test("a form reopened while the app is being asked again opens on the list it al
// THE CLAIM ITSELF: the list this deployment already had, drawn while the new one is in flight.
const key = (await opened.findByLabelText("API Key")) as HTMLInputElement;
expect(opened.getByLabelText("Subdomain")).toBeTruthy();
- expect(view.queryByText(/Asking the app what it needs/)).toBeNull();
+ expect(view.queryByText(/Checking what the app needs/)).toBeNull();
/*
* And somebody starts typing, which is what makes the arrival below a reconcile rather than a
@@ -2685,7 +2639,7 @@ test("a re-check that spent nothing leaves the rejection the record still names"
// The accusation, as the record leaves it.
expect(
- await view.findByText(/was made with your key and did not come back clean/),
+ await view.findByText(/Your key didn't pass a check with/),
).toBeTruthy();
await userEvent.click(view.getByRole("button", { name: "Re-check" }));
@@ -2700,12 +2654,8 @@ test("a re-check that spent nothing leaves the rejection the record still names"
);
// And the three facts this person can act on are all still on the row.
- expect(
- view.getByText(/was made with your key and did not come back clean/),
- ).toBeTruthy();
- expect(view.getByText(/still stands at Composio/)).toBeTruthy();
+ expect(view.getByText(/Your key didn't pass a check with/)).toBeTruthy();
+ expect(view.getByText(/Your account is still connected/)).toBeTruthy();
// Never the absolution, which is the other app's state and not this one's.
- expect(
- view.queryByText(/accepted without being checked against Gmail/),
- ).toBeNull();
+ expect(view.queryByText(/hasn't been checked with Gmail/)).toBeNull();
});
diff --git a/app/tests/channel-history-notice.test.ts b/app/tests/channel-history-notice.test.ts
index fa45817f25..dbf5910f56 100644
--- a/app/tests/channel-history-notice.test.ts
+++ b/app/tests/channel-history-notice.test.ts
@@ -72,7 +72,7 @@ describe("channel history notice", () => {
unreadable: 0,
});
- expect(notice).toContain("temporarily unavailable");
+ expect(notice).toContain("Couldn't load earlier messages");
expect(notice).not.toContain("different CopilotKit project");
expect(notice).not.toContain("fresh history");
});
@@ -86,7 +86,7 @@ describe("channel history notice", () => {
unreadable: 1,
});
- expect(notice).toContain("One earlier message could not be read");
+ expect(notice).toContain("One earlier message couldn't be read");
expect(notice).not.toContain("different CopilotKit project");
});
@@ -101,7 +101,7 @@ describe("channel history notice", () => {
);
expect((await view.findByRole("status")).textContent).toContain(
- "Earlier messages are temporarily unavailable",
+ "Couldn't load earlier messages",
);
expect(view.queryByText(/different CopilotKit project/)).toBeNull();
},
@@ -134,7 +134,7 @@ describe("channel history notice", () => {
unreadable: 0,
});
- expect(notice).toContain("temporarily unavailable");
+ expect(notice).toContain("Couldn't load earlier messages");
expect(notice).not.toContain("different CopilotKit project");
});
@@ -159,6 +159,6 @@ describe("channel history notice", () => {
unreadable: 1,
historyReadFailed: true,
}),
- ).toContain("temporarily unavailable");
+ ).toContain("Couldn't load earlier messages");
});
});
diff --git a/app/tests/channel-history-refresh.test.tsx b/app/tests/channel-history-refresh.test.tsx
index 9f6e814dba..7e9e37722c 100644
--- a/app/tests/channel-history-refresh.test.tsx
+++ b/app/tests/channel-history-refresh.test.tsx
@@ -51,9 +51,9 @@ const local: Message = {
};
const broken = { id: "broken", role: "user", content: null };
const unavailable =
- "Earlier messages are temporarily unavailable. You can keep using this conversation.";
+ "Couldn't load earlier messages. You can keep using this conversation.";
const oneHole =
- "One earlier message could not be read and is not shown. The rest of this conversation is complete.";
+ "One earlier message couldn't be read, so it isn't shown. Everything else is here.";
let originalFetch: typeof fetch;
let history: (threadId: string) => Promise;
let historyReads: string[];
diff --git a/app/tests/channel-new-error-state.test.tsx b/app/tests/channel-new-error-state.test.tsx
index 226f5e7222..e032b87703 100644
--- a/app/tests/channel-new-error-state.test.tsx
+++ b/app/tests/channel-new-error-state.test.tsx
@@ -160,7 +160,7 @@ test("/channel/new reports a failed initial roster load", async () => {
const view = renderChannelNew(queryClientWithFailingAgents());
expect((await view.findByRole("alert")).textContent).toBe(
- "Coworkers couldn't be loaded.",
+ "Couldn't load your Bots. Reload the page.",
);
expect(view.queryByText("No agents found.")).toBeNull();
await expectMessageComposerDisabled(view, true);
@@ -175,7 +175,7 @@ test("/channel/new reports a failed URL-selected hidden detail load", async () =
);
expect((await view.findByRole("alert")).textContent).toBe(
- "Coworker couldn't be loaded.",
+ "Couldn't load that Bot. Reload the page.",
);
expect(view.queryByText("No agents found.")).toBeNull();
await expectMessageComposerDisabled(view, true);
diff --git a/app/tests/channel-pagination.test.tsx b/app/tests/channel-pagination.test.tsx
index 11eb165921..0c86fef606 100644
--- a/app/tests/channel-pagination.test.tsx
+++ b/app/tests/channel-pagination.test.tsx
@@ -270,7 +270,7 @@ test("a failed page preserves rows and waits for an explicit retry", async () =>
act(() => activeObserver().intersect());
expect(await view.findByRole("alert")).toHaveProperty(
"textContent",
- "Could not load older conversations.",
+ "Couldn't load older conversations.",
);
expect(view.getByText("Recent conversation")).toBeTruthy();
expect(ScrollObserver.active.size).toBe(0);
@@ -280,9 +280,7 @@ test("a failed page preserves rows and waits for an explicit retry", async () =>
channels: [channel("Recovered conversation")],
nextCursor: "last-page",
});
- fireEvent.click(
- view.getByRole("button", { name: "Retry loading older conversations" }),
- );
+ fireEvent.click(view.getByRole("button", { name: "Try again" }));
expect(await view.findByText("Recovered conversation")).toBeTruthy();
expect(requests).toEqual([
"/api/channels?cursor=older%2Fpage",
@@ -303,10 +301,10 @@ test("search loads older history explicitly and includes new matches", async ()
const view = renderSidebar();
const user = userEvent.setup({ document: view.container.ownerDocument });
await view.findByText("Recent conversation");
- const search = view.getByRole("textbox", { name: "Search channels" });
+ const search = view.getByRole("textbox", { name: "Search conversations" });
await user.type(search, "Archived");
expect(
- await view.findByText("No loaded channels match your search"),
+ await view.findByText("No loaded conversations match your search"),
).toBeTruthy();
expect(ScrollObserver.active.size).toBe(0);
expect(requests).toEqual([]);
@@ -319,7 +317,9 @@ test("search loads older history explicitly and includes new matches", async ()
view.getByRole("button", { name: "Load older conversations" }),
);
expect(await view.findByText("Archived conversation")).toBeTruthy();
- expect(view.queryByText("No loaded channels match your search")).toBeNull();
+ expect(
+ view.queryByText("No loaded conversations match your search"),
+ ).toBeNull();
await user.click(search);
await user.keyboard("{Control>}a{/Control}{Backspace}");
expect(await view.findByText("Recent conversation")).toBeTruthy();
diff --git a/app/tests/composer-attachments.test.ts b/app/tests/composer-attachments.test.ts
index d217ffc61c..082ff2585a 100644
--- a/app/tests/composer-attachments.test.ts
+++ b/app/tests/composer-attachments.test.ts
@@ -78,8 +78,7 @@ describe("uploadToChannel", () => {
global.fetch = (async () =>
jsonResponse(
{
- error:
- "'icon.svg' is an SVG, which can carry scripts and is not accepted.",
+ error: "'icon.svg' can't be attached. SVG files aren't accepted.",
},
415,
)) as unknown as typeof fetch;
@@ -110,7 +109,7 @@ describe("uploadToChannel", () => {
await expect(
uploadToChannel("chan_1", "group_1")(file("receipt.png", "image/png")),
- ).rejects.toThrow('Could not upload "receipt.png".');
+ ).rejects.toThrow('Couldn\'t upload "receipt.png". Try again.');
});
test("a refusal whose `error` is empty throws the named fallback, not an empty refusal", async () => {
@@ -121,7 +120,7 @@ describe("uploadToChannel", () => {
await expect(
uploadToChannel("chan_1", "group_1")(file("receipt.png", "image/png")),
- ).rejects.toThrow('Could not upload "receipt.png".');
+ ).rejects.toThrow('Couldn\'t upload "receipt.png". Try again.');
});
test("a refusal whose `error` is not a string throws the named fallback", async () => {
@@ -130,7 +129,7 @@ describe("uploadToChannel", () => {
await expect(
uploadToChannel("chan_1", "group_1")(file("receipt.png", "image/png")),
- ).rejects.toThrow('Could not upload "receipt.png".');
+ ).rejects.toThrow('Couldn\'t upload "receipt.png". Try again.');
});
/**
@@ -151,7 +150,7 @@ describe("uploadToChannel", () => {
const upload = uploadToChannel("chan_1", "group_1");
await expect(upload(file("receipt.png", "image/png"))).rejects.toThrow(
- 'Could not upload "receipt.png".',
+ 'Couldn\'t upload "receipt.png". Try again.',
);
await expect(upload(file("receipt.png", "image/png"))).rejects.not.toThrow(
/JSON/,
@@ -169,7 +168,7 @@ describe("uploadToChannel", () => {
await expect(
uploadToChannel("chan_1", "group_1")(file("receipt.png", "image/png")),
- ).rejects.toThrow('Could not upload "receipt.png".');
+ ).rejects.toThrow('Couldn\'t upload "receipt.png". Try again.');
});
});
diff --git a/app/tests/composer-insecure-context.test.tsx b/app/tests/composer-insecure-context.test.tsx
index 70ce0859ef..3c80176ad8 100644
--- a/app/tests/composer-insecure-context.test.tsx
+++ b/app/tests/composer-insecure-context.test.tsx
@@ -109,9 +109,7 @@ test("a pick-time refusal still names the file with no crypto.randomUUID", async
]);
await waitFor(() => expect(view.queryByRole("alert")).not.toBeNull());
- expect(
- view.queryByText(/can carry scripts and is not accepted/),
- ).not.toBeNull();
+ expect(view.queryByText(/SVG files aren't accepted/)).not.toBeNull();
// The rest of the gesture survived the refusal, which is the part the throw used to take with it.
await waitFor(() =>
expect(view.queryByLabelText("Remove notes.txt")).not.toBeNull(),
diff --git a/app/tests/composer-queue-attachments.test.tsx b/app/tests/composer-queue-attachments.test.tsx
index 254d3a0beb..93343834fe 100644
--- a/app/tests/composer-queue-attachments.test.tsx
+++ b/app/tests/composer-queue-attachments.test.tsx
@@ -400,7 +400,7 @@ test("taking a queued message back releases the rows it was carrying", async ()
* both sentences are live — so declining now leaves the wiring between them unpinned, and
* swapping the two arms of that ternary passed the whole suite.
*/
- expect(alert.textContent).toContain("removed along with the queued message");
+ expect(alert.textContent).toContain("removed with its queued message");
expect(alert.textContent).not.toContain("merged");
expect(alert.textContent).not.toContain("at most");
});
diff --git a/app/tests/composer-unnamed-mime.test.tsx b/app/tests/composer-unnamed-mime.test.tsx
index c535dfab3d..2c9a55eb02 100644
--- a/app/tests/composer-unnamed-mime.test.tsx
+++ b/app/tests/composer-unnamed-mime.test.tsx
@@ -127,9 +127,7 @@ test("a file that does name its format is still refused here, in our words", asy
drop(form, [new File([""], "logo.svg", { type: "image/svg+xml" })]);
await waitFor(() => expect(view.queryByRole("alert")).not.toBeNull());
- expect(
- view.queryByText(/can carry scripts and is not accepted/),
- ).not.toBeNull();
+ expect(view.queryByText(/SVG files aren't accepted/)).not.toBeNull();
expect(view.queryByText(/Supported types:/)).toBeNull();
expect(uploaded).toEqual([]);
});
diff --git a/app/tests/composio-route-plugins-error.test.tsx b/app/tests/composio-route-plugins-error.test.tsx
index 410a9ab531..411b04c731 100644
--- a/app/tests/composio-route-plugins-error.test.tsx
+++ b/app/tests/composio-route-plugins-error.test.tsx
@@ -102,7 +102,7 @@ test("a failed plugins read renders an alert and never asks for the directory",
const view = renderPicker();
const alert = await view.findByRole("alert");
- expect(alert.textContent).toContain("plugin settings could not be read");
+ expect(alert.textContent).toContain("Couldn't load the plugin settings");
expect(view.queryByLabelText("Search Composio apps")).toBeNull();
expect(requested).toContain("/api/plugins");
expect(requested).not.toContain("/api/plugins/composio/apps");
diff --git a/app/tests/composio-unconfigured-account.test.tsx b/app/tests/composio-unconfigured-account.test.tsx
index fa6c42e8c2..44b98d41dd 100644
--- a/app/tests/composio-unconfigured-account.test.tsx
+++ b/app/tests/composio-unconfigured-account.test.tsx
@@ -182,9 +182,7 @@ function renderAccount(composioConfigured: boolean) {
test("the app's page says it is not available yet, and names no setting", async () => {
const view = renderAccount(false);
- expect(
- await view.findByText(/Gmail is not available on this OpenBot yet/),
- ).toBeTruthy();
+ expect(await view.findByText(/Gmail isn't set up yet/)).toBeTruthy();
expect(view.queryByText(/COMPOSIO_API_KEY/)).toBeNull();
expect(view.queryByRole("button", { name: "Connect" })).toBeNull();
});
@@ -192,5 +190,5 @@ test("the app's page says it is not available yet, and names no setting", async
test("with the key set, the app's page offers the account as before", async () => {
const view = renderAccount(true);
expect(await view.findByText("Your account")).toBeTruthy();
- expect(view.queryByText(/not available on this OpenBot yet/)).toBeNull();
+ expect(view.queryByText(/isn't set up yet/)).toBeNull();
});
diff --git a/app/tests/connected-account-confirmation.test.tsx b/app/tests/connected-account-confirmation.test.tsx
index 253c269f26..11951a97a3 100644
--- a/app/tests/connected-account-confirmation.test.tsx
+++ b/app/tests/connected-account-confirmation.test.tsx
@@ -18,8 +18,6 @@ import {
} from "@tanstack/react-router";
import { cleanup, render } from "@testing-library/react";
import {
- brokeredAccountPageDescription,
- brokeredConsentTrip,
brokeredDisconnectNote,
brokeredKeyMissing,
disconnectSiblingsNotice,
@@ -36,8 +34,10 @@ import {
* Three things are pinned, and the first two are RENDERED markup because a sentence computed and
* never drawn is not shipped: the sibling list names what a Disconnect would also end and draws
* NOTHING when there is none; an Arcade deployment's page never says Composio, in text or in
- * attributes; and a Composio deployment's three vendor sentences are character-for-character what
- * they were. The fourth is the landing parameter the verifier's redirect carries.
+ * attributes; and a person's own page names no broker on either deployment, because the broker is
+ * an internal a person cannot act on (see the openbot-copy skill). Where a vendor name is shown,
+ * on the admin pages, it comes from the provider vocabulary. The fourth is the landing parameter
+ * the verifier's redirect carries.
*/
beforeAll(() => GlobalRegistrator.register());
@@ -214,12 +214,11 @@ function everythingRendered(view: ReturnType): string {
return `${view.container.textContent ?? ""}\n${view.container.innerHTML}`;
}
-const COMPOSIO_PAGE =
- "Reached through Composio, which holds the account, so a Bot sees only what you can see.";
-const COMPOSIO_DISCONNECT_NOTE =
- "Disconnecting ends the account at Composio, not just here.";
-const COMPOSIO_CONSENT_TRIP =
- "Connecting takes you to Composio and then to the vendor to consent.";
+const PAGE = "Bots use this as you, and see only what you can see.";
+const CONNECTED =
+ "Signed in with Gmail. Bots you allow use your Gmail as you. Disconnecting removes their access everywhere.";
+const DISCONNECTED =
+ "No Bot can use this as you. Connecting takes you to Gmail to approve access.";
// ---- 1 and 2: the sibling list, rendered ----
@@ -253,7 +252,7 @@ test("with no siblings NO sibling copy appears at all, not an empty block", asyn
expect(siblingReads.length).toBeGreaterThan(0);
const text = view.container.textContent ?? "";
expect(text).not.toContain("also ends");
- expect(text).not.toContain("could not be checked");
+ expect(text).not.toContain("couldn't be checked");
expect(text).not.toContain("share this connection");
// No second description under the row's own: the notice slot is not drawn.
const descriptions = view.container.querySelectorAll(
@@ -273,10 +272,10 @@ test("a Composio deployment always answers empty, so it draws no sibling copy ei
test("apps that could not be asked about are a count and never names", () => {
expect(disconnectSiblingsNotice({ siblings: [], unanswered: 0 })).toBeNull();
expect(disconnectSiblingsNotice({ siblings: ["slack"], unanswered: 2 })).toBe(
- "Disconnecting also ends your access to slack, which shares this connection. 2 other apps could not be checked and may be affected too.",
+ "Disconnecting also ends your access to slack, which shares this connection. 2 other apps couldn't be checked and may be affected too.",
);
expect(disconnectSiblingsNotice({ siblings: [], unanswered: 1 })).toBe(
- "1 app on this connection could not be checked, so disconnecting may end it too.",
+ "1 app on this connection couldn't be checked, so disconnecting may end it too.",
);
});
@@ -310,7 +309,7 @@ for (const connected of [true, false]) {
siblings: { siblings: ["googledocs"], unanswered: 1 },
});
const view = renderScreen("?action=Gmail.ListEmails");
- await view.findAllByText(/Arcade/);
+ await view.findByText("You just authorized Gmail.ListEmails.");
if (connected && configured) await view.findByText(/googledocs/);
const rendered = everythingRendered(view);
expect(rendered).not.toMatch(/composio/i);
@@ -319,38 +318,41 @@ for (const connected of [true, false]) {
}
}
-test("on an Arcade deployment the three sentences name Arcade", () => {
- expect(brokeredAccountPageDescription("arcade")).toBe(
- "Reached through Arcade, which holds the account, so a Bot sees only what you can see.",
- );
+test("where a screen does name the broker, the vocabulary names the active one", () => {
expect(brokeredDisconnectNote("arcade")).toBe(
- "Disconnecting ends the account at Arcade, not just here.",
+ "Disconnecting also ends the account at Arcade.",
);
- expect(brokeredConsentTrip("arcade")).toBe(
- "Connecting takes you to Arcade and then to the vendor to consent.",
+ expect(brokeredDisconnectNote("composio")).toBe(
+ "Disconnecting also ends the account at Composio.",
+ );
+ expect(brokeredKeyMissing("arcade")).toBe(
+ "Set ARCADE_API_KEY on this deployment. Until then, this account can't be connected or disconnected here. The app stays turned on, and its tool grants still stand.",
);
-});
-
-// ---- 4: Composio is byte-identical ----
-
-test("on Composio the three vendor sentences are exactly what they always were", () => {
- expect(brokeredAccountPageDescription("composio")).toBe(COMPOSIO_PAGE);
- expect(brokeredDisconnectNote("composio")).toBe(COMPOSIO_DISCONNECT_NOTE);
- expect(brokeredConsentTrip("composio")).toBe(COMPOSIO_CONSENT_TRIP);
expect(brokeredKeyMissing("composio")).toBe(
- "Set COMPOSIO_API_KEY on this deployment. Without it there is no broker to reach, so this account can be neither connected nor ended from here. The app stays enabled and every grant on its tools still stands.",
+ "Set COMPOSIO_API_KEY on this deployment. Until then, this account can't be connected or disconnected here. The app stays turned on, and its tool grants still stand.",
);
});
+// ---- 4: a person's own page names no broker ----
+
+for (const provider of ["composio", "arcade"] as const) {
+ test(`a connected page on ${provider} says what Bots do, and names no broker`, async () => {
+ install({ provider, connected: true });
+ const view = renderScreen();
+ await view.findByText("Disconnect");
+ const rendered = everythingRendered(view);
+ expect(rendered).toContain(PAGE);
+ expect(rendered).not.toMatch(/composio|arcade/i);
+ });
+}
+
test("a connected Composio page renders its sentences verbatim", async () => {
install({ provider: "composio", connected: true });
const view = renderScreen();
- await view.findByText(/Disconnecting ends the account/);
+ await view.findByText(/Disconnecting removes their access/);
const text = view.container.textContent ?? "";
- expect(text).toContain(COMPOSIO_PAGE);
- expect(text).toContain(
- `Connected through Gmail's consent screen. A Bot granted its tools reads your Gmail as you. ${COMPOSIO_DISCONNECT_NOTE}`,
- );
+ expect(text).toContain(PAGE);
+ expect(text).toContain(CONNECTED);
});
test("a disconnected Composio page renders its sentences verbatim", async () => {
@@ -358,9 +360,16 @@ test("a disconnected Composio page renders its sentences verbatim", async () =>
const view = renderScreen();
await view.findByText(/Connecting takes you to/);
const text = view.container.textContent ?? "";
- expect(text).toContain(COMPOSIO_PAGE);
- expect(text).toContain(
- `No Bot can read this as you. ${COMPOSIO_CONSENT_TRIP}`,
+ expect(text).toContain(PAGE);
+ expect(text).toContain(DISCONNECTED);
+});
+
+test("a disconnected Arcade page points at the action list, not at a Connect it does not draw", async () => {
+ install({ provider: "arcade", connected: false });
+ const view = renderScreen();
+ await view.findByText("Approved per action");
+ expect(view.container.textContent).toContain(
+ "No Bot can use this as you. Authorize the actions you want below.",
);
});
diff --git a/app/tests/connected-account-grants.test.tsx b/app/tests/connected-account-grants.test.tsx
index e2f83be12a..77651f58dc 100644
--- a/app/tests/connected-account-grants.test.tsx
+++ b/app/tests/connected-account-grants.test.tsx
@@ -291,7 +291,7 @@ test("Authorize appears for needs-consent and for nothing else", async () => {
expect(buttons("Gmail.Flaky")).not.toContain("Authorize");
});
-test("needs-operator names the missing secret, says an administrator sets it, and offers no Authorize", async () => {
+test("needs-operator names the missing secret, says an admin has to set it, and offers no Authorize", async () => {
install({
provider: "arcade",
connected: true,
@@ -307,7 +307,7 @@ test("needs-operator names the missing secret, says an administrator sets it, an
await waitFor(() => row(view, "Gmail.Setup"));
const text = row(view, "Gmail.Setup").textContent ?? "";
expect(text).toContain("GOOGLE_CLIENT_SECRET");
- expect(text).toContain("An administrator sets it");
+ expect(text).toContain("An admin has to set it");
expect(row(view, "Gmail.Setup").querySelector("button")).toBeNull();
expect(view.queryByText("Authorize")).toBeNull();
});
@@ -348,13 +348,13 @@ test("the route's synthetic toolkit-named unknown reads as could not read and ne
action: "Gmail",
state: "unknown",
reason:
- "This deployment could not read what this account holds just now, so nothing is said about it either way. Try again in a moment.",
+ "Couldn't read what this account allows right now. Try again in a moment.",
},
],
});
const view = renderScreen();
const notice = await view.findByRole("status");
- expect(notice.textContent?.toLowerCase()).toContain("could not read");
+ expect(notice.textContent?.toLowerCase()).toContain("couldn't read");
expect(view.container.querySelector("[data-action]")).toBeNull();
expect(view.container.querySelector('[data-action="Gmail"]')).toBeNull();
expect(
diff --git a/app/tests/connected-accounts-oneshot-params.test.tsx b/app/tests/connected-accounts-oneshot-params.test.tsx
index a4f1195ecd..6c99fc0024 100644
--- a/app/tests/connected-accounts-oneshot-params.test.tsx
+++ b/app/tests/connected-accounts-oneshot-params.test.tsx
@@ -80,21 +80,19 @@ const searchOf = (router: TestRouter) => router.state.location.search;
test("?connected=failed is said once and then dropped from the URL", async () => {
const { view, router } = renderScreen("?connected=failed");
- await view.findByText(/That account could not be connected/);
+ await view.findByText(/Couldn't connect that account/);
await waitFor(() => expect(searchOf(router)).toEqual({}));
// And the sentence survives the strip: the news is read once, not withdrawn.
- expect(
- view.queryByText(/That account could not be connected/),
- ).not.toBeNull();
+ expect(view.queryByText(/Couldn't connect that account/)).not.toBeNull();
expect(router.state.location.href).toBe(PATH);
});
test("?authorized= is said once and then dropped from the URL", async () => {
const { view, router } = renderScreen("?authorized=no-attempt");
- await view.findByText(/There is no authorization in progress/);
+ await view.findByText(/There's no authorization in progress/);
await waitFor(() => expect(searchOf(router)).toEqual({}));
expect(
- view.queryByText(/There is no authorization in progress/),
+ view.queryByText(/There's no authorization in progress/),
).not.toBeNull();
expect(router.state.location.href).toBe(PATH);
});
@@ -102,11 +100,9 @@ test("?authorized= is said once and then dropped from the URL", async () => {
/** The one that was already stripped, kept here so the three are pinned in one place. */
test("?linked= is said once and then dropped from the URL", async () => {
const { view, router } = renderScreen("?linked=failed");
- await view.findByText(/Your GitHub account could not be linked/);
+ await view.findByText(/Couldn't link your GitHub account/);
await waitFor(() => expect(searchOf(router)).toEqual({}));
- expect(
- view.queryByText(/Your GitHub account could not be linked/),
- ).not.toBeNull();
+ expect(view.queryByText(/Couldn't link your GitHub account/)).not.toBeNull();
});
/**
@@ -118,15 +114,13 @@ test("all three arriving together leave nothing behind", async () => {
const { view, router } = renderScreen(
"?connected=failed&authorized=no-attempt&linked=failed",
);
- await view.findByText(/That account could not be connected/);
+ await view.findByText(/Couldn't connect that account/);
await waitFor(() => expect(searchOf(router)).toEqual({}));
expect(router.state.location.href).toBe(PATH);
expect(
- view.queryByText(/There is no authorization in progress/),
- ).not.toBeNull();
- expect(
- view.queryByText(/Your GitHub account could not be linked/),
+ view.queryByText(/There's no authorization in progress/),
).not.toBeNull();
+ expect(view.queryByText(/Couldn't link your GitHub account/)).not.toBeNull();
});
/** Nothing to consume means nothing to clean up, and no navigation at all. */
diff --git a/app/tests/directory-read-fallback.test.ts b/app/tests/directory-read-fallback.test.ts
index 854bf43060..a2aacd1d13 100644
--- a/app/tests/directory-read-fallback.test.ts
+++ b/app/tests/directory-read-fallback.test.ts
@@ -19,14 +19,16 @@ async function failureMessage(provider: "composio" | "arcade") {
throw new Error("the query was expected to fail");
}
-test("a Composio deployment's messageless directory failure is the existing sentence, unchanged", async () => {
+test("a Composio deployment's messageless directory failure is the directory screen's sentence", async () => {
expect(await failureMessage("composio")).toBe(
- "Composio's app directory could not be read.",
+ "Couldn't load Composio's app directory. Reload the page.",
);
});
test("an Arcade deployment's messageless directory failure names Arcade and never Composio", async () => {
const message = await failureMessage("arcade");
- expect(message).toBe("Arcade's app directory could not be read.");
+ expect(message).toBe(
+ "Couldn't load Arcade's app directory. Reload the page.",
+ );
expect(message).not.toMatch(/composio/i);
});
diff --git a/app/tests/gallery-components.test.tsx b/app/tests/gallery-components.test.tsx
index 28c74d05cc..81637bfbab 100644
--- a/app/tests/gallery-components.test.tsx
+++ b/app/tests/gallery-components.test.tsx
@@ -183,7 +183,9 @@ test("a failed answer preserves values and retries without duplicate submissions
await fillForm(view);
await user.click(view.getByRole("button", { name: "Submit answers" }));
await waitFor(() =>
- expect(view.getByRole("alert").textContent).toContain("could not be sent"),
+ expect(view.getByRole("alert").textContent).toContain(
+ "Couldn't send your answers",
+ ),
);
const team = view.getByRole("textbox", { name: "Team" });
expect(team instanceof HTMLInputElement).toBe(true);
diff --git a/app/tests/learning-admin.test.tsx b/app/tests/learning-admin.test.tsx
index 57442b95f6..645ae0839f 100644
--- a/app/tests/learning-admin.test.tsx
+++ b/app/tests/learning-admin.test.tsx
@@ -157,7 +157,7 @@ test.each([
[
{ enabled: true, defaultTarget: null, agents: {} },
{ state: "not_entitled" },
- "plan does not include Learning",
+ "plan doesn't include Learning",
],
] as const)(
"the page says what automatic assignment found (%#)",
diff --git a/app/tests/new-bot-sheet.test.tsx b/app/tests/new-bot-sheet.test.tsx
index fcf2952b39..7249b34cb0 100644
--- a/app/tests/new-bot-sheet.test.tsx
+++ b/app/tests/new-bot-sheet.test.tsx
@@ -161,7 +161,7 @@ test("chooses nothing where the deployment has no Bot of its own", async () => {
const runHere = view.getByRole("button", { name: /Run it here/ });
expect(runHere.hasAttribute("disabled")).toBe(true);
expect(runHere.getAttribute("aria-pressed")).toBe("false");
- expect(runHere.textContent).toContain("has to switch this on");
+ expect(runHere.textContent).toContain("An admin has to turn this on");
expect(
view.getByRole("button", { name: "Continue" }).hasAttribute("disabled"),
).toBe(true);
@@ -217,9 +217,7 @@ test("a framework asks for its address, and refuses an empty one without diallin
await view.findByText("Where can it be reached?");
await user.click(view.getByRole("button", { name: "Continue" }));
- expect(
- await view.findByText("An address is required for an agent you run."),
- ).toBeTruthy();
+ expect(await view.findByText("Enter your agent's address.")).toBeTruthy();
expect(dialled).toEqual([]);
});
diff --git a/app/tests/plugin-read-failure.test.tsx b/app/tests/plugin-read-failure.test.tsx
index e08362272b..03bb085d57 100644
--- a/app/tests/plugin-read-failure.test.tsx
+++ b/app/tests/plugin-read-failure.test.tsx
@@ -226,7 +226,9 @@ test("a connector's admin page says the plugin list failed, not that there is no
const view = renderAdmin(client, `/admin/plugins/${APP_KEY}`);
await waitForFailedRead(client);
- expect(await view.findByText("Plugins could not be loaded.")).toBeTruthy();
+ expect(
+ await view.findByText("Couldn't load plugins. Reload the page."),
+ ).toBeTruthy();
expect(view.queryByText("Not a plugin")).toBeNull();
expect(view.queryByText("Nothing to configure.")).toBeNull();
});
@@ -237,7 +239,9 @@ test("a list that genuinely lacks the connector still says it is not a plugin",
await waitForFailedRead(client);
expect(await view.findByText("Not a plugin")).toBeTruthy();
- expect(view.queryByText("Plugins could not be loaded.")).toBeNull();
+ expect(
+ view.queryByText("Couldn't load plugins. Reload the page."),
+ ).toBeNull();
});
test("a tool's admin page says the plugin list failed, not that the connector is off", async () => {
@@ -248,12 +252,12 @@ test("a tool's admin page says the plugin list failed, not that the connector is
);
await waitForFailedRead(client);
- expect(await view.findByText("Plugins could not be loaded.")).toBeTruthy();
expect(
- view.queryByText("This deployment has not enabled that connector."),
- ).toBeNull();
+ await view.findByText("Couldn't load plugins. Reload the page."),
+ ).toBeTruthy();
+ expect(view.queryByText("This connector isn't turned on.")).toBeNull();
expect(
- view.queryByText("This connector does not advertise a tool by that name."),
+ view.queryByText("This connector has no tool with that name."),
).toBeNull();
});
@@ -265,10 +269,10 @@ test("a list that genuinely lacks the connector still says it is not enabled, on
);
await waitForFailedRead(client);
+ expect(await view.findByText("This connector isn't turned on.")).toBeTruthy();
expect(
- await view.findByText("This deployment has not enabled that connector."),
- ).toBeTruthy();
- expect(view.queryByText("Plugins could not be loaded.")).toBeNull();
+ view.queryByText("Couldn't load plugins. Reload the page."),
+ ).toBeNull();
});
test("a connected account's page says the plugin list failed, not that it is not a service to connect", async () => {
@@ -277,7 +281,5 @@ test("a connected account's page says the plugin list failed, not that it is not
await waitForFailedRead(client);
expect(await view.findByRole("alert")).toBeTruthy();
- expect(
- view.queryByText("This is not a service you connect for yourself."),
- ).toBeNull();
+ expect(view.queryByText("You don't connect this app yourself.")).toBeNull();
});
diff --git a/app/tests/proposed-bot-card.test.tsx b/app/tests/proposed-bot-card.test.tsx
index b9fde26733..b2d13436a4 100644
--- a/app/tests/proposed-bot-card.test.tsx
+++ b/app/tests/proposed-bot-card.test.tsx
@@ -104,7 +104,7 @@ test("a proposal still streaming shows nothing to press", async () => {
// Awaited rather than read synchronously: the router mounts its route on a later tick, so nothing
// at all is in the document on the first one.
- expect(await view.findByText("Writing the coworker…")).toBeTruthy();
+ expect(await view.findByText("Writing the Bot…")).toBeTruthy();
expect(view.queryByRole("button")).toBeNull();
});
diff --git a/app/tests/proposed-skill-card.test.tsx b/app/tests/proposed-skill-card.test.tsx
index 6136defb01..72f3d4e043 100644
--- a/app/tests/proposed-skill-card.test.tsx
+++ b/app/tests/proposed-skill-card.test.tsx
@@ -60,9 +60,7 @@ test("a complete draft shows the whole instruction before anything is written",
).toBeTruthy();
expect(getByText("/weekly-summary")).toBeTruthy();
expect(getByText("google-drive/search_files")).toBeTruthy();
- expect(
- getByText("Nothing is saved until you press the button."),
- ).toBeTruthy();
+ expect(getByText("Nothing is saved until you say so.")).toBeTruthy();
expect(getByRole("button", { name: "Create it" })).toBeTruthy();
});
diff --git a/app/tests/shared-app-notice.test.tsx b/app/tests/shared-app-notice.test.tsx
index 08c7026747..3eb9f38e10 100644
--- a/app/tests/shared-app-notice.test.tsx
+++ b/app/tests/shared-app-notice.test.tsx
@@ -83,7 +83,7 @@ describe("SharedAppNotice", () => {
});
expect(
view.getByText(
- /Team GitHub calls from this Bot are refused until an administrator approves/,
+ /can't use the shared Team GitHub account until an admin approves/,
),
).toBeTruthy();
await userEvent.click(ask);
@@ -98,19 +98,21 @@ describe("SharedAppNotice", () => {
pending = [{ serverId: "gh", id: "r0" }];
const view = draw();
expect(
- await view.findByText("Waiting for an administrator: shared Team GitHub"),
+ await view.findByText(
+ "Waiting for an admin to approve the shared Team GitHub account.",
+ ),
).toBeTruthy();
expect(view.queryByRole("button")).toBeNull();
});
- test("an administrator is told saving approves it", async () => {
+ test("an admin is told saving approves it", async () => {
role = "admin";
apps = [gh(true)];
pending = [];
const view = draw();
expect(
await view.findByText(
- /Saving also approves the shared Team GitHub account for whoever can reach this Bot/,
+ /Saving also lets everyone who can use this Bot use the shared Team GitHub account/,
),
).toBeTruthy();
});
diff --git a/app/tests/shared-grant-dialog.test.tsx b/app/tests/shared-grant-dialog.test.tsx
index 586c123a5c..57171c7b4f 100644
--- a/app/tests/shared-grant-dialog.test.tsx
+++ b/app/tests/shared-grant-dialog.test.tsx
@@ -52,7 +52,7 @@ describe("SharedApprovalFields", () => {
await userEvent.click(view.getByRole("radio", { name: "Only its owner" }));
await userEvent.click(
view.getByRole("checkbox", {
- name: "Outside input (email, Slack, webhooks) may use it",
+ name: "Messages from outside (email, Slack, webhooks) can use it",
}),
);
expect(seen).toEqual({
diff --git a/app/tests/shared-use-requests.test.tsx b/app/tests/shared-use-requests.test.tsx
index 8125172917..917d90fa01 100644
--- a/app/tests/shared-use-requests.test.tsx
+++ b/app/tests/shared-use-requests.test.tsx
@@ -86,7 +86,11 @@ describe("Shared account requests", () => {
).toBeTruthy();
expect(view.getByText("Now: Only its owner")).toBeTruthy();
expect(view.getByText("Asked: Everyone, and outside input")).toBeTruthy();
- expect(view.getByText(/A call was refused/)).toBeTruthy();
+ expect(
+ view.getByText(
+ "An action was blocked. This Bot reaches more people than approved.",
+ ),
+ ).toBeTruthy();
});
test("approving posts once", async () => {
diff --git a/app/tests/skills-read-failure.test.tsx b/app/tests/skills-read-failure.test.tsx
index 2751175a90..a6bcc60556 100644
--- a/app/tests/skills-read-failure.test.tsx
+++ b/app/tests/skills-read-failure.test.tsx
@@ -181,9 +181,11 @@ test("a person's skills page says the skills failed to load, not that they have
await waitForFailedRead(client);
expect(
- await view.findByText("Your skills could not be loaded."),
+ await view.findByText("Couldn't load your skills. Reload the page."),
).toBeTruthy();
- expect(view.queryByText("You don't have any skills yet.")).toBeNull();
+ expect(
+ view.queryByText("No skills yet. Write one with New skill."),
+ ).toBeNull();
});
test("a person with genuinely no skills is still told so", async () => {
@@ -191,8 +193,12 @@ test("a person with genuinely no skills is still told so", async () => {
const view = renderSkills(client);
await waitForFailedRead(client);
- expect(await view.findByText("You don't have any skills yet.")).toBeTruthy();
- expect(view.queryByText("Your skills could not be loaded.")).toBeNull();
+ expect(
+ await view.findByText("No skills yet. Write one with New skill."),
+ ).toBeTruthy();
+ expect(
+ view.queryByText("Couldn't load your skills. Reload the page."),
+ ).toBeNull();
});
test("the admin skills page says the skills failed to load, not that there are none", async () => {
@@ -203,8 +209,12 @@ test("the admin skills page says the skills failed to load, not that there are n
);
await waitForFailedRead(client);
- expect(await view.findByText("Skills could not be loaded.")).toBeTruthy();
- expect(view.queryByText("No skills yet.")).toBeNull();
+ expect(
+ await view.findByText("Couldn't load skills. Reload the page."),
+ ).toBeTruthy();
+ expect(
+ view.queryByText("No skills yet. Write one with Write a skill."),
+ ).toBeNull();
});
test("a deployment with genuinely no skills still says so on the admin page", async () => {
@@ -215,8 +225,10 @@ test("a deployment with genuinely no skills still says so on the admin page", as
);
await waitForFailedRead(client);
- expect(await view.findByText("No skills yet.")).toBeTruthy();
- expect(view.queryByText("Skills could not be loaded.")).toBeNull();
+ expect(
+ await view.findByText("No skills yet. Write one with Write a skill."),
+ ).toBeTruthy();
+ expect(view.queryByText("Couldn't load skills. Reload the page.")).toBeNull();
});
test("opening a skill to edit says it failed to load, not that it no longer exists", async () => {
@@ -224,10 +236,10 @@ test("opening a skill to edit says it failed to load, not that it no longer exis
const view = renderAlone(client, () => );
await waitForFailedRead(client);
- expect(await view.findByText("This skill could not be loaded.")).toBeTruthy();
expect(
- view.queryByText(
- "That skill no longer exists, or it is not yours to edit.",
- ),
+ await view.findByText("Couldn't load this skill. Reload the page."),
+ ).toBeTruthy();
+ expect(
+ view.queryByText("This skill was deleted, or it isn't yours to edit."),
).toBeNull();
});
diff --git a/app/tests/user-preferences.test.tsx b/app/tests/user-preferences.test.tsx
index ea7ec16b56..6b0ddf7986 100644
--- a/app/tests/user-preferences.test.tsx
+++ b/app/tests/user-preferences.test.tsx
@@ -89,7 +89,7 @@ test("shows loading and read errors without enabling a save", async () => {
expect((await view.findByRole("alert")).textContent).toBe(
"Storage unavailable",
);
- expect(view.getByRole("button", { name: "Retry" })).toBeTruthy();
+ expect(view.getByRole("button", { name: "Try again" })).toBeTruthy();
});
test("successful writes update only the current account cache; failures preserve saved values", async () => {
diff --git a/server/src/admin/capabilities.ts b/server/src/admin/capabilities.ts
index be40a260ca..8205d15dce 100644
--- a/server/src/admin/capabilities.ts
+++ b/server/src/admin/capabilities.ts
@@ -171,5 +171,5 @@ export function resolveAllCapabilities(
/** The sentence a refused person reads. */
export function capabilityRefusal(capability: Capability): string {
- return `An administrator has turned off "${CAPABILITY_LABELS[capability].title}" for you, so that was not done.`;
+ return `An admin has turned off "${CAPABILITY_LABELS[capability].title}" for you, so that wasn't done.`;
}
diff --git a/server/src/admin/controls.ts b/server/src/admin/controls.ts
index a2a03d103d..fe456083c6 100644
--- a/server/src/admin/controls.ts
+++ b/server/src/admin/controls.ts
@@ -201,7 +201,7 @@ export function decideFromSnapshot(
if (!decision.allowed) {
return refusal(
`network:${policy.from}`,
- `${decision.reason} An administrator can add it.`,
+ `${decision.reason} An admin can add it.`,
);
}
}
@@ -920,7 +920,7 @@ export async function headlessTurnRefusal(input: {
actorUserId: input.ownerUserId,
payload: { model, source: "configured", headless: true },
}).catch(() => undefined);
- return `This Bot runs on ${model}, which is not on this deployment's model allowlist. An administrator can add it.`;
+ return `This Bot uses ${model}, which isn't an allowed model. Ask an admin to allow it.`;
} catch (error) {
console.error(
JSON.stringify({
diff --git a/server/src/admin/gate.ts b/server/src/admin/gate.ts
index b41fbf6a66..9bf80aade6 100644
--- a/server/src/admin/gate.ts
+++ b/server/src/admin/gate.ts
@@ -275,7 +275,7 @@ async function governRun(
}).catch(() => undefined);
return context.json(
{
- error: `This Bot runs on ${configured}, which is not on this deployment's model allowlist. An administrator can add it.`,
+ error: `This Bot uses ${configured}, which isn't an allowed model. Ask an admin to allow it.`,
},
403,
);
diff --git a/server/src/agents/endpoint.ts b/server/src/agents/endpoint.ts
index e79785249a..52074f657e 100644
--- a/server/src/agents/endpoint.ts
+++ b/server/src/agents/endpoint.ts
@@ -99,12 +99,12 @@ export function checkAgentEndpoint(
return { allowed: true, url: new URL(raw.trim()).toString() };
}
if (!verdict.allowed) {
- // The navigation wording talks about "the assistant opening" a page, which is not what is
+ // The navigation wording talks about "the Bot opening" a page, which is not what is
// happening here, so the reason is restated for the form surface.
return {
allowed: false,
reason: verdict.reason.replace(
- /the assistant is never allowed to open it\.|the assistant is not allowed to open it\./,
+ /the Bot can never open it\.|the Bot can't open it\./,
"an agent may not live there.",
),
};
diff --git a/server/src/agents/lifecycle-routes.ts b/server/src/agents/lifecycle-routes.ts
index 14c08a9b39..e93e50b77f 100644
--- a/server/src/agents/lifecycle-routes.ts
+++ b/server/src/agents/lifecycle-routes.ts
@@ -86,7 +86,7 @@ export function createBotLifecycleRoutes(
if (!agentId.trim())
return context.json({ error: "A Bot id is required." }, 400);
const agent = await profiles.get(context.var.actor, agentId);
- if (!agent) return context.json({ error: "Agent not found." }, 404);
+ if (!agent) return context.json({ error: "Couldn't find that Bot." }, 404);
return agent;
};
diff --git a/server/src/agents/profile-store.ts b/server/src/agents/profile-store.ts
index eda2f224dc..7336a32856 100644
--- a/server/src/agents/profile-store.ts
+++ b/server/src/agents/profile-store.ts
@@ -121,9 +121,7 @@ export class ProtectedAgentError extends Error {
export class ManagedAgentUnavailableError extends Error {
constructor() {
- super(
- "This deployment has no managed Bot. Give the coworker its own AG-UI endpoint.",
- );
+ super("OpenBot has no managed Bot. Give this Bot its own AG-UI endpoint.");
this.name = "ManagedAgentUnavailableError";
}
}
@@ -395,10 +393,8 @@ async function findByTokenHash(
/** Hiding a Team Bot an administrator assigned to this person, which they cannot do. */
export class AgentAssignedError extends Error {
- constructor(id: string) {
- super(
- `Agent ${id} was assigned to you by an administrator and cannot be hidden.`,
- );
+ constructor(_id: string) {
+ super("This Bot was assigned to you by an admin and can't be hidden.");
this.name = "AgentAssignedError";
}
}
diff --git a/server/src/agents/routes.ts b/server/src/agents/routes.ts
index bd716b2deb..b6cb38df45 100644
--- a/server/src/agents/routes.ts
+++ b/server/src/agents/routes.ts
@@ -353,7 +353,7 @@ export function createAgentRoutes(
*/
const agent = await store.get(context.var.actor, agentId);
if (!agent) {
- return context.json({ error: "Agent not found." }, 404);
+ return context.json({ error: "Couldn't find that Bot." }, 404);
}
if (auditStore) {
@@ -416,7 +416,7 @@ export function createAgentRoutes(
context.req.param("agentId"),
);
if (!agent) {
- return context.json({ error: "Agent not found." }, 404);
+ return context.json({ error: "Couldn't find that Bot." }, 404);
}
return context.json({ agent: dto(context.var.actor, agent) });
} catch (error) {
@@ -769,7 +769,8 @@ export function createAgentRoutes(
// Asked of the store, so a Bot somebody may not see is "not found" here as everywhere else,
// rather than a list of who it can reach.
const agent = await store.get(context.var.actor, agentId);
- if (!agent) return context.json({ error: "Agent not found." }, 404);
+ if (!agent)
+ return context.json({ error: "Couldn't find that Bot." }, 404);
return context.json({
handoff: {
enabled: handoff?.enabled ?? false,
@@ -831,17 +832,17 @@ function agentDto(actor: AgentActor, agent: AgentProfile) {
function mapStoreError(context: Context, error: unknown): Response {
if (error instanceof AgentNotFoundError) {
- return context.json({ error: "Agent not found." }, 404);
+ return context.json({ error: "Couldn't find that Bot." }, 404);
}
if (error instanceof AgentNotManageableError) {
+ return context.json({ error: "You can't manage this Bot." }, 403);
+ }
+ if (error instanceof ProtectedAgentError) {
return context.json(
- { error: "You do not have permission to manage this agent." },
+ { error: "This Bot is built in and can't be changed." },
403,
);
}
- if (error instanceof ProtectedAgentError) {
- return context.json({ error: "System-owned agents are protected." }, 403);
- }
if (error instanceof ManagedAgentUnavailableError) {
return context.json({ error: error.message }, 400);
}
diff --git a/server/src/app.ts b/server/src/app.ts
index 6f85443d72..9d26865911 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -571,7 +571,7 @@ export function createApp(
: [];
if (!roles.includes("admin")) {
return context.json(
- { error: "Only an administrator may change identity providers." },
+ { error: "Only an admin can change identity providers." },
403,
);
}
@@ -1016,7 +1016,7 @@ export function createApp(
*/
if (context.var.actor.id === userId && role !== "admin") {
return context.json(
- { error: "You cannot remove your own administrator role." },
+ { error: "You can't remove your own admin role." },
409,
);
}
diff --git a/server/src/auth/email-domain.ts b/server/src/auth/email-domain.ts
index d10efad3be..f18a3d0355 100644
--- a/server/src/auth/email-domain.ts
+++ b/server/src/auth/email-domain.ts
@@ -98,4 +98,4 @@ export function emailDomainAllowed(
/** What a refused person is told. Names the rule, not the list, which is nobody else's business. */
export const DOMAIN_REFUSAL_MESSAGE =
- "This deployment does not admit that email domain. Sign in with your work account, or ask an administrator to add your domain.";
+ "OpenBot doesn't accept that email domain. Sign in with your work account, or ask an admin to add your domain.";
diff --git a/server/src/auth/guards.ts b/server/src/auth/guards.ts
index 79ecc18e7d..e4e6a96283 100644
--- a/server/src/auth/guards.ts
+++ b/server/src/auth/guards.ts
@@ -92,7 +92,7 @@ export function createRequireUser(
export function requireAdmin(context: Context<{ Variables: AppVariables }>) {
if (context.var.actor.role !== "admin") {
- return context.json({ error: "Administrator access required." }, 403);
+ return context.json({ error: "Only an admin can do this." }, 403);
}
return undefined;
diff --git a/server/src/auth/index.ts b/server/src/auth/index.ts
index 99718906b9..d496047db0 100644
--- a/server/src/auth/index.ts
+++ b/server/src/auth/index.ts
@@ -422,7 +422,7 @@ export function createAuth(
},
});
throw new APIError("FORBIDDEN", {
- message: "Your access to this deployment has been removed.",
+ message: "Your access to OpenBot has been removed.",
});
}
return { data: user };
@@ -492,7 +492,7 @@ export function createAuth(
},
});
throw new APIError("FORBIDDEN", {
- message: "Your access to this deployment has been removed.",
+ message: "Your access to OpenBot has been removed.",
});
}
return { data: session };
diff --git a/server/src/channels/attachments.ts b/server/src/channels/attachments.ts
index 98ecc28e6b..c76ae557ec 100644
--- a/server/src/channels/attachments.ts
+++ b/server/src/channels/attachments.ts
@@ -436,8 +436,8 @@ export async function markAttachmentsSent(
const named = unrecorded.map((id) => `"${id}"`).join(", ");
throw new Error(
- `This turn was not run, because ${unrecorded.length === 1 ? "an attachment on your message" : "attachments on your message"} could not be recorded as sent (${named}). ` +
- "A file withdrawn while the turn was being prepared is the usual cause. Nothing was sent to the Bot — attach the file again and resend.",
+ `Your message wasn't sent because ${unrecorded.length === 1 ? "an attachment on it" : "attachments on it"} couldn't be saved (${named}). ` +
+ "Attach the files again and resend.",
);
}
@@ -770,16 +770,13 @@ export function createChannelAttachmentRoutes(
if (!membership) {
return context.json(
- { error: "That file could not be stored just now. Try again." },
+ { error: "Couldn't upload that file. Try again." },
503,
);
}
if (membership.length === 0) {
- return context.json(
- { error: "You are not a member of this channel." },
- 403,
- );
+ return context.json({ error: "You're not in this conversation." }, 403);
}
/*
@@ -816,15 +813,15 @@ export function createChannelAttachmentRoutes(
if (kind === "unsupported-image") {
const error =
mimeType === "image/svg+xml"
- ? `'${name}' is an SVG, which can carry scripts and is not accepted.`
- : `'${name}' is an ${mimeType} image, which this app cannot read.`;
+ ? `'${name}' can't be attached. SVG files aren't accepted.`
+ : `'${name}' can't be attached. OpenBot can't read ${mimeType} images.`;
return context.json({ error }, 415);
}
if (kind === "unsupported") {
return context.json(
{
- error: `'${name}' is not a file type this app can read${describeType(mimeType)}.`,
+ error: `'${name}' isn't a file type OpenBot can read${describeType(mimeType)}.`,
},
415,
);
@@ -1041,7 +1038,7 @@ export function createChannelAttachmentRoutes(
if ("unavailable" in outcome) {
return context.json(
- { error: "That file could not be stored just now. Try again." },
+ { error: "Couldn't upload that file. Try again." },
503,
);
}
@@ -1049,10 +1046,7 @@ export function createChannelAttachmentRoutes(
// The same sentence and the same status as the check at the top of the handler, because from
// the uploader's side it is the same refusal — it just became true later than that check ran.
if ("forbidden" in outcome) {
- return context.json(
- { error: "You are not a member of this channel." },
- 403,
- );
+ return context.json({ error: "You're not in this conversation." }, 403);
}
/*
@@ -1107,7 +1101,7 @@ export function createChannelAttachmentRoutes(
if ("held" in outcome) {
return context.json(
{
- error: `You have ${outcome.held} files waiting to send across your channels, which is as many as one person can hold unsent. Send or remove some before attaching more; any in a channel you can no longer open have to be cleared by whoever runs this deployment.`,
+ error: `You have ${outcome.held} files waiting to send across your conversations, which is the limit. Send or remove some first. Files in a conversation you can't open anymore have to be cleared by an admin.`,
},
409,
);
@@ -1330,7 +1324,7 @@ function noSuchAttachment(context: AttachmentContext) {
*/
function couldNotReadAttachment(context: AttachmentContext) {
return context.json(
- { error: "That attachment could not be read just now. Try again." },
+ { error: "Couldn't open that attachment. Try again." },
503,
);
}
@@ -1717,7 +1711,7 @@ export function createAttachmentRoutes(
if (!removed) {
return context.json(
{
- error: "That attachment could not be withdrawn just now. Try again.",
+ error: "Couldn't remove that attachment. Try again.",
},
503,
);
@@ -1782,7 +1776,7 @@ export function createAttachmentRoutes(
if (!rows) {
return context.json(
{
- error: "That attachment could not be withdrawn just now. Try again.",
+ error: "Couldn't remove that attachment. Try again.",
},
503,
);
diff --git a/server/src/channels/routes.ts b/server/src/channels/routes.ts
index 0ea077fb48..2798aca78a 100644
--- a/server/src/channels/routes.ts
+++ b/server/src/channels/routes.ts
@@ -1229,7 +1229,7 @@ export function createChannelRoutes(
context.req.param("channelId"),
);
if (!channel) {
- return context.json({ error: "Channel not found." }, 404);
+ return context.json({ error: "Couldn't find that conversation." }, 404);
}
return context.json({ channel: channelDto(channel) });
} catch (error) {
@@ -1272,16 +1272,15 @@ function channelSummaryDto(channel: ChannelSummary) {
function mapStoreError(context: Context, error: unknown): Response {
if (error instanceof AgentNotFoundError) {
- return context.json({ error: "Agent not found." }, 404);
+ return context.json({ error: "Couldn't find that Bot." }, 404);
}
if (error instanceof ChannelNotFoundError) {
- return context.json({ error: "Channel not found." }, 404);
+ return context.json({ error: "Couldn't find that conversation." }, 404);
}
if (error instanceof ChannelPackageOwnedError) {
return context.json(
{
- error:
- "This channel is defined by the deployment package, so it cannot be deleted here.",
+ error: "This conversation is built in and can't be deleted.",
},
409,
);
diff --git a/server/src/components/headless.ts b/server/src/components/headless.ts
index 1b629101e4..7ec33d2396 100644
--- a/server/src/components/headless.ts
+++ b/server/src/components/headless.ts
@@ -427,7 +427,7 @@ export async function createHeadlessComponentTools(options: {
if (await store.mayCall(spec.name, functionName)) continue;
return refuse(
spec,
- `${spec.name} has not been granted the function ${functionName}. An administrator grants each function to each component.`,
+ `${spec.name} can't use the function ${functionName}. An admin has to give it to this component.`,
{ function: functionName },
);
}
diff --git a/server/src/components/routes.ts b/server/src/components/routes.ts
index 8c0e95a8e9..c8c1848c61 100644
--- a/server/src/components/routes.ts
+++ b/server/src/components/routes.ts
@@ -252,7 +252,7 @@ export function createComponentRoutes(
*/
for (const functionName of functions) {
if (await store.mayCall(name, functionName)) continue;
- const reason = `${name} has not been granted the function ${functionName}. An administrator grants each function to each component.`;
+ const reason = `${name} can't use the function ${functionName}. An admin has to give it to this component.`;
await audit(context, "component.function_refused", name, {
bot: agentId,
function: functionName,
@@ -345,7 +345,7 @@ export function createComponentRoutes(
if (!(await store.mayCall(name, functionName))) {
return refuse(
- `${name} has not been granted the function ${functionName}. An administrator grants each function to each component.`,
+ `${name} can't use the function ${functionName}. An admin has to give it to this component.`,
);
}
diff --git a/server/src/computer/client.ts b/server/src/computer/client.ts
index 9ddf7a46f3..fbc8d59fe1 100644
--- a/server/src/computer/client.ts
+++ b/server/src/computer/client.ts
@@ -233,8 +233,8 @@ export function createComputerTransport(
}
throw new ComputerUnavailableError(
error instanceof Error && error.name === "TimeoutError"
- ? "The assistant's computer did not respond in time."
- : "The assistant's computer is not running.",
+ ? "The Bot's computer didn't respond in time."
+ : "The Bot's computer isn't running.",
);
}
return response;
@@ -298,7 +298,7 @@ export function createComputerTransport(
// fails must not replace the refusal the caller is told about.
await response.body?.cancel().catch(() => undefined);
throw new ComputerUnavailableError(
- "The assistant's computer returned an invalid file download.",
+ "The Bot's computer sent back a file that couldn't be read.",
);
}
return { body: response.body, bytes };
diff --git a/server/src/computer/routes.ts b/server/src/computer/routes.ts
index 181de16660..c88016e886 100644
--- a/server/src/computer/routes.ts
+++ b/server/src/computer/routes.ts
@@ -368,7 +368,7 @@ export function createComputerRoutes(
actor,
typeof body?.reason === "string" && body.reason.trim()
? body.reason.trim()
- : "The assistant needs a person to continue.",
+ : "It can't continue without you.",
typeof body?.toolCallId === "string" ? body.toolCallId : undefined,
),
),
@@ -732,7 +732,7 @@ export function createComputerRoutes(
return context.json(
{
error:
- "That rule could not be saved, so it has not been applied. The previous boundary is still in force.",
+ "Couldn't save that rule. The previous boundary still applies.",
},
503,
);
diff --git a/server/src/computer/target.ts b/server/src/computer/target.ts
index e6f435e37b..2550e57d6b 100644
--- a/server/src/computer/target.ts
+++ b/server/src/computer/target.ts
@@ -271,7 +271,7 @@ export function checkNavigationTarget(
return {
allowed: false,
reason:
- "That address holds this deployment's own cloud credentials, so the assistant is never allowed to open it.",
+ "That address holds OpenBot's own cloud keys, so the Bot can never open it.",
};
}
@@ -290,7 +290,7 @@ export function checkNavigationTarget(
return {
allowed: false,
reason:
- "That address is inside this deployment's own network, so the assistant is not allowed to open it.",
+ "That address is inside OpenBot's own network, so the Bot can't open it.",
};
}
diff --git a/server/src/dictation/provider.ts b/server/src/dictation/provider.ts
index 5a59d1f826..ea5e4400b0 100644
--- a/server/src/dictation/provider.ts
+++ b/server/src/dictation/provider.ts
@@ -37,7 +37,7 @@ export function createTranscriptionProvider(
throw new TranscriptionError(
response.status === 429
? "The transcription service is busy. Please retry shortly."
- : "The transcription service could not process this recording. Retry or contact your administrator.",
+ : "Couldn't process this recording. Try again, or ask an admin if it keeps happening.",
);
}
const result: unknown = await response.json().catch(() => null);
diff --git a/server/src/dictation/routes.ts b/server/src/dictation/routes.ts
index a593607b51..5c39e56577 100644
--- a/server/src/dictation/routes.ts
+++ b/server/src/dictation/routes.ts
@@ -24,13 +24,15 @@ export function createDictationRoutes(
return context.json({ error: "Invalid dictation request." }, 403);
}
if (!provider)
- return context.json({ error: "Dictation is not configured." }, 503);
+ return context.json(
+ { error: "Dictation isn't set up yet. Ask an admin to turn it on." },
+ 503,
+ );
const actorId = context.var.actor.id;
if (active.has(actorId) || active.size >= 8) {
return context.json(
{
- error:
- "A transcription is already in progress. Please retry shortly.",
+ error: "A transcription is already running. Try again in a moment.",
},
429,
);
@@ -71,7 +73,10 @@ export function createDictationRoutes(
);
}
if (!provider)
- return context.json({ error: "Dictation is not configured." }, 503);
+ return context.json(
+ { error: "Dictation isn't set up yet. Ask an admin to turn it on." },
+ 503,
+ );
const signal = AbortSignal.any([
context.req.raw.signal,
AbortSignal.timeout(DICTATION_TIMEOUT_MS),
@@ -92,7 +97,7 @@ export function createDictationRoutes(
if (signal.aborted) {
return context.json(
{
- error: "Transcription timed out or was cancelled. Please retry.",
+ error: "Transcription timed out or was cancelled. Try again.",
},
504,
);
@@ -102,7 +107,7 @@ export function createDictationRoutes(
error:
error instanceof TranscriptionError
? error.message
- : "The transcription service could not be reached. Please retry.",
+ : "Couldn't reach the transcription service. Try again.",
},
502,
);
diff --git a/server/src/learning/routes.ts b/server/src/learning/routes.ts
index a577b8fa70..b380695364 100644
--- a/server/src/learning/routes.ts
+++ b/server/src/learning/routes.ts
@@ -62,7 +62,7 @@ export function createLearningRoutes(
return new Response(
JSON.stringify({
error:
- "Learning settings could not be accessed. Try again or contact your administrator.",
+ "Couldn't load learning settings. Try again, or ask an admin if it keeps happening.",
}),
{ status: 503, headers: { "content-type": "application/json" } },
);
diff --git a/server/src/memory/ingestion.ts b/server/src/memory/ingestion.ts
index 99647e3da3..205fdc95e4 100644
--- a/server/src/memory/ingestion.ts
+++ b/server/src/memory/ingestion.ts
@@ -239,7 +239,7 @@ export function createMemoryIngestion(options: {
!(await plugins.deploymentConnectionFor(server.id))
) {
throw new MemoryRefusedError(
- `${server.title} is shared across this deployment and no account is connected to it yet. Ask an administrator to connect it.`,
+ `${server.title} uses a shared account, and nobody has connected it yet. Ask an admin to connect it.`,
);
}
if (
@@ -304,7 +304,7 @@ export function createMemoryIngestion(options: {
if (!prompt) return text;
switch (prompt.reason) {
case "shared":
- return `The shared ${prompt.title} account is not connected. An administrator has to connect it before this source can sync.`;
+ return `The shared ${prompt.title} account isn't connected. An admin has to connect it before this source can sync.`;
case "reconnect":
return `Your ${prompt.title} account needs reconnecting. Reconnect it under Connections, then sync again.`;
default:
diff --git a/server/src/plugins/broker.ts b/server/src/plugins/broker.ts
index caa69f7e49..bdd76226e0 100644
--- a/server/src/plugins/broker.ts
+++ b/server/src/plugins/broker.ts
@@ -903,7 +903,7 @@ export class BrokerUnconfiguredError extends BrokerRefusalError {
const { vendor, envVar } = BROKERED_PROFILE[provider];
super(
reader === "person"
- ? "Connected apps are not set up for this deployment, so nothing was asked. An administrator can set them up."
+ ? "Connecting apps isn't set up yet, so nothing was asked. Ask an admin to set it up."
: `${vendor} is not configured for this deployment, so nothing was asked. Set ${envVar} to make the brokered apps available; until it is set there is nothing to connect, nothing to grant and no ${vendor} tool for a Bot to call, and the admin Plugins page shows one row under More apps that goes nowhere.`,
);
this.name = "BrokerUnconfiguredError";
@@ -970,8 +970,8 @@ export class BrokerReturnUrlError extends BrokerRefusalError {
const { vendor } = BROKERED_PROFILE[provider];
super(
address === "unbuilt"
- ? "This deployment built no address to send you back to, so connecting was not begun rather than begun with nowhere to land. Nothing was asked of your account; an administrator can set this deployment's address up."
- : "The address this deployment would send you back to is not a web address, so a consent granted now would end on a page with no way back here. Nothing was asked of your account; an administrator can correct this deployment's address.",
+ ? "OpenBot has no address to bring you back to, so connecting didn't start. Nothing was asked of your account. Ask an admin to set OpenBot's address."
+ : "OpenBot's return address isn't a web address, so connecting didn't start. Nothing was asked of your account. Ask an admin to fix OpenBot's address.",
);
this.operatorMessage =
address === "unbuilt"
diff --git a/server/src/plugins/composio.ts b/server/src/plugins/composio.ts
index 7a8dc07271..10e77dbfc5 100644
--- a/server/src/plugins/composio.ts
+++ b/server/src/plugins/composio.ts
@@ -1210,7 +1210,7 @@ function thrownSentence(error: unknown): string | null {
* is a person's own two-click fix on the page named here.
*/
export function unexplained(toolName: string): string {
- return `${toolName} failed and Composio did not say why. Check that this app is still connected on its Plugins page, then try again.`;
+ return `${toolName} failed without saying why. Check that the app is still connected on Connections, then try again.`;
}
/**
@@ -1657,7 +1657,7 @@ export async function askAction(
}
if (!installed) {
return unreached(
- "Composio is not configured for this deployment, so this action cannot be called.",
+ "Apps like this aren't set up yet, so this action can't be called. An admin has to turn them on.",
);
}
diff --git a/server/src/plugins/mcp.ts b/server/src/plugins/mcp.ts
index 5134c8e60d..49d9db9959 100644
--- a/server/src/plugins/mcp.ts
+++ b/server/src/plugins/mcp.ts
@@ -299,7 +299,7 @@ function vendorFailure(error: unknown): string {
: undefined;
if (status === 401) {
- return "The vendor rejected this credential (401). For a connector reached as the person asking, reconnecting the account is the usual fix; if it persists, the scopes it was granted may not cover this server.";
+ return "The app turned down this account (401). Reconnect the account and try again. If it keeps happening, the account may not have access to this app.";
}
if (status === 403) {
/*
@@ -321,8 +321,8 @@ function vendorFailure(error: unknown): string {
*/
const detail = reasonFrom(error);
return detail
- ? `The vendor accepted the credential and refused the request (403). It said: ${detail}`
- : "The vendor accepted the credential and refused the request (403). The account may lack access, or the API may not be enabled for this project.";
+ ? `The app refused the request (403). It said: ${detail}`
+ : "The app refused the request (403). The account may not have access, or the API may not be turned on for this project.";
}
if (typeof status === "number") {
return `The vendor answered ${status}.`;
diff --git a/server/src/plugins/plugin-store/calls.ts b/server/src/plugins/plugin-store/calls.ts
index 97e6700f3e..fb618cbe42 100644
--- a/server/src/plugins/plugin-store/calls.ts
+++ b/server/src/plugins/plugin-store/calls.ts
@@ -337,7 +337,7 @@ export function callMethods(context: StoreContext) {
});
throw new PluginRefusedError(
contentDecision.reason === "sensitive_content"
- ? "The tool call was refused because its arguments contain credential material."
+ ? "The tool call was refused because its arguments contain a password or key."
: "The tool call was refused because its arguments could not be inspected safely.",
null,
);
@@ -416,7 +416,7 @@ export function callMethods(context: StoreContext) {
})
: {
allowed: false as const,
- message: `${row.title} is shared, and this deployment cannot check who may use it, so it was not called.`,
+ message: `${row.title} is shared, and OpenBot can't check who may use it, so it wasn't called.`,
};
if (!verdict.allowed) {
await recordAuditEvent(auditStore, {
@@ -615,8 +615,8 @@ export function callMethods(context: StoreContext) {
title: row.title,
message:
gatedMode === "shared"
- ? `The shared ${row.title} account is no longer connected at the vendor, so this was not done. An administrator has to connect it again.`
- : `Your ${row.title} account is no longer connected at the vendor, so this was not done. Reconnect it and ask again.`,
+ ? `The shared ${row.title} account is no longer connected, so this wasn't done. Ask an admin to reconnect it.`
+ : `Your ${row.title} account is no longer connected, so this wasn't done. Reconnect it and ask again.`,
reason: gatedMode === "shared" ? "shared" : "reconnect",
at: new Date().toISOString(),
}),
diff --git a/server/src/plugins/plugin-store/context.ts b/server/src/plugins/plugin-store/context.ts
index dfd905e1e5..6f42bf0e6d 100644
--- a/server/src/plugins/plugin-store/context.ts
+++ b/server/src/plugins/plugin-store/context.ts
@@ -262,7 +262,7 @@ export function createStoreContext(options: PluginStoreOptions) {
.limit(1);
if (!dialled)
return {
- refusal: `${serverId} is not an app this deployment has added.`,
+ refusal: `OpenBot has no app called ${serverId}.`,
};
/*
* THE NAME IS THE DIALLED ROW'S; THE ACCOUNT IS THE APP'S.
@@ -289,7 +289,7 @@ export function createStoreContext(options: PluginStoreOptions) {
const app = toolkit ? await brokeredAppRow(toolkit) : null;
if (toolkit && !app)
return {
- refusal: `${serverId} is not an app this deployment has added.`,
+ refusal: `OpenBot has no app called ${serverId}.`,
};
const row = app
? {
@@ -675,7 +675,7 @@ export function createStoreContext(options: PluginStoreOptions) {
? connectAppRefusal({
serverId: row.id,
title: row.title,
- message: `${row.title} is shared across this deployment and no account is connected to it yet. Ask an administrator to connect it.`,
+ message: `${row.title} uses a shared account, and nobody has connected it yet. Ask an admin to connect it.`,
reason: "shared",
})
: connectAppRefusal({
@@ -698,7 +698,7 @@ export function createStoreContext(options: PluginStoreOptions) {
const token = row.credentialId
? await secretFor(
row.credentialId,
- `${row.id} needs a credential this deployment no longer holds. An administrator has to add it again.`,
+ `${row.id} is missing its key, so this can't be done. Ask an admin to add it again.`,
)
: undefined;
return { token };
@@ -789,11 +789,11 @@ export function createStoreContext(options: PluginStoreOptions) {
* no step to take, which is worse than saying nothing.
*/
const noClient = registrationUrl
- ? `${title} has no OAuth client for this deployment, so this cannot be called. Connect ${title} again in Settings: the deployment registers itself with the vendor when somebody connects.`
- : `${title} has no OAuth client registered for this deployment, so this cannot be called. An administrator has to add one.`;
+ ? `${title} needs connecting again, so this can't be done. Reconnect it on Connections and ask again.`
+ : `${title} isn't set up yet, so this can't be done. Ask an admin to finish setting it up.`;
const unusableClient = registrationUrl
- ? `${title} has no usable OAuth client for this deployment. Connect ${title} again in Settings: the deployment registers itself with the vendor on the next connect.`
- : `${title} has no usable OAuth client for this deployment. An administrator has to add one again.`;
+ ? `${title} needs connecting again. Reconnect it on Connections and ask again.`
+ : `${title} isn't set up correctly. Ask an admin to set it up again.`;
/**
* What to say when the vendor has forgotten the client this person's grant was issued under.
*
@@ -807,7 +807,7 @@ export function createStoreContext(options: PluginStoreOptions) {
* refuses — and one that only ever appears to work against a vendor whose acceptance would
* itself be the vulnerability. A new consent is the only thing that produces a usable grant.
*/
- const clientReplaced = `${title} no longer recognises this deployment's OAuth client, so this cannot be called. The deployment has registered itself again — connect ${title} again in Settings.`;
+ const clientReplaced = `${title} needs connecting again, so this wasn't done. Reconnect it on Connections and ask again.`;
if (!row.credentialId) {
// The person did their part; the deployment has not. Refused before anything queues, because
diff --git a/server/src/plugins/route-groups/app-access.ts b/server/src/plugins/route-groups/app-access.ts
index 684591c0f6..a3c2364ccb 100644
--- a/server/src/plugins/route-groups/app-access.ts
+++ b/server/src/plugins/route-groups/app-access.ts
@@ -67,18 +67,18 @@ function unavailableSentence(
): string {
switch (reason) {
case "absent":
- return `${title} is not an app this deployment can switch on from here.`;
+ return `${title} can't be turned on from here.`;
case "composio_unconfigured": {
const { vendor, envVar } = BROKERED_PROFILE[provider];
const article = /^[aeiou]/i.test(vendor) ? "an" : "a";
- return `${title} is ${article} ${vendor} app, and this deployment has no ${vendor} key. Set ${envVar}, then approve this request again.`;
+ return `${title} is ${article} ${vendor} app, and ${envVar} isn't set. Set it, then approve this request again.`;
}
case "scheme_unreadable":
- return `${title} is enabled, but how it connects is not recorded in a way this deployment can read. Remove it and enable it again on its page under Plugins, then approve this request again.`;
+ return `${title} is turned on, but OpenBot can't read how it connects. Remove it and turn it on again under Plugins, then approve this request again.`;
case "oauth_client_missing":
- return `${title} is added, but has no OAuth client yet, so nobody can connect it. Register one on its page under Plugins, then approve this request again.`;
+ return `${title} has no OAuth client yet. Register one on its page under Plugins, then approve this request again.`;
case "token_missing":
- return `${title} is added, but has no token yet. Add one on its page under Plugins, then approve this request again.`;
+ return `${title} has no token yet. Add one on its page under Plugins, then approve this request again.`;
}
}
@@ -194,10 +194,7 @@ export function mountAppAccessRoutes({
sharedAudience,
});
if (!inspected)
- return context.json(
- { error: `This deployment has no app called ${app}.` },
- 404,
- );
+ return context.json({ error: `OpenBot has no app called ${app}.` }, 404);
return context.json({ app: inspected.access });
});
@@ -227,7 +224,7 @@ export function mountAppAccessRoutes({
if (agentId.length > MAX_NAME || app.length > MAX_NAME)
return context.json(
{
- error: `A Bot or an app is named in at most ${MAX_NAME} characters.`,
+ error: `Bot and app names are ${MAX_NAME} characters at most.`,
},
400,
);
@@ -254,7 +251,7 @@ export function mountAppAccessRoutes({
app,
bot: agentId,
reason: "no_such_app",
- error: `This deployment has no app called ${app}.`,
+ error: `OpenBot has no app called ${app}.`,
status: 404,
});
const { access } = inspected;
@@ -265,8 +262,8 @@ export function mountAppAccessRoutes({
reason: `nothing_to_request:${access.state}`,
error:
access.state === "ready"
- ? `${access.displayName} is ready to use. There is nothing to ask an administrator for.`
- : `${access.displayName} is waiting on your own account, not on an administrator. Connect it yourself.`,
+ ? `${access.displayName} is ready to use. There's nothing to ask an admin for.`
+ : `${access.displayName} needs your own account, not an admin. Connect it yourself.`,
status: 409,
});
/*
@@ -283,7 +280,7 @@ export function mountAppAccessRoutes({
bot: agentId,
reason: "too_many_pending",
kind: access.requestKind,
- error: `You already have ${MAX_PENDING_REQUESTS} requests waiting for an administrator. Withdraw one, or wait for them to be answered, then ask again.`,
+ error: `You already have ${MAX_PENDING_REQUESTS} requests waiting for an admin. Withdraw one, or wait for an answer, then ask again.`,
status: 429,
});
const filed = await store.fileAppAccessRequest({
@@ -425,7 +422,7 @@ export function mountAppAccessRoutes({
if (!inspected)
return deny(
"no_such_app",
- `${request.appTitle} is no longer an app this deployment can find.`,
+ `${request.appTitle} isn't available anymore.`,
409,
);
@@ -461,7 +458,7 @@ export function mountAppAccessRoutes({
if (needed)
return deny(
"needs_admin_input",
- `${title} needs ${needed}, which only an administrator can supply. Add it on its page under Plugins, then approve this request again.`,
+ `${title} needs ${needed}. Add it on its page under Plugins, then approve this request again.`,
409,
);
try {
@@ -491,7 +488,7 @@ export function mountAppAccessRoutes({
if (!inspected || inspected.unavailable)
return deny(
"enable_failed",
- `${title} was switched on and still cannot be used. Open its page under Plugins to see why.`,
+ `${title} was turned on but still can't be used. Open its page under Plugins to see why.`,
409,
);
}
@@ -521,13 +518,13 @@ export function mountAppAccessRoutes({
if (advertised.length === 0)
return deny(
"no_tools",
- `${name} advertises no tools to grant yet. Refresh its tools on its page under Plugins, then approve this request again.`,
+ `${name} has no tools to give yet. Refresh its tools on its page under Plugins, then approve this request again.`,
409,
);
if (refs.length === 0)
return deny(
"no_read_only_tools",
- `None of ${name}'s actions is marked read-only, so approving here would grant nothing. Choose which actions this Bot may use on ${name}'s page for it under Plugins, then approve this request again.`,
+ `None of ${name}'s actions are marked read-only, so approving here gives this Bot nothing. Choose its actions on ${name}'s page under Plugins, then approve this request again.`,
409,
);
for (const ref of refs) {
diff --git a/server/src/plugins/route-groups/connections.ts b/server/src/plugins/route-groups/connections.ts
index 9e104d1088..51883c0422 100644
--- a/server/src/plugins/route-groups/connections.ts
+++ b/server/src/plugins/route-groups/connections.ts
@@ -48,7 +48,6 @@ export function mountConnectionRoutes({
connect,
composio,
activeProvider,
- activeVendor,
appOfUrl,
actorEmail,
}: PluginRouteContext) {
@@ -180,7 +179,7 @@ export function mountConnectionRoutes({
if (schemeKind(row.authScheme) === "none") {
return context.json(
{
- error: `${row.title} needs no account, so there is nothing to connect. A Bot granted its tools can use it as it is.`,
+ error: `${row.title} needs no account, so there's nothing to connect. Bots that can use it already work.`,
},
400,
);
@@ -364,7 +363,7 @@ export function mountConnectionRoutes({
} catch (error) {
const refusal = brokerRefusal(
error,
- `${activeVendor} would not say what ${row.title} asks for, and said nothing about why. Try again, and ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ `Couldn't find out what ${row.title} needs to connect. Try again, or ask an admin if it keeps happening.`,
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -394,7 +393,7 @@ export function mountConnectionRoutes({
if (published.length === 0) {
return context.json(
{
- error: `${row.title} publishes no boxes to fill in, so there is nothing you could type that would reach it and no account was made. An administrator has to look at how ${row.title} is set up at ${activeVendor}; removing it on the Plugins page and adding it again is what records the way ${activeVendor} connects it now.`,
+ error: `${row.title} doesn't ask for anything to connect with, so no account was made. Ask an admin to remove it under Plugins and add it again.`,
},
400,
);
@@ -472,7 +471,7 @@ export function mountConnectionRoutes({
const optional = published.map((field) => field.name).join(", ");
return context.json(
{
- error: `${row.title} publishes no box it says has to be filled in — only ${optional} — so nothing you could type would be a credential it insists on, and no account was made. An app that genuinely needs none is one ${activeVendor} publishes as needing no authentication, and ${row.title} is recorded here as an app whose secret you type. An administrator has to look at how ${row.title} is set up at ${activeVendor}; removing it on the Plugins page and adding it again is what records the way ${activeVendor} connects it now.`,
+ error: `${row.title} marks nothing as required (only ${optional}), so no account was made. Ask an admin to remove it under Plugins and add it again.`,
},
400,
);
@@ -538,7 +537,7 @@ export function mountConnectionRoutes({
*/
return context.json(
{
- error: `That is not the form ${row.title} publishes, so nothing was sent to ${activeVendor}. Press Connect again to draw it from what the app asks for now, and fill in the boxes it shows; each one holds text.`,
+ error: `That isn't the form ${row.title} asks for, so nothing was sent. Press Connect again and fill in the form it shows.`,
},
400,
);
@@ -586,7 +585,7 @@ export function mountConnectionRoutes({
if (missing.length > 0) {
return context.json(
{
- error: `${row.title} cannot be connected without ${missing.join(", ")}, so nothing was sent to ${activeVendor}. Fill in every box the form marks required — a box holding only spaces is an empty one — and press Connect again.`,
+ error: `${row.title} needs ${missing.join(", ")}, so nothing was sent. Fill in every required box and press Connect again.`,
},
400,
);
@@ -627,7 +626,7 @@ export function mountConnectionRoutes({
}
const refusal = brokerRefusal(
error,
- `${row.title} could not be connected with what you entered, and ${activeVendor} said nothing about why. Try again, and ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ `Couldn't connect ${row.title} with what you entered. Try again, or ask an admin if it keeps happening.`,
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -654,7 +653,7 @@ export function mountConnectionRoutes({
if (kind === "unreadable") {
return context.json(
{
- error: `This deployment cannot tell how ${row.title} connects, so nothing was sent to ${activeVendor} and no account was made. An administrator has to remove it on the Plugins page and add it again, which records the way ${activeVendor} connects it now.`,
+ error: `OpenBot can't tell how ${row.title} connects, so no account was made. Ask an admin to remove it under Plugins and add it again.`,
},
400,
);
@@ -688,7 +687,8 @@ export function mountConnectionRoutes({
if (!connect?.appUrl) {
return context.json(
{
- error: `This deployment has no app URL configured, so ${activeVendor} would have nowhere to send you back to. Set OPENBOT_APP_URL.`,
+ error:
+ "Connecting apps isn't set up yet. Ask an admin to set OPENBOT_APP_URL.",
},
503,
);
@@ -760,7 +760,7 @@ export function mountConnectionRoutes({
logReturnUrlRefusal(error, serverId);
const refusal = brokerRefusal(
error,
- `${activeVendor} would not begin a connection to ${row.title}, and said nothing about why. Try again, and ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ `Couldn't start connecting ${row.title}. Try again, or ask an admin if it keeps happening.`,
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -771,7 +771,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "This deployment has no public URL configured, so it cannot complete a consent flow. Set OPENBOT_PUBLIC_URL.",
+ "Connecting apps isn't set up yet. Ask an admin to set OPENBOT_PUBLIC_URL.",
},
503,
);
@@ -811,7 +811,7 @@ export function mountConnectionRoutes({
if (error instanceof CatalogueEntryUnknownError) {
return context.json(
{
- error: `${entry.title} has not been added to this deployment yet. An administrator has to add it first.`,
+ error: `${entry.title} hasn't been added yet. Ask an admin to add it.`,
},
409,
);
@@ -822,14 +822,14 @@ export function mountConnectionRoutes({
if (entry.auth.clientRegistration === "dynamic") {
return context.json(
{
- error: `${entry.title} would not register this deployment, or could not be reached. Try again, and check the vendor's status if it persists.`,
+ error: `Couldn't set up ${entry.title}. Try again, or check ${entry.title}'s status if it keeps happening.`,
},
502,
);
}
return context.json(
{
- error: `${entry.title} has no OAuth client registered yet. An administrator has to add one first.`,
+ error: `${entry.title} isn't set up yet. Ask an admin to finish setting it up.`,
},
409,
);
@@ -929,7 +929,7 @@ export function mountConnectionRoutes({
if (!toolkit) {
return {
refusal: {
- error: "That app is not reached through a broker.",
+ error: "That app doesn't connect this way.",
status: 400,
},
};
@@ -953,7 +953,7 @@ export function mountConnectionRoutes({
) {
return {
refusal: {
- error: "An administrator connects shared apps.",
+ error: "Only an admin can connect shared apps.",
status: 403,
},
};
@@ -1017,7 +1017,7 @@ export function mountConnectionRoutes({
action: toolkit,
state: "unknown",
reason:
- "This deployment could not read what this account holds just now, so nothing is said about it either way. Try again in a moment.",
+ "Couldn't read what this account allows right now. Try again in a moment.",
},
];
}
@@ -1096,7 +1096,7 @@ export function mountConnectionRoutes({
*/
const refusal = brokerRefusal(
error,
- `${activeVendor} would not say whether this account is connected, and gave no reason, so what is shown here is the last answer it gave rather than a fresh one. Try again, and ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ "Couldn't check whether this account is connected. This shows the last answer. Try again, or ask an admin if it keeps happening.",
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -1184,7 +1184,7 @@ export function mountConnectionRoutes({
*/
const refusal = brokerRefusal(
error,
- `${activeVendor} would not say whether this connection still works, and gave no reason, so what is shown here is the last answer it gave rather than a fresh one. Press Re-check again, and ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ "Couldn't check whether this connection still works. This shows the last check. Press Re-check again, or ask an admin if it keeps happening.",
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -1251,7 +1251,7 @@ export function mountConnectionRoutes({
*/
const refusal = brokerRefusal(
error,
- `${activeVendor} would not end this account, and gave no reason. Press Disconnect again: the revoke at ${activeVendor} runs before anything here is deleted, so repeating it is safe and is the whole recovery. Ask an administrator to check this deployment's ${activeVendor} key if it persists.`,
+ "Couldn't disconnect this account. Press Disconnect again. It's safe to repeat. Ask an admin if it keeps happening.",
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -1296,7 +1296,7 @@ export function mountConnectionRoutes({
} catch (error) {
const refusal = brokerRefusal(
error,
- "This deployment could not work out which other apps share this connection, and the vendor said nothing about why. Try again.",
+ "Couldn't work out which other apps share this connection. Try again.",
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -1344,7 +1344,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "This deployment's apps are connected once for the whole app, with the Connect button on the app's page; they are not authorized one action at a time.",
+ "Apps here connect once, with Connect on the app's page. They aren't authorized one action at a time.",
},
503,
);
@@ -1370,7 +1370,7 @@ export function mountConnectionRoutes({
!(await store.serverPublishesAction(serverId, action))
) {
return context.json(
- { error: `${title} does not publish an action by that name.` },
+ { error: `${title} has no action by that name.` },
400,
);
}
@@ -1379,7 +1379,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "This deployment has no app URL configured, so there is nowhere to send you back to. Set OPENBOT_APP_URL.",
+ "Connecting apps isn't set up yet. Ask an admin to set OPENBOT_APP_URL.",
},
503,
);
@@ -1396,7 +1396,7 @@ export function mountConnectionRoutes({
logReturnUrlRefusal(error, serverId);
const refusal = brokerRefusal(
error,
- "This deployment could not build the address to send you back to, so nothing was started.",
+ "Couldn't build the address to bring you back to, so nothing was started.",
);
return context.json({ error: refusal.error }, refusal.status);
}
@@ -1506,13 +1506,13 @@ export function mountConnectionRoutes({
}
const refusal = brokerRefusal(
error,
- "The app's vendor refused to start that authorization, so nothing was started. Try again in a moment.",
+ "The app refused to start that authorization. Try again in a moment.",
);
return context.json(
{
error: freed
? refusal.error
- : `${refusal.error} The attempt could not be cleared either; it expires within fifteen minutes, and starting over works after that.`,
+ : `${refusal.error} This attempt expires within fifteen minutes. You can start over after that.`,
},
refusal.status,
);
@@ -1590,7 +1590,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "The app's vendor did not answer clearly, so nobody was sent to a consent screen and nothing was connected. Whether an authorization started at the vendor is unknown, and one that did cannot be finished from here. This attempt expires within fifteen minutes, and starting over works after that.",
+ "The app didn't answer clearly, so nobody was sent to a consent screen and nothing was connected. This attempt expires within fifteen minutes. You can start over after that.",
},
502,
);
@@ -1659,8 +1659,8 @@ export function mountConnectionRoutes({
return context.json(
{
error: freed
- ? "The app's vendor started that authorization, and this deployment could not record it — so it cannot be finished here and nothing was connected. Press Authorize again: the attempt was cleared, so starting over is safe."
- : "The app's vendor started that authorization, and this deployment could not record it — so it cannot be finished here and nothing was connected. The attempt could not be cleared either; it expires within fifteen minutes, and starting over works after that.",
+ ? "The app started that authorization, but OpenBot couldn't record it, so nothing was connected. Press Authorize again. It's safe to start over."
+ : "The app started that authorization, but OpenBot couldn't record it, so nothing was connected. This attempt expires within fifteen minutes. You can start over after that.",
},
500,
);
@@ -1691,7 +1691,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "That authorization was cancelled before the app's vendor finished starting it, so you were not sent on and nothing was connected. Press Authorize to start again.",
+ "That authorization was cancelled before it started, so nothing was connected. Press Authorize to start again.",
},
409,
);
@@ -1797,7 +1797,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "That authorization finished before it could be cancelled, so the account is connected. Disconnect it if you did not mean to connect it.",
+ "That authorization finished before it could be cancelled, so the account is connected. Disconnect it if you didn't mean to connect it.",
},
409,
);
@@ -1924,7 +1924,7 @@ export function mountConnectionRoutes({
return context.json(
{
error:
- "This deployment no longer recognises you, so nothing was recorded. Ask an administrator to restore your access, then start the authorization again from the app's page.",
+ "OpenBot doesn't recognise you anymore, so nothing was recorded. Ask an admin to restore your access, then authorize again from the app's page.",
},
403,
);
diff --git a/server/src/plugins/route-groups/grants.ts b/server/src/plugins/route-groups/grants.ts
index 5000301b88..94dd4011f8 100644
--- a/server/src/plugins/route-groups/grants.ts
+++ b/server/src/plugins/route-groups/grants.ts
@@ -111,7 +111,7 @@ export function mountGrantRoutes({
if (kind === "mcp") {
if (!actor.isAdmin) {
- return "An administrator decides which Bots may reach a tool.";
+ return "Only an admin can choose which Bots use a tool.";
}
// Taking something away is always allowed: see the note on `intent`. It matters more here
// than anywhere else, because the rows this check exists to prevent are the same shape as the
@@ -138,7 +138,7 @@ export function mountGrantRoutes({
*/
const [serverId] = ref.split("/");
if (!(await store.serverExists(serverId ?? ""))) {
- return `${serverId} is not an app this deployment has added, so there is nothing for a Bot to reach. Add it first, and its tools can be granted then.`;
+ return `${serverId} hasn't been added yet. Add it first, then give its tools to a Bot.`;
}
/*
* The Bot has to exist as well, which the `bot` branch below already says in the same words.
@@ -164,7 +164,7 @@ export function mountGrantRoutes({
* this, deliberately, and this had it backwards.
*/
if (!actor.isAdmin) {
- return "An administrator decides which Bots may hand work to another Bot.";
+ return "Only an admin can choose which Bots hand work to another Bot.";
}
// Taking something away is always allowed: see the note on `intent`.
if (intent === "revoke") return null;
@@ -219,7 +219,7 @@ export function mountGrantRoutes({
if (owner === undefined) return `There is no skill called ${ref}.`;
if (owner !== actor.id) {
return owner === null
- ? `${ref} belongs to this deployment. An administrator decides which Bots use it.`
+ ? `${ref} belongs to everyone. Only an admin can choose which Bots use it.`
: `${ref} is somebody else's skill.`;
}
@@ -448,7 +448,7 @@ export function mountGrantRoutes({
return context.json(
{
error:
- "That tool is not configured in a way this deployment can act on. An administrator has to look at the server it belongs to.",
+ "That tool isn't set up correctly. Ask an admin to check the server it belongs to.",
},
500,
);
diff --git a/server/src/plugins/route-groups/skills.ts b/server/src/plugins/route-groups/skills.ts
index 18ef2f984e..918c94d4d7 100644
--- a/server/src/plugins/route-groups/skills.ts
+++ b/server/src/plugins/route-groups/skills.ts
@@ -64,7 +64,7 @@ export function mountSkillRoutes({
const actor = skillActor(context);
if (body.global && !actor.isAdmin) {
return context.json(
- { error: "Only an administrator writes a skill for the deployment." },
+ { error: "Only an admin can write a skill for everyone." },
403,
);
}
diff --git a/server/src/plugins/routes.ts b/server/src/plugins/routes.ts
index f3032f8b03..5df4fd18e0 100644
--- a/server/src/plugins/routes.ts
+++ b/server/src/plugins/routes.ts
@@ -187,7 +187,7 @@ export function createPluginRoutes(
const owner = await store.skillOwner(slug);
if (owner === undefined) return null; // A new skill. Ownership is decided on the way in.
if (owner === null) {
- return `${slug} belongs to this deployment. An administrator looks after it.`;
+ return `${slug} belongs to everyone. Only an admin can change it.`;
}
return owner === actor.id ? null : `${slug} is somebody else's skill.`;
}
diff --git a/server/src/plugins/shared-accounts.ts b/server/src/plugins/shared-accounts.ts
index c68e335631..c87ad5a1a7 100644
--- a/server/src/plugins/shared-accounts.ts
+++ b/server/src/plugins/shared-accounts.ts
@@ -142,7 +142,7 @@ export function accountFor(
if (mode === "shared") return { kind: "deployment" };
return {
kind: "ambiguous",
- message: `${row.title} does not say whether it is reached through each person's account or a shared one, so it was not called. An administrator can choose on its Plugins page.`,
+ message: `${row.title} wasn't called because nobody has chosen whether it uses each person's account or a shared one. An admin can choose on its Plugins page.`,
};
}
diff --git a/server/src/plugins/shared-use-store.ts b/server/src/plugins/shared-use-store.ts
index 58a071655f..8d794a15f4 100644
--- a/server/src/plugins/shared-use-store.ts
+++ b/server/src/plugins/shared-use-store.ts
@@ -728,7 +728,7 @@ export function createSharedUseGate(
audit: AuditStore,
): SharedUseGate {
return async ({ botId, serverId, title, actorId, initiator }) => {
- const message = `This Bot is reachable by more people than an administrator approved for the shared ${title} account. An administrator has been asked to approve it.`;
+ const message = `This Bot reaches more people than an admin approved for the shared ${title} account. An admin has been asked to approve it.`;
const steering = await steeringOf(initiator, store.sourcesOf);
/*
* A RUN WHOSE STEERING CANNOT BE TOLD IS REFUSED WITHOUT A REQUEST. No approval an administrator
diff --git a/server/src/routing/routes.ts b/server/src/routing/routes.ts
index c25b0bd834..2dd256e2ca 100644
--- a/server/src/routing/routes.ts
+++ b/server/src/routing/routes.ts
@@ -51,8 +51,8 @@ export function createRoutingRoutes(
return context.json(
{
error: agentId
- ? "That coworker is not on your roster."
- : "No coworker is available.",
+ ? "That Bot isn't one you can use."
+ : "No Bot is available.",
},
agentId ? 404 : 409,
);
@@ -60,7 +60,7 @@ export function createRoutingRoutes(
if (result.kind === "ambiguous") {
return context.json(
{
- error: "More than one coworker matches that name.",
+ error: "More than one Bot has that name.",
names: result.names,
},
409,
diff --git a/server/src/team-bots/team-bots.ts b/server/src/team-bots/team-bots.ts
index e18ce07fcc..40761ce329 100644
--- a/server/src/team-bots/team-bots.ts
+++ b/server/src/team-bots/team-bots.ts
@@ -375,9 +375,7 @@ export function createTeamBots(options: {
async assign(actor: AgentActor, botId: string, groupName: string) {
if (actor.role !== "admin")
- throw new TeamBotRefusedError(
- "Only an administrator can assign Team Bots.",
- );
+ throw new TeamBotRefusedError("Only an admin can assign Team Bots.");
if (!(await publication(botId)))
throw new TeamBotRefusedError("Publish this Bot before assigning it.");
const group = groupName.trim();
@@ -390,9 +388,7 @@ export function createTeamBots(options: {
async unassign(actor: AgentActor, botId: string, groupName: string) {
if (actor.role !== "admin")
- throw new TeamBotRefusedError(
- "Only an administrator can assign Team Bots.",
- );
+ throw new TeamBotRefusedError("Only an admin can assign Team Bots.");
await database
.delete(teamBotAssignments)
.where(
diff --git a/server/src/voice/routes.ts b/server/src/voice/routes.ts
index fcdcdab11b..f5631f4bdf 100644
--- a/server/src/voice/routes.ts
+++ b/server/src/voice/routes.ts
@@ -36,7 +36,12 @@ export function createVoiceRoutes(
app.get("/config", (context) => {
context.header("Cache-Control", "no-store");
if (!provider || !channels || !profiles)
- return context.json({ error: "Live voice is not configured." }, 503);
+ return context.json(
+ {
+ error: "Voice calls aren't set up yet. Ask an admin to turn them on.",
+ },
+ 503,
+ );
return context.json({ transport: provider.transport });
});
app.post(
@@ -48,7 +53,13 @@ export function createVoiceRoutes(
return context.json({ error: "Invalid voice request." }, 403);
}
if (!provider || !channels || !profiles)
- return context.json({ error: "Live voice is not configured." }, 503);
+ return context.json(
+ {
+ error:
+ "Voice calls aren't set up yet. Ask an admin to turn them on.",
+ },
+ 503,
+ );
const userId = context.var.actor.id;
const now = Date.now();
for (const [id, attempt] of attempts) {
@@ -64,7 +75,7 @@ export function createVoiceRoutes(
context.header("Retry-After", "60");
return context.json(
{
- error: "Too many voice connection attempts. Please retry shortly.",
+ error: "Too many call attempts. Try again in a moment.",
},
429,
);
@@ -110,7 +121,13 @@ export function createVoiceRoutes(
);
}
if (!provider || !channels || !profiles)
- return context.json({ error: "Live voice is not configured." }, 503);
+ return context.json(
+ {
+ error:
+ "Voice calls aren't set up yet. Ask an admin to turn them on.",
+ },
+ 503,
+ );
const offer = "sdp" in body ? body.sdp : undefined;
if (
(provider.transport === "webrtc" || offer !== undefined) &&
@@ -126,20 +143,21 @@ export function createVoiceRoutes(
channel = await channels.get(context.var.actor, body.channelId);
} catch {
return context.json(
- { error: "The channel could not be loaded. Please retry." },
+ { error: "Couldn't load this conversation. Try again." },
503,
);
}
- if (!channel) return context.json({ error: "Channel not found." }, 404);
+ if (!channel)
+ return context.json({ error: "Couldn't find that conversation." }, 404);
if (!channel.active)
return context.json(
- { error: "This channel is no longer active." },
+ { error: "This conversation is no longer active." },
409,
);
const agentId = channel.agentIds[0];
if (channel.agentIds.length !== 1 || !agentId)
return context.json(
- { error: "Live voice requires a channel with exactly one agent." },
+ { error: "Voice calls only work in a conversation with one Bot." },
400,
);
let profile: Awaited>;
@@ -147,12 +165,12 @@ export function createVoiceRoutes(
profile = await profiles.get(context.var.actor, agentId);
} catch {
return context.json(
- { error: "The agent could not be loaded. Please retry." },
+ { error: "Couldn't load this Bot. Try again." },
503,
);
}
if (!profile || profile.deletedAt)
- return context.json({ error: "Agent not found." }, 404);
+ return context.json({ error: "Couldn't find that Bot." }, 404);
const timeout = new AbortController();
const timer = setTimeout(
() => timeout.abort(),
@@ -175,8 +193,7 @@ export function createVoiceRoutes(
return signal.aborted
? context.json(
{
- error:
- "Voice connection timed out or was cancelled. Please retry.",
+ error: "The call timed out or was cancelled. Try again.",
},
504,
)
@@ -187,7 +204,7 @@ export function createVoiceRoutes(
error:
error instanceof VoiceError
? error.message
- : "The voice service could not start a call. Please retry.",
+ : "Couldn't start the call. Try again.",
},
502,
);
diff --git a/server/src/voice/session-routes.ts b/server/src/voice/session-routes.ts
index e1e3305a1e..103524289f 100644
--- a/server/src/voice/session-routes.ts
+++ b/server/src/voice/session-routes.ts
@@ -28,7 +28,10 @@ export function createVoiceSessionRoutes(services?: VoiceSessionServices) {
});
app.get("/", async (context) => {
if (!services)
- return context.json({ error: "Voice history is not configured." }, 503);
+ return context.json(
+ { error: "Saving voice chats isn't set up yet." },
+ 503,
+ );
const channelId = context.req.query("channelId");
const cursor = context.req.query("cursor");
if (!channelId || channelId.length > 256 || (cursor?.length ?? 0) > 2048)
@@ -52,7 +55,10 @@ export function createVoiceSessionRoutes(services?: VoiceSessionServices) {
if (context.req.header("x-openbot-voice") !== "1")
return context.json({ error: "Invalid voice request." }, 403);
if (!services)
- return context.json({ error: "Voice history is not configured." }, 503);
+ return context.json(
+ { error: "Saving voice chats isn't set up yet." },
+ 503,
+ );
if (
context.req.header("content-type")?.split(";")[0]?.trim() !==
"application/json"
@@ -76,7 +82,7 @@ export function createVoiceSessionRoutes(services?: VoiceSessionServices) {
return context.json(
{
error:
- "Voice transcript saved; summary service is busy. Retry shortly.",
+ "Transcript saved. The summary isn't ready yet. Try again in a moment.",
session: saved,
},
429,
@@ -141,7 +147,7 @@ function respondError(
return context.json({ error: error.message }, error.status);
console.error(JSON.stringify({ type: "voice-history-failed" }));
return context.json(
- { error: "Voice history could not be saved or loaded. Please retry." },
+ { error: "Couldn't save or load voice chats. Try again." },
503,
);
}
diff --git a/server/src/voice/sessions.ts b/server/src/voice/sessions.ts
index 6d1d3b2c67..b2cb62e5da 100644
--- a/server/src/voice/sessions.ts
+++ b/server/src/voice/sessions.ts
@@ -172,7 +172,7 @@ export function createVoiceSessionStore(
)
.for("share");
if (rows.length === 0)
- throw new VoiceSessionError("Channel not found.", 404);
+ throw new VoiceSessionError("Couldn't find that conversation.", 404);
if (rows.some((row) => row.deletedAt !== null))
throw new VoiceSessionError("This channel is no longer active.", 409);
}
@@ -182,7 +182,8 @@ export function createVoiceSessionStore(
write = false,
) {
const channel = await channelStore.get(actor, channelId);
- if (!channel) throw new VoiceSessionError("Channel not found.", 404);
+ if (!channel)
+ throw new VoiceSessionError("Couldn't find that conversation.", 404);
if (write && !channel.active)
throw new VoiceSessionError("This channel is no longer active.", 409);
}
diff --git a/server/tests/agent-routes.test.ts b/server/tests/agent-routes.test.ts
index 7d89403eb0..f6f096a7d8 100644
--- a/server/tests/agent-routes.test.ts
+++ b/server/tests/agent-routes.test.ts
@@ -517,20 +517,16 @@ describe("agent lifecycle routes", () => {
const response = await appFor(store).request("http://openbot.test/missing");
expect(response.status).toBe(404);
- expect(await json(response)).toEqual({ error: "Agent not found." });
+ expect(await json(response)).toEqual({ error: "Couldn't find that Bot." });
});
test.each([
- [new AgentNotFoundError("agent-1"), 404, "Agent not found."],
- [
- new AgentNotManageableError("agent-1"),
- 403,
- "You do not have permission to manage this agent.",
- ],
+ [new AgentNotFoundError("agent-1"), 404, "Couldn't find that Bot."],
+ [new AgentNotManageableError("agent-1"), 403, "You can't manage this Bot."],
[
new ProtectedAgentError("agent-1"),
403,
- "System-owned agents are protected.",
+ "This Bot is built in and can't be changed.",
],
])("maps known store errors", async (error, status, message) => {
const store = fakeStore({
diff --git a/server/tests/app-access-tool.test.ts b/server/tests/app-access-tool.test.ts
index 74fcbc06b3..25e9c253c3 100644
--- a/server/tests/app-access-tool.test.ts
+++ b/server/tests/app-access-tool.test.ts
@@ -143,7 +143,7 @@ describe("a refusal carrying a card, on its way to the model", () => {
test("names the Bot that refused, so the card asks about the right one", async () => {
const answer = await refusedWith(
- '[connect-app]{"serverId":"composio-gmail","title":"Gmail","reason":"shared"} Gmail is shared across this deployment and no account is connected to it yet. Ask an administrator to connect it.',
+ '[connect-app]{"serverId":"composio-gmail","title":"Gmail","reason":"shared"} Gmail uses a shared account, and nobody has connected it yet. Ask an admin to connect it.',
);
expect(answer.startsWith(`${REFUSAL_MARKER} `)).toBe(true);
expect(
diff --git a/server/tests/arcade-authorize-cancel.test.ts b/server/tests/arcade-authorize-cancel.test.ts
index ca2891fa1c..4fc12c079f 100644
--- a/server/tests/arcade-authorize-cancel.test.ts
+++ b/server/tests/arcade-authorize-cancel.test.ts
@@ -401,7 +401,7 @@ describe("DELETE /servers/:id/authorize", () => {
status: 409,
body: {
error:
- "That authorization finished before it could be cancelled, so the account is connected. Disconnect it if you did not mean to connect it.",
+ "That authorization finished before it could be cancelled, so the account is connected. Disconnect it if you didn't mean to connect it.",
},
outcomes: ["completed"],
});
@@ -482,7 +482,7 @@ describe("DELETE /servers/:id/authorize", () => {
const response = await cancel(app, movedApp);
expect(response.status).toBe(400);
expect(await response.json()).toEqual({
- error: "That app is not reached through a broker.",
+ error: "That app doesn't connect this way.",
});
} finally {
await database
@@ -565,7 +565,7 @@ describe("DELETE /servers/:id/authorize on a deployment whose broker runs no aut
);
expect(response.status).toBe(400);
expect(await response.json()).toEqual({
- error: "That app is not reached through a broker.",
+ error: "That app doesn't connect this way.",
});
});
@@ -577,7 +577,7 @@ describe("DELETE /servers/:id/authorize on a deployment whose broker runs no aut
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
- error: "An administrator connects shared apps.",
+ error: "Only an admin can connect shared apps.",
});
});
});
diff --git a/server/tests/attachment-routes.test.ts b/server/tests/attachment-routes.test.ts
index 19a421fd05..d14d356c1e 100644
--- a/server/tests/attachment-routes.test.ts
+++ b/server/tests/attachment-routes.test.ts
@@ -1326,7 +1326,7 @@ describe("POST /:channelId/attachments", () => {
expect(response.status).toBe(415);
const { error } = (await response.json()) as { error: string };
- expect(error).toBe("'archive' is not a file type this app can read.");
+ expect(error).toBe("'archive' isn't a file type OpenBot can read.");
});
});
@@ -2012,7 +2012,7 @@ describe("GET /:id", () => {
"application/json",
);
expect((await result.fetched.json()) as { error: string }).toEqual({
- error: "That attachment could not be read just now. Try again.",
+ error: "Couldn't open that attachment. Try again.",
});
// Both reads are written down, naming the row and the asker, because that is what it would take
// to act on either.
@@ -3198,8 +3198,8 @@ describe("markAttachmentsSent", () => {
// A sentence somebody can act on, naming the file and saying the turn did not run — this leaves
// through the run as an AG-UI error and is shown to the person who was waiting.
expect((raised as Error).message).toContain(id);
- expect((raised as Error).message).toContain("was not run");
- expect((raised as Error).message).toContain("attach the file again");
+ expect((raised as Error).message).toContain("wasn't sent");
+ expect((raised as Error).message).toContain("Attach the files again");
// And named for whoever has to act on it from the other side. Every consequence of a missing
// stamp is about a specific person and a specific row.
expect(logged.join(" ")).toContain(memberId);
diff --git a/server/tests/audit.test.ts b/server/tests/audit.test.ts
index 70d2597f66..ee9d80466c 100644
--- a/server/tests/audit.test.ts
+++ b/server/tests/audit.test.ts
@@ -258,7 +258,7 @@ describe("admin audit API", () => {
expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
- error: "Administrator access required.",
+ error: "Only an admin can do this.",
});
});
});
diff --git a/server/tests/channel-routes.test.ts b/server/tests/channel-routes.test.ts
index 9cc5e84237..ffee18aee7 100644
--- a/server/tests/channel-routes.test.ts
+++ b/server/tests/channel-routes.test.ts
@@ -337,12 +337,24 @@ describe("channel routes", () => {
const response = await appFor(store).request("http://openbot.test/missing");
expect(response.status).toBe(404);
- expect(await json(response)).toEqual({ error: "Channel not found." });
+ expect(await json(response)).toEqual({
+ error: "Couldn't find that conversation.",
+ });
});
test.each([
- ["create", new AgentNotFoundError("agent-1"), 404, "Agent not found."],
- ["get", new ChannelNotFoundError("channel-1"), 404, "Channel not found."],
+ [
+ "create",
+ new AgentNotFoundError("agent-1"),
+ 404,
+ "Couldn't find that Bot.",
+ ],
+ [
+ "get",
+ new ChannelNotFoundError("channel-1"),
+ 404,
+ "Couldn't find that conversation.",
+ ],
] as const)(
"maps known store errors from %s",
async (method, error, status, message) => {
@@ -470,7 +482,9 @@ describe("channel routes", () => {
);
expect(response.status).toBe(404);
- expect(await json(response)).toEqual({ error: "Channel not found." });
+ expect(await json(response)).toEqual({
+ error: "Couldn't find that conversation.",
+ });
});
test("keeps authentication in front of marking read", async () => {
@@ -510,8 +524,7 @@ describe("channel routes", () => {
expect(response.status).toBe(409);
expect(await json(response)).toEqual({
- error:
- "This channel is defined by the deployment package, so it cannot be deleted here.",
+ error: "This conversation is built in and can't be deleted.",
});
});
@@ -925,7 +938,9 @@ describe("channel store integration", () => {
`http://openbot.test/${created.id}`,
);
expect(response.status).toBe(404);
- expect(await json(response)).toEqual({ error: "Channel not found." });
+ expect(await json(response)).toEqual({
+ error: "Couldn't find that conversation.",
+ });
});
test("reads linked agent IDs in lexicographic order", async () => {
diff --git a/server/tests/composio-broker-seam.test.ts b/server/tests/composio-broker-seam.test.ts
index f5a239bb66..a5382ab4e4 100644
--- a/server/tests/composio-broker-seam.test.ts
+++ b/server/tests/composio-broker-seam.test.ts
@@ -55,9 +55,11 @@ describe("brokerSentence", () => {
*/
describe("brokerReturnUrl", () => {
test("refuses an address that was never built", () => {
- expect(() => brokerReturnUrl("", "composio")).toThrow(/built no address/);
+ expect(() => brokerReturnUrl("", "composio")).toThrow(
+ /no address to bring you back to/,
+ );
expect(() => brokerReturnUrl(" ", "composio")).toThrow(
- /built no address/,
+ /no address to bring you back to/,
);
expect(() => brokerReturnUrl("", "composio")).toThrow(BrokerReturnUrlError);
});
@@ -70,7 +72,7 @@ describe("brokerReturnUrl", () => {
"javascript:alert(1)",
]) {
expect(() => brokerReturnUrl(unusable, "composio")).toThrow(
- /not a web address/,
+ /isn't a web address/,
);
}
});
diff --git a/server/tests/composio-connection-kinds.test.ts b/server/tests/composio-connection-kinds.test.ts
index 7d41fd3200..844e156bf3 100644
--- a/server/tests/composio-connection-kinds.test.ts
+++ b/server/tests/composio-connection-kinds.test.ts
@@ -1602,7 +1602,7 @@ const SCHEME_CONSUMERS: Consumer[] = [
const pressed = await connectPress(APP.unreadable.id);
expect(pressed.status).toBe(400);
expect(String(pressed.payload.error)).toMatch(
- /cannot tell how .* connects/,
+ /can't tell how .* connects/,
);
/*
diff --git a/server/tests/composio-transport.test.ts b/server/tests/composio-transport.test.ts
index 022d542fa9..4c805c6c1c 100644
--- a/server/tests/composio-transport.test.ts
+++ b/server/tests/composio-transport.test.ts
@@ -2317,7 +2317,7 @@ describe("calling one action", () => {
expect(result.text).not.toContain("must-not-appear");
expect(result.text).not.toContain("unrecognised");
expect(result.text).not.toContain("{");
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
expect(result.text).toContain("GMAIL_FETCH_EMAILS");
});
@@ -2522,7 +2522,7 @@ describe("calling one action", () => {
expect(result.isError).toBe(true);
expect(result.text).toContain("GMAIL_FETCH_EMAILS");
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
});
test("a failure carrying only the vendor's placeholder says something actionable", async () => {
@@ -2548,7 +2548,7 @@ describe("calling one action", () => {
// sentence — "GMAIL_FETCH_EMAILS failed: Error executing the tool GMAIL_FETCH_EMAILS" — is the
// same useless words in a model's context and in the audit row, and passed.
expect(result.text).not.toMatch(/error executing the tool/i);
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
});
test("a failure that carries no message at all still says something actionable", async () => {
@@ -2580,7 +2580,7 @@ describe("calling one action", () => {
expect(result.isError).toBe(true);
expect(result.text.trim()).not.toBe("");
expect(result.text).toContain("GMAIL_FETCH_EMAILS");
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
}
});
@@ -2914,7 +2914,7 @@ describe("calling one action", () => {
);
expect(reported.isError).toBe(true);
- expect(reported.text).toContain("Plugins page");
+ expect(reported.text).toContain("connected on Connections");
});
test("a reported error that is a response dump is refused like the placeholder", async () => {
@@ -2958,7 +2958,7 @@ describe("calling one action", () => {
// And what is said instead names the action and the one thing the reader can do about it,
// which is what every sibling on this path answers an unreadable report with.
expect(result.text).toContain("GMAIL_FETCH_EMAILS");
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
// The data must not be handed over as content beside a reported failure either.
expect(result.text).not.toContain("m1");
}
@@ -3177,7 +3177,7 @@ describe("calling one action", () => {
// refusal CARRIES it. A start-anchored question of the answer lets it through anywhere but the
// first character.
expect(result.text).not.toMatch(/error executing the tool/i);
- expect(result.text).toMatch(/Plugins page/);
+ expect(result.text).toMatch(/connected on Connections/);
});
test("a refusal this deployment authored beats whatever the vendor said", async () => {
diff --git a/server/tests/computer-client.test.ts b/server/tests/computer-client.test.ts
index 7cb4fa2c2c..e7b6d89150 100644
--- a/server/tests/computer-client.test.ts
+++ b/server/tests/computer-client.test.ts
@@ -197,7 +197,7 @@ describe("computer client", () => {
throw new Error("connect ECONNREFUSED");
});
await expect(missing.navigate("https://example.com")).rejects.toThrow(
- "The assistant's computer is not running.",
+ "The Bot's computer isn't running.",
);
const timedOut = clientWith(() => {
@@ -206,7 +206,7 @@ describe("computer client", () => {
throw error;
});
await expect(timedOut.navigate("https://example.com")).rejects.toThrow(
- "The assistant's computer did not respond in time.",
+ "The Bot's computer didn't respond in time.",
);
const badPage = clientWith(
@@ -556,6 +556,6 @@ describe("a person's Stop", () => {
await expect(
transport.post("http://computer", "bot-1", "/click", {}),
- ).rejects.toThrow("The assistant's computer is not running.");
+ ).rejects.toThrow("The Bot's computer isn't running.");
});
});
diff --git a/server/tests/computer-routes.test.ts b/server/tests/computer-routes.test.ts
index cae6ff4090..3c4eae8873 100644
--- a/server/tests/computer-routes.test.ts
+++ b/server/tests/computer-routes.test.ts
@@ -104,7 +104,7 @@ describe("computer fleet listing", () => {
expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
- error: "Administrator access required.",
+ error: "Only an admin can do this.",
});
// Refused before the gateway is asked: a check that runs after the fleet has been read is not a
// check, it is a filter on the response.
diff --git a/server/tests/computer-target.test.ts b/server/tests/computer-target.test.ts
index 20ef0c53e3..9fca5a577d 100644
--- a/server/tests/computer-target.test.ts
+++ b/server/tests/computer-target.test.ts
@@ -30,7 +30,7 @@ describe("navigation targets", () => {
expect(verdict.allowed).toBe(false);
expect(verdict.allowed === false && verdict.reason).toContain(
- "inside this deployment's own network",
+ "inside OpenBot's own network",
);
});
@@ -45,7 +45,7 @@ describe("navigation targets", () => {
expect(verdict.allowed).toBe(false);
expect(verdict.allowed === false && verdict.reason).toContain(
- "cloud credentials",
+ "cloud keys",
);
}
});
@@ -74,7 +74,7 @@ describe("navigation targets", () => {
expect(verdict.allowed).toBe(false);
expect(verdict.allowed === false && verdict.reason).toContain(
- "cloud credentials",
+ "cloud keys",
);
}
});
@@ -90,7 +90,7 @@ describe("navigation targets", () => {
expect(verdict.allowed).toBe(false);
expect(verdict.allowed === false && verdict.reason).toContain(
- "inside this deployment's own network",
+ "inside OpenBot's own network",
);
});
diff --git a/server/tests/dictation-provider.test.ts b/server/tests/dictation-provider.test.ts
index 8236445789..98d5c6e4d1 100644
--- a/server/tests/dictation-provider.test.ts
+++ b/server/tests/dictation-provider.test.ts
@@ -95,7 +95,7 @@ test("upstream errors do not leak provider response bodies", async () => {
new File(["audio"], "audio.webm"),
new AbortController().signal,
),
- ).rejects.toThrow("Retry or contact your administrator");
+ ).rejects.toThrow("Try again, or ask an admin if it keeps happening.");
});
test("malformed success is a visible failure", async () => {
diff --git a/server/tests/enterprise-controls.integration.test.ts b/server/tests/enterprise-controls.integration.test.ts
index c08c3501bc..112d1e887a 100644
--- a/server/tests/enterprise-controls.integration.test.ts
+++ b/server/tests/enterprise-controls.integration.test.ts
@@ -489,7 +489,7 @@ describe("headless turns obey the same switches", () => {
});
try {
const turn = headlessTurn(bot);
- await expect(turn.run()).rejects.toThrow(/allowlist/);
+ await expect(turn.run()).rejects.toThrow(/isn't an allowed model/);
expect(turn.ran).toEqual([]);
} finally {
await put(asAdmin, "/api/admin/enterprise/settings/modelAllowlist", {
@@ -557,7 +557,7 @@ describe("headless turns obey the same switches", () => {
try {
await expect(
hop.deliver({ work, message: "m", shown: "s", assertion: "a" }),
- ).rejects.toThrow(/allowlist/);
+ ).rejects.toThrow(/isn't an allowed model/);
} finally {
await put(asAdmin, "/api/admin/enterprise/settings/modelAllowlist", {
value: { enabled: false, models: [] },
diff --git a/server/tests/gate-grant-is-local.test.ts b/server/tests/gate-grant-is-local.test.ts
index 2fa1906e46..63e7230a4c 100644
--- a/server/tests/gate-grant-is-local.test.ts
+++ b/server/tests/gate-grant-is-local.test.ts
@@ -161,7 +161,7 @@ describe("connectionTokenFor asks the broker where the grant lives", () => {
actorId: asker,
}),
).rejects.toThrow(
- "Team GitHub is shared across this deployment and no account is connected to it yet. Ask an administrator to connect it.",
+ "Team GitHub uses a shared account, and nobody has connected it yet. Ask an admin to connect it.",
);
expect(sentAs).toEqual([]);
});
diff --git a/server/tests/guards.test.ts b/server/tests/guards.test.ts
index 335c1de677..98173ca277 100644
--- a/server/tests/guards.test.ts
+++ b/server/tests/guards.test.ts
@@ -51,7 +51,7 @@ describe("server authorization", () => {
expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
- error: "Administrator access required.",
+ error: "Only an admin can do this.",
});
});
diff --git a/server/tests/headless-components.test.ts b/server/tests/headless-components.test.ts
index d6758971b6..4173a247ef 100644
--- a/server/tests/headless-components.test.ts
+++ b/server/tests/headless-components.test.ts
@@ -277,7 +277,7 @@ describe("headless component tools", () => {
signal: new AbortController().signal,
}),
);
- expect(result).toContain("has not been granted the function auditSummary");
+ expect(result).toContain("can't use the function auditSummary");
expect(result.startsWith(NOT_SHOWN)).toBeTrue();
});
});
diff --git a/server/tests/helpers/refusals.ts b/server/tests/helpers/refusals.ts
index 907b3b01d8..fb065ed1a7 100644
--- a/server/tests/helpers/refusals.ts
+++ b/server/tests/helpers/refusals.ts
@@ -141,7 +141,7 @@ const TABLE = {
/** Shared-mode guard: this deployment's shared account for the app has not been connected yet. */
sharedNotConnected: (app: string = "[^.]+") =>
new RegExp(
- `${app} is shared across this deployment and no account is connected to it yet\\. Ask an administrator to connect it\\.`,
+ `${app} uses a shared account, and nobody has connected it yet\\. Ask an admin to connect it\\.`,
),
/** Shared-mode guard: the run does not say what started it, or what did cannot steer a shared account. */
sharedSteering: (app: string = "[^.]+") =>
@@ -149,7 +149,7 @@ const TABLE = {
/** Shared-mode guard: this Bot's audience reaches further than an administrator approved for the shared account. */
sharedAudience: (app: string = "[^.]+") =>
new RegExp(
- `This Bot is reachable by more people than an administrator approved for the shared ${app} account\\. An administrator has been asked to approve it\\.`,
+ `This Bot reaches more people than an admin approved for the shared ${app} account\\. An admin has been asked to approve it\\.`,
),
};
diff --git a/server/tests/memory-shared-source.test.ts b/server/tests/memory-shared-source.test.ts
index 2d6b977bd1..451f35952a 100644
--- a/server/tests/memory-shared-source.test.ts
+++ b/server/tests/memory-shared-source.test.ts
@@ -66,7 +66,7 @@ describe("a memory source on a Shared app", () => {
toolRef: tool.ref,
}),
).rejects.toThrow(
- /Team GitHub is shared across this deployment and no account is connected to it yet/,
+ /Team GitHub uses a shared account, and nobody has connected it yet/,
);
});
diff --git a/server/tests/pending-authorization-audit-ledger.test.ts b/server/tests/pending-authorization-audit-ledger.test.ts
index c31c9c47cf..85386a2711 100644
--- a/server/tests/pending-authorization-audit-ledger.test.ts
+++ b/server/tests/pending-authorization-audit-ledger.test.ts
@@ -320,8 +320,7 @@ const NON_WRITES: Record<
},
"mint that found its attempt already cancelled": {
file: "server/src/plugins/route-groups/connections.ts",
- anchor:
- "That authorization was cancelled before the app's vendor finished starting it",
+ anchor: "That authorization was cancelled before it started",
name: "mint declined because the attempt was cancelled while the vendor was minting (B5d)",
waived:
"This branch ends no attempt: the attempt it would describe was already ended by the person's cancel, and that cancel filed `mcp.authorization_cancelled` for it. A second row would record the same withdrawal twice. What the branch adds is a refusal to hand out the consent url, and that url, like the `ar_...`, must never reach the trail.",
diff --git a/server/tests/plugin-connect-route.test.ts b/server/tests/plugin-connect-route.test.ts
index 11518a3126..69429f49da 100644
--- a/server/tests/plugin-connect-route.test.ts
+++ b/server/tests/plugin-connect-route.test.ts
@@ -120,7 +120,7 @@ describe("connecting a dynamically registered vendor", () => {
expect(ensureCalls.length).toBe(1);
const body = (await response.json()) as { error: string };
expect(body.error).toBe(
- "Notion would not register this deployment, or could not be reached. Try again, and check the vendor's status if it persists.",
+ "Couldn't set up Notion. Try again, or check Notion's status if it keeps happening.",
);
});
});
@@ -144,7 +144,9 @@ describe("connecting a manually registered vendor (regression pin)", () => {
expect(response.status).toBe(409);
expect(ensureCalls).toEqual([]);
const body = (await response.json()) as { error: string };
- expect(body.error).toContain("no OAuth client registered");
+ expect(body.error).toContain(
+ "isn't set up yet. Ask an admin to finish setting it up.",
+ );
});
});
@@ -174,7 +176,7 @@ describe("connecting a vendor this deployment has not added", () => {
expect(response.status).toBe(409);
const body = (await response.json()) as { error: string };
expect(body.error).toBe(
- "Notion has not been added to this deployment yet. An administrator has to add it first.",
+ "Notion hasn't been added yet. Ask an admin to add it.",
);
});
});
diff --git a/server/tests/plugin-grants-validation.test.ts b/server/tests/plugin-grants-validation.test.ts
index 8038b0b303..d134a70a03 100644
--- a/server/tests/plugin-grants-validation.test.ts
+++ b/server/tests/plugin-grants-validation.test.ts
@@ -126,7 +126,7 @@ describe("POST /api/plugins/grants, for an app this deployment does not have", (
expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
error:
- "missing-app is not an app this deployment has added, so there is nothing for a Bot to reach. Add it first, and its tools can be granted then.",
+ "missing-app hasn't been added yet. Add it first, then give its tools to a Bot.",
});
expect(calls.grants).toEqual([]);
// The server half, not the whole ref: the tool is not what is being looked up.
diff --git a/server/tests/plugin-routes.test.ts b/server/tests/plugin-routes.test.ts
index 846cefece6..94bdb0e23d 100644
--- a/server/tests/plugin-routes.test.ts
+++ b/server/tests/plugin-routes.test.ts
@@ -27,7 +27,7 @@ import { testEnvironment } from "./support/environment";
* source said. Identical on both providers, and it names no vendor and no setting.
*/
const PERSON_UNCONFIGURED =
- "Connected apps are not set up for this deployment, so nothing was asked. An administrator can set them up.";
+ "Connecting apps isn't set up yet, so nothing was asked. Ask an admin to set it up.";
/**
* The handful of reads one route makes, typed against the store that really answers them.
@@ -531,8 +531,7 @@ describe("granting one Bot to another", () => {
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
- error:
- "An administrator decides which Bots may hand work to another Bot.",
+ error: "Only an admin can choose which Bots hand work to another Bot.",
});
expect(calls).toEqual([]);
});
@@ -687,7 +686,7 @@ describe("what a bot grant refusal reveals", () => {
// One sentence for all three, so nothing distinguishes "exists" from "does not".
expect(said.size).toBe(1);
expect([...said][0]).toBe(
- "An administrator decides which Bots may hand work to another Bot.",
+ "Only an admin can choose which Bots hand work to another Bot.",
);
});
@@ -2135,12 +2134,12 @@ describe("connecting a brokered app", () => {
// The pre-0030 row: brokered, connected, and carrying no scheme at all.
const held = await connectAt("composio-slack");
expect(held.status).toBe(400);
- expect(String((await held.json()).error)).toMatch(/cannot tell how/);
+ expect(String((await held.json()).error)).toMatch(/can't tell how/);
// And a scheme the vendor names that this deployment does not.
const drifted = await connectAt("composio-trello");
expect(drifted.status).toBe(400);
- expect(String((await drifted.json()).error)).toMatch(/cannot tell how/);
+ expect(String((await drifted.json()).error)).toMatch(/can't tell how/);
expect(authorized).toEqual([]);
});
@@ -2839,7 +2838,7 @@ describe("connecting a key-type Shared app", () => {
expect(response.status).toBe(403);
expect((await response.json()).error).toBe(
- "An administrator connects shared apps.",
+ "Only an admin can connect shared apps.",
);
// Nothing was asked of Composio and nothing was sent: the refusal happens before either.
expect(asked).toEqual([]);
@@ -3040,10 +3039,10 @@ describe("confirming and ending a brokered connection", () => {
expect(confirmResponse.status).toBe(400);
expect(disconnectResponse.status).toBe(400);
expect((await confirmResponse.json()).error).toBe(
- "That app is not reached through a broker.",
+ "That app doesn't connect this way.",
);
expect((await disconnectResponse.json()).error).toBe(
- "That app is not reached through a broker.",
+ "That app doesn't connect this way.",
);
// And the store was left alone: a confirm here would write a row for an app whose connection
// is not Composio's to answer about, and a disconnect would ask the broker to revoke a grant
@@ -3211,7 +3210,7 @@ describe("re-checking a brokered connection", () => {
expect(response.status).toBe(400);
expect((await response.json()).error).toBe(
- "That app is not reached through a broker.",
+ "That app doesn't connect this way.",
);
expect(rechecked).toEqual([]);
});
diff --git a/server/tests/plugin-store.integration.test.ts b/server/tests/plugin-store.integration.test.ts
index aa4cb17ab9..5f8a76eac3 100644
--- a/server/tests/plugin-store.integration.test.ts
+++ b/server/tests/plugin-store.integration.test.ts
@@ -639,7 +639,7 @@ describe("the policy is asked as well as the grant", () => {
botId: holderId,
actorId,
}),
- ).rejects.toThrow("credential material");
+ ).rejects.toThrow("a password or key");
const rows = await auditRowsFor(ref, holderId, actorId);
const rejected = rows.filter(
@@ -1876,7 +1876,7 @@ describe("refresh token rotation", () => {
*/
const UNREADABLE_PLAINTEXT = `secret_notJsonClient${suite}`;
/** What the person and the trail are told instead, which is the operator's signal. */
- const UNUSABLE = "Notion has no usable OAuth client for this deployment.";
+ const UNUSABLE = "Notion needs connecting again.";
/** The client the suite registered, restored after each test repoints the server. */
let registeredClientId: string | null = null;
@@ -2680,7 +2680,7 @@ describe("a dynamic client the vendor has evicted", () => {
* usable one — only a new consent under the client that now exists.
*/
await expect(call()).rejects.toThrow(
- "Notion no longer recognises this deployment's OAuth client",
+ "Notion needs connecting again, so this wasn't done.",
);
/*
@@ -2723,7 +2723,7 @@ describe("a dynamic client the vendor has evicted", () => {
issue = () => ({ clientId: "dyn-3", clientSecret: "" });
await expect(call()).rejects.toThrow(
- "Notion no longer recognises this deployment's OAuth client",
+ "Notion needs connecting again, so this wasn't done.",
);
expect(registrations.length).toBe(1);
expect(exchanges).toEqual([
@@ -2789,7 +2789,7 @@ describe("a dynamic client the vendor has evicted", () => {
);
await expect(call()).rejects.toThrow(
- "Notion no longer recognises this deployment's OAuth client",
+ "Notion needs connecting again, so this wasn't done.",
);
expect(offered).toEqual([EVICTED.clientId]);
@@ -3580,7 +3580,7 @@ describe("a dynamic client the vendor has evicted", () => {
});
/** What the operator must be told instead: the credential named, and nothing else claimed. */
const UNUSABLE =
- "Notion has no usable OAuth client for this deployment. Connect Notion again in Settings: the deployment registers itself with the vendor on the next connect.";
+ "Notion needs connecting again. Reconnect it on Connections and ask again.";
/**
* Plant arbitrary stored bytes as this server's client, the way {@link putClient} plants a real
diff --git a/server/tests/plugin-user-credential.integration.test.ts b/server/tests/plugin-user-credential.integration.test.ts
index a39fb7128e..988515fab2 100644
--- a/server/tests/plugin-user-credential.integration.test.ts
+++ b/server/tests/plugin-user-credential.integration.test.ts
@@ -423,7 +423,9 @@ describe("a person who has connected", () => {
await expect(
store.callTool({ ref, args: {}, botId, actorId: askerId }),
- ).rejects.toThrow(/OAuth client/);
+ ).rejects.toThrow(
+ /isn't set up yet, so this can't be done\. Ask an admin to finish setting it up\./,
+ );
});
test("goes out with their own token and nobody else's", async () => {
diff --git a/server/tests/routes-vendor-vocabulary.test.ts b/server/tests/routes-vendor-vocabulary.test.ts
index 1985583e14..3ea4ff08a6 100644
--- a/server/tests/routes-vendor-vocabulary.test.ts
+++ b/server/tests/routes-vendor-vocabulary.test.ts
@@ -11,7 +11,13 @@
* ASSERTED ON BOTH PROVIDERS FROM ONE HARNESS, the way `routes-arcade-urls.test.ts` asserts its
* four reads. The Arcade half is what was red: the sentence must name Arcade and must not contain
* the word Composio anywhere. The Composio half is the evidence that nothing about that deployment
- * moved — the same sentence, word for word, as before.
+ * moved — the same sentence, word for word, but for the vendor's name.
+ *
+ * TWO AUDIENCES, TWO RULES. The two admin sentences (removing and enabling an app) name the broker,
+ * because the admin is the one who holds its key. The connect, confirm, re-check and disconnect
+ * sentences answer whoever pressed the button, and since the copy pass they name NO broker at all
+ * (see `.claude/skills/openbot-copy/SKILL.md`): the broker is an internal that person cannot act
+ * on. Neither rule lets a deployment read the other vendor's name.
*/
import { describe, expect, test } from "bun:test";
import { Hono, type MiddlewareHandler } from "hono";
@@ -220,12 +226,22 @@ function deployment(provider: BrokerProvider, wiring: Wiring = {}) {
};
}
-/** Every sentence this file pins, as the request that produces it. */
-const CASES: Record Promise> = {
+/** The admin's sentences, which name the broker, as the request that produces each. */
+const ADMIN_CASES: Record<
+ string,
+ (provider: BrokerProvider) => Promise
+> = {
"removing an app that would not finish": (provider) =>
deployment(provider, { throwing: { removeServer: true } }).remove(),
"enabling an app the broker would not enable": (provider) =>
deployment(provider, { throwing: { addBrokeredApp: true } }).enable(),
+};
+
+/** A person's sentences, which name no broker, as the request that produces each. */
+const PERSON_CASES: Record<
+ string,
+ (provider: BrokerProvider) => Promise
+> = {
"a key app whose form the broker would not publish": (provider) =>
deployment(provider, {
authScheme: "API_KEY",
@@ -277,6 +293,9 @@ const CASES: Record Promise> = {
}).disconnect(),
};
+/** Every sentence this file pins. */
+const CASES = { ...ADMIN_CASES, ...PERSON_CASES };
+
async function sentenceOf(
provider: BrokerProvider,
name: keyof typeof CASES,
@@ -287,8 +306,8 @@ async function sentenceOf(
return body.error as string;
}
-describe("an Arcade deployment's refusals name Arcade and never Composio", () => {
- for (const name of Object.keys(CASES)) {
+describe("an Arcade deployment's admin refusals name Arcade and never Composio", () => {
+ for (const name of Object.keys(ADMIN_CASES)) {
test(name, async () => {
const sentence = await sentenceOf("arcade", name);
expect(sentence).not.toContain("Composio");
@@ -297,8 +316,8 @@ describe("an Arcade deployment's refusals name Arcade and never Composio", () =>
}
});
-describe("and a Composio deployment reads exactly what it read before", () => {
- for (const name of Object.keys(CASES)) {
+describe("and a Composio deployment's admin refusals name Composio and never Arcade", () => {
+ for (const name of Object.keys(ADMIN_CASES)) {
test(name, async () => {
const sentence = await sentenceOf("composio", name);
expect(sentence).not.toContain("Arcade");
@@ -307,6 +326,17 @@ describe("and a Composio deployment reads exactly what it read before", () => {
}
});
+describe("a person's refusals name no broker on either deployment", () => {
+ for (const name of Object.keys(PERSON_CASES)) {
+ for (const provider of ["composio", "arcade"] as const) {
+ test(`${name} (${provider})`, async () => {
+ const sentence = await sentenceOf(provider, name);
+ expect(sentence).not.toMatch(/composio|arcade/i);
+ });
+ }
+ }
+});
+
/**
* And the two sentences differ in NOTHING but the vendor's name — which is what says the fix was a
* substitution rather than a rewrite. Written as the Arcade sentence with "Arcade" put back to
diff --git a/server/tests/routing-limits-validation.test.ts b/server/tests/routing-limits-validation.test.ts
index bf63e52b4b..73d5a57296 100644
--- a/server/tests/routing-limits-validation.test.ts
+++ b/server/tests/routing-limits-validation.test.ts
@@ -68,7 +68,7 @@ describe("POST /api/route text cap", () => {
test("accepts a message at the cap boundary", async () => {
const calls: unknown[] = [];
- // Empty roster -> 409 "No coworker is available.", which still proves the text passed the
+ // Empty roster -> 409 "No Bot is available.", which still proves the text passed the
// cap and reached the router path.
const response = await app(calls).request("http://openbot.test/", {
method: "POST",
diff --git a/server/tests/shared-brokered-routes.test.ts b/server/tests/shared-brokered-routes.test.ts
index 48fe448577..a58cc8f284 100644
--- a/server/tests/shared-brokered-routes.test.ts
+++ b/server/tests/shared-brokered-routes.test.ts
@@ -165,7 +165,7 @@ describe("connecting a shared app", () => {
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
- error: "An administrator connects shared apps.",
+ error: "Only an admin can connect shared apps.",
});
expect(authorizedFor).toEqual([]);
});
@@ -274,7 +274,7 @@ describe("connecting a shared app", () => {
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
- error: "An administrator connects shared apps.",
+ error: "Only an admin can connect shared apps.",
});
expect(connectWithFieldsCalls).toBe(0);
} finally {
@@ -301,7 +301,7 @@ describe("connecting a shared app", () => {
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
- error: "An administrator connects shared apps.",
+ error: "Only an admin can connect shared apps.",
});
expect(authorizedFor).toEqual([]);
expect(connectWithFieldsCalls).toBe(0);
diff --git a/server/tests/shared-brokered-store.integration.test.ts b/server/tests/shared-brokered-store.integration.test.ts
index e6a2c44f84..0c125b87fa 100644
--- a/server/tests/shared-brokered-store.integration.test.ts
+++ b/server/tests/shared-brokered-store.integration.test.ts
@@ -403,7 +403,7 @@ describe("a shared app", () => {
botId: bot,
actorId: asker,
}),
- ).rejects.toThrow(/cannot check who may use it/);
+ ).rejects.toThrow(/can't check who may use it/);
});
});
@@ -729,9 +729,7 @@ describe("an account the vendor no longer holds", () => {
title: "Team GitHub",
reason: "shared",
});
- expect(prompt?.message).toContain(
- "An administrator has to connect it again",
- );
+ expect(prompt?.message).toContain("Ask an admin to reconnect it");
expect(Date.parse(prompt?.at ?? "")).toBeGreaterThanOrEqual(before - 1000);
expect(await failures()).toBe(already + 1);
// The team account's row is gone with the account, so every screen reads it as not connected.
diff --git a/server/tests/store-scheme-sites.test.ts b/server/tests/store-scheme-sites.test.ts
index f6da002b73..10b1408a3b 100644
--- a/server/tests/store-scheme-sites.test.ts
+++ b/server/tests/store-scheme-sites.test.ts
@@ -224,7 +224,7 @@ test("the probe on Composio still reaches the Composio transport", async () => {
expect(probed.outcome).toBe("unreachable");
expect(probed).toHaveProperty("attempted", "GMAIL_GET_ME");
expect((probed as { sentence: string }).sentence).toContain(
- "Composio is not configured for this deployment",
+ "Apps like this aren't set up yet",
);
});
diff --git a/server/tests/voice-session-routes.test.ts b/server/tests/voice-session-routes.test.ts
index 231ba81325..7dcd1d62cd 100644
--- a/server/tests/voice-session-routes.test.ts
+++ b/server/tests/voice-session-routes.test.ts
@@ -47,12 +47,14 @@ function fixture() {
const store: VoiceSessionStore = {
save: async (receivedActor) => {
expect(receivedActor.id).toBe(actor.id);
- if (deny) throw new VoiceSessionError("Channel not found.", 404);
+ if (deny)
+ throw new VoiceSessionError("Couldn't find that conversation.", 404);
calls.push("saved");
return saved;
},
list: async (_actor, channelId, cursor) => {
- if (deny) throw new VoiceSessionError("Channel not found.", 404);
+ if (deny)
+ throw new VoiceSessionError("Couldn't find that conversation.", 404);
expect(channelId).toBe("channel");
expect(cursor).toBe("cursor");
return { sessions: [saved], nextCursor: null };
diff --git a/server/tests/voice-session-store.integration.test.ts b/server/tests/voice-session-store.integration.test.ts
index 60452d9770..349fb739f0 100644
--- a/server/tests/voice-session-store.integration.test.ts
+++ b/server/tests/voice-session-store.integration.test.ts
@@ -89,10 +89,10 @@ test("sessions persist as JSON, are immutable/idempotent, and are shared only wi
expect(await store.save(owner, request)).toEqual(saved);
expect((await store.list(member, channelId)).sessions).toContainEqual(saved);
await expect(store.list(outsider, channelId)).rejects.toThrow(
- "Channel not found",
+ "Couldn't find that conversation.",
);
await expect(store.save(outsider, input())).rejects.toThrow(
- "Channel not found",
+ "Couldn't find that conversation.",
);
await expect(store.save(member, request)).rejects.toThrow("already in use");
await expect(