diff --git a/apps/web/src/chat/threads-api.ts b/apps/web/src/chat/threads-api.ts index 9b4aa1c25c..8e065a488b 100644 --- a/apps/web/src/chat/threads-api.ts +++ b/apps/web/src/chat/threads-api.ts @@ -470,6 +470,9 @@ export type RoomParticipant = { /** For an agent, its current live run's address — empty when none is * live, which `sendToRoom` filters out. */ readonly address: string; + /** The workflow asset's raw name — only present for a `kind: "agent"` + * row; what a released agent's redeploy re-reads/re-pushes source by. */ + readonly assetName?: string; }; const PrincipalPage = type({ @@ -506,6 +509,7 @@ export async function listRoomParticipants(tenantId: string): Promise void; }) { const [text, setText] = useState(""); const send = () => { const trimmed = text.trim(); - if (trimmed === "" || busy) return; + if (trimmed === "" || busy || disabled) return; setText(""); onSend(trimmed); }; @@ -67,6 +70,7 @@ function RoomComposer({ rows={3} placeholder={placeholder} aria-label={placeholder} + disabled={disabled} onChange={(event) => setText(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey) { @@ -75,7 +79,7 @@ function RoomComposer({ } }} /> - @@ -84,17 +88,22 @@ function RoomComposer({ function RoomMessageRow({ message, + participants, onReply, }: { readonly message: RoomMessage; + readonly participants: readonly RoomParticipant[]; /** Undefined in the sub-thread panel, where a row is read-only context. */ readonly onReply?: (message: RoomMessage) => void; }) { + // Avatars read off the participant's display name (Myra → "M"), never the + // run address local part a mail turn otherwise carries. + const displayName = resolveParticipantName(message, participants); return (
@@ -272,6 +281,33 @@ function RoomInfoColumn({ ); } +/** Redeploys one released agent (asset present, no live run — a hub + * restart releases every process-provisioned deployment). Renders nothing; + * one instance per released agent, keyed by asset id, so a mount's own ref + * plus the mutation's `isPending`/`isSuccess` keep StrictMode's double + * render (and any refetch that finds the same agent still released) from + * firing it twice. */ +function AgentRedeployer({ + roomTenantId, + agent, +}: { + readonly roomTenantId: string; + readonly agent: { readonly id: string; readonly name: string; readonly assetName: string }; +}) { + const queryClient = useQueryClient(); + const started = useRef(false); + const redeploy = useMutation({ + mutationFn: () => redeployRoomAgent(roomTenantId, agent), + onSuccess: () => queryClient.invalidateQueries({ queryKey: roomKeys.scope(roomTenantId) }), + onError: (cause) => toast(errorText(cause)), + }); + if (!started.current && !redeploy.isPending && !redeploy.isSuccess) { + started.current = true; + redeploy.mutate(); + } + return null; +} + function Room({ roomTenantId }: { readonly roomTenantId: string }) { const queryClient = useQueryClient(); const [openThread, setOpenThread] = useState(null); @@ -283,6 +319,10 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) { const participants = useQuery({ queryKey: roomKeys.participants(roomTenantId), queryFn: () => listRoomParticipants(roomTenantId), + // Poll while any agent has no live run yet, so the room notices its own + // redeploy finishing without a manual refresh. + refetchInterval: (query) => + (query.state.data ?? []).some((p) => p.kind === "agent" && p.address === "") ? 3000 : false, }); const timeline = useQuery({ queryKey: roomKeys.timeline(roomTenantId), @@ -300,6 +340,12 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) { ); const agents = (participants.data ?? []).filter((participant) => participant.kind === "agent"); + // Released by a hub restart: the asset is still here but nothing is live. + const releasedAgents = agents.filter( + (agent): agent is RoomParticipant & { assetName: string } => + agent.address === "" && agent.assetName !== undefined, + ); + const startingAgent = releasedAgents[0]; const send = useMutation({ mutationFn: ({ content, @@ -337,6 +383,13 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) { return (
+ {releasedAgents.map((agent) => ( + + ))}
setOpenThread(target.messageId)} /> ))} @@ -364,8 +418,13 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) {
send.mutate({ content: text })} /> @@ -381,12 +440,21 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) {
{openedChain.map((message) => ( - + ))}
send.mutate({ content: text, inReplyTo: opened.messageId })} /> diff --git a/apps/web/src/workbench-create.ts b/apps/web/src/workbench-create.ts index 8355081a66..5b9efedd28 100644 --- a/apps/web/src/workbench-create.ts +++ b/apps/web/src/workbench-create.ts @@ -4,7 +4,7 @@ // agent joins a room by being deployed there — with the child's own // resolved offering, which does inherit. -import { listRoomParticipants, sendToRoom } from "@/chat/threads-api"; +import { isMyraAgent, listRoomParticipants, sendToRoom } from "@/chat/threads-api"; import { deployAgentSource } from "./agent-deploy"; import { readAgentSource } from "./agent-source-read"; import { deployMyraSource } from "./myra-deploy"; @@ -116,3 +116,42 @@ export async function createWorkbench(input: CreateWorkbenchInput): Promise { + if (isMyraAgent(agent)) { + const hub = createFetchStockHub(); + const [tenant, offering] = await Promise.all([ + hub.getTenant(roomTenantId), + resolveExistingOffering(roomTenantId), + ]); + if (tenant === null) { + throw new WorkbenchCreateError("this workbench no longer exists", "deploy"); + } + if (offering === null) { + throw new WorkbenchCreateError( + "Connect a model provider in Settings before restarting Myra.", + "deploy", + ); + } + const deployInput = await deployMyraSource({ + tenantId: roomTenantId, + tenantDomain: tenant.domain, + sourceOfferingIds: offering.sourceOfferingIds, + defaultSourceOfferingId: offering.defaultSourceOfferingId, + declaredSources: offering.declaredSources, + }); + await hub.deployWorkflow(roomTenantId, deployInput); + return; + } + const source = await readAgentSource(roomTenantId, agent.id, agent.assetName); + await deployAgentSource({ + tenantId: roomTenantId, + input: { name: agent.name, systemPrompt: source.systemPrompt }, + }); +}