mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 23:01:37 +00:00
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:
commit
b00c0109d0
288 changed files with 7048 additions and 9134 deletions
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() => (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue