diff --git a/apps/web/src/app.css b/apps/web/src/app.css index adc1fc2d5..71c878488 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -4228,11 +4228,11 @@ a.drawer-li:hover, flex: 1; min-height: 0; overflow-y: auto; + padding-top: 72px; } .workbench-main-composer { flex-shrink: 0; - border-top: 1px solid var(--border); - padding: 0.75rem 0; + padding: 0.75rem 1rem; } .workbench-participants { display: flex; @@ -4258,6 +4258,12 @@ a.drawer-li:hover, font-size: 0.78rem; color: var(--muted-foreground); cursor: pointer; + opacity: 0; + transition: opacity 120ms ease-out; +} +.chat-thread-message:hover .workbench-replies-link, +.workbench-replies-link:focus-visible { + opacity: 1; } .workbench-replies-link:hover { color: var(--foreground); diff --git a/apps/web/src/bench/bench-pill.tsx b/apps/web/src/bench/bench-pill.tsx index cf3b70d58..aae462f2d 100644 --- a/apps/web/src/bench/bench-pill.tsx +++ b/apps/web/src/bench/bench-pill.tsx @@ -1,19 +1,31 @@ import { CaretDown } from "@/lib/icons"; import { IdentityAvatar } from "@/chat/avatar"; +import { Skeleton } from "@corbits/react-ui"; import type { WorkerStatus } from "../worker-status"; +function statusLine(status: WorkerStatus, workerName: string | undefined): string { + if (status.text === "Working…") { + return workerName === undefined ? "Working…" : `${workerName} is working…`; + } + if (status.text === "Live") return "Idle"; + return status.text; +} + /** Frosted pill centered over the thread: the bench's worker, its name and * live status. Toggles the bench drawer. */ export function BenchPill({ benchName, worker, status, + rosterReady, open, onToggle, }: { readonly benchName: string; readonly worker: { readonly id: string; readonly name: string } | undefined; readonly status: WorkerStatus; + /** False until the roster resolves; the avatar holds a skeleton. */ + readonly rosterReady: boolean; readonly open: boolean; readonly onToggle: () => void; }) { @@ -25,7 +37,11 @@ export function BenchPill({ aria-controls="bench-drawer" onClick={onToggle} > - {worker === undefined ? null : ( + {worker === undefined ? ( + rosterReady ? null : ( + + ) + ) : ( )} {benchName} - {status.text} + {statusLine(status, worker?.name)} ); diff --git a/apps/web/src/chat/composer.tsx b/apps/web/src/chat/composer.tsx index 13b2b2968..081731d49 100644 --- a/apps/web/src/chat/composer.tsx +++ b/apps/web/src/chat/composer.tsx @@ -1,7 +1,7 @@ // No Stop action: neither `threads-api.ts` nor the hub API expose a way to // cancel a running turn yet. import { useCommandPaletteNavigation } from "@corbits/react-ui"; -import { ArrowUp, CircleNotch, Microphone, Plus } from "@/lib/icons"; +import { ArrowUp, AudioLines, CircleNotch, Plus } from "@/lib/icons"; import { useLayoutEffect, useRef, useState } from "react"; import { activeMention, applyMention, matchMentionQuery, type ActiveMention } from "./mentions"; @@ -172,7 +172,7 @@ export function Composer({ disabled={disabled} onClick={onVoice} > - + )} void; }) { // Avatars read off the participant's real name — the person's own // included, never the "You" transcript label — falling back to the // address local part a mail turn otherwise carries. - const avatarName = resolveAvatarName(message, participants); const matched = participants.find((participant) => sameAddress(participant.address, message.address), ); - const kind = message.author !== "me" && matched?.kind === "agent" ? "agent" : "person"; + const agents = participants.filter((p) => p.kind === "agent"); + // A released or restarted run's address no longer matches the roster; with + // one agent in the bench the sender is unambiguous. + const resolved = matched ?? (agents.length === 1 ? agents[0] : undefined); + const avatarName = + message.author !== "me" && matched === undefined && resolved !== undefined + ? resolved.name + : resolveAvatarName(message, participants); + const kind = message.author !== "me" && resolved?.kind === "agent" ? "agent" : "person"; // The person's own send carries a trailing roster block so agents in the // workbench can hand off to each other; it's never something a person should // see echoed back at them. @@ -104,15 +114,19 @@ function WorkbenchMessageRow({ return ( - + {rosterReady ? ( + + ) : ( + + )} - {avatarName} + {rosterReady ? {avatarName} : } {Number.isNaN(time.getTime()) ? null : ( {time.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })} @@ -271,6 +285,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } benchName={tenant.data?.name ?? "Workbench"} worker={agents[0]} status={workerStatus} + rosterReady={participants.data !== undefined} open={drawerOpen} onToggle={() => setDrawerOpen((open) => !open)} /> @@ -284,6 +299,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } message={message} participants={participants.data ?? []} workbenchTenantId={workbenchTenantId} + rosterReady={participants.data !== undefined} onReply={(target) => setOpenThread(target.messageId)} /> ))} @@ -298,12 +314,12 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } - + ))}