mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 05:02:11 +00:00
Polish Foundry desktop UI: billing redesign, sidebar hover menu, org switching fix
- Redesign billing page with task-hours pricing model (Free: 8h, Pro: 200h/seat) - Add bulk hour purchase packages and Stripe payment management - Remove Usage nav section, add upgrade CTA in Members for free plan - Fix gear icon to open menu on hover with debounced timers - Fix org switching in workspace flyout (portal outside-click detection) - Fix tab strip padding when sidebar is collapsed - Update website components and Tauri config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f6656a90af
commit
ed6e6f6fa5
24 changed files with 1746 additions and 1028 deletions
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "foundry-desktop"
|
||||
name = "foundry"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::{AppHandle, LogicalPosition, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
|
|
@ -95,6 +95,29 @@ pub fn run() {
|
|||
})
|
||||
.invoke_handler(tauri::generate_handler![get_backend_url, backend_health])
|
||||
.setup(|app| {
|
||||
// Create main window programmatically so we can set traffic light position
|
||||
let url = if cfg!(debug_assertions) {
|
||||
WebviewUrl::External("http://localhost:4173".parse().unwrap())
|
||||
} else {
|
||||
WebviewUrl::default()
|
||||
};
|
||||
|
||||
let mut builder = WebviewWindowBuilder::new(app, "main", url)
|
||||
.title("Foundry")
|
||||
.inner_size(1280.0, 800.0)
|
||||
.min_inner_size(900.0, 600.0)
|
||||
.resizable(true)
|
||||
.theme(Some(tauri::Theme::Dark))
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.hidden_title(true);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.traffic_light_position(LogicalPosition::new(14.0, 14.0));
|
||||
}
|
||||
|
||||
builder.build()?;
|
||||
|
||||
// In debug mode, assume the developer is running the backend externally
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[foundry-desktop] Dev mode: skipping sidecar spawn. Run the backend separately.");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
foundry_desktop::run();
|
||||
foundry::run();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,24 +4,12 @@
|
|||
"version": "0.1.0",
|
||||
"identifier": "dev.sandboxagent.foundry",
|
||||
"build": {
|
||||
"beforeDevCommand": "VITE_DESKTOP=1 pnpm --filter @sandbox-agent/foundry-frontend dev",
|
||||
"beforeDevCommand": "FOUNDRY_FRONTEND_CLIENT_MODE=mock VITE_DESKTOP=1 pnpm --filter @sandbox-agent/foundry-frontend dev",
|
||||
"devUrl": "http://localhost:4173",
|
||||
"frontendDist": "../frontend-dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Foundry",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600,
|
||||
"resizable": true,
|
||||
"theme": "Dark",
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { FoundryBillingPlanId } from "@sandbox-agent/foundry-shared";
|
|||
import { Navigate, Outlet, createRootRoute, createRoute, createRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { MockLayout } from "../components/mock-layout";
|
||||
import {
|
||||
MockAccountSettingsPage,
|
||||
MockHostedCheckoutPage,
|
||||
MockOrganizationBillingPage,
|
||||
MockOrganizationSelectorPage,
|
||||
|
|
@ -30,6 +31,12 @@ const signInRoute = createRoute({
|
|||
component: SignInRoute,
|
||||
});
|
||||
|
||||
const accountRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/account",
|
||||
component: AccountRoute,
|
||||
});
|
||||
|
||||
const organizationsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/organizations",
|
||||
|
|
@ -84,6 +91,7 @@ const repoRoute = createRoute({
|
|||
const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
signInRoute,
|
||||
accountRoute,
|
||||
organizationsRoute,
|
||||
organizationSettingsRoute,
|
||||
organizationBillingRoute,
|
||||
|
|
@ -152,6 +160,18 @@ function SignInRoute() {
|
|||
return <MockSignInPage />;
|
||||
}
|
||||
|
||||
function AccountRoute() {
|
||||
const snapshot = useMockAppSnapshot();
|
||||
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
|
||||
return <AppLoadingScreen label="Loading account..." />;
|
||||
}
|
||||
if (snapshot.auth.status === "signed_out") {
|
||||
return <Navigate to="/signin" replace />;
|
||||
}
|
||||
|
||||
return <MockAccountSettingsPage />;
|
||||
}
|
||||
|
||||
function OrganizationsRoute() {
|
||||
const snapshot = useMockAppSnapshot();
|
||||
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSetActiveTabId,
|
||||
onSetLastAgentTabId,
|
||||
onSetOpenDiffs,
|
||||
sidebarCollapsed,
|
||||
onToggleSidebar,
|
||||
onSidebarPeekStart,
|
||||
onSidebarPeekEnd,
|
||||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
}: {
|
||||
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
||||
task: Task;
|
||||
|
|
@ -82,6 +88,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSetActiveTabId: (tabId: string | null) => void;
|
||||
onSetLastAgentTabId: (tabId: string | null) => void;
|
||||
onSetOpenDiffs: (paths: string[]) => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
onToggleSidebar?: () => void;
|
||||
onSidebarPeekStart?: () => void;
|
||||
onSidebarPeekEnd?: () => void;
|
||||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
}) {
|
||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
||||
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
||||
|
|
@ -446,6 +458,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
setTabUnread(activeAgentTab.id, unread);
|
||||
}
|
||||
}}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
onToggleSidebar={onToggleSidebar}
|
||||
onSidebarPeekStart={onSidebarPeekStart}
|
||||
onSidebarPeekEnd={onSidebarPeekEnd}
|
||||
rightSidebarCollapsed={rightSidebarCollapsed}
|
||||
onToggleRightSidebar={onToggleRightSidebar}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -456,11 +474,10 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
backgroundColor: "#09090b",
|
||||
overflow: "hidden",
|
||||
borderTopLeftRadius: "12px",
|
||||
borderTopRightRadius: rightSidebarCollapsed ? "12px" : 0,
|
||||
borderBottomLeftRadius: "24px",
|
||||
borderLeft: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderTop: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderBottom: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderBottomRightRadius: rightSidebarCollapsed ? "24px" : 0,
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
}}
|
||||
>
|
||||
<TabStrip
|
||||
|
|
@ -478,6 +495,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onCloseTab={closeTab}
|
||||
onCloseDiffTab={closeDiffTab}
|
||||
onAddTab={addTab}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
/>
|
||||
{activeDiff ? (
|
||||
<DiffContent
|
||||
|
|
@ -873,6 +891,7 @@ function MockWorkspaceOrgBar() {
|
|||
}
|
||||
|
||||
export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: MockLayoutProps) {
|
||||
const [css] = useStyletron();
|
||||
const navigate = useNavigate();
|
||||
const taskWorkbenchClient = useMemo(() => getTaskWorkbenchClient(workspaceId), [workspaceId]);
|
||||
const viewModel = useSyncExternalStore(
|
||||
|
|
@ -912,6 +931,17 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
||||
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
||||
const [leftSidebarPeeking, setLeftSidebarPeeking] = useState(false);
|
||||
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const startPeek = useCallback(() => {
|
||||
if (peekTimeoutRef.current) clearTimeout(peekTimeoutRef.current);
|
||||
setLeftSidebarPeeking(true);
|
||||
}, []);
|
||||
|
||||
const endPeek = useCallback(() => {
|
||||
peekTimeoutRef.current = setTimeout(() => setLeftSidebarPeeking(false), 200);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
leftWidthRef.current = leftWidth;
|
||||
|
|
@ -1212,154 +1242,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
[activeTask, lastAgentTabIdByTask],
|
||||
);
|
||||
|
||||
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(440px, 100%)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRadius: "12px",
|
||||
background: "rgba(24, 24, 27, 0.98)",
|
||||
backdropFilter: "blur(16px)",
|
||||
boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
||||
padding: "28px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
fontSize: "11px",
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" 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
|
||||
fillRule="evenodd"
|
||||
clipRule="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" strokeWidth="8.5" />
|
||||
</svg>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{starRepoError ? (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255, 110, 110, 0.24)",
|
||||
background: "rgba(255, 110, 110, 0.06)",
|
||||
padding: "10px 12px",
|
||||
color: "#ff9b9b",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
data-testid="onboarding-star-repo-error"
|
||||
>
|
||||
{starRepoError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissStarRepoPrompt}
|
||||
style={{
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRadius: "6px",
|
||||
padding: "8px 14px",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
transition: "all 160ms ease",
|
||||
}}
|
||||
>
|
||||
Maybe later
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={starSandboxAgentRepo}
|
||||
disabled={starRepoPending}
|
||||
style={{
|
||||
border: 0,
|
||||
borderRadius: "6px",
|
||||
padding: "8px 14px",
|
||||
background: starRepoPending ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
|
||||
color: "#ffffff",
|
||||
cursor: starRepoPending ? "progress" : "pointer",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
transition: "all 160ms ease",
|
||||
}}
|
||||
data-testid="onboarding-star-repo-submit"
|
||||
>
|
||||
{starRepoPending ? "Starring..." : "Star the repo"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const onDragMouseDown = useCallback((event: ReactPointerEvent) => {
|
||||
if (event.button !== 0) return;
|
||||
|
|
@ -1397,7 +1279,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
</div>
|
||||
) : null;
|
||||
|
||||
const collapsedToggleStyle: React.CSSProperties = {
|
||||
const collapsedToggleClass = css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
|
|
@ -1409,7 +1291,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
position: "relative",
|
||||
zIndex: 9999,
|
||||
flexShrink: 0,
|
||||
};
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
});
|
||||
|
||||
const sidebarTransition = "width 200ms ease";
|
||||
const contentFrameStyle: React.CSSProperties = {
|
||||
|
|
@ -1420,6 +1303,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
overflow: "hidden",
|
||||
marginBottom: "8px",
|
||||
marginRight: "8px",
|
||||
marginLeft: leftSidebarOpen ? 0 : "8px",
|
||||
};
|
||||
|
||||
if (!activeTask) {
|
||||
|
|
@ -1455,16 +1339,24 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{leftSidebarOpen ? null : (
|
||||
<div style={{ flexShrink: 0, padding: "6px 4px 0 6px", paddingTop: isDesktop ? "38px" : "6px" }}>
|
||||
<div style={collapsedToggleStyle} onClick={() => setLeftSidebarOpen(true)}>
|
||||
<PanelLeft size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={contentFrameStyle}>
|
||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
|
||||
{!leftSidebarOpen || !rightSidebarOpen ? (
|
||||
<div style={{ display: "flex", alignItems: "center", padding: "8px 8px 0 8px" }}>
|
||||
{leftSidebarOpen ? null : (
|
||||
<div className={collapsedToggleClass} onClick={() => setLeftSidebarOpen(true)}>
|
||||
<PanelLeft size={14} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
{rightSidebarOpen ? null : (
|
||||
<div className={collapsedToggleClass} onClick={() => setRightSidebarOpen(true)}>
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<ScrollBody>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1528,13 +1420,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rightSidebarOpen ? null : (
|
||||
<div style={{ flexShrink: 0, padding: "6px 6px 0 4px" }}>
|
||||
<div style={collapsedToggleStyle} onClick={() => setRightSidebarOpen(true)}>
|
||||
<PanelRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1543,7 +1428,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
return (
|
||||
<>
|
||||
{dragRegion}
|
||||
<Shell>
|
||||
<Shell $style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
width: leftSidebarOpen ? `${leftWidth}px` : 0,
|
||||
|
|
@ -1572,13 +1457,59 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{leftSidebarOpen ? null : (
|
||||
<div style={{ flexShrink: 0, padding: "6px 4px 0 6px", paddingTop: isDesktop ? "38px" : "6px" }}>
|
||||
<div style={collapsedToggleStyle} onClick={() => setLeftSidebarOpen(true)}>
|
||||
<PanelLeft size={16} />
|
||||
{!leftSidebarOpen && leftSidebarPeeking ? (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
zIndex: 99,
|
||||
}}
|
||||
onClick={() => setLeftSidebarPeeking(false)}
|
||||
onMouseEnter={endPeek}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: `${leftWidth}px`,
|
||||
zIndex: 100,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "4px 0 24px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
onMouseEnter={startPeek}
|
||||
onMouseLeave={endPeek}
|
||||
>
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
newTaskRepos={viewModel.repos}
|
||||
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||
activeId={activeTask.id}
|
||||
onSelect={(id) => {
|
||||
selectTask(id);
|
||||
setLeftSidebarPeeking(false);
|
||||
}}
|
||||
onCreate={createTask}
|
||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
onToggleSidebar={() => {
|
||||
setLeftSidebarPeeking(false);
|
||||
setLeftSidebarOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<div style={contentFrameStyle}>
|
||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
|
|
@ -1598,6 +1529,15 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
onSetOpenDiffs={(paths) => {
|
||||
setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths }));
|
||||
}}
|
||||
sidebarCollapsed={!leftSidebarOpen}
|
||||
onToggleSidebar={() => {
|
||||
setLeftSidebarPeeking(false);
|
||||
setLeftSidebarOpen(true);
|
||||
}}
|
||||
onSidebarPeekStart={startPeek}
|
||||
onSidebarPeekEnd={endPeek}
|
||||
rightSidebarCollapsed={!rightSidebarOpen}
|
||||
onToggleRightSidebar={() => setRightSidebarOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||
|
|
@ -1626,13 +1566,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rightSidebarOpen ? null : (
|
||||
<div style={{ flexShrink: 0, padding: "6px 6px 0 4px" }}>
|
||||
<div style={collapsedToggleStyle} onClick={() => setRightSidebarOpen(true)}>
|
||||
<PanelRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -108,6 +108,16 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
const contextMenu = useContextMenu();
|
||||
const changedPaths = useMemo(() => new Set(task.fileChanges.map((file) => file.path)), [task.fileChanges]);
|
||||
const isTerminal = task.status === "archived";
|
||||
const [compact, setCompact] = useState(false);
|
||||
const headerRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (!node) return;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setCompact(entry.contentRect.width < 400);
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
}, []);
|
||||
const pullRequestUrl = task.pullRequest != null ? `https://github.com/${task.repoName}/pull/${task.pullRequest.number}` : null;
|
||||
|
||||
const copyFilePath = useCallback(async (path: string) => {
|
||||
|
|
@ -137,121 +147,128 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
);
|
||||
|
||||
return (
|
||||
<SPanel $style={{ backgroundColor: "#09090b" }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
||||
<div className={css({ flex: 1 })} />
|
||||
{!isTerminal ? (
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px" })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (pullRequestUrl) {
|
||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
<SPanel $style={{ backgroundColor: "#09090b", minWidth: 0 }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", overflow: "hidden" }}>
|
||||
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
||||
{!isTerminal ? (
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (pullRequestUrl) {
|
||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
|
||||
onPublishPr();
|
||||
onPublishPr();
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>Push</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={onArchive}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<Archive size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>Archive</span>}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{onToggleSidebar ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggleSidebar}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: "#e4e4e7",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
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} style={{ flexShrink: 0 }} />{" "}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Push</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onArchive}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
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} style={{ flexShrink: 0 }} />{" "}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Archive</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{onToggleSidebar ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggleSidebar}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||
}}
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
>
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PanelHeaderBar>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
import { memo, useRef, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, PanelLeft, Plus } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
CloudUpload,
|
||||
CreditCard,
|
||||
GitPullRequestDraft,
|
||||
ListChecks,
|
||||
LogOut,
|
||||
PanelLeft,
|
||||
Plus,
|
||||
Settings,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
||||
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
|
||||
|
||||
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
||||
|
||||
|
|
@ -400,7 +416,343 @@ export const Sidebar = memo(function Sidebar({
|
|||
})}
|
||||
</div>
|
||||
</ScrollBody>
|
||||
<SidebarFooter />
|
||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||
</SPanel>
|
||||
);
|
||||
});
|
||||
|
||||
const menuButtonStyle = (highlight: boolean) =>
|
||||
({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: highlight ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
color: "rgba(255, 255, 255, 0.75)",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
fontWeight: 400 as const,
|
||||
textAlign: "left" as const,
|
||||
transition: "background 120ms ease, color 120ms ease",
|
||||
}) satisfies React.CSSProperties;
|
||||
|
||||
function SidebarFooter() {
|
||||
const [css] = useStyletron();
|
||||
const navigate = useNavigate();
|
||||
const client = useMockAppClient();
|
||||
const snapshot = useMockAppSnapshot();
|
||||
const organization = activeMockOrganization(snapshot);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [workspaceFlyoutOpen, setWorkspaceFlyoutOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const flyoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const workspaceTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const flyoutRef = useRef<HTMLDivElement>(null);
|
||||
const [flyoutPos, setFlyoutPos] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (workspaceFlyoutOpen && workspaceTriggerRef.current) {
|
||||
const rect = workspaceTriggerRef.current.getBoundingClientRect();
|
||||
setFlyoutPos({ top: rect.top, left: rect.right + 4 });
|
||||
}
|
||||
}, [workspaceFlyoutOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleClick(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
const inContainer = containerRef.current?.contains(target);
|
||||
const inFlyout = flyoutRef.current?.contains(target);
|
||||
if (!inContainer && !inFlyout) {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
const switchToOrg = useCallback(
|
||||
(org: (typeof snapshot.organizations)[number]) => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
void (async () => {
|
||||
await client.selectOrganization(org.id);
|
||||
await navigate({ to: `/workspaces/${org.workspaceId}` as never });
|
||||
})();
|
||||
},
|
||||
[client, navigate],
|
||||
);
|
||||
|
||||
const openFlyout = useCallback(() => {
|
||||
if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current);
|
||||
setWorkspaceFlyoutOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
flyoutTimerRef.current = setTimeout(() => setWorkspaceFlyoutOpen(false), 150);
|
||||
}, []);
|
||||
|
||||
const menuItems: Array<{ icon: React.ReactNode; label: string; danger?: boolean; onClick: () => void }> = [];
|
||||
|
||||
if (organization) {
|
||||
menuItems.push(
|
||||
{
|
||||
icon: <Settings size={14} />,
|
||||
label: "Settings",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/organizations/$organizationId/settings" as never, params: { organizationId: organization.id } as never });
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <CreditCard size={14} />,
|
||||
label: "Billing",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: organization.id } as never });
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
{
|
||||
icon: <User size={14} />,
|
||||
label: "Account",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/account" as never });
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <LogOut size={14} />,
|
||||
label: "Sign Out",
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void (async () => {
|
||||
await client.signOut();
|
||||
await navigate({ to: "/signin" });
|
||||
})();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const popoverStyle = css({
|
||||
borderRadius: "10px",
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
backgroundColor: "#18181b",
|
||||
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
||||
padding: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseEnter={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = setTimeout(() => setOpen(true), 300);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
className={css({ position: "relative", flexShrink: 0 })}
|
||||
>
|
||||
{open ? (
|
||||
<div
|
||||
className={css({
|
||||
position: "absolute",
|
||||
bottom: "100%",
|
||||
left: "8px",
|
||||
right: "8px",
|
||||
marginBottom: "4px",
|
||||
zIndex: 9999,
|
||||
})}
|
||||
>
|
||||
<div className={popoverStyle}>
|
||||
{/* Workspace flyout trigger */}
|
||||
{organization ? (
|
||||
<div ref={workspaceTriggerRef} onMouseEnter={openFlyout} onMouseLeave={closeFlyout}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWorkspaceFlyoutOpen((prev) => !prev)}
|
||||
className={css({
|
||||
...menuButtonStyle(workspaceFlyoutOpen),
|
||||
fontWeight: 500,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
background: `linear-gradient(135deg, ${projectIconColor(organization.settings.displayName)}, ${projectIconColor(organization.settings.displayName + "x")})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{organization.settings.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{organization.settings.displayName}
|
||||
</span>
|
||||
<ChevronRight size={12} className={css({ flexShrink: 0, color: "rgba(255, 255, 255, 0.35)" })} />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Workspace flyout portal */}
|
||||
{workspaceFlyoutOpen && organization && flyoutPos
|
||||
? createPortal(
|
||||
<div
|
||||
ref={flyoutRef}
|
||||
className={css({
|
||||
position: "fixed",
|
||||
top: `${flyoutPos.top}px`,
|
||||
left: `${flyoutPos.left}px`,
|
||||
minWidth: "200px",
|
||||
zIndex: 10000,
|
||||
})}
|
||||
onMouseEnter={() => {
|
||||
openFlyout();
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
closeFlyout();
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<div className={popoverStyle}>
|
||||
{eligibleOrganizations(snapshot).map((org) => {
|
||||
const isActive = organization.id === org.id;
|
||||
return (
|
||||
<button
|
||||
key={org.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isActive) switchToOrg(org);
|
||||
else {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
...menuButtonStyle(isActive),
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? "#ffffff" : "rgba(255, 255, 255, 0.65)",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
background: `linear-gradient(135deg, ${projectIconColor(org.settings.displayName)}, ${projectIconColor(org.settings.displayName + "x")})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{org.settings.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{org.settings.displayName}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
onClick={item.onClick}
|
||||
className={css({
|
||||
...menuButtonStyle(false),
|
||||
color: item.danger ? "#ffa198" : "rgba(255, 255, 255, 0.75)",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: item.danger ? "#ff6b6b" : "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={css({ padding: "8px" })}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
setOpen((prev) => {
|
||||
if (prev) setWorkspaceFlyoutOpen(false);
|
||||
return !prev;
|
||||
});
|
||||
}}
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: open ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
color: open ? "#ffffff" : "#71717a",
|
||||
cursor: "pointer",
|
||||
transition: "all 160ms ease",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#a1a1aa",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const TabStrip = memo(function TabStrip({
|
|||
onCloseTab,
|
||||
onCloseDiffTab,
|
||||
onAddTab,
|
||||
sidebarCollapsed,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTabId: string | null;
|
||||
|
|
@ -36,8 +37,10 @@ export const TabStrip = memo(function TabStrip({
|
|||
onCloseTab: (tabId: string) => void;
|
||||
onCloseDiffTab: (path: string) => void;
|
||||
onAddTab: () => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const contextMenu = useContextMenu();
|
||||
|
||||
return (
|
||||
|
|
@ -53,7 +56,7 @@ export const TabStrip = memo(function TabStrip({
|
|||
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
||||
gap: "4px",
|
||||
backgroundColor: "#09090b",
|
||||
paddingLeft: "6px",
|
||||
paddingLeft: sidebarCollapsed ? "14px" : "6px",
|
||||
height: "41px",
|
||||
minHeight: "41px",
|
||||
overflowX: "auto",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall } from "baseui/typography";
|
||||
import { Clock, MailOpen } from "lucide-react";
|
||||
import { Clock, MailOpen, PanelLeft, PanelRight } from "lucide-react";
|
||||
|
||||
import { PanelHeaderBar } from "./ui";
|
||||
import { type AgentTab, type Task } from "./view-model";
|
||||
|
|
@ -16,6 +16,12 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onCommitEditingField,
|
||||
onCancelEditingField,
|
||||
onSetActiveTabUnread,
|
||||
sidebarCollapsed,
|
||||
onToggleSidebar,
|
||||
onSidebarPeekStart,
|
||||
onSidebarPeekEnd,
|
||||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTab: AgentTab | null | undefined;
|
||||
|
|
@ -26,11 +32,40 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onCommitEditingField: (field: "title" | "branch") => void;
|
||||
onCancelEditingField: () => void;
|
||||
onSetActiveTabUnread: (unread: boolean) => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
onToggleSidebar?: () => void;
|
||||
onSidebarPeekStart?: () => void;
|
||||
onSidebarPeekEnd?: () => void;
|
||||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const needsTrafficLightInset = isDesktop && sidebarCollapsed;
|
||||
|
||||
return (
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
|
||||
{sidebarCollapsed && onToggleSidebar ? (
|
||||
<div
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
onClick={onToggleSidebar}
|
||||
onMouseEnter={onSidebarPeekStart}
|
||||
onMouseLeave={onSidebarPeekEnd}
|
||||
>
|
||||
<PanelLeft size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
{editingField === "title" ? (
|
||||
<input
|
||||
autoFocus
|
||||
|
|
@ -170,6 +205,25 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{activeTab.unread ? "Mark read" : "Mark unread"}</span>
|
||||
</button>
|
||||
) : null}
|
||||
{rightSidebarCollapsed && onToggleRightSidebar ? (
|
||||
<div
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
onClick={onToggleRightSidebar}
|
||||
>
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
</PanelHeaderBar>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue