Merge remote-tracking branch 'origin/main' into foundry-terminal-pane

# Conflicts:
#	factory/packages/backend/src/driver.ts
#	factory/packages/backend/src/integrations/sandbox-agent/client.ts
#	factory/packages/backend/test/helpers/test-driver.ts
#	factory/packages/frontend/src/components/mock-layout.tsx
#	pnpm-lock.yaml
#	sdks/react/src/ProcessTerminal.tsx
This commit is contained in:
Nathan Flurry 2026-03-10 23:59:58 -07:00
commit b00c0109d0
288 changed files with 7048 additions and 9134 deletions

View file

@ -34,8 +34,11 @@ import {
type Message,
type ModelId,
} from "./mock-layout/view-model";
import { backendClient } from "../lib/backend";
import { handoffWorkbenchClient } from "../lib/workbench";
const STAR_SANDBOX_AGENT_REPO_STORAGE_KEY = "hf.onboarding.starSandboxAgentRepo";
function firstAgentTabId(handoff: Handoff): string | null {
return handoff.tabs[0]?.id ?? null;
}
@ -56,12 +59,7 @@ function sanitizeLastAgentTabId(handoff: Handoff, tabId: string | null | undefin
return firstAgentTabId(handoff);
}
function sanitizeActiveTabId(
handoff: Handoff,
tabId: string | null | undefined,
openDiffs: string[],
lastAgentTabId: string | null,
): string | null {
function sanitizeActiveTabId(handoff: Handoff, tabId: string | null | undefined, openDiffs: string[], lastAgentTabId: string | null): string | null {
if (tabId) {
if (handoff.tabs.some((tab) => tab.id === tabId)) {
return tabId;
@ -348,9 +346,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const nextOpenDiffs = openDiffs.filter((candidate) => candidate !== path);
onSetOpenDiffs(nextOpenDiffs);
if (activeTabId === diffTabId(path)) {
onSetActiveTabId(
nextOpenDiffs.length > 0 ? diffTabId(nextOpenDiffs[nextOpenDiffs.length - 1]!) : (lastAgentTabId ?? firstAgentTabId(handoff)),
);
onSetActiveTabId(nextOpenDiffs.length > 0 ? diffTabId(nextOpenDiffs[nextOpenDiffs.length - 1]!) : (lastAgentTabId ?? firstAgentTabId(handoff)));
}
},
[activeTabId, handoff, lastAgentTabId, onSetActiveTabId, onSetOpenDiffs, openDiffs],
@ -368,7 +364,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const changeModel = useCallback(
(model: ModelId) => {
if (!promptTab) {
throw new Error(`Unable to change model for handoff ${handoff.id} without an active prompt tab`);
throw new Error(`Unable to change model for task ${handoff.id} without an active prompt tab`);
}
void handoffWorkbenchClient.changeModel({
@ -504,7 +500,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create the first session</h2>
<p style={{ margin: 0, opacity: 0.75 }}>
Sessions are where you chat with the agent. Start one now to send the first prompt on this handoff.
Sessions are where you chat with the agent. Start one now to send the first prompt on this task.
</p>
<button
type="button"
@ -731,11 +727,22 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const [activeTabIdByHandoff, setActiveTabIdByHandoff] = useState<Record<string, string | null>>({});
const [lastAgentTabIdByHandoff, setLastAgentTabIdByHandoff] = useState<Record<string, string | null>>({});
const [openDiffsByHandoff, setOpenDiffsByHandoff] = useState<Record<string, string[]>>({});
const [starRepoPromptOpen, setStarRepoPromptOpen] = useState(false);
const [starRepoPending, setStarRepoPending] = useState(false);
const [starRepoError, setStarRepoError] = useState<string | null>(null);
const activeHandoff = useMemo(
() => handoffs.find((handoff) => handoff.id === selectedHandoffId) ?? handoffs[0] ?? null,
[handoffs, selectedHandoffId],
);
const activeHandoff = useMemo(() => handoffs.find((handoff) => handoff.id === selectedHandoffId) ?? handoffs[0] ?? null, [handoffs, selectedHandoffId]);
useEffect(() => {
try {
const status = globalThis.localStorage?.getItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY);
if (status !== "completed" && status !== "dismissed") {
setStarRepoPromptOpen(true);
}
} catch {
setStarRepoPromptOpen(true);
}
}, []);
useEffect(() => {
if (activeHandoff) {
@ -762,9 +769,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const openDiffs = activeHandoff ? sanitizeOpenDiffs(activeHandoff, openDiffsByHandoff[activeHandoff.id]) : [];
const lastAgentTabId = activeHandoff ? sanitizeLastAgentTabId(activeHandoff, lastAgentTabIdByHandoff[activeHandoff.id]) : null;
const activeTabId = activeHandoff
? sanitizeActiveTabId(activeHandoff, activeTabIdByHandoff[activeHandoff.id], openDiffs, lastAgentTabId)
: null;
const activeTabId = activeHandoff ? sanitizeActiveTabId(activeHandoff, activeTabIdByHandoff[activeHandoff.id], openDiffs, lastAgentTabId) : null;
const syncRouteSession = useCallback(
(handoffId: string, sessionId: string | null, replace = false) => {
@ -821,15 +826,15 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
void (async () => {
const repoId = activeHandoff?.repoId ?? viewModel.repos[0]?.id ?? "";
if (!repoId) {
throw new Error("Cannot create a handoff without an available repo");
throw new Error("Cannot create a task without an available repo");
}
const task = window.prompt("Describe the handoff task", "Investigate and implement the requested change");
const task = window.prompt("Describe the task", "Investigate and implement the requested change");
if (!task) {
return;
}
const title = window.prompt("Optional handoff title", "")?.trim() || undefined;
const title = window.prompt("Optional task title", "")?.trim() || undefined;
const branch = window.prompt("Optional branch name", "")?.trim() || undefined;
const { handoffId, tabId } = await handoffWorkbenchClient.createHandoff({
repoId,
@ -852,7 +857,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const openDiffTab = useCallback(
(path: string) => {
if (!activeHandoff) {
throw new Error("Cannot open a diff tab without an active handoff");
throw new Error("Cannot open a diff tab without an active task");
}
setOpenDiffsByHandoff((current) => {
const existing = sanitizeOpenDiffs(activeHandoff, current[activeHandoff.id]);
@ -896,10 +901,10 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
(id: string) => {
const currentHandoff = handoffs.find((handoff) => handoff.id === id);
if (!currentHandoff) {
throw new Error(`Unable to rename missing handoff ${id}`);
throw new Error(`Unable to rename missing task ${id}`);
}
const nextTitle = window.prompt("Rename handoff", currentHandoff.title);
const nextTitle = window.prompt("Rename task", currentHandoff.title);
if (nextTitle === null) {
return;
}
@ -918,7 +923,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
(id: string) => {
const currentHandoff = handoffs.find((handoff) => handoff.id === id);
if (!currentHandoff) {
throw new Error(`Unable to rename missing handoff ${id}`);
throw new Error(`Unable to rename missing task ${id}`);
}
const nextBranch = window.prompt("Rename branch", currentHandoff.branch ?? "");
@ -938,14 +943,14 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const archiveHandoff = useCallback(() => {
if (!activeHandoff) {
throw new Error("Cannot archive without an active handoff");
throw new Error("Cannot archive without an active task");
}
void handoffWorkbenchClient.archiveHandoff({ handoffId: activeHandoff.id });
}, [activeHandoff]);
const publishPr = useCallback(() => {
if (!activeHandoff) {
throw new Error("Cannot publish PR without an active handoff");
throw new Error("Cannot publish PR without an active task");
}
void handoffWorkbenchClient.publishPr({ handoffId: activeHandoff.id });
}, [activeHandoff]);
@ -953,7 +958,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const revertFile = useCallback(
(path: string) => {
if (!activeHandoff) {
throw new Error("Cannot revert a file without an active handoff");
throw new Error("Cannot revert a file without an active task");
}
setOpenDiffsByHandoff((current) => ({
...current,
@ -964,7 +969,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
[activeHandoff.id]:
current[activeHandoff.id] === diffTabId(path)
? sanitizeLastAgentTabId(activeHandoff, lastAgentTabIdByHandoff[activeHandoff.id])
: current[activeHandoff.id] ?? null,
: (current[activeHandoff.id] ?? null),
}));
void handoffWorkbenchClient.revertFile({
@ -975,106 +980,232 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
[activeHandoff, lastAgentTabIdByHandoff],
);
const dismissStarRepoPrompt = useCallback(() => {
setStarRepoError(null);
try {
globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "dismissed");
} catch {
// ignore storage failures
}
setStarRepoPromptOpen(false);
}, []);
const starSandboxAgentRepo = useCallback(() => {
setStarRepoPending(true);
setStarRepoError(null);
void backendClient
.starSandboxAgentRepo(workspaceId)
.then(() => {
try {
globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "completed");
} catch {
// ignore storage failures
}
setStarRepoPromptOpen(false);
})
.catch((error) => {
setStarRepoError(error instanceof Error ? error.message : String(error));
})
.finally(() => {
setStarRepoPending(false);
});
}, [workspaceId]);
const starRepoPrompt = starRepoPromptOpen ? (
<div
style={{
position: "fixed",
inset: 0,
zIndex: 10000,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "24px",
background: "rgba(0, 0, 0, 0.68)",
}}
data-testid="onboarding-star-repo-modal"
>
<div
style={{
width: "min(520px, 100%)",
border: "1px solid rgba(255, 255, 255, 0.14)",
borderRadius: "18px",
background: "#111113",
boxShadow: "0 32px 80px rgba(0, 0, 0, 0.45)",
padding: "24px",
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ fontSize: "12px", letterSpacing: "0.08em", textTransform: "uppercase", color: "rgba(255, 255, 255, 0.5)" }}>Onboarding</div>
<h2 style={{ margin: 0, fontSize: "24px", lineHeight: 1.1 }}>Give us support for sandbox agent</h2>
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.72)", lineHeight: 1.5 }}>
Before you keep going, give us support for sandbox agent and star the repo right here in the app.
</p>
</div>
{starRepoError ? (
<div
style={{
borderRadius: "12px",
border: "1px solid rgba(255, 110, 110, 0.32)",
background: "rgba(255, 110, 110, 0.08)",
padding: "12px 14px",
color: "#ffb4b4",
fontSize: "13px",
}}
data-testid="onboarding-star-repo-error"
>
{starRepoError}
</div>
) : null}
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
<button
type="button"
onClick={dismissStarRepoPrompt}
style={{
border: "1px solid rgba(255, 255, 255, 0.14)",
borderRadius: "999px",
padding: "10px 16px",
background: "transparent",
color: "#e4e4e7",
cursor: "pointer",
fontWeight: 600,
}}
>
Maybe later
</button>
<button
type="button"
onClick={starSandboxAgentRepo}
disabled={starRepoPending}
style={{
border: 0,
borderRadius: "999px",
padding: "10px 16px",
background: starRepoPending ? "#7f5539" : "#ff4f00",
color: "#fff",
cursor: starRepoPending ? "progress" : "pointer",
fontWeight: 700,
}}
data-testid="onboarding-star-repo-submit"
>
{starRepoPending ? "Starring..." : "Star the sandbox agent repo"}
</button>
</div>
</div>
</div>
) : null;
if (!activeHandoff) {
return (
<>
<Shell>
<Sidebar
projects={projects}
activeId=""
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
/>
<SPanel>
<ScrollBody>
<div
style={{
minHeight: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "32px",
}}
>
<div
style={{
maxWidth: "420px",
textAlign: "center",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first task</h2>
<p style={{ margin: 0, opacity: 0.75 }}>
{viewModel.repos.length > 0
? "Start from the sidebar to create a task on the first available repo."
: "No repos are available in this workspace yet."}
</p>
<button
type="button"
onClick={createHandoff}
disabled={viewModel.repos.length === 0}
style={{
alignSelf: "center",
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: viewModel.repos.length > 0 ? "#ff4f00" : "#444",
color: "#fff",
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
fontWeight: 600,
}}
>
New task
</button>
</div>
</div>
</ScrollBody>
</SPanel>
<SPanel />
</Shell>
{starRepoPrompt}
</>
);
}
return (
<>
<Shell>
<Sidebar
projects={projects}
activeId=""
activeId={activeHandoff.id}
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
/>
<SPanel>
<ScrollBody>
<div
style={{
minHeight: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "32px",
}}
>
<div
style={{
maxWidth: "420px",
textAlign: "center",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first handoff</h2>
<p style={{ margin: 0, opacity: 0.75 }}>
{viewModel.repos.length > 0
? "Start from the sidebar to create a handoff on the first available repo."
: "No repos are available in this workspace yet."}
</p>
<button
type="button"
onClick={createHandoff}
disabled={viewModel.repos.length === 0}
style={{
alignSelf: "center",
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: viewModel.repos.length > 0 ? "#ff4f00" : "#444",
color: "#fff",
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
fontWeight: 600,
}}
>
New handoff
</button>
</div>
</div>
</ScrollBody>
</SPanel>
<SPanel />
<TranscriptPanel
handoff={activeHandoff}
activeTabId={activeTabId}
lastAgentTabId={lastAgentTabId}
openDiffs={openDiffs}
onSyncRouteSession={syncRouteSession}
onSetActiveTabId={(tabId) => {
setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId }));
}}
onSetLastAgentTabId={(tabId) => {
setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId }));
}}
onSetOpenDiffs={(paths) => {
setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: paths }));
}}
/>
<RightRail
workspaceId={workspaceId}
handoff={activeHandoff}
activeTabId={activeTabId}
onOpenDiff={openDiffTab}
onArchive={archiveHandoff}
onRevertFile={revertFile}
onPublishPr={publishPr}
/>
</Shell>
);
}
return (
<Shell>
<Sidebar
projects={projects}
activeId={activeHandoff.id}
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
/>
<TranscriptPanel
handoff={activeHandoff}
activeTabId={activeTabId}
lastAgentTabId={lastAgentTabId}
openDiffs={openDiffs}
onSyncRouteSession={syncRouteSession}
onSetActiveTabId={(tabId) => {
setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId }));
}}
onSetLastAgentTabId={(tabId) => {
setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId }));
}}
onSetOpenDiffs={(paths) => {
setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: paths }));
}}
/>
<RightRail
workspaceId={workspaceId}
handoff={activeHandoff}
activeTabId={activeTabId}
onOpenDiff={openDiffTab}
onArchive={archiveHandoff}
onRevertFile={revertFile}
onPublishPr={publishPr}
/>
</Shell>
{starRepoPrompt}
</>
);
}

View file

@ -4,13 +4,7 @@ import { LabelXSmall } from "baseui/typography";
import { formatMessageTimestamp, type HistoryEvent } from "./view-model";
export const HistoryMinimap = memo(function HistoryMinimap({
events,
onSelect,
}: {
events: HistoryEvent[];
onSelect: (event: HistoryEvent) => void;
}) {
export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: { events: HistoryEvent[]; onSelect: (event: HistoryEvent) => void }) {
const [css, theme] = useStyletron();
const [open, setOpen] = useState(false);
const [activeEventId, setActiveEventId] = useState<string | null>(events[events.length - 1]?.id ?? null);
@ -49,7 +43,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({
>
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "6px" })}>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ letterSpacing: "0.08em", textTransform: "uppercase" }}>
Handoff Events
Task Events
</LabelXSmall>
<LabelXSmall color={theme.colors.contentTertiary}>{events.length}</LabelXSmall>
</div>

View file

@ -23,11 +23,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
const isUser = message.sender === "client";
const isCopied = copiedMessageId === message.id;
const messageTimestamp = formatMessageTimestamp(message.createdAtMs);
const displayFooter = isUser
? messageTimestamp
: message.durationMs
? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}`
: null;
const displayFooter = isUser ? messageTimestamp : message.durationMs ? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}` : null;
return (
<div
@ -59,11 +55,11 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
borderBottomRightRadius: "4px",
}
: {
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: "transparent",
border: "none",
color: "#e4e4e7",
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "16px",
borderRadius: "0",
padding: "0",
}),
})}
>
@ -90,10 +86,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
})}
>
{displayFooter ? (
<LabelXSmall
color={theme.colors.contentTertiary}
$style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}
>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}>
{displayFooter}
</LabelXSmall>
) : null}
@ -170,12 +163,6 @@ export const MessageList = memo(function MessageList({
}),
message: css({
display: "flex",
'&[data-variant="user"]': {
justifyContent: "flex-end",
},
'&[data-variant="assistant"]': {
justifyContent: "flex-start",
},
}),
messageContent: messageContentClass,
messageText: css({
@ -200,6 +187,11 @@ export const MessageList = memo(function MessageList({
return (
<>
<style>{`
[data-variant="user"] > [data-slot="message-content"] {
margin-left: auto;
}
`}</style>
{historyEvents.length > 0 ? <HistoryMinimap events={historyEvents} onSelect={onSelectHistoryEvent} /> : null}
<div
ref={scrollRef}
@ -238,14 +230,7 @@ export const MessageList = memo(function MessageList({
return null;
}
return (
<TranscriptMessageBody
message={message}
messageRefs={messageRefs}
copiedMessageId={copiedMessageId}
onCopyMessage={onCopyMessage}
/>
);
return <TranscriptMessageBody message={message} messageRefs={messageRefs} copiedMessageId={copiedMessageId} onCopyMessage={onCopyMessage} />;
}}
isThinking={Boolean(tab && tab.status === "running" && transcriptEntries.length > 0)}
renderThinkingState={() => (

View file

@ -1,7 +1,7 @@
import { memo, useState } from "react";
import { useStyletron } from "baseui";
import { StatefulPopover, PLACEMENT } from "baseui/popover";
import { ChevronDown, Star } from "lucide-react";
import { ChevronDown, ChevronUp, Star } from "lucide-react";
import { AgentIcon } from "./ui";
import { MODEL_GROUPS, modelLabel, providerAgent, type ModelId } from "./view-model";
@ -23,7 +23,7 @@ const ModelPickerContent = memo(function ModelPickerContent({
const [hoveredId, setHoveredId] = useState<ModelId | null>(null);
return (
<div className={css({ minWidth: "200px", padding: "4px 0" })}>
<div className={css({ minWidth: "220px", padding: "6px 0" })}>
{MODEL_GROUPS.map((group) => (
<div key={group.provider}>
<div
@ -62,7 +62,10 @@ const ModelPickerContent = memo(function ModelPickerContent({
fontSize: "12px",
fontWeight: isActive ? 600 : 400,
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)" },
borderRadius: "6px",
marginLeft: "4px",
marginRight: "4px",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.08)" },
})}
>
<AgentIcon agent={agent} size={12} />
@ -100,22 +103,26 @@ export const ModelPicker = memo(function ModelPicker({
onSetDefault: (id: ModelId) => void;
}) {
const [css, theme] = useStyletron();
const [isOpen, setIsOpen] = useState(false);
return (
<StatefulPopover
placement={PLACEMENT.topLeft}
triggerType="click"
autoFocus={false}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
overrides={{
Body: {
style: {
backgroundColor: "#000000",
borderTopLeftRadius: "8px",
borderTopRightRadius: "8px",
borderBottomLeftRadius: "8px",
borderBottomRightRadius: "8px",
border: `1px solid ${theme.colors.borderOpaque}`,
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.6)",
backgroundColor: "rgba(32, 32, 32, 0.98)",
backdropFilter: "blur(12px)",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
borderBottomLeftRadius: "10px",
borderBottomRightRadius: "10px",
border: "1px solid rgba(255, 255, 255, 0.10)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
zIndex: 100,
},
},
@ -126,15 +133,7 @@ export const ModelPicker = memo(function ModelPicker({
},
},
}}
content={({ close }) => (
<ModelPickerContent
value={value}
defaultModel={defaultModel}
onChange={onChange}
onSetDefault={onSetDefault}
close={close}
/>
)}
content={({ close }) => <ModelPickerContent value={value} defaultModel={defaultModel} onChange={onChange} onSetDefault={onSetDefault} close={close} />}
>
<div className={css({ display: "inline-flex" })}>
<button
@ -149,13 +148,13 @@ export const ModelPicker = memo(function ModelPicker({
fontSize: "12px",
fontWeight: 500,
color: theme.colors.contentSecondary,
backgroundColor: theme.colors.backgroundTertiary,
border: `1px solid ${theme.colors.borderOpaque}`,
":hover": { color: theme.colors.contentPrimary },
backgroundColor: "rgba(255, 255, 255, 0.10)",
border: "1px solid rgba(255, 255, 255, 0.14)",
":hover": { color: theme.colors.contentPrimary, backgroundColor: "rgba(255, 255, 255, 0.14)" },
})}
>
{modelLabel(value)}
<ChevronDown size={11} />
{isOpen ? <ChevronDown size={11} /> : <ChevronUp size={11} />}
</button>
</div>
</StatefulPopover>

View file

@ -43,25 +43,27 @@ export const PromptComposer = memo(function PromptComposer({
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: `1px solid ${theme.colors.borderOpaque}`,
borderRadius: "16px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`,
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
transition: "border-color 200ms ease",
":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" },
display: "flex",
flexDirection: "column",
}),
input: css({
display: "block",
width: "100%",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`,
padding: "12px 58px 12px 14px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 20}px`,
padding: "14px 58px 8px 14px",
background: "transparent",
border: "none",
borderRadius: "16px",
borderRadius: "16px 16px 0 0",
color: theme.colors.contentPrimary,
fontSize: "13px",
fontFamily: "inherit",
resize: "none",
outline: "none",
lineHeight: "1.4",
maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT}px`,
maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT + 40}px`,
boxSizing: "border-box",
overflowY: "hidden",
"::placeholder": { color: theme.colors.contentSecondary },
@ -101,7 +103,7 @@ export const PromptComposer = memo(function PromptComposer({
<div
className={css({
padding: "12px 16px",
borderTop: `1px solid ${theme.colors.borderOpaque}`,
borderTop: "none",
flexShrink: 0,
display: "flex",
flexDirection: "column",
@ -130,11 +132,7 @@ export const PromptComposer = memo(function PromptComposer({
<span>
{fileName(attachment.filePath)}:{attachment.lineNumber}
</span>
<X
size={10}
className={css({ cursor: "pointer", opacity: 0.6, ":hover": { opacity: 1 } })}
onClick={() => onRemoveAttachment(attachment.id)}
/>
<X size={10} className={css({ cursor: "pointer", opacity: 0.6, ":hover": { opacity: 1 } })} onClick={() => onRemoveAttachment(attachment.id)} />
</div>
))}
</div>
@ -155,17 +153,21 @@ export const PromptComposer = memo(function PromptComposer({
}}
placeholder={placeholder}
inputRef={textareaRef}
rows={1}
rows={2}
allowEmptySubmit={isRunning}
submitLabel={isRunning ? "Stop" : "Send"}
classNames={composerClassNames}
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <ArrowUpFromLine size={16} />)}
/>
<ModelPicker
value={model}
defaultModel={defaultModel}
onChange={onChangeModel}
onSetDefault={onSetDefaultModel}
renderFooter={() => (
<div className={css({ padding: "0 10px 8px" })}>
<ModelPicker
value={model}
defaultModel={defaultModel}
onChange={onChangeModel}
onSetDefault={onSetDefaultModel}
/>
</div>
)}
/>
</div>
);

View file

@ -1,16 +1,7 @@
import { memo, useCallback, useMemo, useState, type MouseEvent } from "react";
import { useStyletron } from "baseui";
import { LabelSmall } from "baseui/typography";
import {
Archive,
ArrowUpFromLine,
ChevronRight,
FileCode,
FilePlus,
FileX,
FolderOpen,
GitPullRequest,
} from "lucide-react";
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest } from "lucide-react";
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
import { type FileTreeNode, type Handoff, diffTabId } from "./view-model";
@ -86,13 +77,7 @@ const FileTree = memo(function FileTree({
<span>{node.name}</span>
</div>
{node.isDir && !isCollapsed && node.children ? (
<FileTree
nodes={node.children}
depth={depth + 1}
onSelectFile={onSelectFile}
onFileContextMenu={onFileContextMenu}
changedPaths={changedPaths}
/>
<FileTree nodes={node.children} depth={depth + 1} onSelectFile={onSelectFile} onFileContextMenu={onFileContextMenu} changedPaths={changedPaths} />
) : null}
</div>
);
@ -366,13 +351,7 @@ export const RightSidebar = memo(function RightSidebar({
) : (
<div className={css({ padding: "6px 0" })}>
{handoff.fileTree.length > 0 ? (
<FileTree
nodes={handoff.fileTree}
depth={0}
onSelectFile={onOpenDiff}
onFileContextMenu={openFileMenu}
changedPaths={changedPaths}
/>
<FileTree nodes={handoff.fileTree} depth={0} onSelectFile={onOpenDiff} onFileContextMenu={openFileMenu} changedPaths={changedPaths} />
) : (
<div className={css({ padding: "20px 0", textAlign: "center" })}>
<LabelSmall color={theme.colors.contentTertiary}>No files yet</LabelSmall>

View file

@ -1,17 +1,26 @@
import { memo, useState } from "react";
import { useStyletron } from "baseui";
import { LabelSmall, LabelXSmall } from "baseui/typography";
import { CloudUpload, GitPullRequestDraft, Plus } from "lucide-react";
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
import { formatRelativeAge, type Handoff, type ProjectSection } from "./view-model";
import {
ContextMenuOverlay,
HandoffIndicator,
PanelHeaderBar,
SPanel,
ScrollBody,
useContextMenu,
} from "./ui";
import { ContextMenuOverlay, HandoffIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
function projectInitial(label: string): string {
const parts = label.split("/");
const name = parts[parts.length - 1] ?? label;
return name.charAt(0).toUpperCase();
}
function projectIconColor(label: string): string {
let hash = 0;
for (let i = 0; i < label.length; i++) {
hash = (hash * 31 + label.charCodeAt(i)) | 0;
}
return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]!;
}
export const Sidebar = memo(function Sidebar({
projects,
@ -32,13 +41,25 @@ export const Sidebar = memo(function Sidebar({
}) {
const [css, theme] = useStyletron();
const contextMenu = useContextMenu();
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({});
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
return (
<SPanel>
<style>{`
[data-project-header]:hover [data-chevron] {
display: inline-flex !important;
}
[data-project-header]:hover [data-project-icon] {
display: none !important;
}
`}</style>
<PanelHeaderBar>
<LabelSmall color={theme.colors.contentPrimary} $style={{ fontWeight: 600, flex: 1, fontSize: "13px" }}>
Handoffs
<LabelSmall
color={theme.colors.contentPrimary}
$style={{ fontWeight: 600, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
>
<ListChecks size={14} />
Tasks
</LabelSmall>
<button
onClick={onCreate}
@ -63,157 +84,173 @@ export const Sidebar = memo(function Sidebar({
<ScrollBody>
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
{projects.map((project) => {
const visibleCount = expandedProjects[project.id] ? project.handoffs.length : Math.min(project.handoffs.length, 5);
const hiddenCount = Math.max(0, project.handoffs.length - visibleCount);
const isCollapsed = collapsedProjects[project.id] === true;
return (
<div key={project.id} className={css({ display: "flex", flexDirection: "column", gap: "4px" })}>
<div
onClick={() =>
setCollapsedProjects((current) => ({
...current,
[project.id]: !current[project.id],
}))
}
data-project-header
className={css({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 8px 4px",
gap: "8px",
cursor: "pointer",
userSelect: "none",
":hover": { opacity: 0.8 },
})}
>
<LabelSmall
color={theme.colors.contentSecondary}
$style={{
fontSize: "11px",
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{project.label}
</LabelSmall>
<div className={css({ display: "flex", alignItems: "center", gap: "4px", overflow: "hidden" })}>
<div className={css({ position: "relative", width: "14px", height: "14px", flexShrink: 0 })}>
<span
className={css({
position: "absolute",
inset: 0,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "3px",
fontSize: "9px",
fontWeight: 700,
lineHeight: 1,
color: "#fff",
backgroundColor: projectIconColor(project.label),
})}
data-project-icon
>
{projectInitial(project.label)}
</span>
<span className={css({ position: "absolute", inset: 0, display: "none", alignItems: "center", justifyContent: "center" })} data-chevron>
{isCollapsed ? (
<ChevronDown size={12} color={theme.colors.contentTertiary} />
) : (
<ChevronUp size={12} color={theme.colors.contentTertiary} />
)}
</span>
</div>
<LabelSmall
color={theme.colors.contentSecondary}
$style={{
fontSize: "11px",
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{project.label}
</LabelSmall>
</div>
<LabelXSmall color={theme.colors.contentTertiary}>
{formatRelativeAge(project.updatedAtMs)}
</LabelXSmall>
</div>
{project.handoffs.slice(0, visibleCount).map((handoff) => {
const isActive = handoff.id === activeId;
const isDim = handoff.status === "archived";
const isRunning = handoff.tabs.some((tab) => tab.status === "running");
const hasUnread = handoff.tabs.some((tab) => tab.unread);
const isDraft = handoff.pullRequest == null || handoff.pullRequest.status === "draft";
const totalAdded = handoff.fileChanges.reduce((sum, file) => sum + file.added, 0);
const totalRemoved = handoff.fileChanges.reduce((sum, file) => sum + file.removed, 0);
const hasDiffs = totalAdded > 0 || totalRemoved > 0;
{!isCollapsed && project.handoffs.map((handoff) => {
const isActive = handoff.id === activeId;
const isDim = handoff.status === "archived";
const isRunning = handoff.tabs.some((tab) => tab.status === "running");
const hasUnread = handoff.tabs.some((tab) => tab.unread);
const isDraft = handoff.pullRequest == null || handoff.pullRequest.status === "draft";
const totalAdded = handoff.fileChanges.reduce((sum, file) => sum + file.added, 0);
const totalRemoved = handoff.fileChanges.reduce((sum, file) => sum + file.removed, 0);
const hasDiffs = totalAdded > 0 || totalRemoved > 0;
return (
<div
key={handoff.id}
onClick={() => onSelect(handoff.id)}
onContextMenu={(event) =>
contextMenu.open(event, [
{ label: "Rename handoff", onClick: () => onRenameHandoff(handoff.id) },
{ label: "Rename branch", onClick: () => onRenameBranch(handoff.id) },
{ label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) },
])
}
className={css({
padding: "12px",
borderRadius: "8px",
border: isActive ? "1px solid rgba(255, 255, 255, 0.2)" : "1px solid transparent",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer",
transition: "all 200ms ease",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
borderColor: theme.colors.borderOpaque,
},
})}
>
<div className={css({ display: "flex", alignItems: "center", gap: "8px" })}>
<div
className={css({
width: "14px",
minWidth: "14px",
height: "14px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
})}
>
<HandoffIndicator isRunning={isRunning} hasUnread={hasUnread} isDraft={isDraft} />
</div>
<LabelSmall
$style={{
fontWeight: 600,
flex: 1,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
color={isDim ? theme.colors.contentSecondary : theme.colors.contentPrimary}
>
{handoff.title}
</LabelSmall>
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0 })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
return (
<div
key={handoff.id}
onClick={() => onSelect(handoff.id)}
onContextMenu={(event) =>
contextMenu.open(event, [
{ label: "Rename task", onClick: () => onRenameHandoff(handoff.id) },
{ label: "Rename branch", onClick: () => onRenameBranch(handoff.id) },
{ label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) },
])
}
className={css({
padding: "12px",
borderRadius: "8px",
border: "1px solid transparent",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer",
transition: "all 200ms ease",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
},
})}
>
<div className={css({ display: "flex", alignItems: "center", gap: "8px" })}>
<div
className={css({
width: "14px",
minWidth: "14px",
height: "14px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
})}
>
<HandoffIndicator isRunning={isRunning} hasUnread={hasUnread} isDraft={isDraft} />
</div>
<LabelSmall
$style={{
fontWeight: 600,
flex: 1,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
color={isDim ? theme.colors.contentSecondary : theme.colors.contentPrimary}
>
{handoff.title}
</LabelSmall>
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0 })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
</div>
) : null}
</div>
<div className={css({ display: "flex", alignItems: "center", marginTop: "4px", gap: "6px" })}>
<LabelXSmall
color={theme.colors.contentTertiary}
$style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 1,
}}
>
{handoff.repoName}
</LabelXSmall>
{handoff.pullRequest != null ? (
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
<LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
#{handoff.pullRequest.number}
</LabelXSmall>
{handoff.pullRequest.status === "draft" ? <CloudUpload size={11} color="#ff4f00" /> : null}
</span>
) : (
<GitPullRequestDraft size={11} color={theme.colors.contentTertiary} />
)}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ marginLeft: "auto", flexShrink: 0 }}>
{formatRelativeAge(handoff.updatedAtMs)}
</LabelXSmall>
</div>
</div>
) : null}
</div>
<div className={css({ display: "flex", alignItems: "center", marginTop: "4px", gap: "6px" })}>
<LabelXSmall
color={theme.colors.contentTertiary}
$style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 1,
}}
>
{handoff.repoName}
</LabelXSmall>
{handoff.pullRequest != null ? (
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
<LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
#{handoff.pullRequest.number}
</LabelXSmall>
{handoff.pullRequest.status === "draft" ? <CloudUpload size={11} color="#ff4f00" /> : null}
</span>
) : (
<GitPullRequestDraft size={11} color={theme.colors.contentTertiary} />
)}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ marginLeft: "auto", flexShrink: 0 }}>
{formatRelativeAge(handoff.updatedAtMs)}
</LabelXSmall>
</div>
</div>
);
);
})}
{hiddenCount > 0 ? (
<button
type="button"
onClick={() =>
setExpandedProjects((current) => ({
...current,
[project.id]: true,
}))
}
className={css({
all: "unset",
padding: "8px 12px 10px 34px",
color: theme.colors.contentSecondary,
fontSize: "12px",
cursor: "pointer",
":hover": { color: theme.colors.contentPrimary },
})}
>
Show {hiddenCount} more
</button>
) : null}
</div>
);
})}

View file

@ -1,7 +1,7 @@
import { memo } from "react";
import { useStyletron } from "baseui";
import { LabelSmall } from "baseui/typography";
import { MailOpen } from "lucide-react";
import { Clock, MailOpen } from "lucide-react";
import { PanelHeaderBar } from "./ui";
import { type AgentTab, type Handoff } from "./view-model";
@ -46,7 +46,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
}}
className={css({
all: "unset",
fontWeight: 600,
fontWeight: 500,
fontSize: "14px",
color: theme.colors.contentPrimary,
borderBottom: "1px solid rgba(255, 255, 255, 0.3)",
@ -58,7 +58,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
<LabelSmall
title="Rename"
color={theme.colors.contentPrimary}
$style={{ fontWeight: 600, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
$style={{ fontWeight: 500, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
onClick={() => onStartEditingField("title", handoff.title)}
>
{handoff.title}
@ -113,6 +113,24 @@ export const TranscriptHeader = memo(function TranscriptHeader({
)
) : null}
<div className={css({ flex: 1 })} />
<div
className={css({
display: "flex",
alignItems: "center",
gap: "5px",
padding: "3px 10px",
borderRadius: "6px",
backgroundColor: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.08)",
fontSize: "11px",
fontWeight: 500,
color: theme.colors.contentSecondary,
whiteSpace: "nowrap",
})}
>
<Clock size={11} />
<span>847 min used</span>
</div>
{activeTab ? (
<button
onClick={() => onSetActiveTabUnread(!activeTab.unread)}

View file

@ -129,7 +129,10 @@ export const HandoffIndicator = memo(function HandoffIndicator({
const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 1200 1200" fill="none" style={{ flexShrink: 0 }}>
<path fill="#D97757" d="M 233.96 800.21 L 468.64 668.54 L 472.59 657.1 L 468.64 650.74 L 457.21 650.74 L 417.99 648.32 L 283.89 644.7 L 167.6 639.87 L 54.93 633.83 L 26.58 627.79 L 0 592.75 L 2.74 575.28 L 26.58 559.25 L 60.72 562.23 L 136.19 567.38 L 249.42 575.19 L 331.57 580.03 L 453.26 592.67 L 472.59 592.67 L 475.33 584.86 L 468.72 580.03 L 463.57 575.19 L 346.39 495.79 L 219.54 411.87 L 153.1 363.54 L 117.18 339.06 L 99.06 316.11 L 91.25 266.01 L 123.87 230.09 L 167.68 233.07 L 178.87 236.05 L 223.25 270.2 L 318.04 343.57 L 441.83 434.74 L 459.95 449.8 L 467.19 444.64 L 468.08 441.02 L 459.95 427.41 L 392.62 305.72 L 320.78 181.93 L 288.81 130.63 L 280.35 99.87 C 277.37 87.22 275.19 76.59 275.19 63.62 L 312.32 13.21 L 332.86 6.6 L 382.39 13.21 L 403.25 31.33 L 434.01 101.72 L 483.87 212.54 L 561.18 363.22 L 583.81 407.92 L 595.89 449.32 L 600.4 461.96 L 608.21 461.96 L 608.21 454.71 L 614.58 369.83 L 626.34 265.61 L 637.77 131.52 L 641.72 93.75 L 660.4 48.48 L 697.53 24 L 726.52 37.85 L 750.36 72 L 747.06 94.07 L 732.89 186.2 L 705.1 330.52 L 686.98 427.17 L 697.53 427.17 L 709.61 415.09 L 758.5 350.17 L 840.64 247.49 L 876.89 206.74 L 919.17 161.72 L 946.31 140.3 L 997.61 140.3 L 1035.38 196.43 L 1018.47 254.42 L 965.64 321.42 L 921.83 378.2 L 859.01 462.77 L 819.79 530.42 L 823.41 535.81 L 832.75 534.93 L 974.66 504.72 L 1051.33 490.87 L 1142.82 475.17 L 1184.21 494.5 L 1188.72 514.15 L 1172.46 554.34 L 1074.6 578.5 L 959.84 601.45 L 788.94 641.88 L 786.85 643.41 L 789.26 646.39 L 866.26 653.64 L 899.19 655.41 L 979.81 655.41 L 1129.93 666.6 L 1169.15 692.54 L 1192.67 724.27 L 1188.72 748.43 L 1128.32 779.19 L 1046.82 759.87 L 856.59 714.6 L 791.36 698.34 L 782.34 698.34 L 782.34 703.73 L 836.7 756.89 L 936.32 846.85 L 1061.07 962.82 L 1067.44 991.49 L 1051.41 1014.12 L 1034.5 1011.7 L 924.89 929.23 L 882.6 892.11 L 786.85 811.49 L 780.48 811.49 L 780.48 819.95 L 802.55 852.24 L 919.09 1027.41 L 925.13 1081.13 L 916.67 1098.6 L 886.47 1109.15 L 853.29 1103.11 L 785.07 1007.36 L 714.68 899.52 L 657.91 802.87 L 650.98 806.82 L 617.48 1167.7 L 601.77 1186.15 L 565.53 1200 L 535.33 1177.05 L 519.3 1139.92 L 535.33 1066.55 L 554.66 970.79 L 570.36 894.68 L 584.54 800.13 L 592.99 768.72 L 592.43 766.63 L 585.5 767.52 L 514.23 865.37 L 405.83 1011.87 L 320.05 1103.68 L 299.52 1111.81 L 263.92 1093.37 L 267.22 1060.43 L 287.11 1031.11 L 405.83 880.11 L 477.42 786.52 L 523.65 732.48 L 523.33 724.67 L 520.59 724.67 L 205.29 929.4 L 149.15 936.64 L 124.99 914.01 L 127.97 876.89 L 139.41 864.81 L 234.2 799.57 Z" />
<path
fill="#D97757"
d="M 233.96 800.21 L 468.64 668.54 L 472.59 657.1 L 468.64 650.74 L 457.21 650.74 L 417.99 648.32 L 283.89 644.7 L 167.6 639.87 L 54.93 633.83 L 26.58 627.79 L 0 592.75 L 2.74 575.28 L 26.58 559.25 L 60.72 562.23 L 136.19 567.38 L 249.42 575.19 L 331.57 580.03 L 453.26 592.67 L 472.59 592.67 L 475.33 584.86 L 468.72 580.03 L 463.57 575.19 L 346.39 495.79 L 219.54 411.87 L 153.1 363.54 L 117.18 339.06 L 99.06 316.11 L 91.25 266.01 L 123.87 230.09 L 167.68 233.07 L 178.87 236.05 L 223.25 270.2 L 318.04 343.57 L 441.83 434.74 L 459.95 449.8 L 467.19 444.64 L 468.08 441.02 L 459.95 427.41 L 392.62 305.72 L 320.78 181.93 L 288.81 130.63 L 280.35 99.87 C 277.37 87.22 275.19 76.59 275.19 63.62 L 312.32 13.21 L 332.86 6.6 L 382.39 13.21 L 403.25 31.33 L 434.01 101.72 L 483.87 212.54 L 561.18 363.22 L 583.81 407.92 L 595.89 449.32 L 600.4 461.96 L 608.21 461.96 L 608.21 454.71 L 614.58 369.83 L 626.34 265.61 L 637.77 131.52 L 641.72 93.75 L 660.4 48.48 L 697.53 24 L 726.52 37.85 L 750.36 72 L 747.06 94.07 L 732.89 186.2 L 705.1 330.52 L 686.98 427.17 L 697.53 427.17 L 709.61 415.09 L 758.5 350.17 L 840.64 247.49 L 876.89 206.74 L 919.17 161.72 L 946.31 140.3 L 997.61 140.3 L 1035.38 196.43 L 1018.47 254.42 L 965.64 321.42 L 921.83 378.2 L 859.01 462.77 L 819.79 530.42 L 823.41 535.81 L 832.75 534.93 L 974.66 504.72 L 1051.33 490.87 L 1142.82 475.17 L 1184.21 494.5 L 1188.72 514.15 L 1172.46 554.34 L 1074.6 578.5 L 959.84 601.45 L 788.94 641.88 L 786.85 643.41 L 789.26 646.39 L 866.26 653.64 L 899.19 655.41 L 979.81 655.41 L 1129.93 666.6 L 1169.15 692.54 L 1192.67 724.27 L 1188.72 748.43 L 1128.32 779.19 L 1046.82 759.87 L 856.59 714.6 L 791.36 698.34 L 782.34 698.34 L 782.34 703.73 L 836.7 756.89 L 936.32 846.85 L 1061.07 962.82 L 1067.44 991.49 L 1051.41 1014.12 L 1034.5 1011.7 L 924.89 929.23 L 882.6 892.11 L 786.85 811.49 L 780.48 811.49 L 780.48 819.95 L 802.55 852.24 L 919.09 1027.41 L 925.13 1081.13 L 916.67 1098.6 L 886.47 1109.15 L 853.29 1103.11 L 785.07 1007.36 L 714.68 899.52 L 657.91 802.87 L 650.98 806.82 L 617.48 1167.7 L 601.77 1186.15 L 565.53 1200 L 535.33 1177.05 L 519.3 1139.92 L 535.33 1066.55 L 554.66 970.79 L 570.36 894.68 L 584.54 800.13 L 592.99 768.72 L 592.43 766.63 L 585.5 767.52 L 514.23 865.37 L 405.83 1011.87 L 320.05 1103.68 L 299.52 1111.81 L 263.92 1093.37 L 267.22 1060.43 L 287.11 1031.11 L 405.83 880.11 L 477.42 786.52 L 523.65 732.48 L 523.33 724.67 L 520.59 724.67 L 205.29 929.4 L 149.15 936.64 L 124.99 914.01 L 127.97 876.89 L 139.41 864.81 L 234.2 799.57 Z"
/>
</svg>
);
});
@ -137,7 +140,10 @@ const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) {
const OpenAIIcon = memo(function OpenAIIcon({ size = 14 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364l2.0153-1.1639a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="#ffffff" />
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364l2.0153-1.1639a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="#ffffff"
/>
</svg>
);
});

View file

@ -86,7 +86,7 @@ const DetailRail = styled("aside", ({ $theme }) => ({
const FILTER_OPTIONS: SelectItem[] = [
{ id: "active", label: "Active + Unmapped" },
{ id: "archived", label: "Archived Handoffs" },
{ id: "archived", label: "Archived Tasks" },
{ id: "unmapped", label: "Unmapped Only" },
{ id: "all", label: "All Branches" },
];
@ -137,7 +137,7 @@ function branchTestIdToken(value: string): string {
function useSessionEvents(
handoff: HandoffRecord | null,
sessionId: string | null
sessionId: string | null,
): ReturnType<typeof useQuery<{ items: SandboxSessionEventRecord[]; nextCursor?: string }, Error>> {
return useQuery({
queryKey: ["workspace", handoff?.workspaceId ?? "", "session", handoff?.handoffId ?? "", sessionId ?? ""],
@ -147,15 +147,10 @@ function useSessionEvents(
if (!handoff?.activeSandboxId || !sessionId) {
return { items: [] };
}
return backendClient.listSandboxSessionEvents(
handoff.workspaceId,
handoff.providerId,
handoff.activeSandboxId,
{
sessionId,
limit: 120,
}
);
return backendClient.listSandboxSessionEvents(handoff.workspaceId, handoff.providerId, handoff.activeSandboxId, {
sessionId,
limit: 120,
});
},
});
}
@ -343,19 +338,11 @@ function MetaRow({ label, value, mono = false }: { label: string; value: string;
>
<LabelXSmall color="contentSecondary">{label}</LabelXSmall>
{mono ? (
<MonoLabelSmall
marginTop="0"
marginBottom="0"
overrides={{ Block: { style: { textAlign: "right", wordBreak: "break-word" } } }}
>
<MonoLabelSmall marginTop="0" marginBottom="0" overrides={{ Block: { style: { textAlign: "right", wordBreak: "break-word" } } }}>
{value}
</MonoLabelSmall>
) : (
<LabelSmall
marginTop="0"
marginBottom="0"
overrides={{ Block: { style: { textAlign: "right", wordBreak: "break-word" } } }}
>
<LabelSmall marginTop="0" marginBottom="0" overrides={{ Block: { style: { textAlign: "right", wordBreak: "break-word" } } }}>
{value}
</LabelSmall>
)}
@ -407,7 +394,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
refetchInterval: 2_500,
queryFn: async () => {
if (!selectedHandoffId) {
throw new Error("No handoff");
throw new Error("No task selected");
}
return backendClient.getHandoff(workspaceId, selectedHandoffId);
},
@ -483,17 +470,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
});
}, [repos, rows]);
const selectedSummary = useMemo(
() => rows.find((row) => row.handoffId === selectedHandoffId) ?? rows[0] ?? null,
[rows, selectedHandoffId]
);
const selectedSummary = useMemo(() => rows.find((row) => row.handoffId === selectedHandoffId) ?? rows[0] ?? null, [rows, selectedHandoffId]);
const selectedForSession = repoOverviewMode ? null : (handoffDetailQuery.data ?? null);
const activeSandbox = useMemo(() => {
if (!selectedForSession) return null;
const byActive = selectedForSession.activeSandboxId
? selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null
? (selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null)
: null;
return byActive ?? selectedForSession.sandboxes[0] ?? null;
}, [selectedForSession]);
@ -539,7 +523,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
handoffSessionId: selectedForSession?.activeSessionId ?? null,
sessions: sessionRows,
}),
[activeSessionId, selectedForSession?.activeSessionId, sessionRows]
[activeSessionId, selectedForSession?.activeSessionId, sessionRows],
);
const resolvedSessionId = sessionSelection.sessionId;
const staleSessionId = sessionSelection.staleSessionId;
@ -548,7 +532,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
const startSessionFromHandoff = async (): Promise<{ id: string; status: "running" | "idle" | "error" }> => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this handoff");
throw new Error("No sandbox is available for this task");
}
return backendClient.createSandboxSession({
workspaceId,
@ -581,7 +565,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
const sendPrompt = useMutation({
mutationFn: async (prompt: string) => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this handoff");
throw new Error("No sandbox is available for this task");
}
const sessionId = await ensureSessionForPrompt();
await backendClient.sendSandboxPrompt({
@ -716,17 +700,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.repoId, label: repo.remoteUrl })), [repos]);
const selectedRepoOption = repoOptions.find((option) => option.id === createRepoId) ?? null;
const selectedAgentOption = useMemo(
() => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!),
[newAgentType]
);
const selectedAgentOption = useMemo(() => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!), [newAgentType]);
const selectedFilterOption = useMemo(
() => createOption(FILTER_OPTIONS.find((option) => option.id === overviewFilter) ?? FILTER_OPTIONS[0]!),
[overviewFilter]
[overviewFilter],
);
const sessionOptions = useMemo(
() => sessionRows.map((session) => createOption({ id: session.id, label: `${session.id} (${session.status ?? "running"})` })),
[sessionRows]
[sessionRows],
);
const selectedSessionOption = sessionOptions.find((option) => option.id === resolvedSessionId) ?? null;
@ -746,11 +727,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
if (!selectedOverviewBranch) {
return filteredOverviewBranches[0] ?? null;
}
return (
filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ??
filteredOverviewBranches[0] ??
null
);
return filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ?? filteredOverviewBranches[0] ?? null;
}, [filteredOverviewBranches, selectedOverviewBranch]);
useEffect(() => {
@ -799,7 +776,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
},
},
}),
[theme.colors.backgroundSecondary, theme.colors.borderOpaque]
[theme.colors.backgroundSecondary, theme.colors.borderOpaque],
);
return (
@ -815,14 +792,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
gap: theme.sizing.scale400,
})}
>
<div
className={css({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "2px",
})}
>
<div
className={css({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "2px",
})}
>
<LabelXSmall color="contentTertiary">Workspace</LabelXSmall>
<div
className={css({
@ -857,7 +834,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
borderTop: `1px solid ${theme.colors.borderOpaque}`,
})}
>
<LabelXSmall color="contentSecondary">Handoffs</LabelXSmall>
<LabelXSmall color="contentSecondary">Tasks</LabelXSmall>
</div>
</PanelHeader>
@ -869,7 +846,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
) : null}
{!handoffsQuery.isLoading && repoGroups.length === 0 ? (
<EmptyState>No repos or handoffs yet. Add a repo to start a workspace.</EmptyState>
<EmptyState>No repos or tasks yet. Add a repo to start a workspace.</EmptyState>
) : null}
{repoGroups.map((group) => (
@ -985,7 +962,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid={group.repoId === createRepoId ? "handoff-create-open" : `handoff-create-open-${group.repoId}`}
>
Create Handoff
Create Task
</Button>
</div>
</section>
@ -1198,7 +1175,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
{formatRelativeAge(branch.updatedAt)}
</ParagraphSmall>
<StatusPill kind={branch.handoffId ? "positive" : "warning"}>
{branch.handoffId ? "handoff" : "unmapped"}
{branch.handoffId ? "task" : "unmapped"}
</StatusPill>
{branch.trackedInStack ? <StatusPill kind="neutral">stack</StatusPill> : null}
</div>
@ -1291,13 +1268,11 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid={`repo-overview-create-${branchToken}`}
>
Create Handoff
Create Task
</Button>
) : null}
<StatusPill kind={branchKind(branch)}>
{branch.conflictsWithMain ? "conflict" : "ok"}
</StatusPill>
<StatusPill kind={branchKind(branch)}>{branch.conflictsWithMain ? "conflict" : "ok"}</StatusPill>
</div>
</div>
</div>
@ -1331,11 +1306,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
>
<Bot size={16} />
<HeadingXSmall marginTop="0" marginBottom="0">
{selectedForSession ? selectedForSession.title ?? "Determining title..." : "No handoff selected"}
{selectedForSession ? selectedForSession.title ?? "Determining title..." : "No task selected"}
</HeadingXSmall>
{selectedForSession ? (
<StatusPill kind={statusKind(selectedForSession.status)}>{selectedForSession.status}</StatusPill>
) : null}
{selectedForSession ? <StatusPill kind={statusKind(selectedForSession.status)}>{selectedForSession.status}</StatusPill> : null}
</div>
{selectedForSession && !resolvedSessionId ? (
@ -1364,7 +1337,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
})}
>
{!selectedForSession ? (
<EmptyState>Select a handoff from the left sidebar.</EmptyState>
<EmptyState>Select a task from the left sidebar.</EmptyState>
) : (
<>
<div
@ -1440,12 +1413,12 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
: !activeSandbox?.sandboxId
? selectedForSession.statusMessage
? `Sandbox unavailable: ${selectedForSession.statusMessage}`
: "This handoff is still provisioning its sandbox."
: staleSessionId
? `Session ${staleSessionId} is unavailable. Start a new session to continue.`
: resolvedSessionId
? "No transcript events yet. Send a prompt to start this session."
: "No active session for this handoff."}
: "This task is still provisioning its sandbox."
: staleSessionId
? `Session ${staleSessionId} is unavailable. Start a new session to continue.`
: resolvedSessionId
? "No transcript events yet. Send a prompt to start this session."
: "No active session for this task."}
</EmptyState>
) : null}
@ -1462,14 +1435,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
key={entry.id}
data-testid="session-transcript-entry"
className={css({
borderLeft: `2px solid ${
entry.sender === "agent" ? "rgba(29, 111, 95, 0.45)" : "rgba(32, 108, 176, 0.45)"
}`,
border: `1px solid ${
entry.sender === "agent" ? "rgba(29, 111, 95, 0.22)" : "rgba(32, 108, 176, 0.22)"
}`,
backgroundColor:
entry.sender === "agent" ? "rgba(29, 111, 95, 0.07)" : "rgba(32, 108, 176, 0.07)",
borderLeft: `2px solid ${entry.sender === "agent" ? "rgba(29, 111, 95, 0.45)" : "rgba(32, 108, 176, 0.45)"}`,
border: `1px solid ${entry.sender === "agent" ? "rgba(29, 111, 95, 0.22)" : "rgba(32, 108, 176, 0.22)"}`,
backgroundColor: entry.sender === "agent" ? "rgba(29, 111, 95, 0.07)" : "rgba(32, 108, 176, 0.07)",
padding: `12px ${theme.sizing.scale400}`,
})}
>
@ -1535,11 +1503,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
void sendPrompt.mutateAsync(prompt);
}}
disabled={
sendPrompt.isPending ||
createSession.isPending ||
!selectedForSession ||
!activeSandbox?.sandboxId ||
draft.trim().length === 0
sendPrompt.isPending || createSession.isPending || !selectedForSession || !activeSandbox?.sandboxId || draft.trim().length === 0
}
>
<span
@ -1565,7 +1529,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
<DetailRail>
<PanelHeader>
<HeadingSmall marginTop="0" marginBottom="0">
{repoOverviewMode ? "Repo Details" : "Handoff Details"}
{repoOverviewMode ? "Repo Details" : "Task Details"}
</HeadingSmall>
</PanelHeader>
@ -1618,7 +1582,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
<MetaRow label="Commit" value={selectedBranchOverview.commitSha.slice(0, 10)} mono />
<MetaRow label="Diff" value={formatDiffStat(selectedBranchOverview.diffStat)} />
<MetaRow
label="Handoff"
label="Task"
value={selectedBranchOverview.handoffTitle ?? selectedBranchOverview.handoffId ?? "-"}
/>
</div>
@ -1628,7 +1592,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
)
) : !selectedForSession ? (
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
No handoff selected.
No task selected.
</ParagraphSmall>
) : (
<>
@ -1644,7 +1608,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
gap: theme.sizing.scale300,
})}
>
<MetaRow label="Handoff" value={selectedForSession.handoffId} mono />
<MetaRow label="Task" value={selectedForSession.handoffId} mono />
<MetaRow label="Sandbox" value={selectedForSession.activeSandboxId ?? "-"} mono />
<MetaRow label="Session" value={resolvedSessionId ?? "-"} mono />
</div>
@ -1711,9 +1675,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
</LabelSmall>
</div>
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
{selectedForSession.statusMessage
? selectedForSession.statusMessage
: "Open transcript in the center panel for details."}
{selectedForSession.statusMessage ? selectedForSession.statusMessage : "Open transcript in the center panel for details."}
</ParagraphSmall>
</div>
) : null}
@ -1773,7 +1735,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
overrides={modalOverrides}
>
<ModalHeader>Create Handoff</ModalHeader>
<ModalHeader>Create Task</ModalHeader>
<ModalBody>
<div
className={css({
@ -1783,7 +1745,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
})}
>
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
Pick a repo, describe the task, and the backend will create a handoff.
Pick a repo, describe the task, and the backend will create a task.
</ParagraphSmall>
<div>
@ -1921,7 +1883,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid="handoff-create-submit"
>
Create Handoff
Create Task
</Button>
</ModalFooter>
</Modal>