mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
Foundry UI polish: terminal empty state, history minimap redesign, styling tweaks (#242)
- Hide terminal pane body when no terminal tabs exist - Redesign history minimap from orange bar to single icon with popover dropdown - Simplify popover items to single-line user messages with ellipsis - Adjust min-used badge hover padding - Add right padding to message list for history icon clearance Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f09b9090bb
commit
fde8b481bd
20 changed files with 4164 additions and 1018 deletions
|
|
@ -475,6 +475,7 @@ export async function getWorkbenchTask(c: any): Promise<any> {
|
|||
fileChanges: gitState.fileChanges,
|
||||
diffs: gitState.diffs,
|
||||
fileTree: gitState.fileTree,
|
||||
minutesUsed: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
|||
fileChanges: [],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 0,
|
||||
};
|
||||
|
||||
this.updateState((current) => ({
|
||||
|
|
|
|||
|
|
@ -434,6 +434,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 42,
|
||||
},
|
||||
{
|
||||
id: "h2",
|
||||
|
|
@ -533,6 +534,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 187,
|
||||
},
|
||||
{
|
||||
id: "h3",
|
||||
|
|
@ -606,6 +608,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 23,
|
||||
},
|
||||
// ── rivet-dev/rivet ──
|
||||
{
|
||||
|
|
@ -740,6 +743,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 5,
|
||||
},
|
||||
{
|
||||
id: "h5",
|
||||
|
|
@ -795,6 +799,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 312,
|
||||
},
|
||||
// ── rivet-dev/cloud ──
|
||||
{
|
||||
|
|
@ -903,6 +908,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 0,
|
||||
},
|
||||
// ── rivet-dev/engine-ee ──
|
||||
{
|
||||
|
|
@ -1016,6 +1022,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
},
|
||||
],
|
||||
minutesUsed: 78,
|
||||
},
|
||||
// ── rivet-dev/engine-ee (archived) ──
|
||||
{
|
||||
|
|
@ -1057,6 +1064,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 15,
|
||||
},
|
||||
// ── rivet-dev/secure-exec ──
|
||||
{
|
||||
|
|
@ -1109,6 +1117,7 @@ export function buildInitialTasks(): Task[] {
|
|||
],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 3,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
const e = {};
|
||||
export { e as default };
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap";:root{color-scheme:dark;font-family:IBM Plex Sans,Segoe UI,sans-serif;background:var(--f-surface-primary, #000000);color:var(--f-text-primary, #ffffff)}html,body,#root{height:100%}body{margin:0;background:var(--f-surface-primary, #000000);color:var(--f-text-primary, #ffffff);overflow:hidden}*{box-sizing:border-box;-webkit-user-select:none;user-select:none}input,textarea,pre,code,[data-selectable]{-webkit-user-select:text;user-select:text}a{color:inherit}@keyframes hf-spin{to{transform:rotate(360deg)}}button,input,textarea,select{font:inherit}code,pre{font-family:IBM Plex Mono,SFMono-Regular,monospace}.mock-diff-header{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--f-border-default, rgba(255, 255, 255, .12));background:var(--f-surface-secondary, #111111)}.mock-diff-path{color:var(--f-text-primary, #fafafa);font-family:IBM Plex Mono,SFMono-Regular,monospace;font-size:13px;font-weight:600}.mock-diff-stats{display:flex;gap:8px;margin-left:4px;font-size:12px}.mock-diff-added{color:var(--f-status-success, #7ee787)}.mock-diff-removed{color:var(--f-status-error, #ffa198)}.mock-diff-body{font-family:IBM Plex Mono,SFMono-Regular,monospace;font-size:12px;line-height:20px}.mock-diff-row{display:flex;align-items:stretch;min-height:20px}.mock-diff-row[data-kind=add]{background:#2ea0431f}.mock-diff-row[data-kind=remove]{background:#f851491a}.mock-diff-row[data-kind=hunk]{background:#ffffff0a;border-bottom:1px solid var(--f-border-default, rgba(255, 255, 255, .12))}.mock-diff-row[data-kind=hunk]:not(:first-child){border-top:1px solid var(--f-border-default, rgba(255, 255, 255, .12))}.mock-diff-gutter{position:relative;width:36px;flex-shrink:0;padding:0 8px 0 0;font-size:11px;line-height:20px;text-align:right;-webkit-user-select:none;user-select:none}.mock-diff-line-number{display:block;color:var(--f-text-tertiary, #71717a);opacity:.5}.mock-diff-line-text{flex:1;padding:0 10px;overflow:hidden;color:var(--f-text-secondary, #a1a1aa);font-size:12px;font-weight:400;line-height:20px;text-overflow:ellipsis;white-space:pre}.mock-diff-row[data-kind=add] .mock-diff-line-text{color:var(--f-status-success, #7ee787)}.mock-diff-row[data-kind=remove] .mock-diff-line-text{color:var(--f-status-error, #ffa198)}.mock-diff-row[data-kind=hunk] .mock-diff-line-text{color:var(--f-text-tertiary, #71717a)}.mock-diff-row[data-kind=hunk] .mock-diff-line-text{font-size:11px;font-weight:600}.mock-diff-attach-button{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:var(--f-accent, #ff4f00);cursor:pointer;opacity:0;pointer-events:none;z-index:1}.mock-diff-row:not([data-kind=hunk]):hover{background:#ff4f000f}.mock-diff-row:not([data-kind=hunk]):hover .mock-diff-attach-button{opacity:1;pointer-events:auto;background:var(--f-accent-subtle, rgba(255, 79, 0, .1))}.mock-diff-row:not([data-kind=hunk]):hover .mock-diff-line-number{opacity:0}.mock-diff-empty{padding:40px;text-align:center}.mock-diff-empty-copy{color:var(--f-text-tertiary, #71717a);font-size:14px;line-height:1.4}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,198 @@
|
|||
import { _ as u } from "./index-D0-B2Qgl.js";
|
||||
var P = {};
|
||||
function S() {
|
||||
return typeof process?.versions?.bun == "string" ? !0 : (P?.npm_config_user_agent || "").includes("bun/");
|
||||
}
|
||||
var I = new Set(["EACCES", "EPERM", "ENOEXEC"]);
|
||||
function A(n) {
|
||||
if (!n || typeof n != "object") return !1;
|
||||
const e = n.code;
|
||||
return typeof e == "string" && I.has(e);
|
||||
}
|
||||
function R(n, e) {
|
||||
if (process.platform === "win32") return !0;
|
||||
try {
|
||||
return e.accessSync(n, e.constants.X_OK), !0;
|
||||
} catch {}
|
||||
try {
|
||||
return e.chmodSync(n, 493), !0;
|
||||
} catch (o) {
|
||||
if (A(o)) return !1;
|
||||
throw o;
|
||||
}
|
||||
}
|
||||
function T(n) {
|
||||
const { binPath: e, trustPackages: o, bunInstallBlocks: s, genericInstallCommands: r, binaryName: a } = n,
|
||||
t = [`${a ?? "sandbox-agent"} binary is not executable: ${e}`];
|
||||
if (S()) {
|
||||
t.push("Allow Bun to run postinstall scripts for native binaries and reinstall:");
|
||||
for (const c of s) {
|
||||
t.push(`${c.label}:`);
|
||||
for (const d of c.commands) t.push(` ${d}`);
|
||||
}
|
||||
return (
|
||||
t.push(`Or run: chmod +x "${e}"`),
|
||||
t.join(`
|
||||
`)
|
||||
);
|
||||
}
|
||||
if ((t.push("Postinstall scripts for native packages did not run, so the binary was left non-executable."), r && r.length > 0)) {
|
||||
t.push("Reinstall with scripts enabled:");
|
||||
for (const c of r) t.push(` ${c}`);
|
||||
} else t.push("Reinstall with scripts enabled for:"), t.push(` ${o}`);
|
||||
return (
|
||||
t.push(`Or run: chmod +x "${e}"`),
|
||||
t.join(`
|
||||
`)
|
||||
);
|
||||
}
|
||||
var x = {},
|
||||
$ = {
|
||||
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
|
||||
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
|
||||
"linux-x64": "@sandbox-agent/cli-linux-x64",
|
||||
"linux-arm64": "@sandbox-agent/cli-linux-arm64",
|
||||
"win32-x64": "@sandbox-agent/cli-win32-x64",
|
||||
},
|
||||
b = "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
|
||||
function k() {
|
||||
return typeof process < "u" && !!process.versions?.node;
|
||||
}
|
||||
async function V(n, e) {
|
||||
if (!k()) throw new Error("Autospawn requires a Node.js runtime.");
|
||||
const { spawn: o } = await u(async () => {
|
||||
const { spawn: p } = await import("./__vite-browser-external-BIHI7g3E.js");
|
||||
return { spawn: p };
|
||||
}, []),
|
||||
s = await u(() => import("./__vite-browser-external-BIHI7g3E.js"), []),
|
||||
r = await u(() => import("./__vite-browser-external-BIHI7g3E.js"), []),
|
||||
a = await u(() => import("./__vite-browser-external-BIHI7g3E.js"), []),
|
||||
i = await u(() => import("./__vite-browser-external-BIHI7g3E.js"), []),
|
||||
{ createRequire: t } = await u(async () => {
|
||||
const { createRequire: p } = await import("./__vite-browser-external-BIHI7g3E.js");
|
||||
return { createRequire: p };
|
||||
}, []),
|
||||
c = n.host ?? "127.0.0.1",
|
||||
d = n.port ?? (await C(i, c)),
|
||||
_ = c === "0.0.0.0" || c === "::" ? "127.0.0.1" : c,
|
||||
m = n.token ?? s.randomBytes(24).toString("hex"),
|
||||
E = n.timeoutMs ?? 15e3,
|
||||
w = n.log ?? "inherit",
|
||||
f = n.binaryPath ?? B(r, a) ?? O(t(import.meta.url), a, r) ?? N(r, a);
|
||||
if (!f) throw new Error("sandbox-agent binary not found. Install @sandbox-agent/cli or set SANDBOX_AGENT_BIN.");
|
||||
if (!R(f, r))
|
||||
throw new Error(
|
||||
T({
|
||||
binPath: f,
|
||||
trustPackages: b,
|
||||
bunInstallBlocks: [
|
||||
{ label: "Project install", commands: [`bun pm trust ${b}`, "bun add sandbox-agent"] },
|
||||
{ label: "Global install", commands: [`bun pm -g trust ${b}`, "bun add -g sandbox-agent"] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
const v = w === "inherit" ? "inherit" : w === "silent" ? "ignore" : "pipe",
|
||||
y = ["server", "--host", c, "--port", String(d), "--token", m],
|
||||
l = o(f, y, { stdio: v, env: { ...x, ...(n.env ?? {}) } }),
|
||||
h = D(l),
|
||||
g = `http://${_}:${d}`;
|
||||
return (
|
||||
await G(g, e ?? globalThis.fetch, E, l, m),
|
||||
{
|
||||
baseUrl: g,
|
||||
token: m,
|
||||
child: l,
|
||||
dispose: async () => {
|
||||
if (l.exitCode !== null) {
|
||||
h.dispose();
|
||||
return;
|
||||
}
|
||||
l.kill("SIGTERM"), (await M(l, 5e3)) || l.kill("SIGKILL"), h.dispose();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
function B(n, e) {
|
||||
const o = x.SANDBOX_AGENT_BIN;
|
||||
if (!o) return null;
|
||||
const s = e.resolve(o);
|
||||
return n.existsSync(s) ? s : null;
|
||||
}
|
||||
function O(n, e, o) {
|
||||
const s = `${process.platform}-${process.arch}`,
|
||||
r = $[s];
|
||||
if (!r) return null;
|
||||
try {
|
||||
const a = n.resolve(`${r}/package.json`),
|
||||
i = process.platform === "win32" ? "sandbox-agent.exe" : "sandbox-agent",
|
||||
t = e.join(e.dirname(a), "bin", i);
|
||||
return o.existsSync(t) ? t : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function N(n, e) {
|
||||
const o = x.PATH ?? "",
|
||||
s = process.platform === "win32" ? ";" : ":",
|
||||
r = o.split(s).filter(Boolean),
|
||||
a = process.platform === "win32" ? "sandbox-agent.exe" : "sandbox-agent";
|
||||
for (const i of r) {
|
||||
const t = e.join(i, a);
|
||||
if (n.existsSync(t)) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function C(n, e) {
|
||||
return new Promise((o, s) => {
|
||||
const r = n.createServer();
|
||||
r.unref(),
|
||||
r.on("error", s),
|
||||
r.listen(0, e, () => {
|
||||
const a = r.address();
|
||||
r.close(() => o(a.port));
|
||||
});
|
||||
});
|
||||
}
|
||||
async function G(n, e, o, s, r) {
|
||||
if (!e) throw new Error("Fetch API is not available; provide a fetch implementation.");
|
||||
const a = Date.now();
|
||||
let i;
|
||||
for (; Date.now() - a < o; ) {
|
||||
if (s.exitCode !== null) throw new Error("sandbox-agent exited before becoming healthy.");
|
||||
try {
|
||||
const t = await e(`${n}/v1/health`, { headers: { Authorization: `Bearer ${r}` } });
|
||||
if (t.ok) return;
|
||||
i = `status ${t.status}`;
|
||||
} catch (t) {
|
||||
i = t instanceof Error ? t.message : String(t);
|
||||
}
|
||||
await new Promise((t) => setTimeout(t, 200));
|
||||
}
|
||||
throw new Error(`Timed out waiting for sandbox-agent health (${i ?? "unknown error"}).`);
|
||||
}
|
||||
async function M(n, e) {
|
||||
return n.exitCode !== null
|
||||
? !0
|
||||
: new Promise((o) => {
|
||||
const s = setTimeout(() => o(!1), e);
|
||||
n.once("exit", () => {
|
||||
clearTimeout(s), o(!0);
|
||||
});
|
||||
});
|
||||
}
|
||||
function D(n) {
|
||||
const e = () => {
|
||||
n.exitCode === null && n.kill("SIGTERM");
|
||||
};
|
||||
return (
|
||||
process.once("exit", e),
|
||||
process.once("SIGINT", e),
|
||||
process.once("SIGTERM", e),
|
||||
{
|
||||
dispose: () => {
|
||||
process.off("exit", e), process.off("SIGINT", e), process.off("SIGTERM", e);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
export { k as isNodeRuntime, V as spawnSandboxAgent };
|
||||
5
foundry/packages/desktop/frontend-dist/favicon.svg
Normal file
5
foundry/packages/desktop/frontend-dist/favicon.svg
Normal 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 |
23
foundry/packages/desktop/frontend-dist/index.html
Normal file
23
foundry/packages/desktop/frontend-dist/index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!--
|
||||
<script type="module">
|
||||
if (import.meta.env.DEV) {
|
||||
import("react-grab");
|
||||
import("@react-grab/mcp/client");
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
<script>if(window.__TAURI_INTERNALS__)document.documentElement.dataset.tauri="1"</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>
|
||||
<script type="module" crossorigin src="/assets/index-D0-B2Qgl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-5GPxonOP.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
foundry/packages/desktop/src-tauri/sidecars/foundry-backend-aarch64-apple-darwin
Executable file
BIN
foundry/packages/desktop/src-tauri/sidecars/foundry-backend-aarch64-apple-darwin
Executable file
Binary file not shown.
BIN
foundry/packages/desktop/src-tauri/sidecars/foundry-backend-x86_64-apple-darwin
Executable file
BIN
foundry/packages/desktop/src-tauri/sidecars/foundry-backend-x86_64-apple-darwin
Executable file
Binary file not shown.
|
|
@ -16,11 +16,11 @@ import { TranscriptHeader } from "./mock-layout/transcript-header";
|
|||
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell } from "./mock-layout/ui";
|
||||
import {
|
||||
buildDisplayMessages,
|
||||
buildHistoryEvents,
|
||||
diffPath,
|
||||
diffTabId,
|
||||
formatThinkingDuration,
|
||||
isDiffTab,
|
||||
buildHistoryEvents,
|
||||
type Task,
|
||||
type HistoryEvent,
|
||||
type LineAttachment,
|
||||
|
|
@ -79,6 +79,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSidebarPeekEnd,
|
||||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
onNavigateToUsage,
|
||||
}: {
|
||||
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
||||
task: Task;
|
||||
|
|
@ -95,6 +96,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSidebarPeekEnd?: () => void;
|
||||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
onNavigateToUsage?: () => void;
|
||||
}) {
|
||||
const t = useFoundryTokens();
|
||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
||||
|
|
@ -466,6 +468,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSidebarPeekEnd={onSidebarPeekEnd}
|
||||
rightSidebarCollapsed={rightSidebarCollapsed}
|
||||
onToggleRightSidebar={onToggleRightSidebar}
|
||||
onNavigateToUsage={onNavigateToUsage}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -694,7 +697,7 @@ const RightRail = memo(function RightRail({
|
|||
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);
|
||||
|
||||
return Math.min(Math.max(nextHeight, RIGHT_RAIL_MIN_SECTION_HEIGHT), maxHeight);
|
||||
return Math.min(Math.max(nextHeight, 43), maxHeight);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -770,43 +773,36 @@ const RightRail = memo(function RightRail({
|
|||
onToggleSidebar={onToggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Resize terminal panel"
|
||||
onPointerDown={startResize}
|
||||
className={css({
|
||||
height: `${RIGHT_RAIL_SPLITTER_HEIGHT}px`,
|
||||
flexShrink: 0,
|
||||
cursor: "ns-resize",
|
||||
position: "relative",
|
||||
backgroundColor: t.surfacePrimary,
|
||||
borderRight: `1px solid ${t.borderDefault}`,
|
||||
":before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
width: "42px",
|
||||
height: "4px",
|
||||
borderRadius: "999px",
|
||||
transform: "translate(-50%, -50%)",
|
||||
backgroundColor: t.borderMedium,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
height: `${terminalHeight}px`,
|
||||
minHeight: `${RIGHT_RAIL_MIN_SECTION_HEIGHT}px`,
|
||||
minHeight: "43px",
|
||||
backgroundColor: t.surfacePrimary,
|
||||
overflow: "hidden",
|
||||
borderBottomRightRadius: "12px",
|
||||
borderRight: `1px solid ${t.borderDefault}`,
|
||||
borderBottom: `1px solid ${t.borderDefault}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
})}
|
||||
>
|
||||
<TerminalPane workspaceId={workspaceId} taskId={task.id} />
|
||||
<TerminalPane
|
||||
workspaceId={workspaceId}
|
||||
taskId={task.id}
|
||||
onStartResize={startResize}
|
||||
isExpanded={(() => {
|
||||
const railHeight = railRef.current?.getBoundingClientRect().height ?? 0;
|
||||
return railHeight > 0 && terminalHeight >= railHeight * 0.7;
|
||||
})()}
|
||||
onExpand={() => {
|
||||
const railHeight = railRef.current?.getBoundingClientRect().height ?? 0;
|
||||
const maxHeight = Math.max(RIGHT_RAIL_MIN_SECTION_HEIGHT, railHeight - RIGHT_RAIL_SPLITTER_HEIGHT - 42);
|
||||
setTerminalHeight(maxHeight);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
setTerminalHeight(43);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -906,6 +902,13 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
);
|
||||
const tasks = viewModel.tasks ?? [];
|
||||
const rawProjects = viewModel.projects ?? [];
|
||||
const appSnapshot = useMockAppSnapshot();
|
||||
const activeOrg = activeMockOrganization(appSnapshot);
|
||||
const navigateToUsage = useCallback(() => {
|
||||
if (activeOrg) {
|
||||
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } });
|
||||
}
|
||||
}, [activeOrg, navigate]);
|
||||
const [projectOrder, setProjectOrder] = useState<string[] | null>(null);
|
||||
const projects = useMemo(() => {
|
||||
if (!projectOrder) return rawProjects;
|
||||
|
|
@ -916,15 +919,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
}
|
||||
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 [activeTabIdByTask, setActiveTabIdByTask] = useState<Record<string, string | null>>({});
|
||||
const [lastAgentTabIdByTask, setLastAgentTabIdByTask] = useState<Record<string, string | null>>({});
|
||||
const [openDiffsByTask, setOpenDiffsByTask] = useState<Record<string, string[]>>({});
|
||||
|
|
@ -948,6 +942,30 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
peekTimeoutRef.current = setTimeout(() => setLeftSidebarPeeking(false), 200);
|
||||
}, []);
|
||||
|
||||
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 [taskOrderByProject, setTaskOrderByProject] = useState<Record<string, string[]>>({});
|
||||
const reorderTasks = useCallback(
|
||||
(projectId: string, fromIndex: number, toIndex: number) => {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
if (!project) return;
|
||||
const currentOrder = taskOrderByProject[projectId] ?? project.tasks.map((t) => t.id);
|
||||
const ids = [...currentOrder];
|
||||
const [moved] = ids.splice(fromIndex, 1);
|
||||
ids.splice(toIndex, 0, moved!);
|
||||
setTaskOrderByProject((prev) => ({ ...prev, [projectId]: ids }));
|
||||
},
|
||||
[projects, taskOrderByProject],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
leftWidthRef.current = leftWidth;
|
||||
window.localStorage.setItem(LEFT_WIDTH_STORAGE_KEY, String(leftWidth));
|
||||
|
|
@ -1340,6 +1358,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
taskOrderByProject={taskOrderByProject}
|
||||
onReorderTasks={reorderTasks}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1458,6 +1478,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
taskOrderByProject={taskOrderByProject}
|
||||
onReorderTasks={reorderTasks}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1507,6 +1529,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
taskOrderByProject={taskOrderByProject}
|
||||
onReorderTasks={reorderTasks}
|
||||
onToggleSidebar={() => {
|
||||
setLeftSidebarPeeking(false);
|
||||
setLeftSidebarOpen(true);
|
||||
|
|
@ -1543,6 +1567,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
onSidebarPeekEnd={endPeek}
|
||||
rightSidebarCollapsed={!rightSidebarOpen}
|
||||
onToggleRightSidebar={() => setRightSidebarOpen(true)}
|
||||
onNavigateToUsage={navigateToUsage}
|
||||
/>
|
||||
</div>
|
||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { memo, useEffect, useState } from "react";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelXSmall } from "baseui/typography";
|
||||
import { History } from "lucide-react";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { formatMessageTimestamp, type HistoryEvent } from "./view-model";
|
||||
|
|
@ -9,13 +10,18 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
|
|||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeEventId, setActiveEventId] = useState<string | null>(events[events.length - 1]?.id ?? null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!events.some((event) => event.id === activeEventId)) {
|
||||
setActiveEventId(events[events.length - 1]?.id ?? null);
|
||||
}
|
||||
}, [activeEventId, events]);
|
||||
if (!open) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
if (events.length === 0) {
|
||||
return null;
|
||||
|
|
@ -23,112 +29,100 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={css({
|
||||
position: "absolute",
|
||||
top: "20px",
|
||||
right: "16px",
|
||||
zIndex: 3,
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "12px",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "6px",
|
||||
})}
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setOpen((prev) => !prev);
|
||||
}}
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: open ? t.textSecondary : t.textTertiary,
|
||||
backgroundColor: open ? t.interactiveHover : "transparent",
|
||||
transition: "background 200ms ease, color 200ms ease",
|
||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||
})}
|
||||
>
|
||||
<History size={14} />
|
||||
</div>
|
||||
|
||||
{open ? (
|
||||
<div
|
||||
className={css({
|
||||
width: "220px",
|
||||
width: "240px",
|
||||
maxHeight: "320px",
|
||||
overflowY: "auto",
|
||||
padding: "8px",
|
||||
borderRadius: "10px",
|
||||
backgroundColor: "rgba(32, 32, 32, 0.98)",
|
||||
backdropFilter: "blur(12px)",
|
||||
border: `1px solid ${t.borderDefault}`,
|
||||
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${t.interactiveSubtle}`,
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "6px" })}>
|
||||
<LabelXSmall color={t.textTertiary} $style={{ letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
||||
Task Events
|
||||
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "6px", padding: "0 4px" })}>
|
||||
<LabelXSmall color={t.textTertiary} $style={{ letterSpacing: "0.02em" }}>
|
||||
Task events
|
||||
</LabelXSmall>
|
||||
<LabelXSmall color={t.textTertiary}>{events.length}</LabelXSmall>
|
||||
</div>
|
||||
<div className={css({ display: "flex", flexDirection: "column", gap: "6px" })}>
|
||||
{events.map((event) => {
|
||||
const isActive = event.id === activeEventId;
|
||||
return (
|
||||
<button
|
||||
key={event.id}
|
||||
type="button"
|
||||
onMouseEnter={() => setActiveEventId(event.id)}
|
||||
onFocus={() => setActiveEventId(event.id)}
|
||||
onClick={() => onSelect(event)}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
padding: "9px 10px",
|
||||
borderRadius: "12px",
|
||||
cursor: "pointer",
|
||||
backgroundColor: isActive ? t.borderSubtle : "transparent",
|
||||
color: isActive ? t.textPrimary : t.textSecondary,
|
||||
transition: "background 160ms ease, color 160ms ease",
|
||||
":hover": {
|
||||
backgroundColor: t.borderSubtle,
|
||||
color: t.textPrimary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={css({ minWidth: 0, display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
})}
|
||||
>
|
||||
{event.preview}
|
||||
</div>
|
||||
<LabelXSmall color={t.textTertiary}>{event.sessionName}</LabelXSmall>
|
||||
</div>
|
||||
<LabelXSmall color={t.textTertiary}>{formatMessageTimestamp(event.createdAtMs)}</LabelXSmall>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className={css({ display: "flex", flexDirection: "column", gap: "2px" })}>
|
||||
{events.map((event) => (
|
||||
<button
|
||||
key={event.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelect(event);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
padding: "6px 8px",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
color: t.textSecondary,
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
textAlign: "left",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
transition: "background 160ms ease, color 160ms ease",
|
||||
":hover": {
|
||||
backgroundColor: t.interactiveHover,
|
||||
color: t.textPrimary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{event.preview}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={css({
|
||||
width: "18px",
|
||||
padding: "4px 0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
alignItems: "stretch",
|
||||
})}
|
||||
>
|
||||
{events.map((event) => {
|
||||
const isActive = event.id === activeEventId;
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={css({
|
||||
height: "3px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: isActive ? t.accent : t.textMuted,
|
||||
opacity: isActive ? 1 : 0.75,
|
||||
transition: "background 160ms ease, opacity 160ms ease",
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export const MessageList = memo(function MessageList({
|
|||
<div
|
||||
ref={scrollRef}
|
||||
className={css({
|
||||
padding: "16px 20px 16px 20px",
|
||||
padding: "16px 52px 16px 20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { StatefulPopover, PLACEMENT } from "baseui/popover";
|
||||
import { ChevronDown, ChevronUp, Star } from "lucide-react";
|
||||
import { ChevronUp, Star } from "lucide-react";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { AgentIcon } from "./ui";
|
||||
|
|
@ -107,6 +107,7 @@ export const ModelPicker = memo(function ModelPicker({
|
|||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<StatefulPopover
|
||||
|
|
@ -140,10 +141,13 @@ export const ModelPicker = memo(function ModelPicker({
|
|||
>
|
||||
<div className={css({ display: "inline-flex" })}>
|
||||
<button
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
|
@ -153,14 +157,14 @@ export const ModelPicker = memo(function ModelPicker({
|
|||
borderRadius: "6px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
color: t.textSecondary,
|
||||
backgroundColor: t.borderDefault,
|
||||
border: `1px solid ${t.borderMedium}`,
|
||||
":hover": { color: t.textPrimary, backgroundColor: t.borderMedium },
|
||||
color: t.textTertiary,
|
||||
backgroundColor: "transparent",
|
||||
transition: "background 200ms ease, color 200ms ease",
|
||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||
})}
|
||||
>
|
||||
{modelLabel(value)}
|
||||
{isOpen ? <ChevronDown size={11} /> : <ChevronUp size={11} />}
|
||||
{(isHovered || isOpen) && <ChevronUp size={11} />}
|
||||
</button>
|
||||
</div>
|
||||
</StatefulPopover>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export const Sidebar = memo(function Sidebar({
|
|||
onRenameTask,
|
||||
onRenameBranch,
|
||||
onReorderProjects,
|
||||
taskOrderByProject,
|
||||
onReorderTasks,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
projects: ProjectSection[];
|
||||
|
|
@ -65,14 +67,89 @@ export const Sidebar = memo(function Sidebar({
|
|||
onRenameTask: (id: string) => void;
|
||||
onRenameBranch: (id: string) => void;
|
||||
onReorderProjects: (fromIndex: number, toIndex: number) => void;
|
||||
taskOrderByProject: Record<string, string[]>;
|
||||
onReorderTasks: (projectId: string, fromIndex: number, toIndex: number) => void;
|
||||
onToggleSidebar?: () => void;
|
||||
}) {
|
||||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
const contextMenu = useContextMenu();
|
||||
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
|
||||
const dragIndexRef = useRef<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
const [hoveredProjectId, setHoveredProjectId] = useState<string | null>(null);
|
||||
|
||||
// Mouse-based drag and drop state
|
||||
type DragState =
|
||||
| { type: "project"; fromIdx: number; overIdx: number | null }
|
||||
| { type: "task"; projectId: string; fromIdx: number; overIdx: number | null }
|
||||
| null;
|
||||
const [drag, setDrag] = useState<DragState>(null);
|
||||
const dragRef = useRef<DragState>(null);
|
||||
const startYRef = useRef(0);
|
||||
const didDragRef = useRef(false);
|
||||
|
||||
// Attach global mousemove/mouseup when dragging
|
||||
useEffect(() => {
|
||||
if (!drag) return;
|
||||
const onMove = (e: MouseEvent) => {
|
||||
// Detect which element is under the cursor using data attributes
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el) return;
|
||||
const projectEl = (el as HTMLElement).closest?.("[data-project-idx]") as HTMLElement | null;
|
||||
const taskEl = (el as HTMLElement).closest?.("[data-task-idx]") as HTMLElement | null;
|
||||
|
||||
if (drag.type === "project" && projectEl) {
|
||||
const overIdx = Number(projectEl.dataset.projectIdx);
|
||||
if (overIdx !== drag.overIdx) {
|
||||
setDrag({ ...drag, overIdx });
|
||||
dragRef.current = { ...drag, overIdx };
|
||||
}
|
||||
} else if (drag.type === "task" && taskEl) {
|
||||
const overProjectId = taskEl.dataset.taskProjectId ?? "";
|
||||
const overIdx = Number(taskEl.dataset.taskIdx);
|
||||
if (overProjectId === drag.projectId && overIdx !== drag.overIdx) {
|
||||
setDrag({ ...drag, overIdx });
|
||||
dragRef.current = { ...drag, overIdx };
|
||||
}
|
||||
}
|
||||
// Mark that we actually moved (to distinguish from clicks)
|
||||
if (Math.abs(e.clientY - startYRef.current) > 4) {
|
||||
didDragRef.current = true;
|
||||
}
|
||||
};
|
||||
const onUp = () => {
|
||||
const d = dragRef.current;
|
||||
if (d && didDragRef.current && d.overIdx !== null && d.fromIdx !== d.overIdx) {
|
||||
if (d.type === "project") {
|
||||
onReorderProjects(d.fromIdx, d.overIdx);
|
||||
} else {
|
||||
onReorderTasks(d.projectId, d.fromIdx, d.overIdx);
|
||||
}
|
||||
}
|
||||
dragRef.current = null;
|
||||
didDragRef.current = false;
|
||||
setDrag(null);
|
||||
};
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
};
|
||||
}, [drag, onReorderProjects, onReorderTasks]);
|
||||
|
||||
const [createMenuOpen, setCreateMenuOpen] = useState(false);
|
||||
const createMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createMenuOpen) return;
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (createMenuRef.current && !createMenuRef.current.contains(event.target as Node)) {
|
||||
setCreateMenuOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [createMenuOpen]);
|
||||
|
||||
return (
|
||||
<SPanel>
|
||||
|
|
@ -155,123 +232,185 @@ export const Sidebar = memo(function Sidebar({
|
|||
<PanelLeft size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-disabled={newTaskRepos.length === 0}
|
||||
onClick={() => {
|
||||
if (newTaskRepos.length === 0) {
|
||||
return;
|
||||
}
|
||||
onCreate();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (newTaskRepos.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter" || event.key === " ") onCreate();
|
||||
}}
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: newTaskRepos.length > 0 ? t.borderMedium : t.interactiveHover,
|
||||
color: t.textPrimary,
|
||||
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "background 200ms ease",
|
||||
flexShrink: 0,
|
||||
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
|
||||
":hover": newTaskRepos.length > 0 ? { backgroundColor: "rgba(255, 255, 255, 0.20)" } : undefined,
|
||||
})}
|
||||
>
|
||||
<Plus size={14} style={{ display: "block" }} />
|
||||
<div ref={createMenuRef} className={css({ position: "relative", flexShrink: 0 })}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-disabled={newTaskRepos.length === 0}
|
||||
onClick={() => {
|
||||
if (newTaskRepos.length === 0) return;
|
||||
if (newTaskRepos.length === 1) {
|
||||
onSelectNewTaskRepo(newTaskRepos[0]!.id);
|
||||
onCreate();
|
||||
} else {
|
||||
setCreateMenuOpen((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (newTaskRepos.length === 0) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
if (newTaskRepos.length === 1) {
|
||||
onSelectNewTaskRepo(newTaskRepos[0]!.id);
|
||||
onCreate();
|
||||
} else {
|
||||
setCreateMenuOpen((prev) => !prev);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: newTaskRepos.length > 0 ? t.borderMedium : t.interactiveHover,
|
||||
color: t.textPrimary,
|
||||
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "background 200ms ease",
|
||||
flexShrink: 0,
|
||||
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
|
||||
":hover": newTaskRepos.length > 0 ? { backgroundColor: "rgba(255, 255, 255, 0.20)" } : undefined,
|
||||
})}
|
||||
>
|
||||
<Plus size={14} style={{ display: "block" }} />
|
||||
</div>
|
||||
{createMenuOpen && newTaskRepos.length > 1 ? (
|
||||
<div
|
||||
className={css({
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
right: 0,
|
||||
marginTop: "4px",
|
||||
zIndex: 9999,
|
||||
minWidth: "200px",
|
||||
borderRadius: "10px",
|
||||
border: `1px solid ${t.borderDefault}`,
|
||||
backgroundColor: t.surfaceElevated,
|
||||
boxShadow: `${t.shadow}, 0 0 0 1px ${t.interactiveSubtle}`,
|
||||
padding: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
maxHeight: "240px",
|
||||
overflowY: "auto",
|
||||
})}
|
||||
>
|
||||
{newTaskRepos.map((repo) => (
|
||||
<button
|
||||
key={repo.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectNewTaskRepo(repo.id);
|
||||
setCreateMenuOpen(false);
|
||||
onCreate();
|
||||
}}
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: t.textSecondary,
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
transition: "background 200ms ease, color 200ms ease",
|
||||
":hover": {
|
||||
backgroundColor: t.interactiveHover,
|
||||
color: t.textPrimary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
background: `linear-gradient(135deg, ${projectIconColor(repo.label)}, ${projectIconColor(repo.label + "x")})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
color: t.textOnAccent,
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{projectInitial(repo.label)}
|
||||
</span>
|
||||
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>{repo.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PanelHeaderBar>
|
||||
<div className={css({ padding: "0 8px 8px", display: "flex", flexDirection: "column", gap: "6px" })}>
|
||||
<LabelXSmall color={t.textTertiary} $style={{ textTransform: "uppercase", letterSpacing: "0.04em" }}>
|
||||
Repo
|
||||
</LabelXSmall>
|
||||
<select
|
||||
value={selectedNewTaskRepoId}
|
||||
disabled={newTaskRepos.length === 0}
|
||||
onChange={(event) => {
|
||||
onSelectNewTaskRepo(event.currentTarget.value);
|
||||
}}
|
||||
className={css({
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
border: `1px solid ${t.borderDefault}`,
|
||||
backgroundColor: t.interactiveHover,
|
||||
color: t.textPrimary,
|
||||
fontSize: "12px",
|
||||
padding: "8px 10px",
|
||||
outline: "none",
|
||||
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
|
||||
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
|
||||
})}
|
||||
>
|
||||
{newTaskRepos.length === 0 ? <option value="">No repos available</option> : null}
|
||||
{newTaskRepos.map((repo) => (
|
||||
<option key={repo.id} value={repo.id}>
|
||||
{repo.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<ScrollBody>
|
||||
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
{projects.map((project, projectIndex) => {
|
||||
const isCollapsed = collapsedProjects[project.id] === true;
|
||||
const isDragOver = dragOverIndex === projectIndex && dragIndexRef.current !== projectIndex;
|
||||
const isProjectDropTarget = drag?.type === "project" && drag.overIdx === projectIndex && drag.fromIdx !== projectIndex;
|
||||
const isBeingDragged = drag?.type === "project" && drag.fromIdx === projectIndex && didDragRef.current;
|
||||
const orderedTaskIds = taskOrderByProject[project.id];
|
||||
const orderedTasks = orderedTaskIds
|
||||
? (() => {
|
||||
const byId = new Map(project.tasks.map((t) => [t.id, t]));
|
||||
const sorted = orderedTaskIds.map((id) => byId.get(id)).filter(Boolean) as typeof project.tasks;
|
||||
for (const t of project.tasks) {
|
||||
if (!orderedTaskIds.includes(t.id)) sorted.push(t);
|
||||
}
|
||||
return sorted;
|
||||
})()
|
||||
: project.tasks;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={project.id}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
dragIndexRef.current = projectIndex;
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", String(projectIndex));
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDragOverIndex(projectIndex);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setDragOverIndex((current) => (current === projectIndex ? null : current));
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
const fromIndex = dragIndexRef.current;
|
||||
if (fromIndex != null && fromIndex !== projectIndex) {
|
||||
onReorderProjects(fromIndex, projectIndex);
|
||||
}
|
||||
dragIndexRef.current = null;
|
||||
setDragOverIndex(null);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
dragIndexRef.current = null;
|
||||
setDragOverIndex(null);
|
||||
}}
|
||||
data-project-idx={projectIndex}
|
||||
className={css({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
borderTop: isDragOver ? `2px solid ${t.accent}` : "2px solid transparent",
|
||||
transition: "border-color 150ms ease",
|
||||
position: "relative",
|
||||
opacity: isBeingDragged ? 0.4 : 1,
|
||||
transition: "opacity 150ms ease",
|
||||
"::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "-2px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2px",
|
||||
backgroundColor: isProjectDropTarget ? t.textPrimary : "transparent",
|
||||
transition: "background-color 100ms ease",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
setCollapsedProjects((current) => ({
|
||||
...current,
|
||||
[project.id]: !current[project.id],
|
||||
}))
|
||||
}
|
||||
onMouseEnter={() => setHoveredProjectId(project.id)}
|
||||
onMouseLeave={() => setHoveredProjectId((cur) => (cur === project.id ? null : cur))}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 0) return;
|
||||
startYRef.current = event.clientY;
|
||||
didDragRef.current = false;
|
||||
setHoveredProjectId(null);
|
||||
const state: DragState = { type: "project", fromIdx: projectIndex, overIdx: null };
|
||||
dragRef.current = state;
|
||||
setDrag(state);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!didDragRef.current) {
|
||||
setCollapsedProjects((current) => ({
|
||||
...current,
|
||||
[project.id]: !current[project.id],
|
||||
}));
|
||||
}
|
||||
}}
|
||||
data-project-header
|
||||
className={css({
|
||||
display: "flex",
|
||||
|
|
@ -281,7 +420,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
gap: "8px",
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
":hover": { opacity: 0.8 },
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px", overflow: "hidden" })}>
|
||||
|
|
@ -323,11 +461,43 @@ export const Sidebar = memo(function Sidebar({
|
|||
{project.label}
|
||||
</LabelSmall>
|
||||
</div>
|
||||
{isCollapsed ? <LabelXSmall color={t.textTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall> : null}
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
|
||||
{isCollapsed ? <LabelXSmall color={t.textTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall> : null}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setHoveredProjectId(null);
|
||||
onSelectNewTaskRepo(project.id);
|
||||
onCreate();
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className={css({
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: "none",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
cursor: "pointer",
|
||||
color: t.textTertiary,
|
||||
opacity: hoveredProjectId === project.id ? 1 : 0,
|
||||
transition: "opacity 150ms ease, background 200ms ease, color 200ms ease",
|
||||
pointerEvents: hoveredProjectId === project.id ? "auto" : "none",
|
||||
":hover": { backgroundColor: t.interactiveHover, color: t.textSecondary },
|
||||
})}
|
||||
title={`New task in ${project.label}`}
|
||||
>
|
||||
<Plus size={12} color={t.textTertiary} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed &&
|
||||
project.tasks.map((task) => {
|
||||
orderedTasks.map((task, taskIndex) => {
|
||||
const isActive = task.id === activeId;
|
||||
const isDim = task.status === "archived";
|
||||
const isRunning = task.tabs.some((tab) => tab.status === "running");
|
||||
|
|
@ -336,11 +506,30 @@ export const Sidebar = memo(function Sidebar({
|
|||
const totalAdded = task.fileChanges.reduce((sum, file) => sum + file.added, 0);
|
||||
const totalRemoved = task.fileChanges.reduce((sum, file) => sum + file.removed, 0);
|
||||
const hasDiffs = totalAdded > 0 || totalRemoved > 0;
|
||||
const isTaskDropTarget = drag?.type === "task" && drag.projectId === project.id && drag.overIdx === taskIndex && drag.fromIdx !== taskIndex;
|
||||
const isTaskBeingDragged = drag?.type === "task" && drag.projectId === project.id && drag.fromIdx === taskIndex && didDragRef.current;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
onClick={() => onSelect(task.id)}
|
||||
data-task-idx={taskIndex}
|
||||
data-task-project-id={project.id}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 0) return;
|
||||
// Only start task drag if not already in a project drag
|
||||
if (dragRef.current) return;
|
||||
event.stopPropagation();
|
||||
startYRef.current = event.clientY;
|
||||
didDragRef.current = false;
|
||||
const state: DragState = { type: "task", projectId: project.id, fromIdx: taskIndex, overIdx: null };
|
||||
dragRef.current = state;
|
||||
setDrag(state);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!didDragRef.current) {
|
||||
onSelect(task.id);
|
||||
}
|
||||
}}
|
||||
onContextMenu={(event) =>
|
||||
contextMenu.open(event, [
|
||||
{ label: "Rename task", onClick: () => onRenameTask(task.id) },
|
||||
|
|
@ -351,10 +540,21 @@ export const Sidebar = memo(function Sidebar({
|
|||
className={css({
|
||||
padding: "8px 12px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid transparent",
|
||||
position: "relative",
|
||||
backgroundColor: isActive ? t.interactiveHover : "transparent",
|
||||
opacity: isTaskBeingDragged ? 0.4 : 1,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
transition: "all 150ms ease",
|
||||
"::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "-2px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2px",
|
||||
backgroundColor: isTaskDropTarget ? t.textPrimary : "transparent",
|
||||
transition: "background-color 100ms ease",
|
||||
},
|
||||
":hover": {
|
||||
backgroundColor: t.interactiveHover,
|
||||
},
|
||||
|
|
@ -410,9 +610,52 @@ export const Sidebar = memo(function Sidebar({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Bottom drop zone for dragging to end of task list */}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
data-task-idx={orderedTasks.length}
|
||||
data-task-project-id={project.id}
|
||||
className={css({
|
||||
minHeight: "4px",
|
||||
position: "relative",
|
||||
"::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2px",
|
||||
backgroundColor:
|
||||
drag?.type === "task" && drag.projectId === project.id && drag.overIdx === orderedTasks.length && drag.fromIdx !== orderedTasks.length
|
||||
? t.textPrimary
|
||||
: "transparent",
|
||||
transition: "background-color 100ms ease",
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Bottom drop zone for dragging project to end of list */}
|
||||
<div
|
||||
data-project-idx={projects.length}
|
||||
className={css({
|
||||
minHeight: "4px",
|
||||
position: "relative",
|
||||
"::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2px",
|
||||
backgroundColor:
|
||||
drag?.type === "project" && drag.overIdx === projects.length && drag.fromIdx !== projects.length ? t.textPrimary : "transparent",
|
||||
transition: "background-color 100ms ease",
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ScrollBody>
|
||||
<SidebarFooter />
|
||||
|
|
@ -450,7 +693,6 @@ function SidebarFooter() {
|
|||
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);
|
||||
|
|
@ -469,7 +711,6 @@ function SidebarFooter() {
|
|||
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);
|
||||
}
|
||||
|
|
@ -557,21 +798,7 @@ function SidebarFooter() {
|
|||
});
|
||||
|
||||
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 })}
|
||||
>
|
||||
<div ref={containerRef} className={css({ position: "relative", flexShrink: 0 })}>
|
||||
{open ? (
|
||||
<div
|
||||
className={css({
|
||||
|
|
@ -638,14 +865,9 @@ function SidebarFooter() {
|
|||
})}
|
||||
onMouseEnter={() => {
|
||||
openFlyout();
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
closeFlyout();
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<div className={popoverStyle}>
|
||||
|
|
@ -726,7 +948,6 @@ function SidebarFooter() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
setOpen((prev) => {
|
||||
if (prev) setWorkspaceFlyoutOpen(false);
|
||||
return !prev;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
import { memo } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall } from "baseui/typography";
|
||||
import { Clock, MailOpen, PanelLeft, PanelRight } from "lucide-react";
|
||||
import { Clock, PanelLeft, PanelRight } from "lucide-react";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { PanelHeaderBar } from "./ui";
|
||||
|
|
@ -23,6 +23,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onSidebarPeekEnd,
|
||||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
onNavigateToUsage,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTab: AgentTab | null | undefined;
|
||||
|
|
@ -39,6 +40,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onSidebarPeekEnd?: () => void;
|
||||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
onNavigateToUsage?: () => void;
|
||||
}) {
|
||||
const [css] = useStyletron();
|
||||
const t = useFoundryTokens();
|
||||
|
|
@ -161,52 +163,32 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
) : null}
|
||||
<div className={css({ flex: 1 })} />
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onNavigateToUsage}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") onNavigateToUsage?.();
|
||||
}}
|
||||
className={css({
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: "3px 10px",
|
||||
padding: "4px 12px",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: t.interactiveHover,
|
||||
border: `1px solid ${t.borderSubtle}`,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: t.textSecondary,
|
||||
color: t.textTertiary,
|
||||
whiteSpace: "nowrap",
|
||||
cursor: "pointer",
|
||||
transition: "background 200ms ease, color 200ms ease",
|
||||
":hover": { backgroundColor: t.interactiveHover, color: t.textSecondary },
|
||||
})}
|
||||
>
|
||||
<Clock size={11} style={{ flexShrink: 0 }} />
|
||||
<span>847 min used</span>
|
||||
<span>{task.minutesUsed ?? 0} min used</span>
|
||||
</div>
|
||||
{activeTab ? (
|
||||
<button
|
||||
onClick={() => onSetActiveTabUnread(!activeTab.unread)}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: t.textSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
||||
})}
|
||||
>
|
||||
<MailOpen size={12} style={{ flexShrink: 0 }} />{" "}
|
||||
<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({
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export interface WorkbenchTask {
|
|||
fileChanges: WorkbenchFileChange[];
|
||||
diffs: Record<string, string>;
|
||||
fileTree: WorkbenchFileTreeNode[];
|
||||
minutesUsed: number;
|
||||
}
|
||||
|
||||
export interface WorkbenchRepo {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue