Add Foundry Tauri v2 desktop app with UI polish
- Scaffold Tauri v2 desktop package (foundry/packages/desktop) - Sidecar build script compiles backend into standalone Bun binary - Frontend build script packages Vite output for Tauri webview - macOS glass-effect app icon following Big Sur design standards - Collapsible sidebars with smooth width transitions - Inset content framing with borders and nested border-radius (Outer R = Inner R + Padding) - iMessage-style chat bubble styling with proper corner radii - Styled composer input with matching border-radius - Vertical separator between chat and right sidebar - Website download button component - Cargo workspace exclude for standalone Tauri build Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22
foundry/packages/desktop/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@sandbox-agent/foundry-desktop",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"build:sidecar": "tsx scripts/build-sidecar.ts",
|
||||
"build:frontend": "tsx scripts/build-frontend.ts",
|
||||
"build:all": "pnpm build:sidecar && pnpm build:frontend && pnpm build",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"tsx": "^4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2"
|
||||
}
|
||||
}
|
||||
42
foundry/packages/desktop/scripts/build-frontend.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { execSync } from "node:child_process";
|
||||
import { cpSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const desktopRoot = resolve(__dirname, "..");
|
||||
const repoRoot = resolve(desktopRoot, "../../..");
|
||||
const frontendDist = resolve(desktopRoot, "../frontend/dist");
|
||||
const destDir = resolve(desktopRoot, "frontend-dist");
|
||||
|
||||
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`> ${cmd}`);
|
||||
execSync(cmd, {
|
||||
stdio: "inherit",
|
||||
cwd: opts?.cwd ?? repoRoot,
|
||||
env: { ...process.env, ...opts?.env },
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: Build the frontend with the desktop-specific backend endpoint
|
||||
console.log("\n=== Building frontend for desktop ===\n");
|
||||
run("pnpm --filter @sandbox-agent/foundry-frontend build", {
|
||||
env: {
|
||||
VITE_HF_BACKEND_ENDPOINT: "http://127.0.0.1:7741/api/rivet",
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Copy dist to frontend-dist/
|
||||
console.log("\n=== Copying frontend build output ===\n");
|
||||
if (existsSync(destDir)) {
|
||||
rmSync(destDir, { recursive: true });
|
||||
}
|
||||
cpSync(frontendDist, destDir, { recursive: true });
|
||||
|
||||
// Step 3: Strip react-scan script from index.html (it loads unconditionally)
|
||||
const indexPath = resolve(destDir, "index.html");
|
||||
let html = readFileSync(indexPath, "utf-8");
|
||||
html = html.replace(/<script\s+src="https:\/\/unpkg\.com\/react-scan\/dist\/auto\.global\.js"[^>]*><\/script>\s*/g, "");
|
||||
writeFileSync(indexPath, html);
|
||||
|
||||
console.log("\n=== Frontend build complete ===\n");
|
||||
68
foundry/packages/desktop/scripts/build-sidecar.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { execSync } from "node:child_process";
|
||||
import { mkdirSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const desktopRoot = resolve(__dirname, "..");
|
||||
const sidecarDir = resolve(desktopRoot, "src-tauri/sidecars");
|
||||
|
||||
const isDev = process.argv.includes("--dev");
|
||||
|
||||
// Detect current architecture
|
||||
function currentTarget(): string {
|
||||
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
||||
return `${arch}-apple-darwin`;
|
||||
}
|
||||
|
||||
// Target triples to build
|
||||
const targets: Array<{ bunTarget: string; tripleTarget: string }> = isDev
|
||||
? [
|
||||
{
|
||||
bunTarget: process.arch === "arm64" ? "bun-darwin-arm64" : "bun-darwin-x64",
|
||||
tripleTarget: currentTarget(),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
bunTarget: "bun-darwin-arm64",
|
||||
tripleTarget: "aarch64-apple-darwin",
|
||||
},
|
||||
{
|
||||
bunTarget: "bun-darwin-x64",
|
||||
tripleTarget: "x86_64-apple-darwin",
|
||||
},
|
||||
];
|
||||
|
||||
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`> ${cmd}`);
|
||||
execSync(cmd, {
|
||||
stdio: "inherit",
|
||||
cwd: opts?.cwd ?? desktopRoot,
|
||||
env: { ...process.env, ...opts?.env },
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: Build the backend with tsup
|
||||
console.log("\n=== Building backend with tsup ===\n");
|
||||
run("pnpm --filter @sandbox-agent/foundry-backend build", {
|
||||
cwd: resolve(desktopRoot, "../../.."),
|
||||
});
|
||||
|
||||
// Step 2: Compile standalone binaries with bun
|
||||
mkdirSync(sidecarDir, { recursive: true });
|
||||
|
||||
const backendEntry = resolve(desktopRoot, "../backend/dist/index.js");
|
||||
|
||||
if (!existsSync(backendEntry)) {
|
||||
console.error(`Backend build output not found at ${backendEntry}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const { bunTarget, tripleTarget } of targets) {
|
||||
const outfile = resolve(sidecarDir, `foundry-backend-${tripleTarget}`);
|
||||
console.log(`\n=== Compiling sidecar for ${tripleTarget} ===\n`);
|
||||
run(`bun build --compile --target ${bunTarget} ${backendEntry} --outfile ${outfile}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Sidecar build complete ===\n");
|
||||
15
foundry/packages/desktop/src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "foundry-desktop"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
3
foundry/packages/desktop/src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
30
foundry/packages/desktop/src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capability for Foundry desktop",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
"allow": [
|
||||
{
|
||||
"name": "sidecars/foundry-backend",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
{
|
||||
"name": "sidecars/foundry-backend",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"default":{"identifier":"default","description":"Default capability for Foundry desktop","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"sidecars/foundry-backend","sidecar":true}]},{"identifier":"shell:allow-spawn","allow":[{"args":true,"name":"sidecars/foundry-backend","sidecar":true}]}]}}
|
||||
2564
foundry/packages/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
2564
foundry/packages/desktop/src-tauri/gen/schemas/macOS-schema.json
Normal file
BIN
foundry/packages/desktop/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
130
foundry/packages/desktop/src-tauri/icons/icon-source.svg
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- macOS squircle clip path (continuous corner radius ~22.37% = 229px) -->
|
||||
<clipPath id="squircle-clip">
|
||||
<path d="
|
||||
M 229 0
|
||||
H 795
|
||||
C 921.5 0 1024 102.5 1024 229
|
||||
V 795
|
||||
C 1024 921.5 921.5 1024 795 1024
|
||||
H 229
|
||||
C 102.5 1024 0 921.5 0 795
|
||||
V 229
|
||||
C 0 102.5 102.5 0 229 0
|
||||
Z
|
||||
"/>
|
||||
</clipPath>
|
||||
|
||||
<!-- macOS squircle shape for stroke -->
|
||||
<path id="squircle-path" d="
|
||||
M 229 0
|
||||
H 795
|
||||
C 921.5 0 1024 102.5 1024 229
|
||||
V 795
|
||||
C 1024 921.5 921.5 1024 795 1024
|
||||
H 229
|
||||
C 102.5 1024 0 921.5 0 795
|
||||
V 229
|
||||
C 0 102.5 102.5 0 229 0
|
||||
Z
|
||||
"/>
|
||||
|
||||
<!-- Base dark gradient with subtle depth -->
|
||||
<linearGradient id="base-gradient" x1="0" y1="0" x2="1024" y2="1024" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#1A1A1A"/>
|
||||
<stop offset="0.5" stop-color="#0F0F0F"/>
|
||||
<stop offset="1" stop-color="#0A0A0A"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Glass highlight overlay (top-left lighter) -->
|
||||
<linearGradient id="glass-overlay" x1="0" y1="0" x2="1024" y2="1024" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="white" stop-opacity="0.12"/>
|
||||
<stop offset="0.35" stop-color="white" stop-opacity="0.04"/>
|
||||
<stop offset="0.65" stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="black" stop-opacity="0.15"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Top edge highlight (light reflection) -->
|
||||
<linearGradient id="top-highlight" x1="256" y1="0" x2="768" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="0.2" stop-color="white" stop-opacity="0.4"/>
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0.6"/>
|
||||
<stop offset="0.8" stop-color="white" stop-opacity="0.4"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Inner border glow -->
|
||||
<filter id="inner-border-glow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Subtle inner shadow for depth -->
|
||||
<radialGradient id="vignette" cx="0.5" cy="0.4" r="0.65" fx="0.5" fy="0.35">
|
||||
<stop offset="0" stop-color="white" stop-opacity="0.03"/>
|
||||
<stop offset="0.7" stop-color="black" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="black" stop-opacity="0.2"/>
|
||||
</radialGradient>
|
||||
|
||||
<!-- Inner rounded rect for the border -->
|
||||
<rect id="inner-border-rect" x="152" y="152" width="720" height="720" rx="205"/>
|
||||
|
||||
<!-- Glow filter for inner border -->
|
||||
<filter id="glow" x="-5%" y="-5%" width="110%" height="110%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="8" result="blur"/>
|
||||
<feFlood flood-color="#F0F0F0" flood-opacity="0.15" result="color"/>
|
||||
<feComposite in="color" in2="blur" operator="in" result="glow"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="glow"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Specular highlight on the Y mark -->
|
||||
<linearGradient id="logo-gradient" x1="380" y1="340" x2="640" y2="730" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFFFFF"/>
|
||||
<stop offset="0.6" stop-color="#F0F0F0"/>
|
||||
<stop offset="1" stop-color="#E0E0E0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Outer squircle shape -->
|
||||
<g clip-path="url(#squircle-clip)">
|
||||
<!-- Base fill with gradient -->
|
||||
<use href="#squircle-path" fill="url(#base-gradient)"/>
|
||||
|
||||
<!-- Vignette/radial depth -->
|
||||
<use href="#squircle-path" fill="url(#vignette)"/>
|
||||
|
||||
<!-- Glass overlay -->
|
||||
<use href="#squircle-path" fill="url(#glass-overlay)"/>
|
||||
|
||||
<!-- Inner rounded border with glow -->
|
||||
<g filter="url(#glow)">
|
||||
<rect x="152" y="152" width="720" height="720" rx="205"
|
||||
fill="none" stroke="#F0F0F0" stroke-width="67" stroke-opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
<!-- Y-shaped / triskelion logo mark, scaled from 130x128 to fit ~1024 -->
|
||||
<!-- Original viewBox was 130x128, scaling factor ~7.87. Centered in the icon. -->
|
||||
<g transform="translate(512, 512) scale(7.87) translate(-65, -64)">
|
||||
<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="url(#logo-gradient)"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Outer squircle border (subtle) -->
|
||||
<use href="#squircle-path" fill="none" stroke="#333333" stroke-width="1.5" stroke-opacity="0.5"/>
|
||||
|
||||
<!-- Top edge highlight reflection -->
|
||||
<path d="
|
||||
M 229 2
|
||||
H 795
|
||||
C 920 2 1022 103 1022 229
|
||||
" fill="none" stroke="url(#top-highlight)" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/icon.icns
Normal file
BIN
foundry/packages/desktop/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6 KiB |
|
After Width: | Height: | Size: 6 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
foundry/packages/desktop/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
131
foundry/packages/desktop/src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
struct BackendState {
|
||||
child: Mutex<Option<CommandChild>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_backend_url() -> String {
|
||||
"http://127.0.0.1:7741".to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn backend_health() -> Result<bool, String> {
|
||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
||||
Ok(resp) => Ok(resp.status().is_success()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_backend(timeout_secs: u64) -> Result<(), String> {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||
|
||||
loop {
|
||||
if start.elapsed() > timeout {
|
||||
return Err(format!(
|
||||
"Backend failed to start within {} seconds",
|
||||
timeout_secs
|
||||
));
|
||||
}
|
||||
|
||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
||||
Ok(resp) if resp.status().is_success() => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_backend(app: &AppHandle) -> Result<(), String> {
|
||||
let sidecar = app
|
||||
.shell()
|
||||
.sidecar("sidecars/foundry-backend")
|
||||
.map_err(|e| format!("Failed to create sidecar command: {}", e))?
|
||||
.args(["start", "--host", "127.0.0.1", "--port", "7741"]);
|
||||
|
||||
let (mut rx, child) = sidecar
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn backend sidecar: {}", e))?;
|
||||
|
||||
// Store the child process handle for cleanup
|
||||
let state = app.state::<BackendState>();
|
||||
*state.child.lock().unwrap() = Some(child);
|
||||
|
||||
// Log sidecar stdout/stderr in a background task
|
||||
tauri::async_runtime::spawn(async move {
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
eprintln!("[foundry-backend] {}", String::from_utf8_lossy(&line));
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
eprintln!("[foundry-backend] {}", String::from_utf8_lossy(&line));
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
eprintln!(
|
||||
"[foundry-backend] process exited with code {:?}",
|
||||
payload.code
|
||||
);
|
||||
break;
|
||||
}
|
||||
CommandEvent::Error(err) => {
|
||||
eprintln!("[foundry-backend] error: {}", err);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(BackendState {
|
||||
child: Mutex::new(None),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_backend_url, backend_health])
|
||||
.setup(|app| {
|
||||
// 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.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = spawn_backend(&handle) {
|
||||
eprintln!("[foundry-desktop] Failed to start backend: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
match wait_for_backend(30).await {
|
||||
Ok(()) => eprintln!("[foundry-desktop] Backend is ready."),
|
||||
Err(e) => eprintln!("[foundry-desktop] {}", e),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
let state = window.state::<BackendState>();
|
||||
let child = state.child.lock().unwrap().take();
|
||||
if let Some(child) = child {
|
||||
let _ = child.kill();
|
||||
eprintln!("[foundry-desktop] Backend sidecar killed.");
|
||||
}
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Foundry");
|
||||
}
|
||||
5
foundry/packages/desktop/src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
foundry_desktop::run();
|
||||
}
|
||||
43
foundry/packages/desktop/src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||
"productName": "Foundry",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.sandboxagent.foundry",
|
||||
"build": {
|
||||
"beforeDevCommand": "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
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["dmg", "app"],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"macOS": {
|
||||
"signingIdentity": null
|
||||
},
|
||||
"externalBin": ["sidecars/foundry-backend"]
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
}
|
||||
</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" />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
|
|||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useStyletron } from "baseui";
|
||||
|
||||
import { PanelLeft, PanelRight } from "lucide-react";
|
||||
|
||||
import { DiffContent } from "./mock-layout/diff-content";
|
||||
import { MessageList } from "./mock-layout/message-list";
|
||||
import { PromptComposer } from "./mock-layout/prompt-composer";
|
||||
|
|
@ -454,8 +456,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
backgroundColor: "#09090b",
|
||||
overflow: "hidden",
|
||||
borderTopLeftRadius: "12px",
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
<TabStrip
|
||||
|
|
@ -641,6 +646,7 @@ const RightRail = memo(function RightRail({
|
|||
onArchive,
|
||||
onRevertFile,
|
||||
onPublishPr,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
task: Task;
|
||||
|
|
@ -649,6 +655,7 @@ const RightRail = memo(function RightRail({
|
|||
onArchive: () => void;
|
||||
onRevertFile: (path: string) => void;
|
||||
onPublishPr: () => void;
|
||||
onToggleSidebar?: () => void;
|
||||
}) {
|
||||
const [css] = useStyletron();
|
||||
const railRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -728,6 +735,8 @@ const RightRail = memo(function RightRail({
|
|||
minHeight: `${RIGHT_RAIL_MIN_SECTION_HEIGHT}px`,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
})}
|
||||
>
|
||||
<RightSidebar
|
||||
|
|
@ -737,6 +746,7 @@ const RightRail = memo(function RightRail({
|
|||
onArchive={onArchive}
|
||||
onRevertFile={onRevertFile}
|
||||
onPublishPr={onPublishPr}
|
||||
onToggleSidebar={onToggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -750,6 +760,7 @@ const RightRail = memo(function RightRail({
|
|||
cursor: "ns-resize",
|
||||
position: "relative",
|
||||
backgroundColor: "#050505",
|
||||
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
":before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
|
|
@ -769,6 +780,9 @@ const RightRail = memo(function RightRail({
|
|||
minHeight: `${RIGHT_RAIL_MIN_SECTION_HEIGHT}px`,
|
||||
backgroundColor: "#080506",
|
||||
overflow: "hidden",
|
||||
borderBottomRightRadius: "12px",
|
||||
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderBottom: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
})}
|
||||
>
|
||||
<TerminalPane workspaceId={workspaceId} taskId={task.id} />
|
||||
|
|
@ -896,6 +910,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
const leftWidthRef = useRef(leftWidth);
|
||||
const rightWidthRef = useRef(rightWidth);
|
||||
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
||||
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
leftWidthRef.current = leftWidth;
|
||||
|
|
@ -1195,100 +1211,330 @@ 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;
|
||||
// Tauri v2 IPC: invoke start_dragging on the webview window
|
||||
const ipc = (window as Record<string, unknown>).__TAURI_INTERNALS__ as { invoke: (cmd: string, args?: unknown) => Promise<unknown> } | undefined;
|
||||
if (ipc?.invoke) {
|
||||
ipc.invoke("plugin:window|start_dragging").catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
const dragRegion = isDesktop ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "38px",
|
||||
zIndex: 9998,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{/* Background drag target – sits behind interactive elements */}
|
||||
<div
|
||||
onPointerDown={onDragMouseDown}
|
||||
style={
|
||||
{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
WebkitAppRegion: "drag",
|
||||
pointerEvents: "auto",
|
||||
zIndex: 0,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const collapsedToggleStyle: React.CSSProperties = {
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
position: "relative",
|
||||
zIndex: 9999,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const sidebarTransition = "width 200ms ease";
|
||||
const contentFrameStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
overflow: "hidden",
|
||||
marginBottom: "8px",
|
||||
marginRight: "8px",
|
||||
};
|
||||
|
||||
if (!activeTask) {
|
||||
return (
|
||||
<>
|
||||
<MockWorkspaceOrgBar />
|
||||
{dragRegion}
|
||||
<Shell>
|
||||
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
newTaskRepos={viewModel.repos}
|
||||
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||
activeId=""
|
||||
onSelect={selectTask}
|
||||
onCreate={createTask}
|
||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: leftSidebarOpen ? `${leftWidth}px` : 0,
|
||||
flexShrink: 0,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
transition: sidebarTransition,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: `${leftWidth}px`, flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
newTaskRepos={viewModel.repos}
|
||||
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||
activeId=""
|
||||
onSelect={selectTask}
|
||||
onCreate={createTask}
|
||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} />
|
||||
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
|
||||
<ScrollBody>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "32px",
|
||||
}}
|
||||
>
|
||||
{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 }}>
|
||||
<ScrollBody>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "420px",
|
||||
textAlign: "center",
|
||||
minHeight: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "32px",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first task</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>
|
||||
{viewModel.repos.length > 0 ? "Choose a repo, then create a task." : "No repos are available in this workspace yet."}
|
||||
</p>
|
||||
{viewModel.repos.length > 0 ? (
|
||||
<label style={{ display: "flex", flexDirection: "column", gap: "6px", textAlign: "left" }}>
|
||||
<span style={{ fontSize: "11px", fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase", opacity: 0.7 }}>Repo</span>
|
||||
<select
|
||||
value={selectedNewTaskRepoId}
|
||||
onChange={(event) => {
|
||||
setSelectedNewTaskRepoId(event.currentTarget.value);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "10px",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
padding: "10px 12px",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
color: "#f4f4f5",
|
||||
}}
|
||||
>
|
||||
{viewModel.repos.map((repo) => (
|
||||
<option key={repo.id} value={repo.id}>
|
||||
{repo.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={createTask}
|
||||
disabled={viewModel.repos.length === 0}
|
||||
<div
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
border: 0,
|
||||
borderRadius: "999px",
|
||||
padding: "10px 18px",
|
||||
background: viewModel.repos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "#444",
|
||||
color: "#e4e4e7",
|
||||
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
|
||||
fontWeight: 600,
|
||||
maxWidth: "420px",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
New task
|
||||
</button>
|
||||
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first task</h2>
|
||||
<p style={{ margin: 0, opacity: 0.75 }}>
|
||||
{viewModel.repos.length > 0
|
||||
? "Start from the sidebar to create a task on the first available repo."
|
||||
: "No repos are available in this workspace yet."}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={createTask}
|
||||
disabled={viewModel.repos.length === 0}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
border: 0,
|
||||
borderRadius: "999px",
|
||||
padding: "10px 18px",
|
||||
background: viewModel.repos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "#444",
|
||||
color: "#e4e4e7",
|
||||
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
New task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollBody>
|
||||
</SPanel>
|
||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||
<div
|
||||
style={{
|
||||
width: rightSidebarOpen ? `${rightWidth}px` : 0,
|
||||
flexShrink: 0,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
transition: sidebarTransition,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: `${rightWidth}px`, flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<SPanel />
|
||||
</div>
|
||||
</ScrollBody>
|
||||
</SPanel>
|
||||
<PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} />
|
||||
<div style={{ width: `${rightWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<SPanel />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1296,55 +1542,97 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
|
||||
return (
|
||||
<>
|
||||
<MockWorkspaceOrgBar />
|
||||
{dragRegion}
|
||||
<Shell>
|
||||
<div style={{ width: `${leftWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
newTaskRepos={viewModel.repos}
|
||||
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||
activeId={activeTask.id}
|
||||
onSelect={selectTask}
|
||||
onCreate={createTask}
|
||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: leftSidebarOpen ? `${leftWidth}px` : 0,
|
||||
flexShrink: 0,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
transition: sidebarTransition,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: `${leftWidth}px`, flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
newTaskRepos={viewModel.repos}
|
||||
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||
activeId={activeTask.id}
|
||||
onSelect={selectTask}
|
||||
onCreate={createTask}
|
||||
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||
onMarkUnread={markTaskUnread}
|
||||
onRenameTask={renameTask}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
onToggleSidebar={() => setLeftSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} />
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<TranscriptPanel
|
||||
taskWorkbenchClient={taskWorkbenchClient}
|
||||
task={activeTask}
|
||||
activeTabId={activeTabId}
|
||||
lastAgentTabId={lastAgentTabId}
|
||||
openDiffs={openDiffs}
|
||||
onSyncRouteSession={syncRouteSession}
|
||||
onSetActiveTabId={(tabId) => {
|
||||
setActiveTabIdByTask((current) => ({ ...current, [activeTask.id]: tabId }));
|
||||
{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}
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<TranscriptPanel
|
||||
taskWorkbenchClient={taskWorkbenchClient}
|
||||
task={activeTask}
|
||||
activeTabId={activeTabId}
|
||||
lastAgentTabId={lastAgentTabId}
|
||||
openDiffs={openDiffs}
|
||||
onSyncRouteSession={syncRouteSession}
|
||||
onSetActiveTabId={(tabId) => {
|
||||
setActiveTabIdByTask((current) => ({ ...current, [activeTask.id]: tabId }));
|
||||
}}
|
||||
onSetLastAgentTabId={(tabId) => {
|
||||
setLastAgentTabIdByTask((current) => ({ ...current, [activeTask.id]: tabId }));
|
||||
}}
|
||||
onSetOpenDiffs={(paths) => {
|
||||
setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||
<div
|
||||
style={{
|
||||
width: rightSidebarOpen ? `${rightWidth}px` : 0,
|
||||
flexShrink: 0,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
transition: sidebarTransition,
|
||||
}}
|
||||
onSetLastAgentTabId={(tabId) => {
|
||||
setLastAgentTabIdByTask((current) => ({ ...current, [activeTask.id]: tabId }));
|
||||
}}
|
||||
onSetOpenDiffs={(paths) => {
|
||||
setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} />
|
||||
<div style={{ width: `${rightWidth}px`, flexShrink: 0, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||
<RightRail
|
||||
workspaceId={workspaceId}
|
||||
task={activeTask}
|
||||
activeTabId={activeTabId}
|
||||
onOpenDiff={openDiffTab}
|
||||
onArchive={archiveTask}
|
||||
onRevertFile={revertFile}
|
||||
onPublishPr={publishPr}
|
||||
/>
|
||||
>
|
||||
<div style={{ minWidth: `${rightWidth}px`, flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<RightRail
|
||||
workspaceId={workspaceId}
|
||||
task={activeTask}
|
||||
activeTabId={activeTabId}
|
||||
onOpenDiff={openDiffTab}
|
||||
onArchive={archiveTask}
|
||||
onRevertFile={revertFile}
|
||||
onPublishPr={publishPr}
|
||||
onToggleSidebar={() => setRightSidebarOpen(false)}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -43,15 +43,15 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
|
|||
>
|
||||
<div
|
||||
className={css({
|
||||
maxWidth: "100%",
|
||||
padding: "12px 16px",
|
||||
borderTopLeftRadius: "16px",
|
||||
borderTopRightRadius: "16px",
|
||||
maxWidth: "80%",
|
||||
...(isUser
|
||||
? {
|
||||
padding: "12px 16px",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.10)",
|
||||
color: "#e4e4e7",
|
||||
borderBottomLeftRadius: "16px",
|
||||
borderTopLeftRadius: "18px",
|
||||
borderTopRightRadius: "18px",
|
||||
borderBottomLeftRadius: "18px",
|
||||
borderBottomRightRadius: "4px",
|
||||
}
|
||||
: {
|
||||
|
|
@ -155,7 +155,7 @@ export const MessageList = memo(function MessageList({
|
|||
);
|
||||
|
||||
const messageContentClass = css({
|
||||
maxWidth: "80%",
|
||||
maxWidth: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
|
@ -201,7 +201,7 @@ export const MessageList = memo(function MessageList({
|
|||
<div
|
||||
ref={scrollRef}
|
||||
className={css({
|
||||
padding: "16px 220px 16px 44px",
|
||||
padding: "16px 20px 16px 20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
position: "relative",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
border: `1px solid ${theme.colors.borderOpaque}`,
|
||||
borderRadius: "16px",
|
||||
borderRadius: "12px",
|
||||
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
|
||||
transition: "border-color 200ms ease",
|
||||
":focus-within": { borderColor: "rgba(255, 255, 255, 0.15)" },
|
||||
|
|
@ -56,7 +56,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
padding: "14px 58px 8px 14px",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
borderRadius: "16px 16px 0 0",
|
||||
borderRadius: "12px 12px 0 0",
|
||||
color: theme.colors.contentPrimary,
|
||||
fontSize: "13px",
|
||||
fontFamily: "inherit",
|
||||
|
|
@ -77,7 +77,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
padding: "0",
|
||||
margin: "0",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer",
|
||||
position: "absolute",
|
||||
right: "12px",
|
||||
|
|
@ -112,7 +112,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
return (
|
||||
<div
|
||||
className={css({
|
||||
padding: "12px 16px",
|
||||
padding: "12px 12px",
|
||||
borderTop: "none",
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useCallback, useMemo, useState, type MouseEvent } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall } from "baseui/typography";
|
||||
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest } from "lucide-react";
|
||||
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
|
||||
|
||||
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
|
||||
|
|
@ -93,6 +93,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
onArchive,
|
||||
onRevertFile,
|
||||
onPublishPr,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTabId: string | null;
|
||||
|
|
@ -100,6 +101,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
onArchive: () => void;
|
||||
onRevertFile: (path: string) => void;
|
||||
onPublishPr: () => void;
|
||||
onToggleSidebar?: () => void;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const [rightTab, setRightTab] = useState<"changes" | "files">("changes");
|
||||
|
|
@ -171,7 +173,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
{pullRequestUrl ? "Open PR" : "Publish PR"}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
|
|
@ -195,7 +197,8 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
})}
|
||||
>
|
||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} /> Push
|
||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />{" "}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Push</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onArchive}
|
||||
|
|
@ -220,13 +223,49 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
})}
|
||||
>
|
||||
<Archive size={12} style={{ flexShrink: 0 }} /> Archive
|
||||
<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}
|
||||
</PanelHeaderBar>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", borderTop: "1px solid rgba(255, 255, 255, 0.10)" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderTop: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderTopRightRadius: "12px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: "flex",
|
||||
|
|
@ -237,6 +276,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
height: "41px",
|
||||
minHeight: "41px",
|
||||
flexShrink: 0,
|
||||
borderTopRightRadius: "12px",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useRef, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, PanelLeft, Plus } from "lucide-react";
|
||||
|
||||
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
||||
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
|
|
@ -34,6 +34,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
onRenameTask,
|
||||
onRenameBranch,
|
||||
onReorderProjects,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
projects: ProjectSection[];
|
||||
newTaskRepos: Array<{ id: string; label: string }>;
|
||||
|
|
@ -46,6 +47,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
onRenameTask: (id: string) => void;
|
||||
onRenameBranch: (id: string) => void;
|
||||
onReorderProjects: (fromIndex: number, toIndex: number) => void;
|
||||
onToggleSidebar?: () => void;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const contextMenu = useContextMenu();
|
||||
|
|
@ -63,6 +65,45 @@ export const Sidebar = memo(function Sidebar({
|
|||
display: none !important;
|
||||
}
|
||||
`}</style>
|
||||
{import.meta.env.VITE_DESKTOP ? (
|
||||
<div
|
||||
className={css({
|
||||
height: "38px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
paddingRight: "10px",
|
||||
flexShrink: 0,
|
||||
position: "relative",
|
||||
zIndex: 9999,
|
||||
})}
|
||||
>
|
||||
{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)" },
|
||||
})}
|
||||
>
|
||||
<PanelLeft size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
|
||||
<LabelSmall
|
||||
color={theme.colors.contentPrimary}
|
||||
|
|
@ -71,6 +112,30 @@ export const Sidebar = memo(function Sidebar({
|
|||
<ListChecks size={14} />
|
||||
Tasks
|
||||
</LabelSmall>
|
||||
{!import.meta.env.VITE_DESKTOP && 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)" },
|
||||
})}
|
||||
>
|
||||
<PanelLeft size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,8 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<MailOpen size={12} style={{ flexShrink: 0 }} /> {activeTab.unread ? "Mark read" : "Mark unread"}
|
||||
<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}
|
||||
</PanelHeaderBar>
|
||||
|
|
|
|||
|
|
@ -205,4 +205,6 @@ export const PanelHeaderBar = styled("div", ({ $theme }) => ({
|
|||
backgroundColor: $theme.colors.backgroundTertiary,
|
||||
gap: "8px",
|
||||
flexShrink: 0,
|
||||
position: "relative" as const,
|
||||
zIndex: 9999,
|
||||
}));
|
||||
|
|
|
|||