+ >(() => new Map());
+ React.useEffect(() => {
+ const stored = sessionStorage.getItem(BOARD_REPLAY_STORAGE_KEY);
+ if (!stored) return;
+ sessionStorage.removeItem(BOARD_REPLAY_STORAGE_KEY);
+ try {
+ const parsed: unknown = JSON.parse(stored);
+ if (Array.isArray(parsed)) {
+ setReplayReferences(
+ parsed.flatMap((value) => {
+ const references = parseBoardReferences([
+ ["buzz:board-ref", "1", JSON.stringify(value)],
+ ]);
+ return references;
+ }),
+ );
+ }
+ } catch {
+ // Malformed cross-route replay state fails closed.
+ }
+ }, []);
const workstreamChannels = filterWorkstreamChannels(channelsQuery.data ?? []);
+ const discussionChannel =
+ workstreamChannels.find((channel) => channel.id === discussionChannelId) ??
+ null;
+ const discussionMessages = useChannelMessagesQuery(discussionChannel);
+ const sendMessage = useSendMessageMutation(
+ discussionChannel,
+ identityQuery.data,
+ );
+ const selectedKeys = React.useMemo(
+ () => new Set(selectedReferences.map(boardReferenceKey)),
+ [selectedReferences],
+ );
+ const replayKeys = React.useMemo(
+ () => new Set(replayReferences.map(boardReferenceKey)),
+ [replayReferences],
+ );
+ const liveReferences = React.useMemo(
+ () =>
+ new Map(
+ [...liveReferencesByChannel.values()]
+ .flat()
+ .map((reference) => [reference.identity, reference]),
+ ),
+ [liveReferencesByChannel],
+ );
+ const onReferencesChange = React.useCallback(
+ (channelId: string, references: readonly BoardReference[]) => {
+ setLiveReferencesByChannel((current) => {
+ const previous = current.get(channelId);
+ if (JSON.stringify(previous) === JSON.stringify(references))
+ return current;
+ const next = new Map(current);
+ next.set(channelId, references);
+ return next;
+ });
+ },
+ [],
+ );
+ const toggleReference = React.useCallback((reference: BoardReference) => {
+ setDiscussionChannelId(
+ (current) => current ?? reference.placement.workstreamId,
+ );
+ setSelectedReferences((current) => {
+ if (
+ current.length > 0 &&
+ current[0].placement.workstreamId !== reference.placement.workstreamId
+ )
+ return current;
+ const key = boardReferenceKey(reference);
+ return current.some((item) => boardReferenceKey(item) === key)
+ ? current.filter((item) => boardReferenceKey(item) !== key)
+ : [...current, reference];
+ });
+ }, []);
const activeTurnsByChannel = useActiveAgentTurnsByChannel();
const activeWorkstreamTurns = React.useMemo(
() => getActiveWorkstreamTurns(workstreamChannels, activeTurnsByChannel),
@@ -105,6 +204,162 @@ export function WorkstreamBoardScreen() {
description="Live canvases for active workstream channels."
title="Workstream Board"
/>
+
+
+ {replayReferences.length > 0 ? (
+
+ ) : null}
+
+ {discussionMode ? (
+
+
+ Reference tray ({selectedReferences.length})
+
+
+ {selectedReferences.map((reference, index) => (
+ -
+
+ {index + 1}. {reference.kind}: {reference.snapshot.label}
+
+
+
+ ))}
+
+
+ ) : null}
{channelsQuery.isLoading ? (
Loading workstreams…
@@ -131,6 +386,13 @@ export function WorkstreamBoardScreen() {
+ onReferencesChange(channel.id, references)
+ }
+ replayReferenceKeys={replayKeys}
+ selectedReferenceKeys={selectedKeys}
currentOwnerPubkey={identityQuery.data?.pubkey}
key={channel.id}
onSelect={(channelId) => void goChannel(channelId)}
diff --git a/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx b/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
index a629dfbc09..2769e67bad 100644
--- a/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
+++ b/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
@@ -1,5 +1,7 @@
import * as React from "react";
import { Hash } from "lucide-react";
+import type { BoardReference } from "@/features/workstream-board/lib/boardReferences";
+import { boardReferenceKey } from "@/features/workstream-board/lib/boardReferences";
import type { ActiveChannelTurnSummary } from "@/features/agents/activeAgentTurnsStore";
import {
@@ -12,6 +14,7 @@ import {
useWorkstreamPullRequestStatuses,
} from "@/features/workstream-board/lib/workstreamPullRequestStatus";
import {
+ parseWorkstreamWaitMessageLink,
waitsForCard,
type WorkstreamWait,
} from "@/features/workstream-board/lib/workstreamWaits";
@@ -34,15 +37,25 @@ type WorkstreamCardProps = {
createdAt: string | null | undefined,
) => void;
onSelect: (channelId: string) => void;
+ discussionMode?: boolean;
+ selectedReferenceKeys?: ReadonlySet;
+ replayReferenceKeys?: ReadonlySet;
+ onToggleReference?: (reference: BoardReference) => void;
+ onReferencesChange?: (references: readonly BoardReference[]) => void;
};
export function WorkstreamCard({
activeWorking,
channel,
+ discussionMode = false,
currentOwnerPubkey,
onSelect,
onWaitsChange,
+ onToggleReference,
+ onReferencesChange,
profiles,
+ replayReferenceKeys,
+ selectedReferenceKeys,
}: WorkstreamCardProps) {
const canvasQuery = useCanvasQuery(channel.id);
const detailsQuery = useChannelDetailsQuery(channel.id);
@@ -87,18 +100,102 @@ export function WorkstreamCard({
normalizePubkey(wait.actor.pubkey) ===
normalizePubkey(currentOwnerPubkey),
);
+ const discussionReferences = React.useMemo(() => {
+ if (viewModel.status !== "ready") return [];
+ const placement = {
+ workstreamId: channel.id,
+ workstreamLabel: channel.name,
+ sectionId: "workstream",
+ sectionLabel: "Workstream",
+ };
+ const result: BoardReference[] = [
+ {
+ kind: "workstream",
+ identity: channel.id,
+ snapshot: { label: channel.name, detail: viewModel.card.synopsis },
+ placement,
+ },
+ ];
+ for (const reference of references) {
+ result.push({
+ kind: "pull-request",
+ identity: reference.identity,
+ snapshot: { label: reference.rawUrl },
+ placement: {
+ ...placement,
+ sectionId: "pull-requests",
+ sectionLabel: "Pull requests",
+ },
+ });
+ }
+ for (const assignee of viewModel.card.assignees) {
+ const identity = normalizePubkey(assignee.pubkey);
+ if (/^[0-9a-f]{64}$/.test(identity))
+ result.push({
+ kind: "agent",
+ identity,
+ snapshot: { label: assignee.name },
+ placement: {
+ ...placement,
+ sectionId: "agents",
+ sectionLabel: "Agents",
+ },
+ });
+ }
+ result.push({
+ kind: "agent-group",
+ identity: `orchestrator:${normalizePubkey(viewModel.card.orchestrator.pubkey)}`,
+ snapshot: {
+ label: `${viewModel.card.orchestrator.name}'s team`,
+ detail: "Orchestrator group",
+ },
+ placement: { ...placement, sectionId: "agents", sectionLabel: "Agents" },
+ });
+ for (const wait of waits) {
+ const destination = parseWorkstreamWaitMessageLink(wait.message);
+ if (!destination) continue;
+ result.push({
+ kind: "thread-blocker",
+ identity: wait.key,
+ destination: {
+ channelId: destination.channelId,
+ messageId: destination.messageId,
+ ...(destination.threadRootId
+ ? { threadRootId: destination.threadRootId }
+ : {}),
+ },
+ snapshot: { label: wait.reason, detail: wait.actor.name },
+ placement: {
+ ...placement,
+ sectionId: "blockers",
+ sectionLabel: "Blockers",
+ },
+ });
+ }
+ return result;
+ }, [channel.id, channel.name, references, viewModel, waits]);
+ React.useEffect(() => {
+ onReferencesChange?.(discussionReferences);
+ }, [discussionReferences, onReferencesChange]);
return (
+ replayReferenceKeys?.has(boardReferenceKey(reference)),
+ ) && "ring-2 ring-primary ring-offset-2 ring-offset-background",
)}
data-testid={`workstream-card-${channel.id}`}
>