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: 8 additions & 2 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/bench/bench-pill.tsx
Original file line number Diff line number Diff line change
@@ -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;
}) {
Expand All @@ -25,7 +37,11 @@ export function BenchPill({
aria-controls="bench-drawer"
onClick={onToggle}
>
{worker === undefined ? null : (
{worker === undefined ? (
rosterReady ? null : (
<Skeleton className="size-6 rounded-full" />
)
) : (
<IdentityAvatar
kind="agent"
name={worker.name}
Expand All @@ -34,7 +50,7 @@ export function BenchPill({
/>
)}
<b>{benchName}</b>
<span className="bench-pill-status">{status.text}</span>
<span className="bench-pill-status">{statusLine(status, worker?.name)}</span>
<CaretDown size={14} aria-hidden="true" />
</button>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/chat/composer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -172,7 +172,7 @@ export function Composer({
disabled={disabled}
onClick={onVoice}
>
<Microphone aria-hidden="true" />
<AudioLines aria-hidden="true" />
</button>
)}
<button
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export {
Maximize2 as ArrowsOut,
MessageCircle as ChatCircle,
MessageCircleMore as ChatCircleDots,
AudioLines,
Mic as Microphone,
MicOff as MicrophoneSlash,
Minimize2 as ArrowsIn,
Expand Down
39 changes: 28 additions & 11 deletions apps/web/src/pages/workbench-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// See docs/chat-mail-threading.md. The bench pill opens a drawer that pushes
// the thread left; the drawer's Information tab carries the bench overview.

import { Button, EmptyState, PageShell, toast } from "@corbits/react-ui";
import { Button, EmptyState, PageShell, Skeleton, toast } from "@corbits/react-ui";
import { WarningCircle } from "@/lib/icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -52,22 +52,32 @@ function WorkbenchMessageRow({
message,
participants,
workbenchTenantId,
rosterReady,
onReply,
}: {
readonly message: WorkbenchMessage;
readonly participants: readonly WorkbenchParticipant[];
readonly workbenchTenantId: string;
/** False until the roster resolves, so a raw run id never shows as sender. */
readonly rosterReady: boolean;
/** Undefined in the sub-thread panel, where a row is read-only context. */
readonly onReply?: (message: WorkbenchMessage) => 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.
Expand Down Expand Up @@ -104,15 +114,19 @@ function WorkbenchMessageRow({
return (
<div className="chat-thread-message" data-author={message.author}>
<span className="shell-ch-avatar">
<IdentityAvatar
kind={kind}
name={avatarName}
principalId={matched?.id ?? message.address}
/>
{rosterReady ? (
<IdentityAvatar
kind={kind}
name={avatarName}
principalId={resolved?.id ?? message.address}
/>
) : (
<Skeleton className="size-8 rounded-full" />
)}
</span>
<div className="chat-thread-body">
<div className="chat-thread-head">
<b>{avatarName}</b>
{rosterReady ? <b>{avatarName}</b> : <Skeleton className="h-4 w-20" />}
{Number.isNaN(time.getTime()) ? null : (
<time dateTime={message.at}>
{time.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}
Expand Down Expand Up @@ -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)}
/>
Expand All @@ -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)}
/>
))}
Expand All @@ -298,12 +314,12 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string }
</PageShell>
</div>
<div className="workbench-main-composer">
<PageShell width="prose" className="page-fill">
<PageShell width="full" className="page-fill">
<ProviderSkipBanner />
<Composer
placeholder={
startingAgent === undefined
? "Message this workbench"
? `Message ${tenant.data?.name ?? "this workbench"}`
: `${startingAgent.name} is starting…`
}
busy={send.isPending}
Expand Down Expand Up @@ -333,6 +349,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string }
message={message}
participants={participants.data ?? []}
workbenchTenantId={workbenchTenantId}
rosterReady={participants.data !== undefined}
/>
))}
</div>
Expand Down
Loading