Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions openbot/app/src/components/agents/new-bot-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
testAgentConnection,
} from "@/lib/agents/queries";
import { isComposing } from "@/lib/composing";
import { appConfig } from "@/lib/generated/application-config";
import { cn } from "@/lib/utils";
import { queryClient } from "@/query-client";

Expand Down Expand Up @@ -493,7 +494,7 @@ const VISIBILITY_OPTIONS: Array<{
{
value: "public",
title: "Public",
description: "Everyone in your OpenBot can find and use it.",
description: `Everyone in your ${appConfig.brand.productName} can find and use it.`,
icon: IconUsers,
},
];
Expand Down Expand Up @@ -685,7 +686,7 @@ function RuntimeStep({
<span className="block font-semibold text-base">Run it here</span>
<span className="mt-1 block text-pretty text-muted-foreground text-sm">
{builtInAvailable
? "OpenBot runs it for you. Nothing to set up, and it can start right away."
? `${appConfig.brand.productName} runs it for you. Nothing to set up, and it can start right away.`
: "Not available yet. An admin has to turn this on first."}
</span>
</span>
Expand Down Expand Up @@ -786,8 +787,9 @@ function EndpointStep({
/>
) : (
<FieldDescription>
Any agent that speaks AG-UI works. OpenBot connects to it, so it
has to be reachable from OpenBot's server.
Any agent that speaks AG-UI works. {appConfig.brand.productName}{" "}
connects to it, so it has to be reachable from{" "}
{appConfig.brand.productName}'s server.
</FieldDescription>
)}
</Field>
Expand Down
3 changes: 2 additions & 1 deletion openbot/app/src/components/agents/proposed-bot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type ProposedBot,
wasCreated,
} from "@/lib/agents/proposal";
import { appConfig } from "@/lib/generated/application-config";

/**
* The coworker a Bot has written, put in front of the person before it exists.
Expand Down Expand Up @@ -137,7 +138,7 @@ export function ProposedBotCard({
action={<Badge tone="caution">Waiting on you</Badge>}
caption={
clashes
? `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.`
? `There's already a ${clashes.title}, and it's ${clashes.ownership === "the deployment's" ? `built into ${appConfig.brand.productName}` : 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)}
Expand Down
39 changes: 36 additions & 3 deletions openbot/app/src/components/channels/channel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ import {
type ChannelSummary,
channelKeys,
} from "@/lib/channels/queries";
import {
forgetRunError,
recalledRunError,
rememberRunError,
} from "@/lib/channels/run-error-memory";
import {
claimSeed,
interruptsToAnswer,
Expand Down Expand Up @@ -892,7 +897,25 @@ export function ChannelChat({
});

// Run failures arrive as events and are reported only for turns started in this mount.
const [runError, setRunError] = useState<string | null>(null);
const [runError, setRunErrorState] = useState<string | null>(null);
/**
* Every report goes through here, so the note kept for a reload is always the one on screen, kept
* with the conversation's last message so a reload shows it only while it is still the latest
* news (`lib/channels/run-error-memory.ts`). A new turn's `null` forgets it.
*/
const setRunError = useCallback(
(said: string | null) => {
setRunErrorState(said);
if (said === null) forgetRunError(channel.id);
else
rememberRunError(
channel.id,
said,
agentRef.current.messages.at(-1)?.id ?? null,
);
},
[channel.id],
);
const awaitingReply = useRef(false);
/**
* WHY THIS TURN ENDED WITHOUT AN ANSWER, KEPT WHERE `deliver` CAN STILL SEE IT — because the one
Expand Down Expand Up @@ -1377,7 +1400,7 @@ export function ChannelChat({
},
});
return () => subscription?.unsubscribe();
}, [agent, runtimeAgentId]);
}, [agent, runtimeAgentId, setRunError]);

/** Stable reference for effects and component callbacks. */
const sayRef = useRef(say);
Expand Down Expand Up @@ -1613,6 +1636,16 @@ export function ChannelChat({
...(task?.queued ?? []).map((reply) => reply.messageId),
]);

/*
* The failed turn's sentence from before a reload, while nothing has happened since: read only
* once history is back and no turn is going, and only while the last message is the one it was
* about. See `lib/channels/run-error-memory.ts`.
*/
const recalledError =
runError === null && !restoring && turnsInFlight === 0 && !agent.isRunning
? recalledRunError(channel.id, agent.messages.at(-1)?.id ?? null)
: null;

return (
// Activity renderers resolve their agent through this SDK context. It must match useAgent's
// channel instance so an action continues this thread instead of looking for a default agent.
Expand Down Expand Up @@ -1831,7 +1864,7 @@ export function ChannelChat({
stopped={
runError && THREAD_LOCKED.test(runError)
? STILL_FINISHING
: (runError ?? undefined)
: (runError ?? recalledError ?? undefined)
}
/>
</div>
Expand Down
19 changes: 16 additions & 3 deletions openbot/app/src/components/channels/conversation-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import { PageSheetHost } from "@/components/duet/page-sheet";
import {
stepFor,
usePatientStep,
useReportChatPresence,
} from "@/components/shell/chat-presence";
import { ChatSuggestionCard } from "@/components/shell/chat-suggestion";
Expand Down Expand Up @@ -222,12 +223,24 @@ export function ConversationView({
// The app shell's orb follows Chat: thinking for the turn, listening to the draft. Not while
// history is restoring: joining the thread is a run of its own, and it is not an answer.
const working = (busy || inFlight) && !restoring;
/*
* What moves when the turn does: another message, the last one growing as it is written, or a
* tool call added to it. The pill's quiet clock (`usePatientStep`) starts again on each.
*/
const last = messages.at(-1);
const progress = `${messages.length}:${JSON.stringify(last?.content ?? "").length}:${
last && "toolCalls" in last ? (last.toolCalls?.length ?? 0) : 0
}`;
const step = usePatientStep(
// Read off the messages on every render, as the transcript reads them, because the agent
// grows the same array in place. Only Chat's conversation has a pill to say it in.
inShell ? stepFor(messages, working, approvals) : null,
progress,
);
const presence = useReportChatPresence(inShell, {
working,
failed: Boolean(stopped),
// Read off the messages on every render, as the transcript reads them, because the agent
// grows the same array in place. Only Chat's conversation has a pill to say it in.
step: inShell ? stepFor(messages, working, approvals) : null,
step,
});

/**
Expand Down
3 changes: 2 additions & 1 deletion openbot/app/src/components/delivery/reaching-you.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
startChatLinkMutationOptions,
startSmsMutationOptions,
} from "@/lib/delivery";
import { appConfig } from "@/lib/generated/application-config";
import { queryClient } from "@/query-client";

/**
Expand Down Expand Up @@ -513,7 +514,7 @@ function ConnectDialog({
<Item size="sm" variant="muted">
<ItemContent>
<ItemDescription className="line-clamp-none">
Send this message to the OpenBot app in{" "}
Send this message to the {appConfig.brand.productName} app in{" "}
{linked.platform === "teams" ? "Microsoft Teams" : "Slack"}{" "}
within {Math.ceil((linked.expiresAt - Date.now()) / 60_000)}{" "}
minutes. It links that account to this conversation and Bot.
Expand Down
18 changes: 14 additions & 4 deletions openbot/app/src/components/duet/page-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ export function PageSheet({
<div className="mx-auto flex min-h-0 w-full max-w-[430px] flex-1 flex-col px-3 pt-[max(env(safe-area-inset-top),12px)] pb-[max(env(safe-area-inset-bottom),10px)]">
<div
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden motion-reduce:transition-none",
// A size container, so the header below can ask how much room it has.
"@container flex min-h-0 flex-1 flex-col overflow-hidden motion-reduce:transition-none",
morph.morphing
? "transition-opacity duration-150 ease-out"
: "transition-transform duration-300 ease-out motion-safe:group-data-starting-style:translate-y-6",
Expand All @@ -145,22 +146,31 @@ export function PageSheet({
data-morph-hidden={morph.hidden || undefined}
ref={morph.sheetRef}
>
{/*
* THE TITLE GETS A ROW OF ITS OWN WHEN THE TEXT IS LARGE. Back and "…" are 44px each
* in rem, so at 200% text they took 176px of a 342px row between them, and the title
* was squeezed into a column a few letters wide. It broke anywhere to fit, one syllable
* per line ("Prep / note / s: / Hea / rth"). Below 20rem of room, which is 320px at the
* default size and so no phone at it, the title moves under the two discs and takes the
* whole width. It breaks only between words, and a title longer than two lines ends in
* an ellipsis, with the whole of it still the dialog's name.
*/}
<header className="relative grid min-h-14 shrink-0 grid-cols-[auto_1fr_auto] items-center gap-2 py-1">
<button
aria-label="Back"
className="grid size-11 place-items-center rounded-full text-duet-ink outline-none transition-colors hover:bg-duet-tile focus-visible:ring-3 focus-visible:ring-duet-violet/40 active:bg-duet-tile"
className="col-start-1 row-start-1 grid size-11 place-items-center rounded-full text-duet-ink outline-none transition-colors hover:bg-duet-tile focus-visible:ring-3 focus-visible:ring-duet-violet/40 active:bg-duet-tile"
onClick={morph.close}
type="button"
>
<DuetIcon name="chevronLeft" size={22} stroke={2.1} />
</button>
<DialogPrimitive.Title className="type-row min-w-0 justify-self-center rounded-full bg-duet-card px-4 py-2 text-center font-semibold [overflow-wrap:anywhere]">
<DialogPrimitive.Title className="type-row col-start-2 row-start-1 line-clamp-2 min-w-0 justify-self-center text-balance rounded-[1.25rem] bg-duet-card px-4 py-2 text-center font-semibold [overflow-wrap:break-word] @max-[20rem]:col-span-3 @max-[20rem]:col-start-1 @max-[20rem]:row-start-2">
{title}
</DialogPrimitive.Title>
<button
aria-label="Page actions"
aria-expanded={actions}
className="grid size-11 place-items-center rounded-full bg-duet-card text-duet-ink"
className="col-start-3 row-start-1 grid size-11 place-items-center rounded-full bg-duet-card text-duet-ink"
onClick={() => setActions(!actions)}
type="button"
>
Expand Down
14 changes: 11 additions & 3 deletions openbot/app/src/components/duet/page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ function Block({
{block.text}
</p>
) : (
<p className="type-body mt-3 whitespace-pre-line text-duet-ink leading-[27px] [overflow-wrap:anywhere] first:mt-0">
/*
* LINE HEIGHTS HERE ARE RATIOS, NOT PIXELS. `type-body` is in rem so it follows the person's
* text size, and a pixel line height beside it does not: at 200% the text grew to 34px and
* the lines stayed 27px apart, so every line overlapped the one above. 1.6 is the 27px the
* reader was drawn with, at the default 17px.
*/
<p className="type-body mt-3 whitespace-pre-line text-duet-ink leading-[1.6] [overflow-wrap:anywhere] first:mt-0">
{block.text}
</p>
);
Expand All @@ -123,13 +129,15 @@ function Block({
<ul className="mt-3 flex flex-col gap-2 first:mt-0">
{block.items.map((item, index) => (
<li
className="type-body flex gap-3 text-duet-ink leading-[26px] [overflow-wrap:anywhere]"
// 26px at the default size, as a ratio for the reason the paragraph above gives.
className="type-body flex gap-3 text-duet-ink leading-[1.53] [overflow-wrap:anywhere]"
// biome-ignore lint/suspicious/noArrayIndexKey: two bullets may say the same thing.
key={index}
>
{/* Centred on the first line at any text size: half the line, less half the dot. */}
<span
aria-hidden
className="mt-[11px] size-[5px] shrink-0 rounded-full bg-duet-ink-3"
className="mt-[calc(0.765em-2.5px)] size-[5px] shrink-0 rounded-full bg-duet-ink-3"
/>
<span className="min-w-0">{item}</span>
</li>
Expand Down
12 changes: 8 additions & 4 deletions openbot/app/src/components/gallery/activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { callComponentFunction } from "@/lib/components/queries";
import { useActiveBotId } from "@/lib/copilot/active-bot";
import { useConversation } from "@/lib/copilot/conversation";
import type { GalleryComponent } from "@/lib/copilot/gallery-registry";
import { appConfig } from "@/lib/generated/application-config";
import { keyedLookup } from "@/lib/lookup";
import { GalleryFrame } from "./frame";
import { seriesColour } from "./palette";
Expand Down Expand Up @@ -110,7 +111,10 @@ export function ActivityReportCard({

if (state.status === "reading") {
return (
<GalleryFrame caption="Reading from OpenBot" title={title ?? "Report"}>
<GalleryFrame
caption={`Reading from ${appConfig.brand.productName}`}
title={title ?? "Report"}
>
<p className="text-sm text-muted-foreground">Reading…</p>
</GalleryFrame>
);
Expand Down Expand Up @@ -180,7 +184,7 @@ function ActivityChart({
</Button>
) : undefined
}
caption={`Counted from OpenBot's audit trail, last ${data.days} days`}
caption={`Counted from ${appConfig.brand.productName}'s audit trail, last ${data.days} days`}
title={title ?? "Bot activity"}
>
<ul className="flex flex-col gap-2">
Expand Down Expand Up @@ -223,7 +227,7 @@ function RefusalList({
return (
<GalleryFrame title={title ?? "Recent refusals"}>
<p className="text-sm text-muted-foreground">
OpenBot hasn't refused anything.
{appConfig.brand.productName} hasn't refused anything.
</p>
</GalleryFrame>
);
Expand All @@ -246,7 +250,7 @@ function RefusalList({
</Button>
) : undefined
}
caption="Read from OpenBot's audit trail"
caption={`Read from ${appConfig.brand.productName}'s audit trail`}
title={title ?? "Recent refusals"}
>
<ul className="flex flex-col gap-2">
Expand Down
5 changes: 3 additions & 2 deletions openbot/app/src/components/gallery/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GALLERY_COMPONENTS } from "@/lib/copilot/gallery-registry";
import { appConfig } from "@/lib/generated/application-config";

/**
* The answers a Bot can give, drawn rather than described.
Expand Down Expand Up @@ -117,8 +118,8 @@ function PreviewOf({ name }: { name: string }) {
*/
return (
<p className="text-muted-foreground text-sm">
Shows OpenBot's own records. There's nothing to show until a Bot asks
for it.
Shows {appConfig.brand.productName}'s own records. There's nothing to
show until a Bot asks for it.
</p>
);
}
Expand Down
6 changes: 4 additions & 2 deletions openbot/app/src/components/memory/sources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { agentListQueryOptions } from "@/lib/agents/queries";
import { appConfig } from "@/lib/generated/application-config";
import {
addMemorySourceMutationOptions,
availableMemorySourcesQueryOptions,
Expand Down Expand Up @@ -203,7 +204,8 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
<DialogHeader>
<DialogTitle>Add a source</DialogTitle>
<DialogDescription>
OpenBot reads it now, then every 15 minutes while it's on.
{appConfig.brand.productName} reads it now, then every 15 minutes
while it's on.
</DialogDescription>
</DialogHeader>
<DialogBody className="mt-4 overflow-y-auto">
Expand Down Expand Up @@ -235,7 +237,7 @@ export function MemorySources({ agentId: fixedBot }: { agentId?: string }) {
.filter(Boolean);
else if (setting.schema.type === "object")
throw new Error(
"This one has settings OpenBot can't show. Choose another.",
`This one has settings ${appConfig.brand.productName} can't show. Choose another.`,
);
else values[setting.name] = value;
}
Expand Down
Loading
Loading