mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 23:02:18 +00:00
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:
parent
e792a720a0
commit
e03484848e
9 changed files with 558 additions and 415 deletions
|
|
@ -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}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue