diff --git a/apps/web/src/bench/pending-opening-message.tsx b/apps/web/src/bench/pending-opening-message.tsx
new file mode 100644
index 000000000..2850368b5
--- /dev/null
+++ b/apps/web/src/bench/pending-opening-message.tsx
@@ -0,0 +1,79 @@
+// The first message of a new workbench, shown as a pending own bubble until
+// the agent is live and the send lands. Past the cap it stays here with a
+// retry; it is only cleared once the send succeeds.
+
+import { Button } from "@corbits/react-ui";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useEffect, useRef, useState } from "react";
+
+import { sendToWorkbench, type WorkbenchParticipant } from "@/chat/threads-api";
+import { workbenchKeys } from "../chat-path";
+import { clearOpeningMessage, readOpeningMessage } from "../opening-message";
+
+const CAP_MS = 60_000;
+
+export function PendingOpeningMessage({
+ workbenchTenantId,
+ participants,
+}: {
+ readonly workbenchTenantId: string;
+ readonly participants: readonly WorkbenchParticipant[];
+}) {
+ const queryClient = useQueryClient();
+ const [text, setText] = useState(() => readOpeningMessage(workbenchTenantId));
+ const [timedOut, setTimedOut] = useState(false);
+ const attempted = useRef(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setTimedOut(true), CAP_MS);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const send = useMutation({
+ mutationFn: (content: string) => sendToWorkbench({ workbenchTenantId, participants, content }),
+ onSuccess: () => {
+ clearOpeningMessage(workbenchTenantId);
+ setText(null);
+ void queryClient.invalidateQueries({ queryKey: workbenchKeys.scope(workbenchTenantId) });
+ },
+ });
+
+ if (text === null) return null;
+
+ const live = participants.some((p) => p.kind === "agent" && p.address.includes("@"));
+ if (live && !attempted.current && !send.isPending) {
+ attempted.current = true;
+ send.mutate(text);
+ }
+ const failed = send.isError || (timedOut && !live);
+
+ return (
+
+
+
{text}
+ {failed ? (
+
+ This message hasn't been sent yet.{" "}
+
+
+ ) : (
+
Sending once the agent is ready…
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/opening-message.ts b/apps/web/src/opening-message.ts
new file mode 100644
index 000000000..ccccba353
--- /dev/null
+++ b/apps/web/src/opening-message.ts
@@ -0,0 +1,31 @@
+// A new workbench's first message outlives the /new page: it is parked here
+// until the workbench's agent is live and the send lands, so it is never lost.
+
+import { reportError } from "@corbits/error-sink";
+
+const key = (tenantId: string): string => `workbench-opening-message:${tenantId}`;
+
+export function parkOpeningMessage(tenantId: string, text: string): void {
+ try {
+ sessionStorage.setItem(key(tenantId), text);
+ } catch (cause) {
+ reportError(cause, { operation: "opening_message_park", tenantId });
+ }
+}
+
+export function readOpeningMessage(tenantId: string): string | null {
+ try {
+ return sessionStorage.getItem(key(tenantId));
+ } catch (cause) {
+ reportError(cause, { operation: "opening_message_read", tenantId });
+ return null;
+ }
+}
+
+export function clearOpeningMessage(tenantId: string): void {
+ try {
+ sessionStorage.removeItem(key(tenantId));
+ } catch (cause) {
+ reportError(cause, { operation: "opening_message_clear", tenantId });
+ }
+}
diff --git a/apps/web/src/pages/new-workbench-picker.tsx b/apps/web/src/pages/new-workbench-picker.tsx
index 5a70f3e4f..cb12b07a4 100644
--- a/apps/web/src/pages/new-workbench-picker.tsx
+++ b/apps/web/src/pages/new-workbench-picker.tsx
@@ -26,11 +26,9 @@ const GENERIC_CREATE_FAILURE = "Something went wrong creating this workbench. Tr
export function describeWorkbenchCreateFailure(cause: unknown, refId?: string): string {
if (!(cause instanceof WorkbenchCreateError)) return GENERIC_CREATE_FAILURE;
const message =
- cause.stage === "opening-message"
- ? "Workbench created, but we couldn't send the opening message. Try again from the workbench."
- : cause.stage === "deploy"
- ? "Workbench created, but its agent couldn't be deployed into it. Try again from the workbench."
- : GENERIC_CREATE_FAILURE;
+ cause.stage === "deploy"
+ ? "Workbench created, but its agent couldn't be deployed into it. Try again from the workbench."
+ : GENERIC_CREATE_FAILURE;
return refId === undefined ? message : `${message} Reference: ${refId}`;
}
diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx
index 6b47b8736..ee7a71c13 100644
--- a/apps/web/src/pages/workbench-page.tsx
+++ b/apps/web/src/pages/workbench-page.tsx
@@ -26,6 +26,7 @@ import {
import { VoiceOverlay } from "../voice/voice-overlay";
import { BenchDrawer } from "../bench/bench-drawer";
import { BenchPill } from "../bench/bench-pill";
+import { PendingOpeningMessage } from "../bench/pending-opening-message";
import { ArtifactsTab } from "../bench/artifacts-tab";
import { useWorkerStatus } from "../worker-status";
import { GrantsTab } from "../bench/grants-tab";
@@ -283,6 +284,10 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string }
onReply={(target) => setOpenThread(target.messageId)}
/>
))}
+
{send.error === null ? null : (
{errorText(send.error)}
diff --git a/apps/web/src/workbench-create.ts b/apps/web/src/workbench-create.ts
index e5ef35c0f..9c2c49875 100644
--- a/apps/web/src/workbench-create.ts
+++ b/apps/web/src/workbench-create.ts
@@ -1,17 +1,18 @@
// An agent joins a workbench by being deployed into the child tenant
// itself — its deploy route rejects the parent's inherited asset.
-import { isMyraAgent, listWorkbenchParticipants, sendToWorkbench } from "@/chat/threads-api";
+import { isMyraAgent } from "@/chat/threads-api";
import { agentSlugFromSourceAssetName, deployAgentSource } from "./agent-deploy";
import { readAgentSource } from "./agent-source-read";
import { deployMyraSource } from "./myra-deploy";
+import { parkOpeningMessage } from "./opening-message";
import { createFetchStockHub } from "./needs-converge";
import { resolveExistingOffering } from "./onboarding/provider-connect-step";
export class WorkbenchCreateError extends Error {
constructor(
message: string,
- readonly stage: "create" | "deploy" | "opening-message",
+ readonly stage: "create" | "deploy",
/** Set once the workbench tenant exists, so a later-stage failure can
* still land the person in the workbench it created. */
readonly tenantId?: string,
@@ -71,6 +72,11 @@ export async function createWorkbench(input: CreateWorkbenchInput): Promise participant.kind === "agent");
- // A deployment's run address exists only once the deploy settles; a
- // workbench whose agent has not surfaced yet keeps the opening message
- // for the person to send from the workbench itself.
- if (agents.length > 0) {
- await sendToWorkbench({
- workbenchTenantId: tenantId,
- participants,
- content: input.openingMessage,
- });
- }
- } catch (cause) {
- throw failure(cause, "opening-message", tenantId);
- }
- }
-
return tenantId;
}