Foundry UI polish: favicon, icon alignment, and border refinements (#236)

Add Foundry favicon, fix icon centering across sidebar/composer/header
buttons, restore center panel top-left border curve, and position right
sidebar border between header actions and tab strip.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicholas Kissel 2026-03-11 02:59:58 -07:00 committed by GitHub
parent e792a720a0
commit e03484848e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 558 additions and 415 deletions

View file

@ -9,6 +9,7 @@
}
</script>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Foundry</title>
</head>

View file

@ -0,0 +1,5 @@
<svg width="130" height="128" viewBox="0 0 130 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="1" width="126" height="126" rx="44" fill="#0F0F0F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M88.0429 44.2658C89.3803 43.625 90.8907 44.1955 91.5731 45.3776C92.2556 46.5596 91.9945 48.1529 90.7709 48.9907L72.3923 62.885C71.8013 63.2262 71.4248 63.7062 71.1029 64.2861C70.781 64.8659 70.5554 65.3922 70.5443 66.0553L67.7403 88.9495C67.521 90.3894 66.4114 91.423 64.9867 91.4576C63.5619 91.4922 62.3731 90.3429 62.24 88.9751L59.3859 66.0642C59.3971 65.4011 59.2126 64.8489 58.8714 64.2579C58.5302 63.6669 58.1442 63.231 57.5643 62.9091L39.15 48.9819C38.032 48.1828 37.6311 46.5786 38.3734 45.362C39.1157 44.1454 40.5656 43.7013 41.9223 44.2314L63.1512 53.2502C63.731 53.5721 64.2996 53.6398 64.9627 53.651C65.6259 53.6622 66.2298 53.5761 66.8208 53.2349L88.0429 44.2658Z" fill="white"/>
<rect x="19.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" stroke-width="8.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1,018 B

View file

@ -1,14 +1,4 @@
import {
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
type PointerEvent as ReactPointerEvent,
} from "react";
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type PointerEvent as ReactPointerEvent } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useStyletron } from "baseui";
@ -455,111 +445,189 @@ const TranscriptPanel = memo(function TranscriptPanel({
}
}}
/>
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: "#09090b", borderTopLeftRadius: "12px", borderLeft: "1px solid rgba(255, 255, 255, 0.10)", borderTop: "1px solid rgba(255, 255, 255, 0.10)", overflow: "hidden" }}>
<TabStrip
handoff={handoff}
activeTabId={activeTabId}
openDiffs={openDiffs}
editingSessionTabId={editingSessionTabId}
editingSessionName={editingSessionName}
onEditingSessionNameChange={setEditingSessionName}
onSwitchTab={switchTab}
onStartRenamingTab={startRenamingTab}
onCommitSessionRename={commitTabRename}
onCancelSessionRename={cancelTabRename}
onSetTabUnread={setTabUnread}
onCloseTab={closeTab}
onCloseDiffTab={closeDiffTab}
onAddTab={addTab}
/>
{activeDiff ? (
<DiffContent
filePath={activeDiff}
file={handoff.fileChanges.find((file) => file.path === activeDiff)}
diff={handoff.diffs[activeDiff]}
onAddAttachment={addAttachment}
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
backgroundColor: "#09090b",
overflow: "hidden",
borderTopLeftRadius: "12px",
borderLeft: "1px solid rgba(255, 255, 255, 0.10)",
borderTop: "1px solid rgba(255, 255, 255, 0.10)",
}}
>
<TabStrip
handoff={handoff}
activeTabId={activeTabId}
openDiffs={openDiffs}
editingSessionTabId={editingSessionTabId}
editingSessionName={editingSessionName}
onEditingSessionNameChange={setEditingSessionName}
onSwitchTab={switchTab}
onStartRenamingTab={startRenamingTab}
onCommitSessionRename={commitTabRename}
onCancelSessionRename={cancelTabRename}
onSetTabUnread={setTabUnread}
onCloseTab={closeTab}
onCloseDiffTab={closeDiffTab}
onAddTab={addTab}
/>
) : handoff.tabs.length === 0 ? (
<ScrollBody>
<div
style={{
minHeight: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "32px",
}}
>
{activeDiff ? (
<DiffContent
filePath={activeDiff}
file={handoff.fileChanges.find((file) => file.path === activeDiff)}
diff={handoff.diffs[activeDiff]}
onAddAttachment={addAttachment}
/>
) : handoff.tabs.length === 0 ? (
<ScrollBody>
<div
style={{
maxWidth: "420px",
textAlign: "center",
minHeight: "100%",
display: "flex",
flexDirection: "column",
gap: "12px",
alignItems: "center",
justifyContent: "center",
padding: "32px",
}}
>
<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 task.
</p>
<button
type="button"
onClick={addTab}
<div
style={{
alignSelf: "center",
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: "rgba(255, 255, 255, 0.12)",
color: "#e4e4e7",
cursor: "pointer",
fontWeight: 600,
maxWidth: "420px",
textAlign: "center",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
New session
</button>
<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 task.</p>
<button
type="button"
onClick={addTab}
style={{
alignSelf: "center",
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: "rgba(255, 255, 255, 0.12)",
color: "#e4e4e7",
cursor: "pointer",
fontWeight: 600,
}}
>
New session
</button>
</div>
</div>
</div>
</ScrollBody>
) : (
<ScrollBody>
<MessageList
tab={activeAgentTab}
scrollRef={scrollRef}
messageRefs={messageRefs}
historyEvents={historyEvents}
onSelectHistoryEvent={jumpToHistoryEvent}
copiedMessageId={copiedMessageId}
onCopyMessage={(message) => {
void copyMessage(message);
}}
thinkingTimerLabel={thinkingTimerLabel}
</ScrollBody>
) : (
<ScrollBody>
<MessageList
tab={activeAgentTab}
scrollRef={scrollRef}
messageRefs={messageRefs}
historyEvents={historyEvents}
onSelectHistoryEvent={jumpToHistoryEvent}
copiedMessageId={copiedMessageId}
onCopyMessage={(message) => {
void copyMessage(message);
}}
thinkingTimerLabel={thinkingTimerLabel}
/>
</ScrollBody>
)}
{!isTerminal && promptTab ? (
<PromptComposer
draft={draft}
textareaRef={textareaRef}
placeholder={!promptTab.created ? "Describe your task..." : "Send a message..."}
attachments={attachments}
defaultModel={defaultModel}
model={promptTab.model}
isRunning={promptTab.status === "running"}
onDraftChange={(value) => updateDraft(value, attachments)}
onSend={sendMessage}
onStop={stopAgent}
onRemoveAttachment={removeAttachment}
onChangeModel={changeModel}
onSetDefaultModel={setDefaultModel}
/>
</ScrollBody>
)}
{!isTerminal && promptTab ? (
<PromptComposer
draft={draft}
textareaRef={textareaRef}
placeholder={!promptTab.created ? "Describe your task..." : "Send a message..."}
attachments={attachments}
defaultModel={defaultModel}
model={promptTab.model}
isRunning={promptTab.status === "running"}
onDraftChange={(value) => updateDraft(value, attachments)}
onSend={sendMessage}
onStop={stopAgent}
onRemoveAttachment={removeAttachment}
onChangeModel={changeModel}
onSetDefaultModel={setDefaultModel}
/>
) : null}
) : null}
</div>
</SPanel>
);
});
const LEFT_SIDEBAR_DEFAULT_WIDTH = 340;
const RIGHT_SIDEBAR_DEFAULT_WIDTH = 380;
const SIDEBAR_MIN_WIDTH = 220;
const SIDEBAR_MAX_WIDTH = 600;
const RESIZE_HANDLE_WIDTH = 1;
const LEFT_WIDTH_STORAGE_KEY = "openhandoff:foundry-left-sidebar-width";
const RIGHT_WIDTH_STORAGE_KEY = "openhandoff:foundry-right-sidebar-width";
function readStoredWidth(key: string, fallback: number): number {
if (typeof window === "undefined") return fallback;
const stored = window.localStorage.getItem(key);
const parsed = stored ? Number.parseInt(stored, 10) : Number.NaN;
return Number.isFinite(parsed) ? Math.min(Math.max(parsed, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH) : fallback;
}
const PanelResizeHandle = memo(function PanelResizeHandle({ onResizeStart, onResize }: { onResizeStart: () => void; onResize: (deltaX: number) => void }) {
const handlePointerDown = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault();
const startX = event.clientX;
onResizeStart();
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const handlePointerMove = (moveEvent: PointerEvent) => {
onResize(moveEvent.clientX - startX);
};
const stopResize = () => {
document.body.style.cursor = "";
document.body.style.userSelect = "";
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", stopResize);
};
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", stopResize, { once: true });
},
[onResize, onResizeStart],
);
return (
<div
role="separator"
aria-orientation="vertical"
onPointerDown={handlePointerDown}
style={{
width: `${RESIZE_HANDLE_WIDTH}px`,
flexShrink: 0,
cursor: "col-resize",
backgroundColor: "transparent",
position: "relative",
zIndex: 1,
}}
>
<div
style={{
position: "absolute",
top: 0,
bottom: 0,
left: "-3px",
right: "-3px",
}}
/>
</div>
);
});
const RIGHT_RAIL_MIN_SECTION_HEIGHT = 180;
const RIGHT_RAIL_SPLITTER_HEIGHT = 10;
const DEFAULT_TERMINAL_HEIGHT = 320;
@ -596,10 +664,7 @@ const RightRail = memo(function RightRail({
const clampTerminalHeight = useCallback((nextHeight: number) => {
const railHeight = railRef.current?.getBoundingClientRect().height ?? 0;
const maxHeight = Math.max(
RIGHT_RAIL_MIN_SECTION_HEIGHT,
railHeight - RIGHT_RAIL_MIN_SECTION_HEIGHT - RIGHT_RAIL_SPLITTER_HEIGHT,
);
const maxHeight = Math.max(RIGHT_RAIL_MIN_SECTION_HEIGHT, railHeight - RIGHT_RAIL_MIN_SECTION_HEIGHT - RIGHT_RAIL_SPLITTER_HEIGHT);
return Math.min(Math.max(nextHeight, RIGHT_RAIL_MIN_SECTION_HEIGHT), maxHeight);
}, []);
@ -652,6 +717,7 @@ const RightRail = memo(function RightRail({
ref={railRef}
className={css({
minHeight: 0,
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "#090607",
@ -736,18 +802,54 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
}
return ordered;
}, [rawProjects, projectOrder]);
const reorderProjects = useCallback((fromIndex: number, toIndex: number) => {
const ids = projects.map((p) => p.id);
const [moved] = ids.splice(fromIndex, 1);
ids.splice(toIndex, 0, moved!);
setProjectOrder(ids);
}, [projects]);
const reorderProjects = useCallback(
(fromIndex: number, toIndex: number) => {
const ids = projects.map((p) => p.id);
const [moved] = ids.splice(fromIndex, 1);
ids.splice(toIndex, 0, moved!);
setProjectOrder(ids);
},
[projects],
);
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 [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH));
const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH));
const leftWidthRef = useRef(leftWidth);
const rightWidthRef = useRef(rightWidth);
useEffect(() => {
leftWidthRef.current = leftWidth;
window.localStorage.setItem(LEFT_WIDTH_STORAGE_KEY, String(leftWidth));
}, [leftWidth]);
useEffect(() => {
rightWidthRef.current = rightWidth;
window.localStorage.setItem(RIGHT_WIDTH_STORAGE_KEY, String(rightWidth));
}, [rightWidth]);
const startLeftRef = useRef(leftWidth);
const startRightRef = useRef(rightWidth);
const onLeftResize = useCallback((deltaX: number) => {
setLeftWidth(Math.min(Math.max(startLeftRef.current + deltaX, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH));
}, []);
const onLeftResizeStart = useCallback(() => {
startLeftRef.current = leftWidthRef.current;
}, []);
const onRightResize = useCallback((deltaX: number) => {
setRightWidth(Math.min(Math.max(startRightRef.current - deltaX, SIDEBAR_MIN_WIDTH), SIDEBAR_MAX_WIDTH));
}, []);
const onRightResizeStart = useCallback(() => {
startRightRef.current = rightWidthRef.current;
}, []);
const activeHandoff = useMemo(() => handoffs.find((handoff) => handoff.id === selectedHandoffId) ?? handoffs[0] ?? null, [handoffs, selectedHandoffId]);
@ -1058,7 +1160,9 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ fontSize: "11px", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600, color: "rgba(255, 255, 255, 0.4)" }}>Welcome to Foundry</div>
<div style={{ fontSize: "11px", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600, color: "rgba(255, 255, 255, 0.4)" }}>
Welcome to Foundry
</div>
<h2 style={{ margin: 0, fontSize: "18px", fontWeight: 500, lineHeight: 1.3 }}>Support Sandbox Agent</h2>
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.55)", fontSize: "13px", lineHeight: 1.6 }}>
Star the repo to help us grow and stay up to date with new releases.
@ -1127,17 +1231,20 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
return (
<>
<Shell>
<Sidebar
projects={projects}
activeId=""
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/>
<SPanel $style={{ backgroundColor: "#09090b" }}>
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<Sidebar
projects={projects}
activeId=""
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/>
</div>
<PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} />
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
<ScrollBody>
<div
style={{
@ -1184,7 +1291,10 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
</div>
</ScrollBody>
</SPanel>
<SPanel />
<PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} />
<div style={{ width: `${rightWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<SPanel />
</div>
</Shell>
{starRepoPrompt}
</>
@ -1194,41 +1304,49 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
return (
<>
<Shell>
<Sidebar
projects={projects}
activeId={activeHandoff.id}
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/>
<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}
/>
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<Sidebar
projects={projects}
activeId={activeHandoff.id}
onSelect={selectHandoff}
onCreate={createHandoff}
onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/>
</div>
<PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} />
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
<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 }));
}}
/>
</div>
<PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} />
<div style={{ width: `${rightWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
<RightRail
workspaceId={workspaceId}
handoff={activeHandoff}
activeTabId={activeTabId}
onOpenDiff={openDiffTab}
onArchive={archiveHandoff}
onRevertFile={revertFile}
onPublishPr={publishPr}
/>
</div>
</Shell>
{starRepoPrompt}
</>

View file

@ -69,9 +69,14 @@ export const PromptComposer = memo(function PromptComposer({
"::placeholder": { color: theme.colors.contentSecondary },
}),
submit: css({
all: "unset",
appearance: "none",
WebkitAppearance: "none",
boxSizing: "border-box",
width: "32px",
height: "32px",
padding: "0",
margin: "0",
border: "none",
borderRadius: "6px",
cursor: "pointer",
position: "absolute",
@ -80,6 +85,8 @@ export const PromptComposer = memo(function PromptComposer({
display: "flex",
alignItems: "center",
justifyContent: "center",
lineHeight: 0,
fontSize: 0,
color: theme.colors.contentPrimary,
transition: "background 200ms ease",
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
@ -92,9 +99,12 @@ export const PromptComposer = memo(function PromptComposer({
},
}),
submitContent: css({
display: "inline-flex",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
lineHeight: 0,
color: isRunning ? theme.colors.contentPrimary : "#ffffff",
}),
};
@ -157,15 +167,10 @@ export const PromptComposer = memo(function PromptComposer({
allowEmptySubmit={isRunning}
submitLabel={isRunning ? "Stop" : "Send"}
classNames={composerClassNames}
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <SendHorizonal size={16} />)}
renderSubmitContent={() => (isRunning ? <Square size={16} style={{ display: "block" }} /> : <SendHorizonal size={16} style={{ display: "block" }} />)}
renderFooter={() => (
<div className={css({ padding: "0 10px 8px" })}>
<ModelPicker
value={model}
defaultModel={defaultModel}
onChange={onChangeModel}
onSetDefault={onSetDefaultModel}
/>
<ModelPicker value={model} defaultModel={defaultModel} onChange={onChangeModel} onSetDefault={onSetDefaultModel} />
</div>
)}
/>

View file

@ -151,220 +151,230 @@ export const RightSidebar = memo(function RightSidebar({
}}
className={css({
all: "unset",
display: "flex",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 12px",
borderRadius: "8px",
fontSize: "12px",
fontWeight: 500,
lineHeight: 1,
color: "#e4e4e7",
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
})}
>
<GitPullRequest size={12} />
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
{pullRequestUrl ? "Open PR" : "Publish PR"}
</button>
<button
className={css({
all: "unset",
display: "flex",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 12px",
borderRadius: "8px",
fontSize: "12px",
fontWeight: 500,
lineHeight: 1,
color: "#e4e4e7",
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
})}
>
<ArrowUpFromLine size={12} /> Push
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} /> Push
</button>
<button
onClick={onArchive}
className={css({
all: "unset",
display: "flex",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 12px",
borderRadius: "8px",
fontSize: "12px",
fontWeight: 500,
lineHeight: 1,
color: "#e4e4e7",
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
})}
>
<Archive size={12} /> Archive
<Archive size={12} style={{ flexShrink: 0 }} /> Archive
</button>
</div>
) : null}
</PanelHeaderBar>
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", borderTop: `1px solid rgba(255, 255, 255, 0.10)` }}>
<div
className={css({
display: "flex",
alignItems: "stretch",
gap: "4px",
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: "#09090b",
height: "41px",
minHeight: "41px",
flexShrink: 0,
})}
>
<button
onClick={() => setRightTab("changes")}
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", borderTop: "1px solid rgba(255, 255, 255, 0.10)" }}>
<div
className={css({
all: "unset",
display: "flex",
alignItems: "center",
gap: "6px",
padding: "4px 12px",
marginTop: "6px",
marginBottom: "6px",
marginLeft: "6px",
borderRadius: "8px",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
whiteSpace: "nowrap",
color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
alignItems: "stretch",
gap: "4px",
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: "#09090b",
height: "41px",
minHeight: "41px",
flexShrink: 0,
})}
>
Changes
{handoff.fileChanges.length > 0 ? (
<span
className={css({
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
minWidth: "16px",
height: "16px",
padding: "0 5px",
background: "#3f3f46",
color: "#a1a1aa",
fontSize: "9px",
fontWeight: 700,
borderRadius: "8px",
})}
>
{handoff.fileChanges.length}
</span>
) : null}
</button>
<button
onClick={() => setRightTab("files")}
className={css({
all: "unset",
display: "flex",
alignItems: "center",
padding: "4px 12px",
marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
whiteSpace: "nowrap",
color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})}
>
All Files
</button>
</div>
<ScrollBody>
{rightTab === "changes" ? (
<div className={css({ padding: "10px 14px", display: "flex", flexDirection: "column", gap: "2px" })}>
{handoff.fileChanges.length === 0 ? (
<div className={css({ padding: "20px 0", textAlign: "center" })}>
<LabelSmall color={theme.colors.contentTertiary}>No changes yet</LabelSmall>
</div>
<button
onClick={() => setRightTab("changes")}
className={css({
all: "unset",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "4px 12px",
marginTop: "6px",
marginBottom: "6px",
marginLeft: "6px",
borderRadius: "8px",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
lineHeight: 1,
whiteSpace: "nowrap",
color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})}
>
Changes
{handoff.fileChanges.length > 0 ? (
<span
className={css({
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
minWidth: "16px",
height: "16px",
padding: "0 5px",
background: "#3f3f46",
color: "#a1a1aa",
fontSize: "9px",
fontWeight: 700,
borderRadius: "8px",
})}
>
{handoff.fileChanges.length}
</span>
) : null}
{handoff.fileChanges.map((file) => {
const isActive = activeTabId === diffTabId(file.path);
const TypeIcon = file.type === "A" ? FilePlus : file.type === "D" ? FileX : FileCode;
const iconColor = file.type === "A" ? "#7ee787" : file.type === "D" ? "#ffa198" : theme.colors.contentTertiary;
return (
<div
key={file.path}
onClick={() => onOpenDiff(file.path)}
onContextMenu={(event) => openFileMenu(event, file.path)}
className={css({
display: "flex",
alignItems: "center",
gap: "8px",
padding: "6px 10px",
borderRadius: "6px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)" },
})}
>
<TypeIcon size={14} color={iconColor} style={{ flexShrink: 0 }} />
<div
className={css({
flex: 1,
minWidth: 0,
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "12px",
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
})}
>
{file.path}
</div>
</button>
<button
onClick={() => setRightTab("files")}
className={css({
all: "unset",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
padding: "4px 12px",
marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
lineHeight: 1,
whiteSpace: "nowrap",
color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})}
>
All Files
</button>
</div>
<ScrollBody>
{rightTab === "changes" ? (
<div className={css({ padding: "10px 14px", display: "flex", flexDirection: "column", gap: "2px" })}>
{handoff.fileChanges.length === 0 ? (
<div className={css({ padding: "20px 0", textAlign: "center" })}>
<LabelSmall color={theme.colors.contentTertiary}>No changes yet</LabelSmall>
</div>
) : null}
{handoff.fileChanges.map((file) => {
const isActive = activeTabId === diffTabId(file.path);
const TypeIcon = file.type === "A" ? FilePlus : file.type === "D" ? FileX : FileCode;
const iconColor = file.type === "A" ? "#7ee787" : file.type === "D" ? "#ffa198" : theme.colors.contentTertiary;
return (
<div
key={file.path}
onClick={() => onOpenDiff(file.path)}
onContextMenu={(event) => openFileMenu(event, file.path)}
className={css({
display: "flex",
alignItems: "center",
gap: "6px",
flexShrink: 0,
fontSize: "11px",
fontFamily: '"IBM Plex Mono", monospace',
gap: "8px",
padding: "6px 10px",
borderRadius: "6px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)" },
})}
>
<span className={css({ color: "#7ee787" })}>+{file.added}</span>
<span className={css({ color: "#ffa198" })}>-{file.removed}</span>
<span className={css({ color: iconColor, fontWeight: 600, width: "10px", textAlign: "center" })}>{file.type}</span>
<TypeIcon size={14} color={iconColor} style={{ flexShrink: 0 }} />
<div
className={css({
flex: 1,
minWidth: 0,
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "12px",
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
})}
>
{file.path}
</div>
<div
className={css({
display: "flex",
alignItems: "center",
gap: "6px",
flexShrink: 0,
fontSize: "11px",
fontFamily: '"IBM Plex Mono", monospace',
})}
>
<span className={css({ color: "#7ee787" })}>+{file.added}</span>
<span className={css({ color: "#ffa198" })}>-{file.removed}</span>
<span className={css({ color: iconColor, fontWeight: 600, width: "10px", textAlign: "center" })}>{file.type}</span>
</div>
</div>
);
})}
</div>
) : (
<div className={css({ padding: "6px 0" })}>
{handoff.fileTree.length > 0 ? (
<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>
</div>
);
})}
</div>
) : (
<div className={css({ padding: "6px 0" })}>
{handoff.fileTree.length > 0 ? (
<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>
</div>
)}
</div>
)}
</ScrollBody>
)}
</div>
)}
</ScrollBody>
</div>
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
</SPanel>

View file

@ -60,15 +60,19 @@ export const Sidebar = memo(function Sidebar({
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
<LabelSmall
color={theme.colors.contentPrimary}
$style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
$style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px", lineHeight: 1 }}
>
<ListChecks size={14} />
Tasks
</LabelSmall>
<button
<div
role="button"
tabIndex={0}
onClick={onCreate}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") onCreate();
}}
className={css({
all: "unset",
width: "26px",
height: "26px",
borderRadius: "8px",
@ -79,11 +83,12 @@ export const Sidebar = memo(function Sidebar({
alignItems: "center",
justifyContent: "center",
transition: "background 200ms ease",
flexShrink: 0,
":hover": { backgroundColor: "rgba(255, 255, 255, 0.20)" },
})}
>
<Plus size={14} />
</button>
<Plus size={14} style={{ display: "block" }} />
</div>
</PanelHeaderBar>
<ScrollBody>
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
@ -191,97 +196,93 @@ export const Sidebar = memo(function Sidebar({
{project.label}
</LabelSmall>
</div>
{isCollapsed ? (
<LabelXSmall color={theme.colors.contentTertiary}>
{formatRelativeAge(project.updatedAtMs)}
</LabelXSmall>
) : null}
{isCollapsed ? <LabelXSmall color={theme.colors.contentTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall> : null}
</div>
{!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;
{!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 task", onClick: () => onRenameHandoff(handoff.id) },
{ label: "Rename branch", onClick: () => onRenameBranch(handoff.id) },
{ label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) },
])
}
className={css({
padding: "8px 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: hasUnread ? 600 : 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
flexShrink: 1,
}}
color={hasUnread ? "#ffffff" : theme.colors.contentSecondary}
>
{handoff.title}
</LabelSmall>
{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} />
)}
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0, marginLeft: "auto" })}>
<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: "8px 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>
) : null}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
{formatRelativeAge(handoff.updatedAtMs)}
</LabelXSmall>
<LabelSmall
$style={{
fontWeight: hasUnread ? 600 : 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
flexShrink: 1,
}}
color={hasUnread ? "#ffffff" : theme.colors.contentSecondary}
>
{handoff.title}
</LabelSmall>
{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} />
)}
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0, marginLeft: "auto" })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
</div>
) : null}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
{formatRelativeAge(handoff.updatedAtMs)}
</LabelXSmall>
</div>
</div>
</div>
);
})}
);
})}
</div>
);
})}

View file

@ -216,6 +216,7 @@ export const TabStrip = memo(function TabStrip({
padding: "0 10px",
cursor: "pointer",
opacity: 0.4,
lineHeight: 0,
":hover": { opacity: 0.7 },
flexShrink: 0,
})}

View file

@ -115,7 +115,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
<div className={css({ flex: 1 })} />
<div
className={css({
display: "flex",
display: "inline-flex",
alignItems: "center",
gap: "5px",
padding: "3px 10px",
@ -124,11 +124,12 @@ export const TranscriptHeader = memo(function TranscriptHeader({
border: "1px solid rgba(255, 255, 255, 0.08)",
fontSize: "11px",
fontWeight: 500,
lineHeight: 1,
color: theme.colors.contentSecondary,
whiteSpace: "nowrap",
})}
>
<Clock size={11} />
<Clock size={11} style={{ flexShrink: 0 }} />
<span>847 min used</span>
</div>
{activeTab ? (
@ -136,20 +137,22 @@ export const TranscriptHeader = memo(function TranscriptHeader({
onClick={() => onSetActiveTabUnread(!activeTab.unread)}
className={css({
all: "unset",
display: "flex",
boxSizing: "border-box",
display: "inline-flex",
alignItems: "center",
gap: "5px",
padding: "4px 10px",
borderRadius: "6px",
fontSize: "11px",
fontWeight: 500,
lineHeight: 1,
color: theme.colors.contentSecondary,
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
})}
>
<MailOpen size={12} /> {activeTab.unread ? "Mark read" : "Mark unread"}
<MailOpen size={12} style={{ flexShrink: 0 }} /> {activeTab.unread ? "Mark read" : "Mark unread"}
</button>
) : null}
</PanelHeaderBar>

View file

@ -175,16 +175,15 @@ export const TabAvatar = memo(function TabAvatar({ tab }: { tab: AgentTab }) {
});
export const Shell = styled("div", ({ $theme }) => ({
display: "grid",
gap: "1px",
display: "flex",
height: "100dvh",
backgroundColor: $theme.colors.backgroundSecondary,
gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1.5fr) 380px",
overflow: "hidden",
}));
export const SPanel = styled("section", ({ $theme }) => ({
minHeight: 0,
flex: 1,
display: "flex",
flexDirection: "column" as const,
backgroundColor: $theme.colors.backgroundSecondary,