Add Foundry mobile layout with Tauri iOS/Android support
- Add responsive mobile layout with bottom pill tab bar, swipe navigation, and task list as home screen - Add platform detection (useIsMobile hook) with viewport breakpoint and VITE_MOBILE build flag - Mobile-optimize settings/billing/account pages (single-column layout with horizontal tabs) - Add iOS safe area inset handling with 47px minimum padding - Scaffold Tauri v2 mobile targets (iOS/Android) with platform-gated sidecar and capabilities - Add notification sound support and mobile build script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
@ -245,6 +245,7 @@ async function buildAppSnapshot(c: any, sessionId: string): Promise<FoundryAppSn
|
||||||
name: session.currentUserName ?? session.currentUserGithubLogin ?? "GitHub user",
|
name: session.currentUserName ?? session.currentUserGithubLogin ?? "GitHub user",
|
||||||
email: session.currentUserEmail ?? "",
|
email: session.currentUserEmail ?? "",
|
||||||
githubLogin: session.currentUserGithubLogin ?? "",
|
githubLogin: session.currentUserGithubLogin ?? "",
|
||||||
|
avatarUrl: session.currentUserGithubLogin ? `https://github.com/${session.currentUserGithubLogin}.png` : null,
|
||||||
roleLabel: session.currentUserRoleLabel ?? "GitHub user",
|
roleLabel: session.currentUserRoleLabel ?? "GitHub user",
|
||||||
eligibleOrganizationIds: organizations.map((organization) => organization.id),
|
eligibleOrganizationIds: organizations.map((organization) => organization.id),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface MockFoundryUser {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
eligibleOrganizationIds: string[];
|
eligibleOrganizationIds: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +23,8 @@ export interface MockFoundryOrganizationMember {
|
||||||
email: string;
|
email: string;
|
||||||
role: "owner" | "admin" | "member";
|
role: "owner" | "admin" | "member";
|
||||||
state: "active" | "invited";
|
state: "active" | "invited";
|
||||||
|
avatarUrl: string | null;
|
||||||
|
githubLogin: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockFoundryInvoice {
|
export interface MockFoundryInvoice {
|
||||||
|
|
@ -162,6 +165,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
name: "Nathan",
|
name: "Nathan",
|
||||||
email: "nathan@acme.dev",
|
email: "nathan@acme.dev",
|
||||||
githubLogin: "nathan",
|
githubLogin: "nathan",
|
||||||
|
avatarUrl: "https://github.com/NathanFlurry.png",
|
||||||
roleLabel: "Founder",
|
roleLabel: "Founder",
|
||||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||||
},
|
},
|
||||||
|
|
@ -170,6 +174,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
name: "Maya",
|
name: "Maya",
|
||||||
email: "maya@acme.dev",
|
email: "maya@acme.dev",
|
||||||
githubLogin: "maya",
|
githubLogin: "maya",
|
||||||
|
avatarUrl: "https://github.com/octocat.png",
|
||||||
roleLabel: "Staff Engineer",
|
roleLabel: "Staff Engineer",
|
||||||
eligibleOrganizationIds: ["acme"],
|
eligibleOrganizationIds: ["acme"],
|
||||||
},
|
},
|
||||||
|
|
@ -178,6 +183,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
name: "Jamie",
|
name: "Jamie",
|
||||||
email: "jamie@rivet.dev",
|
email: "jamie@rivet.dev",
|
||||||
githubLogin: "jamie",
|
githubLogin: "jamie",
|
||||||
|
avatarUrl: "https://github.com/defunkt.png",
|
||||||
roleLabel: "Platform Lead",
|
roleLabel: "Platform Lead",
|
||||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||||
},
|
},
|
||||||
|
|
@ -213,7 +219,17 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
paymentMethodLabel: "No card required",
|
paymentMethodLabel: "No card required",
|
||||||
invoices: [],
|
invoices: [],
|
||||||
},
|
},
|
||||||
members: [{ id: "member-nathan", name: "Nathan", email: "nathan@acme.dev", role: "owner", state: "active" }],
|
members: [
|
||||||
|
{
|
||||||
|
id: "member-nathan",
|
||||||
|
name: "Nathan",
|
||||||
|
email: "nathan@acme.dev",
|
||||||
|
role: "owner",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/NathanFlurry.png",
|
||||||
|
githubLogin: "NathanFlurry",
|
||||||
|
},
|
||||||
|
],
|
||||||
seatAssignments: ["nathan@acme.dev"],
|
seatAssignments: ["nathan@acme.dev"],
|
||||||
repoCatalog: ["nathan/personal-site"],
|
repoCatalog: ["nathan/personal-site"],
|
||||||
},
|
},
|
||||||
|
|
@ -251,10 +267,34 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
members: [
|
members: [
|
||||||
{ id: "member-acme-nathan", name: "Nathan", email: "nathan@acme.dev", role: "owner", state: "active" },
|
{
|
||||||
{ id: "member-acme-maya", name: "Maya", email: "maya@acme.dev", role: "admin", state: "active" },
|
id: "member-acme-nathan",
|
||||||
{ id: "member-acme-priya", name: "Priya", email: "priya@acme.dev", role: "member", state: "active" },
|
name: "Nathan",
|
||||||
{ id: "member-acme-devon", name: "Devon", email: "devon@acme.dev", role: "member", state: "invited" },
|
email: "nathan@acme.dev",
|
||||||
|
role: "owner",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/NathanFlurry.png",
|
||||||
|
githubLogin: "NathanFlurry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "member-acme-maya",
|
||||||
|
name: "Maya",
|
||||||
|
email: "maya@acme.dev",
|
||||||
|
role: "admin",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/octocat.png",
|
||||||
|
githubLogin: "octocat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "member-acme-priya",
|
||||||
|
name: "Priya",
|
||||||
|
email: "priya@acme.dev",
|
||||||
|
role: "member",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/mona.png",
|
||||||
|
githubLogin: "mona",
|
||||||
|
},
|
||||||
|
{ id: "member-acme-devon", name: "Devon", email: "devon@acme.dev", role: "member", state: "invited", avatarUrl: null, githubLogin: null },
|
||||||
],
|
],
|
||||||
seatAssignments: ["nathan@acme.dev", "maya@acme.dev"],
|
seatAssignments: ["nathan@acme.dev", "maya@acme.dev"],
|
||||||
repoCatalog: ["acme/backend", "acme/frontend", "acme/infra"],
|
repoCatalog: ["acme/backend", "acme/frontend", "acme/infra"],
|
||||||
|
|
@ -290,9 +330,33 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
invoices: [{ id: "inv-rivet-001", label: "Team pilot", issuedAt: "2026-03-04", amountUsd: 0, status: "paid" }],
|
invoices: [{ id: "inv-rivet-001", label: "Team pilot", issuedAt: "2026-03-04", amountUsd: 0, status: "paid" }],
|
||||||
},
|
},
|
||||||
members: [
|
members: [
|
||||||
{ id: "member-rivet-jamie", name: "Jamie", email: "jamie@rivet.dev", role: "owner", state: "active" },
|
{
|
||||||
{ id: "member-rivet-nathan", name: "Nathan", email: "nathan@acme.dev", role: "member", state: "active" },
|
id: "member-rivet-jamie",
|
||||||
{ id: "member-rivet-lena", name: "Lena", email: "lena@rivet.dev", role: "admin", state: "active" },
|
name: "Jamie",
|
||||||
|
email: "jamie@rivet.dev",
|
||||||
|
role: "owner",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/defunkt.png",
|
||||||
|
githubLogin: "defunkt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "member-rivet-nathan",
|
||||||
|
name: "Nathan",
|
||||||
|
email: "nathan@acme.dev",
|
||||||
|
role: "member",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/NathanFlurry.png",
|
||||||
|
githubLogin: "NathanFlurry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "member-rivet-lena",
|
||||||
|
name: "Lena",
|
||||||
|
email: "lena@rivet.dev",
|
||||||
|
role: "admin",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/mojombo.png",
|
||||||
|
githubLogin: "mojombo",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
seatAssignments: ["jamie@rivet.dev"],
|
seatAssignments: ["jamie@rivet.dev"],
|
||||||
repoCatalog: ["rivet/dashboard", "rivet/agents", "rivet/billing", "rivet/infrastructure"],
|
repoCatalog: ["rivet/dashboard", "rivet/agents", "rivet/billing", "rivet/infrastructure"],
|
||||||
|
|
@ -327,7 +391,17 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||||
paymentMethodLabel: "No card required",
|
paymentMethodLabel: "No card required",
|
||||||
invoices: [],
|
invoices: [],
|
||||||
},
|
},
|
||||||
members: [{ id: "member-jamie", name: "Jamie", email: "jamie@rivet.dev", role: "owner", state: "active" }],
|
members: [
|
||||||
|
{
|
||||||
|
id: "member-jamie",
|
||||||
|
name: "Jamie",
|
||||||
|
email: "jamie@rivet.dev",
|
||||||
|
role: "owner",
|
||||||
|
state: "active",
|
||||||
|
avatarUrl: "https://github.com/defunkt.png",
|
||||||
|
githubLogin: "defunkt",
|
||||||
|
},
|
||||||
|
],
|
||||||
seatAssignments: ["jamie@rivet.dev"],
|
seatAssignments: ["jamie@rivet.dev"],
|
||||||
repoCatalog: ["jamie/demo-app"],
|
repoCatalog: ["jamie/demo-app"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ class MockWorkbenchStore implements TaskWorkbenchClient {
|
||||||
diffs: {},
|
diffs: {},
|
||||||
fileTree: [],
|
fileTree: [],
|
||||||
minutesUsed: 0,
|
minutesUsed: 0,
|
||||||
|
presence: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateState((current) => ({
|
this.updateState((current) => ({
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,10 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 42,
|
minutesUsed: 42,
|
||||||
|
presence: [
|
||||||
|
{ memberId: "member-acme-nathan", name: "Nathan", avatarUrl: "https://github.com/NathanFlurry.png", lastSeenAtMs: minutesAgo(1) },
|
||||||
|
{ memberId: "member-acme-maya", name: "Maya", avatarUrl: "https://github.com/octocat.png", lastSeenAtMs: minutesAgo(0), typing: true },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "h2",
|
id: "h2",
|
||||||
|
|
@ -535,6 +539,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 187,
|
minutesUsed: 187,
|
||||||
|
presence: [{ memberId: "member-acme-priya", name: "Priya", avatarUrl: "https://github.com/mona.png", lastSeenAtMs: minutesAgo(0) }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "h3",
|
id: "h3",
|
||||||
|
|
@ -609,6 +614,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 23,
|
minutesUsed: 23,
|
||||||
|
presence: [],
|
||||||
},
|
},
|
||||||
// ── rivet-dev/rivet ──
|
// ── rivet-dev/rivet ──
|
||||||
{
|
{
|
||||||
|
|
@ -744,6 +750,11 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 5,
|
minutesUsed: 5,
|
||||||
|
presence: [
|
||||||
|
{ memberId: "member-acme-nathan", name: "Nathan", avatarUrl: "https://github.com/NathanFlurry.png", lastSeenAtMs: minutesAgo(0) },
|
||||||
|
{ memberId: "member-acme-maya", name: "Maya", avatarUrl: "https://github.com/octocat.png", lastSeenAtMs: minutesAgo(2) },
|
||||||
|
{ memberId: "member-acme-priya", name: "Priya", avatarUrl: "https://github.com/mona.png", lastSeenAtMs: minutesAgo(5) },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "h5",
|
id: "h5",
|
||||||
|
|
@ -800,6 +811,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
diffs: {},
|
diffs: {},
|
||||||
fileTree: [],
|
fileTree: [],
|
||||||
minutesUsed: 312,
|
minutesUsed: 312,
|
||||||
|
presence: [{ memberId: "member-acme-maya", name: "Maya", avatarUrl: "https://github.com/octocat.png", lastSeenAtMs: minutesAgo(45) }],
|
||||||
},
|
},
|
||||||
// ── rivet-dev/cloud ──
|
// ── rivet-dev/cloud ──
|
||||||
{
|
{
|
||||||
|
|
@ -909,6 +921,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 0,
|
minutesUsed: 0,
|
||||||
|
presence: [],
|
||||||
},
|
},
|
||||||
// ── rivet-dev/engine-ee ──
|
// ── rivet-dev/engine-ee ──
|
||||||
{
|
{
|
||||||
|
|
@ -1023,6 +1036,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minutesUsed: 78,
|
minutesUsed: 78,
|
||||||
|
presence: [],
|
||||||
},
|
},
|
||||||
// ── rivet-dev/engine-ee (archived) ──
|
// ── rivet-dev/engine-ee (archived) ──
|
||||||
{
|
{
|
||||||
|
|
@ -1065,6 +1079,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
diffs: {},
|
diffs: {},
|
||||||
fileTree: [],
|
fileTree: [],
|
||||||
minutesUsed: 15,
|
minutesUsed: 15,
|
||||||
|
presence: [],
|
||||||
},
|
},
|
||||||
// ── rivet-dev/secure-exec ──
|
// ── rivet-dev/secure-exec ──
|
||||||
{
|
{
|
||||||
|
|
@ -1118,6 +1133,7 @@ export function buildInitialTasks(): Task[] {
|
||||||
diffs: {},
|
diffs: {},
|
||||||
fileTree: [],
|
fileTree: [],
|
||||||
minutesUsed: 3,
|
minutesUsed: 3,
|
||||||
|
presence: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
"dev:ios": "VITE_MOBILE=1 tauri ios dev",
|
||||||
|
"dev:android": "VITE_MOBILE=1 tauri android dev",
|
||||||
"build": "tauri build",
|
"build": "tauri build",
|
||||||
|
"build:ios": "tauri ios build",
|
||||||
|
"build:android": "tauri android build",
|
||||||
"build:sidecar": "tsx scripts/build-sidecar.ts",
|
"build:sidecar": "tsx scripts/build-sidecar.ts",
|
||||||
"build:frontend": "tsx scripts/build-frontend.ts",
|
"build:frontend": "tsx scripts/build-frontend.ts",
|
||||||
|
"build:frontend:mobile": "tsx scripts/build-frontend-mobile.ts",
|
||||||
"build:all": "pnpm build:sidecar && pnpm build:frontend && pnpm build",
|
"build:all": "pnpm build:sidecar && pnpm build:frontend && pnpm build",
|
||||||
|
"build:all:ios": "pnpm build:frontend:mobile && pnpm build:ios",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
42
foundry/packages/desktop/scripts/build-frontend-mobile.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 for mobile (no hardcoded backend endpoint)
|
||||||
|
console.log("\n=== Building frontend for mobile ===\n");
|
||||||
|
run("pnpm --filter @sandbox-agent/foundry-frontend build", {
|
||||||
|
env: {
|
||||||
|
VITE_MOBILE: "1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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=== Mobile frontend build complete ===\n");
|
||||||
|
|
@ -3,13 +3,20 @@ name = "foundry"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["staticlib", "cdylib", "lib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { version = "1", features = ["time"] }
|
||||||
|
|
||||||
|
# Shell plugin is desktop-only (used for sidecar spawning).
|
||||||
|
# Exclude iOS and Android targets.
|
||||||
|
[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies]
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capability for Foundry desktop",
|
"description": "Default capability for Foundry desktop",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
|
"platforms": ["macOS", "windows", "linux"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"identifier": "mobile",
|
||||||
|
"description": "Capability for Foundry mobile (iOS/Android)",
|
||||||
|
"windows": ["main"],
|
||||||
|
"platforms": ["iOS", "android"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
3
foundry/packages/desktop/src-tauri/gen/apple/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
xcuserdata/
|
||||||
|
build/
|
||||||
|
Externals/
|
||||||
|
After Width: | Height: | Size: 1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,116 @@
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-20x20@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-20x20@3x.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-29x29@2x-1.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-29x29@3x.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "40x40",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-40x40@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "40x40",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-40x40@3x.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "60x60",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-60x60@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "60x60",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "AppIcon-60x60@3x.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-20x20@1x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-20x20@2x-1.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-29x29@1x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-29x29@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "40x40",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-40x40@1x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "40x40",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-40x40@2x-1.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "76x76",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-76x76@1x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "76x76",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-76x76@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "83.5x83.5",
|
||||||
|
"idiom": "ipad",
|
||||||
|
"filename": "AppIcon-83.5x83.5@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "1024x1024",
|
||||||
|
"idiom": "ios-marketing",
|
||||||
|
"filename": "AppIcon-512@2x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>debugging</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17122"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="s0d-6b-0kx">
|
||||||
|
<objects>
|
||||||
|
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
21
foundry/packages/desktop/src-tauri/gen/apple/Podfile
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Uncomment the next line to define a global platform for your project
|
||||||
|
|
||||||
|
target 'foundry_iOS' do
|
||||||
|
platform :ios, '14.0'
|
||||||
|
# Pods for foundry_iOS
|
||||||
|
end
|
||||||
|
|
||||||
|
target 'foundry_macOS' do
|
||||||
|
platform :osx, '11.0'
|
||||||
|
# Pods for foundry_macOS
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
|
||||||
|
config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ffi {
|
||||||
|
extern "C" {
|
||||||
|
void start_app();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include "bindings/bindings.h"
|
||||||
|
|
||||||
|
int main(int argc, char * argv[]) {
|
||||||
|
ffi::start_app();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 77;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
18AA2DEF7E37375920E5C2A0 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 5839511CE1969F167776E5BA /* assets */; };
|
||||||
|
41F61AA6A6395C0E1A22A01D /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C796B246B7EFC59BE22BEAE /* Metal.framework */; };
|
||||||
|
46451E98CD8CC918B270DF4E /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CA3839E082C9EE1FBE47017 /* CoreGraphics.framework */; };
|
||||||
|
5823D1D3CF428AB8675028CA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 916B38007890BE6F5229D88A /* QuartzCore.framework */; };
|
||||||
|
63828790DD4F121B20E1314C /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 61706E406133F9FEA10A02CF /* libapp.a */; };
|
||||||
|
6DF20201B9860B37359D8373 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0AF0861142E98564AE14D /* WebKit.framework */; };
|
||||||
|
84F2A1451B638E04DC7D9C77 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FFD378E56937244899E32B9 /* UIKit.framework */; };
|
||||||
|
A391FE2BDE082320542B786E /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9503D8DA5A521E10DE980308 /* MetalKit.framework */; };
|
||||||
|
A9D64B11D3591BA69A9FF24D /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 41768F9AEEC30C6F3A657349 /* main.mm */; };
|
||||||
|
C2EC09AB9A1F311F389731DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 73239B7F777F17978C5F9FB3 /* LaunchScreen.storyboard */; };
|
||||||
|
E05514D9EF3C91D648336FAF /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BE59BDC6646E6E88C37132A /* Security.framework */; };
|
||||||
|
EF8153BCCF98E828B21D22B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C2D23C60F5F67FF1B8EF9BE /* Assets.xcassets */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
0FFD378E56937244899E32B9 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||||
|
1BE59BDC6646E6E88C37132A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||||
|
2C2D23C60F5F67FF1B8EF9BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
41768F9AEEC30C6F3A657349 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
|
||||||
|
46675940F25D441B14F4D9BD /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
|
||||||
|
4BF135A484B3FD794B6BE633 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = "<group>"; };
|
||||||
|
4C796B246B7EFC59BE22BEAE /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
|
||||||
|
4CA3839E082C9EE1FBE47017 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||||
|
5839511CE1969F167776E5BA /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; };
|
||||||
|
61706E406133F9FEA10A02CF /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = "<group>"; };
|
||||||
|
6C52C06D919E696C88F980CA /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = "<group>"; };
|
||||||
|
73239B7F777F17978C5F9FB3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
7A2D8F7E3AD2D4401F4C016B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
7CD23EC29DD8E675CF62425F /* foundry_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = foundry_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
7FB0AF0861142E98564AE14D /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||||
|
916B38007890BE6F5229D88A /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||||
|
9503D8DA5A521E10DE980308 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
|
||||||
|
E33D510DA88C9082AA568044 /* foundry_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = foundry_iOS.entitlements; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
2A55D5F956F7DFC89A1EF6A3 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
63828790DD4F121B20E1314C /* libapp.a in Frameworks */,
|
||||||
|
46451E98CD8CC918B270DF4E /* CoreGraphics.framework in Frameworks */,
|
||||||
|
41F61AA6A6395C0E1A22A01D /* Metal.framework in Frameworks */,
|
||||||
|
A391FE2BDE082320542B786E /* MetalKit.framework in Frameworks */,
|
||||||
|
5823D1D3CF428AB8675028CA /* QuartzCore.framework in Frameworks */,
|
||||||
|
E05514D9EF3C91D648336FAF /* Security.framework in Frameworks */,
|
||||||
|
84F2A1451B638E04DC7D9C77 /* UIKit.framework in Frameworks */,
|
||||||
|
6DF20201B9860B37359D8373 /* WebKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
1C4A0974016D1DFD8F8C4DDB /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4CA3839E082C9EE1FBE47017 /* CoreGraphics.framework */,
|
||||||
|
61706E406133F9FEA10A02CF /* libapp.a */,
|
||||||
|
4C796B246B7EFC59BE22BEAE /* Metal.framework */,
|
||||||
|
9503D8DA5A521E10DE980308 /* MetalKit.framework */,
|
||||||
|
916B38007890BE6F5229D88A /* QuartzCore.framework */,
|
||||||
|
1BE59BDC6646E6E88C37132A /* Security.framework */,
|
||||||
|
0FFD378E56937244899E32B9 /* UIKit.framework */,
|
||||||
|
7FB0AF0861142E98564AE14D /* WebKit.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
2FD745FA11757414C89CF97B /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7CD23EC29DD8E675CF62425F /* foundry_iOS.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3BB81CF687DD217A69E52F28 /* src */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
6C52C06D919E696C88F980CA /* lib.rs */,
|
||||||
|
4BF135A484B3FD794B6BE633 /* main.rs */,
|
||||||
|
);
|
||||||
|
name = src;
|
||||||
|
path = ../../src;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
57E35B817B051AAFCDF3B583 /* bindings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
46675940F25D441B14F4D9BD /* bindings.h */,
|
||||||
|
);
|
||||||
|
path = bindings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8A9C64E5F1382764F31A9C3E = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5839511CE1969F167776E5BA /* assets */,
|
||||||
|
2C2D23C60F5F67FF1B8EF9BE /* Assets.xcassets */,
|
||||||
|
73239B7F777F17978C5F9FB3 /* LaunchScreen.storyboard */,
|
||||||
|
D848E3356050EA6A0C214268 /* Externals */,
|
||||||
|
A8C8B8DF2C7F39FBBD9C49F8 /* foundry_iOS */,
|
||||||
|
F7BF3354D37B8C8E0475FD89 /* Sources */,
|
||||||
|
3BB81CF687DD217A69E52F28 /* src */,
|
||||||
|
1C4A0974016D1DFD8F8C4DDB /* Frameworks */,
|
||||||
|
2FD745FA11757414C89CF97B /* Products */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8C8B8DF2C7F39FBBD9C49F8 /* foundry_iOS */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E33D510DA88C9082AA568044 /* foundry_iOS.entitlements */,
|
||||||
|
7A2D8F7E3AD2D4401F4C016B /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = foundry_iOS;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D848E3356050EA6A0C214268 /* Externals */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = Externals;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
F5B02224172A9BB7345E5123 /* foundry */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
41768F9AEEC30C6F3A657349 /* main.mm */,
|
||||||
|
57E35B817B051AAFCDF3B583 /* bindings */,
|
||||||
|
);
|
||||||
|
path = foundry;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
F7BF3354D37B8C8E0475FD89 /* Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F5B02224172A9BB7345E5123 /* foundry */,
|
||||||
|
);
|
||||||
|
path = Sources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
B133A72EA416CD7D39FE3E92 /* foundry_iOS */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = D445EACB90281484F8FFC686 /* Build configuration list for PBXNativeTarget "foundry_iOS" */;
|
||||||
|
buildPhases = (
|
||||||
|
FB6BE1E1013DC03BA292046D /* Build Rust Code */,
|
||||||
|
5D12D268150FB53F48EDE74C /* Sources */,
|
||||||
|
0391435C164ED2F1E2300246 /* Resources */,
|
||||||
|
2A55D5F956F7DFC89A1EF6A3 /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = foundry_iOS;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = foundry_iOS;
|
||||||
|
productReference = 7CD23EC29DD8E675CF62425F /* foundry_iOS.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
376A77EF2CEF8254085FAA46 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1430;
|
||||||
|
TargetAttributes = {
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 98E4F3D9D125FB8973826CB2 /* Build configuration list for PBXProject "foundry" */;
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
Base,
|
||||||
|
en,
|
||||||
|
);
|
||||||
|
mainGroup = 8A9C64E5F1382764F31A9C3E;
|
||||||
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
preferredProjectObjectVersion = 77;
|
||||||
|
productRefGroup = 2FD745FA11757414C89CF97B /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
B133A72EA416CD7D39FE3E92 /* foundry_iOS */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
0391435C164ED2F1E2300246 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
EF8153BCCF98E828B21D22B8 /* Assets.xcassets in Resources */,
|
||||||
|
C2EC09AB9A1F311F389731DA /* LaunchScreen.storyboard in Resources */,
|
||||||
|
18AA2DEF7E37375920E5C2A0 /* assets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
FB6BE1E1013DC03BA292046D /* Build Rust Code */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Build Rust Code";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a",
|
||||||
|
"$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
5D12D268150FB53F48EDE74C /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A9D64B11D3591BA69A9FF24D /* main.mm in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
2B285D0ECCDEA51DB6635985 /* debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"DEBUG=1",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = debug;
|
||||||
|
};
|
||||||
|
362B62575264ECB0984A8FC7 /* release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
|
ARCHS = (
|
||||||
|
arm64,
|
||||||
|
);
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = foundry_iOS/foundry_iOS.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"\".\"",
|
||||||
|
);
|
||||||
|
INFOPLIST_FILE = foundry_iOS/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
|
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.sandboxagent.foundry;
|
||||||
|
PRODUCT_NAME = "Foundry";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALID_ARCHS = arm64;
|
||||||
|
};
|
||||||
|
name = release;
|
||||||
|
};
|
||||||
|
6503634097DB1CB8102F723F /* debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
|
ARCHS = (
|
||||||
|
arm64,
|
||||||
|
);
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = foundry_iOS/foundry_iOS.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"\".\"",
|
||||||
|
);
|
||||||
|
INFOPLIST_FILE = foundry_iOS/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
|
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.sandboxagent.foundry;
|
||||||
|
PRODUCT_NAME = "Foundry";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALID_ARCHS = arm64;
|
||||||
|
};
|
||||||
|
name = debug;
|
||||||
|
};
|
||||||
|
7DD9B8183C37530AE219A355 /* release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
98E4F3D9D125FB8973826CB2 /* Build configuration list for PBXProject "foundry" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
2B285D0ECCDEA51DB6635985 /* debug */,
|
||||||
|
7DD9B8183C37530AE219A355 /* release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = debug;
|
||||||
|
};
|
||||||
|
D445EACB90281484F8FFC686 /* Build configuration list for PBXNativeTarget "foundry_iOS" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
6503634097DB1CB8102F723F /* debug */,
|
||||||
|
362B62575264ECB0984A8FC7 /* release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = debug;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 376A77EF2CEF8254085FAA46 /* Project object */;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildSystemType</key>
|
||||||
|
<string>Original</string>
|
||||||
|
<key>DisableBuildSystemDeprecationDiagnostic</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1430"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B133A72EA416CD7D39FE3E92"
|
||||||
|
BuildableName = "Foundry.app"
|
||||||
|
BlueprintName = "foundry_iOS"
|
||||||
|
ReferencedContainer = "container:foundry.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B133A72EA416CD7D39FE3E92"
|
||||||
|
BuildableName = "Foundry.app"
|
||||||
|
BlueprintName = "foundry_iOS"
|
||||||
|
ReferencedContainer = "container:foundry.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_BACKTRACE"
|
||||||
|
value = "full"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_LOG"
|
||||||
|
value = "info"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B133A72EA416CD7D39FE3E92"
|
||||||
|
BuildableName = "Foundry.app"
|
||||||
|
BlueprintName = "foundry_iOS"
|
||||||
|
ReferencedContainer = "container:foundry.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_BACKTRACE"
|
||||||
|
value = "full"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_LOG"
|
||||||
|
value = "info"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B133A72EA416CD7D39FE3E92"
|
||||||
|
BuildableName = "Foundry.app"
|
||||||
|
BlueprintName = "foundry_iOS"
|
||||||
|
ReferencedContainer = "container:foundry.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_BACKTRACE"
|
||||||
|
value = "full"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "RUST_LOG"
|
||||||
|
value = "info"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
<string>metal</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
||||||
88
foundry/packages/desktop/src-tauri/gen/apple/project.yml
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
name: foundry
|
||||||
|
options:
|
||||||
|
bundleIdPrefix: dev.sandboxagent.foundry
|
||||||
|
deploymentTarget:
|
||||||
|
iOS: 14.0
|
||||||
|
fileGroups: [../../src]
|
||||||
|
configs:
|
||||||
|
debug: debug
|
||||||
|
release: release
|
||||||
|
settingGroups:
|
||||||
|
app:
|
||||||
|
base:
|
||||||
|
PRODUCT_NAME: Foundry
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: dev.sandboxagent.foundry
|
||||||
|
targetTemplates:
|
||||||
|
app:
|
||||||
|
type: application
|
||||||
|
sources:
|
||||||
|
- path: Sources
|
||||||
|
scheme:
|
||||||
|
environmentVariables:
|
||||||
|
RUST_BACKTRACE: full
|
||||||
|
RUST_LOG: info
|
||||||
|
settings:
|
||||||
|
groups: [app]
|
||||||
|
targets:
|
||||||
|
foundry_iOS:
|
||||||
|
type: application
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- path: Sources
|
||||||
|
- path: Assets.xcassets
|
||||||
|
- path: Externals
|
||||||
|
- path: foundry_iOS
|
||||||
|
- path: assets
|
||||||
|
buildPhase: resources
|
||||||
|
type: folder
|
||||||
|
- path: LaunchScreen.storyboard
|
||||||
|
info:
|
||||||
|
path: foundry_iOS/Info.plist
|
||||||
|
properties:
|
||||||
|
LSRequiresIPhoneOS: true
|
||||||
|
UILaunchStoryboardName: LaunchScreen
|
||||||
|
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||||
|
UISupportedInterfaceOrientations:
|
||||||
|
- UIInterfaceOrientationPortrait
|
||||||
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
|
- UIInterfaceOrientationLandscapeRight
|
||||||
|
UISupportedInterfaceOrientations~ipad:
|
||||||
|
- UIInterfaceOrientationPortrait
|
||||||
|
- UIInterfaceOrientationPortraitUpsideDown
|
||||||
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
|
- UIInterfaceOrientationLandscapeRight
|
||||||
|
CFBundleShortVersionString: 0.1.0
|
||||||
|
CFBundleVersion: "0.1.0"
|
||||||
|
entitlements:
|
||||||
|
path: foundry_iOS/foundry_iOS.entitlements
|
||||||
|
scheme:
|
||||||
|
environmentVariables:
|
||||||
|
RUST_BACKTRACE: full
|
||||||
|
RUST_LOG: info
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
ENABLE_BITCODE: false
|
||||||
|
ARCHS: [arm64]
|
||||||
|
VALID_ARCHS: arm64
|
||||||
|
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||||
|
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||||
|
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
|
||||||
|
groups: [app]
|
||||||
|
dependencies:
|
||||||
|
- framework: libapp.a
|
||||||
|
embed: false
|
||||||
|
- sdk: CoreGraphics.framework
|
||||||
|
- sdk: Metal.framework
|
||||||
|
- sdk: MetalKit.framework
|
||||||
|
- sdk: QuartzCore.framework
|
||||||
|
- sdk: Security.framework
|
||||||
|
- sdk: UIKit.framework
|
||||||
|
- sdk: WebKit.framework
|
||||||
|
preBuildScripts:
|
||||||
|
- script: pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
||||||
|
name: Build Rust Code
|
||||||
|
basedOnDependencyAnalysis: false
|
||||||
|
outputFiles:
|
||||||
|
- $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a
|
||||||
|
- $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a
|
||||||
|
|
@ -1 +1,24 @@
|
||||||
{"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}]}]}}
|
{
|
||||||
|
"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 }] }
|
||||||
|
],
|
||||||
|
"platforms": ["macOS", "windows", "linux"]
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"identifier": "mobile",
|
||||||
|
"description": "Capability for Foundry mobile (iOS/Android)",
|
||||||
|
"local": true,
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"],
|
||||||
|
"platforms": ["iOS", "android"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
2216
foundry/packages/desktop/src-tauri/gen/schemas/iOS-schema.json
Normal file
2216
foundry/packages/desktop/src-tauri/gen/schemas/mobile-schema.json
Normal file
|
|
@ -1,28 +1,44 @@
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{AppHandle, LogicalPosition, Manager, WebviewUrl, WebviewWindowBuilder};
|
|
||||||
|
#[cfg(not(mobile))]
|
||||||
|
use tauri::{LogicalPosition, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
|
#[cfg(not(mobile))]
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
use tauri_plugin_shell::process::CommandChild;
|
||||||
|
#[cfg(not(mobile))]
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
struct BackendState {
|
struct BackendState {
|
||||||
|
#[cfg(not(mobile))]
|
||||||
child: Mutex<Option<CommandChild>>,
|
child: Mutex<Option<CommandChild>>,
|
||||||
|
backend_url: Mutex<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_backend_url() -> String {
|
fn get_backend_url(state: tauri::State<BackendState>) -> String {
|
||||||
"http://127.0.0.1:7741".to_string()
|
state.backend_url.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn backend_health() -> Result<bool, String> {
|
fn set_backend_url(url: String, state: tauri::State<BackendState>) {
|
||||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
*state.backend_url.lock().unwrap() = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn backend_health(state: tauri::State<'_, BackendState>) -> Result<bool, String> {
|
||||||
|
let base = state.backend_url.lock().unwrap().clone();
|
||||||
|
let url = format!("{}/api/rivet/metadata", base);
|
||||||
|
match reqwest::get(&url).await {
|
||||||
Ok(resp) => Ok(resp.status().is_success()),
|
Ok(resp) => Ok(resp.status().is_success()),
|
||||||
Err(_) => Ok(false),
|
Err(_) => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_backend(timeout_secs: u64) -> Result<(), String> {
|
#[cfg(not(mobile))]
|
||||||
|
async fn wait_for_backend(base_url: String, timeout_secs: u64) -> Result<(), String> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||||
|
let url = format!("{}/api/rivet/metadata", base_url);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if start.elapsed() > timeout {
|
if start.elapsed() > timeout {
|
||||||
|
|
@ -32,7 +48,7 @@ async fn wait_for_backend(timeout_secs: u64) -> Result<(), String> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
match reqwest::get(&url).await {
|
||||||
Ok(resp) if resp.status().is_success() => return Ok(()),
|
Ok(resp) if resp.status().is_success() => return Ok(()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +57,8 @@ async fn wait_for_backend(timeout_secs: u64) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_backend(app: &AppHandle) -> Result<(), String> {
|
#[cfg(not(mobile))]
|
||||||
|
fn spawn_backend(app: &tauri::AppHandle) -> Result<(), String> {
|
||||||
let sidecar = app
|
let sidecar = app
|
||||||
.shell()
|
.shell()
|
||||||
.sidecar("sidecars/foundry-backend")
|
.sidecar("sidecars/foundry-backend")
|
||||||
|
|
@ -88,65 +105,95 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> {
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
let builder = tauri::Builder::default();
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
|
// Shell plugin is desktop-only (used for sidecar spawning)
|
||||||
|
#[cfg(not(mobile))]
|
||||||
|
let builder = builder.plugin(tauri_plugin_shell::init());
|
||||||
|
|
||||||
|
builder
|
||||||
.manage(BackendState {
|
.manage(BackendState {
|
||||||
|
#[cfg(not(mobile))]
|
||||||
child: Mutex::new(None),
|
child: Mutex::new(None),
|
||||||
|
backend_url: Mutex::new("http://127.0.0.1:7741".to_string()),
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![get_backend_url, backend_health])
|
.invoke_handler(tauri::generate_handler![
|
||||||
.setup(|app| {
|
get_backend_url,
|
||||||
// Create main window programmatically so we can set traffic light position
|
set_backend_url,
|
||||||
let url = if cfg!(debug_assertions) {
|
backend_health
|
||||||
WebviewUrl::External("http://localhost:4173".parse().unwrap())
|
])
|
||||||
} else {
|
.setup(|_app| {
|
||||||
WebviewUrl::default()
|
#[cfg(not(mobile))]
|
||||||
};
|
let app = _app;
|
||||||
|
// On desktop, create window programmatically for traffic light position
|
||||||
let mut builder = WebviewWindowBuilder::new(app, "main", url)
|
#[cfg(not(mobile))]
|
||||||
.title("Foundry")
|
|
||||||
.inner_size(1280.0, 800.0)
|
|
||||||
.min_inner_size(900.0, 600.0)
|
|
||||||
.resizable(true)
|
|
||||||
.theme(Some(tauri::Theme::Dark))
|
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
||||||
.hidden_title(true);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
{
|
||||||
builder = builder.traffic_light_position(LogicalPosition::new(14.0, 14.0));
|
let url = if cfg!(debug_assertions) {
|
||||||
}
|
WebviewUrl::External("http://localhost:4173".parse().unwrap())
|
||||||
|
} else {
|
||||||
|
WebviewUrl::default()
|
||||||
|
};
|
||||||
|
|
||||||
builder.build()?;
|
let mut win_builder = WebviewWindowBuilder::new(app, "main", url)
|
||||||
|
.title("Foundry")
|
||||||
|
.inner_size(1280.0, 800.0)
|
||||||
|
.min_inner_size(900.0, 600.0)
|
||||||
|
.resizable(true)
|
||||||
|
.theme(Some(tauri::Theme::Dark))
|
||||||
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.hidden_title(true);
|
||||||
|
|
||||||
// In debug mode, assume the developer is running the backend externally
|
#[cfg(target_os = "macos")]
|
||||||
if cfg!(debug_assertions) {
|
{
|
||||||
eprintln!("[foundry-desktop] Dev mode: skipping sidecar spawn. Run the backend separately.");
|
win_builder =
|
||||||
return Ok(());
|
win_builder.traffic_light_position(LogicalPosition::new(14.0, 14.0));
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
win_builder.build()?;
|
||||||
Ok(()) => eprintln!("[foundry-desktop] Backend is ready."),
|
}
|
||||||
Err(e) => eprintln!("[foundry-desktop] {}", e),
|
|
||||||
|
// On mobile, Tauri creates the webview automatically — no window setup needed.
|
||||||
|
// The backend URL will be set by the frontend via set_backend_url.
|
||||||
|
|
||||||
|
// Sidecar spawning is desktop-only
|
||||||
|
#[cfg(not(mobile))]
|
||||||
|
{
|
||||||
|
// In debug mode, assume the developer is running the backend externally
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
eprintln!(
|
||||||
|
"[foundry] 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] Failed to start backend: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match wait_for_backend("http://127.0.0.1:7741".to_string(), 30).await {
|
||||||
|
Ok(()) => eprintln!("[foundry] Backend is ready."),
|
||||||
|
Err(e) => eprintln!("[foundry] {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let tauri::WindowEvent::Destroyed = event {
|
if let tauri::WindowEvent::Destroyed = event {
|
||||||
let state = window.state::<BackendState>();
|
#[cfg(not(mobile))]
|
||||||
let child = state.child.lock().unwrap().take();
|
{
|
||||||
if let Some(child) = child {
|
let state = window.state::<BackendState>();
|
||||||
let _ = child.kill();
|
let child = state.child.lock().unwrap().take();
|
||||||
eprintln!("[foundry-desktop] Backend sidecar killed.");
|
if let Some(child) = child {
|
||||||
|
let _ = child.kill();
|
||||||
|
eprintln!("[foundry] Backend sidecar killed.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let _ = window; // suppress unused warning on mobile
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
|
||||||
10
foundry/packages/desktop/src-tauri/tauri.android.conf.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "FOUNDRY_FRONTEND_CLIENT_MODE=mock VITE_MOBILE=1 pnpm --filter @sandbox-agent/foundry-frontend dev --host 0.0.0.0 --port 4173"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"externalBin": []
|
||||||
|
},
|
||||||
|
"plugins": {}
|
||||||
|
}
|
||||||
11
foundry/packages/desktop/src-tauri/tauri.ios.conf.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "FOUNDRY_FRONTEND_CLIENT_MODE=mock VITE_MOBILE=1 pnpm --filter @sandbox-agent/foundry-frontend dev --host 0.0.0.0 --port 4173",
|
||||||
|
"devUrl": "http://localhost:4173"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"externalBin": []
|
||||||
|
},
|
||||||
|
"plugins": {}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<script>if(window.__TAURI_INTERNALS__)document.documentElement.dataset.tauri="1"</script>
|
<script>if(window.__TAURI_INTERNALS__)document.documentElement.dataset.tauri="1"</script>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<title>Foundry</title>
|
<title>Foundry</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
BIN
foundry/packages/frontend/public/sounds/notification-1.mp3
Normal file
BIN
foundry/packages/frontend/public/sounds/notification-2.mp3
Normal file
|
|
@ -2,8 +2,10 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useStyletron } from "baseui";
|
import { useStyletron } from "baseui";
|
||||||
|
|
||||||
|
import type { WorkbenchPresence } from "@sandbox-agent/foundry-shared";
|
||||||
import { PanelLeft, PanelRight } from "lucide-react";
|
import { PanelLeft, PanelRight } from "lucide-react";
|
||||||
import { useFoundryTokens } from "../app/theme";
|
import { useFoundryTokens } from "../app/theme";
|
||||||
|
import { useAgentDoneNotification } from "../lib/notification-sound";
|
||||||
|
|
||||||
import { DiffContent } from "./mock-layout/diff-content";
|
import { DiffContent } from "./mock-layout/diff-content";
|
||||||
import { MessageList } from "./mock-layout/message-list";
|
import { MessageList } from "./mock-layout/message-list";
|
||||||
|
|
@ -11,15 +13,17 @@ import { PromptComposer } from "./mock-layout/prompt-composer";
|
||||||
import { RightSidebar } from "./mock-layout/right-sidebar";
|
import { RightSidebar } from "./mock-layout/right-sidebar";
|
||||||
import { Sidebar } from "./mock-layout/sidebar";
|
import { Sidebar } from "./mock-layout/sidebar";
|
||||||
import { TabStrip } from "./mock-layout/tab-strip";
|
import { TabStrip } from "./mock-layout/tab-strip";
|
||||||
import { TerminalPane } from "./mock-layout/terminal-pane";
|
import { TerminalPane, type ProcessTab } from "./mock-layout/terminal-pane";
|
||||||
import { TranscriptHeader } from "./mock-layout/transcript-header";
|
import { TranscriptHeader } from "./mock-layout/transcript-header";
|
||||||
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell } from "./mock-layout/ui";
|
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell, Tooltip } from "./mock-layout/ui";
|
||||||
import {
|
import {
|
||||||
buildDisplayMessages,
|
buildDisplayMessages,
|
||||||
diffPath,
|
diffPath,
|
||||||
diffTabId,
|
diffTabId,
|
||||||
formatThinkingDuration,
|
formatThinkingDuration,
|
||||||
isDiffTab,
|
isDiffTab,
|
||||||
|
isTerminalTab,
|
||||||
|
terminalTabId,
|
||||||
buildHistoryEvents,
|
buildHistoryEvents,
|
||||||
type Task,
|
type Task,
|
||||||
type HistoryEvent,
|
type HistoryEvent,
|
||||||
|
|
@ -27,8 +31,10 @@ import {
|
||||||
type Message,
|
type Message,
|
||||||
type ModelId,
|
type ModelId,
|
||||||
} from "./mock-layout/view-model";
|
} from "./mock-layout/view-model";
|
||||||
import { activeMockOrganization, useMockAppSnapshot } from "../lib/mock-app";
|
import { activeMockOrganization, activeMockUser, useMockAppSnapshot } from "../lib/mock-app";
|
||||||
|
import { useIsMobile } from "../lib/platform";
|
||||||
import { getTaskWorkbenchClient } from "../lib/workbench";
|
import { getTaskWorkbenchClient } from "../lib/workbench";
|
||||||
|
import { MobileLayout } from "./mock-layout/mobile-layout";
|
||||||
|
|
||||||
function firstAgentTabId(task: Task): string | null {
|
function firstAgentTabId(task: Task): string | null {
|
||||||
return task.tabs[0]?.id ?? null;
|
return task.tabs[0]?.id ?? null;
|
||||||
|
|
@ -58,12 +64,127 @@ function sanitizeActiveTabId(task: Task, tabId: string | null | undefined, openD
|
||||||
if (isDiffTab(tabId) && openDiffs.includes(diffPath(tabId))) {
|
if (isDiffTab(tabId) && openDiffs.includes(diffPath(tabId))) {
|
||||||
return tabId;
|
return tabId;
|
||||||
}
|
}
|
||||||
|
if (isTerminalTab(tabId)) {
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentTabId;
|
return openDiffs.length > 0 ? diffTabId(openDiffs[openDiffs.length - 1]!) : lastAgentTabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TypingIndicator({ presence, currentUserId }: { presence: WorkbenchPresence[]; currentUserId: string | null }) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const typingMembers = presence.filter((member) => member.typing && member.memberId !== currentUserId);
|
||||||
|
const isTyping = typingMembers.length > 0;
|
||||||
|
const [animState, setAnimState] = useState<"in" | "out" | "hidden">(isTyping ? "in" : "hidden");
|
||||||
|
const lastMembersRef = useRef(typingMembers);
|
||||||
|
|
||||||
|
if (isTyping) {
|
||||||
|
lastMembersRef.current = typingMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTyping) {
|
||||||
|
setAnimState("in");
|
||||||
|
} else if (lastMembersRef.current.length > 0) {
|
||||||
|
setAnimState("out");
|
||||||
|
}
|
||||||
|
}, [isTyping]);
|
||||||
|
|
||||||
|
if (animState === "hidden") return null;
|
||||||
|
|
||||||
|
const members = lastMembersRef.current;
|
||||||
|
if (members.length === 0) return null;
|
||||||
|
const label =
|
||||||
|
members.length === 1
|
||||||
|
? `${members[0]!.name} is typing`
|
||||||
|
: members.length === 2
|
||||||
|
? `${members[0]!.name} & ${members[1]!.name} are typing`
|
||||||
|
: `${members[0]!.name} & ${members.length - 1} others are typing`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
padding: "6px 20px",
|
||||||
|
flexShrink: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
animationName: animState === "in" ? "hf-typing-in" : "hf-typing-out",
|
||||||
|
animationDuration: "0.2s",
|
||||||
|
animationTimingFunction: "ease-out",
|
||||||
|
animationFillMode: "forwards",
|
||||||
|
})}
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
if (animState === "out") setAnimState("hidden");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={css({ display: "flex", alignItems: "center" })}>
|
||||||
|
{members.slice(0, 3).map((member) =>
|
||||||
|
member.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
key={member.memberId}
|
||||||
|
src={member.avatarUrl}
|
||||||
|
alt=""
|
||||||
|
className={css({
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: `1.5px solid ${t.surfacePrimary}`,
|
||||||
|
marginLeft: "-4px",
|
||||||
|
":first-child": { marginLeft: 0 },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={member.memberId}
|
||||||
|
className={css({
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: t.borderMedium,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "9px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.textSecondary,
|
||||||
|
border: `1.5px solid ${t.surfacePrimary}`,
|
||||||
|
marginLeft: "-4px",
|
||||||
|
":first-child": { marginLeft: 0 },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{member.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className={css({ fontSize: "12px", color: t.textTertiary })}>
|
||||||
|
{label}
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={css({
|
||||||
|
animationName: "hf-dot-fade",
|
||||||
|
animationDuration: "1.4s",
|
||||||
|
animationIterationCount: "infinite",
|
||||||
|
animationFillMode: "both",
|
||||||
|
animationDelay: `${i * 0.2}s`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const TranscriptPanel = memo(function TranscriptPanel({
|
const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
workspaceId,
|
||||||
taskWorkbenchClient,
|
taskWorkbenchClient,
|
||||||
task,
|
task,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
|
|
@ -80,7 +201,18 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
rightSidebarCollapsed,
|
rightSidebarCollapsed,
|
||||||
onToggleRightSidebar,
|
onToggleRightSidebar,
|
||||||
onNavigateToUsage,
|
onNavigateToUsage,
|
||||||
|
terminalTabOpen,
|
||||||
|
onOpenTerminalTab,
|
||||||
|
onCloseTerminalTab,
|
||||||
|
terminalProcessTabs,
|
||||||
|
onTerminalProcessTabsChange,
|
||||||
|
terminalActiveTabId,
|
||||||
|
onTerminalActiveTabIdChange,
|
||||||
|
terminalCustomNames,
|
||||||
|
onTerminalCustomNamesChange,
|
||||||
|
mobile,
|
||||||
}: {
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
||||||
task: Task;
|
task: Task;
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
|
|
@ -97,8 +229,20 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
rightSidebarCollapsed?: boolean;
|
rightSidebarCollapsed?: boolean;
|
||||||
onToggleRightSidebar?: () => void;
|
onToggleRightSidebar?: () => void;
|
||||||
onNavigateToUsage?: () => void;
|
onNavigateToUsage?: () => void;
|
||||||
|
terminalTabOpen?: boolean;
|
||||||
|
onOpenTerminalTab?: () => void;
|
||||||
|
onCloseTerminalTab?: () => void;
|
||||||
|
terminalProcessTabs?: ProcessTab[];
|
||||||
|
onTerminalProcessTabsChange?: (tabs: ProcessTab[]) => void;
|
||||||
|
terminalActiveTabId?: string | null;
|
||||||
|
onTerminalActiveTabIdChange?: (id: string | null) => void;
|
||||||
|
terminalCustomNames?: Record<string, string>;
|
||||||
|
onTerminalCustomNamesChange?: (names: Record<string, string>) => void;
|
||||||
|
mobile?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
const transcriptAppSnapshot = useMockAppSnapshot();
|
||||||
|
const currentUser = activeMockUser(transcriptAppSnapshot);
|
||||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
||||||
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
||||||
const [editValue, setEditValue] = useState("");
|
const [editValue, setEditValue] = useState("");
|
||||||
|
|
@ -111,9 +255,11 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const messageRefs = useRef(new Map<string, HTMLDivElement>());
|
const messageRefs = useRef(new Map<string, HTMLDivElement>());
|
||||||
const activeDiff = activeTabId && isDiffTab(activeTabId) ? diffPath(activeTabId) : null;
|
const activeDiff = activeTabId && isDiffTab(activeTabId) ? diffPath(activeTabId) : null;
|
||||||
const activeAgentTab = activeDiff ? null : (task.tabs.find((candidate) => candidate.id === activeTabId) ?? task.tabs[0] ?? null);
|
const activeTerminal = activeTabId && isTerminalTab(activeTabId) ? true : false;
|
||||||
|
const activeAgentTab = activeDiff || activeTerminal ? null : (task.tabs.find((candidate) => candidate.id === activeTabId) ?? task.tabs[0] ?? null);
|
||||||
const promptTab = task.tabs.find((candidate) => candidate.id === lastAgentTabId) ?? task.tabs[0] ?? null;
|
const promptTab = task.tabs.find((candidate) => candidate.id === lastAgentTabId) ?? task.tabs[0] ?? null;
|
||||||
const isTerminal = task.status === "archived";
|
const isTerminal = task.status === "archived";
|
||||||
|
useAgentDoneNotification(promptTab?.status);
|
||||||
const historyEvents = useMemo(() => buildHistoryEvents(task.tabs), [task.tabs]);
|
const historyEvents = useMemo(() => buildHistoryEvents(task.tabs), [task.tabs]);
|
||||||
const activeMessages = useMemo(() => buildDisplayMessages(activeAgentTab), [activeAgentTab]);
|
const activeMessages = useMemo(() => buildDisplayMessages(activeAgentTab), [activeAgentTab]);
|
||||||
const draft = promptTab?.draft.text ?? "";
|
const draft = promptTab?.draft.text ?? "";
|
||||||
|
|
@ -271,7 +417,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
(tabId: string) => {
|
(tabId: string) => {
|
||||||
onSetActiveTabId(tabId);
|
onSetActiveTabId(tabId);
|
||||||
|
|
||||||
if (!isDiffTab(tabId)) {
|
if (!isDiffTab(tabId) && !isTerminalTab(tabId)) {
|
||||||
onSetLastAgentTabId(tabId);
|
onSetLastAgentTabId(tabId);
|
||||||
const tab = task.tabs.find((candidate) => candidate.id === tabId);
|
const tab = task.tabs.find((candidate) => candidate.id === tabId);
|
||||||
if (tab?.unread) {
|
if (tab?.unread) {
|
||||||
|
|
@ -448,28 +594,30 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SPanel>
|
<SPanel>
|
||||||
<TranscriptHeader
|
{!mobile && (
|
||||||
task={task}
|
<TranscriptHeader
|
||||||
activeTab={activeAgentTab}
|
task={task}
|
||||||
editingField={editingField}
|
activeTab={activeAgentTab}
|
||||||
editValue={editValue}
|
editingField={editingField}
|
||||||
onEditValueChange={setEditValue}
|
editValue={editValue}
|
||||||
onStartEditingField={startEditingField}
|
onEditValueChange={setEditValue}
|
||||||
onCommitEditingField={commitEditingField}
|
onStartEditingField={startEditingField}
|
||||||
onCancelEditingField={cancelEditingField}
|
onCommitEditingField={commitEditingField}
|
||||||
onSetActiveTabUnread={(unread) => {
|
onCancelEditingField={cancelEditingField}
|
||||||
if (activeAgentTab) {
|
onSetActiveTabUnread={(unread) => {
|
||||||
setTabUnread(activeAgentTab.id, unread);
|
if (activeAgentTab) {
|
||||||
}
|
setTabUnread(activeAgentTab.id, unread);
|
||||||
}}
|
}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
}}
|
||||||
onToggleSidebar={onToggleSidebar}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
onSidebarPeekStart={onSidebarPeekStart}
|
onToggleSidebar={onToggleSidebar}
|
||||||
onSidebarPeekEnd={onSidebarPeekEnd}
|
onSidebarPeekStart={onSidebarPeekStart}
|
||||||
rightSidebarCollapsed={rightSidebarCollapsed}
|
onSidebarPeekEnd={onSidebarPeekEnd}
|
||||||
onToggleRightSidebar={onToggleRightSidebar}
|
rightSidebarCollapsed={rightSidebarCollapsed}
|
||||||
onNavigateToUsage={onNavigateToUsage}
|
onToggleRightSidebar={onToggleRightSidebar}
|
||||||
/>
|
onNavigateToUsage={onNavigateToUsage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -478,11 +626,15 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
backgroundColor: t.surfacePrimary,
|
backgroundColor: t.surfacePrimary,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderTopLeftRadius: "12px",
|
...(mobile
|
||||||
borderTopRightRadius: rightSidebarCollapsed ? "12px" : 0,
|
? {}
|
||||||
borderBottomLeftRadius: "24px",
|
: {
|
||||||
borderBottomRightRadius: rightSidebarCollapsed ? "24px" : 0,
|
borderTopLeftRadius: "12px",
|
||||||
border: `1px solid ${t.borderDefault}`,
|
borderTopRightRadius: rightSidebarCollapsed ? "12px" : 0,
|
||||||
|
borderBottomLeftRadius: "24px",
|
||||||
|
borderBottomRightRadius: rightSidebarCollapsed ? "24px" : 0,
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabStrip
|
<TabStrip
|
||||||
|
|
@ -500,9 +652,26 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onCloseTab={closeTab}
|
onCloseTab={closeTab}
|
||||||
onCloseDiffTab={closeDiffTab}
|
onCloseDiffTab={closeDiffTab}
|
||||||
onAddTab={addTab}
|
onAddTab={addTab}
|
||||||
|
terminalTabOpen={terminalTabOpen}
|
||||||
|
onCloseTerminalTab={onCloseTerminalTab}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
/>
|
/>
|
||||||
{activeDiff ? (
|
{activeTerminal ? (
|
||||||
|
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
|
||||||
|
<TerminalPane
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
taskId={task.id}
|
||||||
|
isExpanded
|
||||||
|
hideHeader
|
||||||
|
processTabs={terminalProcessTabs}
|
||||||
|
onProcessTabsChange={onTerminalProcessTabsChange}
|
||||||
|
activeProcessTabId={terminalActiveTabId}
|
||||||
|
onActiveProcessTabIdChange={onTerminalActiveTabIdChange}
|
||||||
|
customTabNames={terminalCustomNames}
|
||||||
|
onCustomTabNamesChange={onTerminalCustomNamesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : activeDiff ? (
|
||||||
<DiffContent
|
<DiffContent
|
||||||
filePath={activeDiff}
|
filePath={activeDiff}
|
||||||
file={task.fileChanges.find((file) => file.path === activeDiff)}
|
file={task.fileChanges.find((file) => file.path === activeDiff)}
|
||||||
|
|
@ -563,25 +732,30 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
void copyMessage(message);
|
void copyMessage(message);
|
||||||
}}
|
}}
|
||||||
thinkingTimerLabel={thinkingTimerLabel}
|
thinkingTimerLabel={thinkingTimerLabel}
|
||||||
|
userName={currentUser?.name ?? null}
|
||||||
|
userAvatarUrl={currentUser?.avatarUrl ?? null}
|
||||||
/>
|
/>
|
||||||
</ScrollBody>
|
</ScrollBody>
|
||||||
)}
|
)}
|
||||||
{!isTerminal && promptTab ? (
|
{!isTerminal && !activeTerminal && promptTab ? (
|
||||||
<PromptComposer
|
<>
|
||||||
draft={draft}
|
<TypingIndicator presence={task.presence} currentUserId={currentUser?.id ?? null} />
|
||||||
textareaRef={textareaRef}
|
<PromptComposer
|
||||||
placeholder={!promptTab.created ? "Describe your task..." : "Send a message..."}
|
draft={draft}
|
||||||
attachments={attachments}
|
textareaRef={textareaRef}
|
||||||
defaultModel={defaultModel}
|
placeholder={!promptTab.created ? "Describe your task..." : "Send a message..."}
|
||||||
model={promptTab.model}
|
attachments={attachments}
|
||||||
isRunning={promptTab.status === "running"}
|
defaultModel={defaultModel}
|
||||||
onDraftChange={(value) => updateDraft(value, attachments)}
|
model={promptTab.model}
|
||||||
onSend={sendMessage}
|
isRunning={promptTab.status === "running"}
|
||||||
onStop={stopAgent}
|
onDraftChange={(value) => updateDraft(value, attachments)}
|
||||||
onRemoveAttachment={removeAttachment}
|
onSend={sendMessage}
|
||||||
onChangeModel={changeModel}
|
onStop={stopAgent}
|
||||||
onSetDefaultModel={setDefaultModel}
|
onRemoveAttachment={removeAttachment}
|
||||||
/>
|
onChangeModel={changeModel}
|
||||||
|
onSetDefaultModel={setDefaultModel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</SPanel>
|
</SPanel>
|
||||||
|
|
@ -670,6 +844,14 @@ const RightRail = memo(function RightRail({
|
||||||
onRevertFile,
|
onRevertFile,
|
||||||
onPublishPr,
|
onPublishPr,
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
|
onOpenTerminalTab,
|
||||||
|
terminalTabOpen,
|
||||||
|
terminalProcessTabs,
|
||||||
|
onTerminalProcessTabsChange,
|
||||||
|
terminalActiveTabId,
|
||||||
|
onTerminalActiveTabIdChange,
|
||||||
|
terminalCustomNames,
|
||||||
|
onTerminalCustomNamesChange,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
task: Task;
|
task: Task;
|
||||||
|
|
@ -679,6 +861,14 @@ const RightRail = memo(function RightRail({
|
||||||
onRevertFile: (path: string) => void;
|
onRevertFile: (path: string) => void;
|
||||||
onPublishPr: () => void;
|
onPublishPr: () => void;
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
onOpenTerminalTab?: () => void;
|
||||||
|
terminalTabOpen?: boolean;
|
||||||
|
terminalProcessTabs?: ProcessTab[];
|
||||||
|
onTerminalProcessTabsChange?: (tabs: ProcessTab[]) => void;
|
||||||
|
terminalActiveTabId?: string | null;
|
||||||
|
onTerminalActiveTabIdChange?: (id: string | null) => void;
|
||||||
|
terminalCustomNames?: Record<string, string>;
|
||||||
|
onTerminalCustomNamesChange?: (names: Record<string, string>) => void;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
|
@ -761,6 +951,13 @@ const RightRail = memo(function RightRail({
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
...(terminalTabOpen
|
||||||
|
? {
|
||||||
|
borderBottomRightRadius: "12px",
|
||||||
|
borderBottom: `1px solid ${t.borderDefault}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<RightSidebar
|
<RightSidebar
|
||||||
|
|
@ -775,14 +972,14 @@ const RightRail = memo(function RightRail({
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
height: `${terminalHeight}px`,
|
height: terminalTabOpen ? 0 : `${terminalHeight}px`,
|
||||||
minHeight: "43px",
|
minHeight: terminalTabOpen ? 0 : "43px",
|
||||||
backgroundColor: t.surfacePrimary,
|
backgroundColor: t.surfacePrimary,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderBottomRightRadius: "12px",
|
borderBottomRightRadius: "12px",
|
||||||
borderRight: `1px solid ${t.borderDefault}`,
|
borderRight: terminalTabOpen ? "none" : `1px solid ${t.borderDefault}`,
|
||||||
borderBottom: `1px solid ${t.borderDefault}`,
|
borderBottom: terminalTabOpen ? "none" : `1px solid ${t.borderDefault}`,
|
||||||
display: "flex",
|
display: terminalTabOpen ? "none" : "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
@ -802,6 +999,13 @@ const RightRail = memo(function RightRail({
|
||||||
onCollapse={() => {
|
onCollapse={() => {
|
||||||
setTerminalHeight(43);
|
setTerminalHeight(43);
|
||||||
}}
|
}}
|
||||||
|
onOpenTerminalTab={onOpenTerminalTab}
|
||||||
|
processTabs={terminalProcessTabs}
|
||||||
|
onProcessTabsChange={onTerminalProcessTabsChange}
|
||||||
|
activeProcessTabId={terminalActiveTabId}
|
||||||
|
onActiveProcessTabIdChange={onTerminalActiveTabIdChange}
|
||||||
|
customTabNames={terminalCustomNames}
|
||||||
|
onCustomTabNamesChange={onTerminalCustomNamesChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -909,6 +1113,11 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } });
|
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } });
|
||||||
}
|
}
|
||||||
}, [activeOrg, navigate]);
|
}, [activeOrg, navigate]);
|
||||||
|
const navigateToSettings = useCallback(() => {
|
||||||
|
if (activeOrg) {
|
||||||
|
void navigate({ to: "/organizations/$organizationId/settings" as never, params: { organizationId: activeOrg.id } as never });
|
||||||
|
}
|
||||||
|
}, [activeOrg, navigate]);
|
||||||
const [projectOrder, setProjectOrder] = useState<string[] | null>(null);
|
const [projectOrder, setProjectOrder] = useState<string[] | null>(null);
|
||||||
const projects = useMemo(() => {
|
const projects = useMemo(() => {
|
||||||
if (!projectOrder) return rawProjects;
|
if (!projectOrder) return rawProjects;
|
||||||
|
|
@ -922,6 +1131,10 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
const [activeTabIdByTask, setActiveTabIdByTask] = useState<Record<string, string | null>>({});
|
const [activeTabIdByTask, setActiveTabIdByTask] = useState<Record<string, string | null>>({});
|
||||||
const [lastAgentTabIdByTask, setLastAgentTabIdByTask] = useState<Record<string, string | null>>({});
|
const [lastAgentTabIdByTask, setLastAgentTabIdByTask] = useState<Record<string, string | null>>({});
|
||||||
const [openDiffsByTask, setOpenDiffsByTask] = useState<Record<string, string[]>>({});
|
const [openDiffsByTask, setOpenDiffsByTask] = useState<Record<string, string[]>>({});
|
||||||
|
const [terminalTabOpenByTask, setTerminalTabOpenByTask] = useState<Record<string, boolean>>({});
|
||||||
|
const [terminalProcessTabsByTask, setTerminalProcessTabsByTask] = useState<Record<string, ProcessTab[]>>({});
|
||||||
|
const [terminalActiveTabIdByTask, setTerminalActiveTabIdByTask] = useState<Record<string, string | null>>({});
|
||||||
|
const [terminalCustomNamesByTask, setTerminalCustomNamesByTask] = useState<Record<string, Record<string, string>>>({});
|
||||||
const [selectedNewTaskRepoId, setSelectedNewTaskRepoId] = useState("");
|
const [selectedNewTaskRepoId, setSelectedNewTaskRepoId] = useState("");
|
||||||
const [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH));
|
const [leftWidth, setLeftWidth] = useState(() => readStoredWidth(LEFT_WIDTH_STORAGE_KEY, LEFT_SIDEBAR_DEFAULT_WIDTH));
|
||||||
const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH));
|
const [rightWidth, setRightWidth] = useState(() => readStoredWidth(RIGHT_WIDTH_STORAGE_KEY, RIGHT_SIDEBAR_DEFAULT_WIDTH));
|
||||||
|
|
@ -1021,6 +1234,10 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
}, [activeTask, tasks, navigate, workspaceId]);
|
}, [activeTask, tasks, navigate, workspaceId]);
|
||||||
|
|
||||||
const openDiffs = activeTask ? sanitizeOpenDiffs(activeTask, openDiffsByTask[activeTask.id]) : [];
|
const openDiffs = activeTask ? sanitizeOpenDiffs(activeTask, openDiffsByTask[activeTask.id]) : [];
|
||||||
|
const terminalTabOpen = activeTask ? (terminalTabOpenByTask[activeTask.id] ?? false) : false;
|
||||||
|
const terminalProcessTabs = activeTask ? (terminalProcessTabsByTask[activeTask.id] ?? []) : [];
|
||||||
|
const terminalActiveTabId = activeTask ? (terminalActiveTabIdByTask[activeTask.id] ?? null) : null;
|
||||||
|
const terminalCustomNames = activeTask ? (terminalCustomNamesByTask[activeTask.id] ?? {}) : {};
|
||||||
const lastAgentTabId = activeTask ? sanitizeLastAgentTabId(activeTask, lastAgentTabIdByTask[activeTask.id]) : null;
|
const lastAgentTabId = activeTask ? sanitizeLastAgentTabId(activeTask, lastAgentTabIdByTask[activeTask.id]) : null;
|
||||||
const activeTabId = activeTask ? sanitizeActiveTabId(activeTask, activeTabIdByTask[activeTask.id], openDiffs, lastAgentTabId) : null;
|
const activeTabId = activeTask ? sanitizeActiveTabId(activeTask, activeTabIdByTask[activeTask.id], openDiffs, lastAgentTabId) : null;
|
||||||
|
|
||||||
|
|
@ -1115,29 +1332,32 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
})();
|
})();
|
||||||
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
}, [activeTask, selectedSessionId, syncRouteSession, taskWorkbenchClient]);
|
||||||
|
|
||||||
const createTask = useCallback(() => {
|
const createTask = useCallback(
|
||||||
void (async () => {
|
(overrideRepoId?: string) => {
|
||||||
const repoId = selectedNewTaskRepoId;
|
void (async () => {
|
||||||
if (!repoId) {
|
const repoId = overrideRepoId || selectedNewTaskRepoId;
|
||||||
throw new Error("Cannot create a task without an available repo");
|
if (!repoId) {
|
||||||
}
|
throw new Error("Cannot create a task without an available repo");
|
||||||
|
}
|
||||||
|
|
||||||
const { taskId, tabId } = await taskWorkbenchClient.createTask({
|
const { taskId, tabId } = await taskWorkbenchClient.createTask({
|
||||||
repoId,
|
repoId,
|
||||||
task: "New task",
|
task: "New task",
|
||||||
model: "gpt-4o",
|
model: "gpt-4o",
|
||||||
title: "New task",
|
title: "New task",
|
||||||
});
|
});
|
||||||
await navigate({
|
await navigate({
|
||||||
to: "/workspaces/$workspaceId/tasks/$taskId",
|
to: "/workspaces/$workspaceId/tasks/$taskId",
|
||||||
params: {
|
params: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
taskId,
|
taskId,
|
||||||
},
|
},
|
||||||
search: { sessionId: tabId ?? undefined },
|
search: { sessionId: tabId ?? undefined },
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}, [navigate, selectedNewTaskRepoId, workspaceId]);
|
},
|
||||||
|
[navigate, selectedNewTaskRepoId, workspaceId],
|
||||||
|
);
|
||||||
|
|
||||||
const openDiffTab = useCallback(
|
const openDiffTab = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
|
|
@ -1163,6 +1383,46 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
[activeTask],
|
[activeTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openTerminalTab = useCallback(() => {
|
||||||
|
if (!activeTask) return;
|
||||||
|
setTerminalTabOpenByTask((current) => ({ ...current, [activeTask.id]: true }));
|
||||||
|
setActiveTabIdByTask((current) => ({ ...current, [activeTask.id]: terminalTabId() }));
|
||||||
|
}, [activeTask]);
|
||||||
|
|
||||||
|
const closeTerminalTab = useCallback(() => {
|
||||||
|
if (!activeTask) return;
|
||||||
|
setTerminalTabOpenByTask((current) => ({ ...current, [activeTask.id]: false }));
|
||||||
|
const currentActive = activeTabIdByTask[activeTask.id];
|
||||||
|
if (currentActive && isTerminalTab(currentActive)) {
|
||||||
|
const fallback = lastAgentTabIdByTask[activeTask.id] ?? activeTask.tabs[0]?.id ?? null;
|
||||||
|
setActiveTabIdByTask((current) => ({ ...current, [activeTask.id]: fallback }));
|
||||||
|
}
|
||||||
|
}, [activeTask, activeTabIdByTask, lastAgentTabIdByTask]);
|
||||||
|
|
||||||
|
const setTerminalProcessTabs = useCallback(
|
||||||
|
(tabs: ProcessTab[]) => {
|
||||||
|
if (!activeTask) return;
|
||||||
|
setTerminalProcessTabsByTask((current) => ({ ...current, [activeTask.id]: tabs }));
|
||||||
|
},
|
||||||
|
[activeTask],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setTerminalActiveTabId = useCallback(
|
||||||
|
(id: string | null) => {
|
||||||
|
if (!activeTask) return;
|
||||||
|
setTerminalActiveTabIdByTask((current) => ({ ...current, [activeTask.id]: id }));
|
||||||
|
},
|
||||||
|
[activeTask],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setTerminalCustomNames = useCallback(
|
||||||
|
(names: Record<string, string>) => {
|
||||||
|
if (!activeTask) return;
|
||||||
|
setTerminalCustomNamesByTask((current) => ({ ...current, [activeTask.id]: names }));
|
||||||
|
},
|
||||||
|
[activeTask],
|
||||||
|
);
|
||||||
|
|
||||||
const selectTask = useCallback(
|
const selectTask = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const task = tasks.find((candidate) => candidate.id === id) ?? null;
|
const task = tasks.find((candidate) => candidate.id === id) ?? null;
|
||||||
|
|
@ -1265,6 +1525,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
[activeTask, lastAgentTabIdByTask],
|
[activeTask, lastAgentTabIdByTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||||
const onDragMouseDown = useCallback((event: ReactPointerEvent) => {
|
const onDragMouseDown = useCallback((event: ReactPointerEvent) => {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
|
|
@ -1274,6 +1536,58 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
ipc.invoke("plugin:window|start_dragging").catch(() => {});
|
ipc.invoke("plugin:window|start_dragging").catch(() => {});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Mobile layout: single-panel stack navigation with bottom tab bar
|
||||||
|
if (isMobile && activeTask) {
|
||||||
|
return (
|
||||||
|
<MobileLayout
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
task={activeTask}
|
||||||
|
tasks={tasks}
|
||||||
|
projects={projects}
|
||||||
|
repos={viewModel.repos}
|
||||||
|
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||||
|
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||||
|
onSelectTask={selectTask}
|
||||||
|
onCreateTask={createTask}
|
||||||
|
onMarkUnread={markTaskUnread}
|
||||||
|
onRenameTask={renameTask}
|
||||||
|
onRenameBranch={renameBranch}
|
||||||
|
onReorderProjects={reorderProjects}
|
||||||
|
taskOrderByProject={taskOrderByProject}
|
||||||
|
onReorderTasks={reorderTasks}
|
||||||
|
activeTabId={activeTabId}
|
||||||
|
transcriptPanel={
|
||||||
|
<TranscriptPanel
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
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 }))}
|
||||||
|
onNavigateToUsage={navigateToUsage}
|
||||||
|
mobile
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onOpenDiff={openDiffTab}
|
||||||
|
onArchive={archiveTask}
|
||||||
|
onRevertFile={revertFile}
|
||||||
|
onPublishPr={publishPr}
|
||||||
|
terminalProcessTabs={terminalProcessTabs}
|
||||||
|
onTerminalProcessTabsChange={setTerminalProcessTabs}
|
||||||
|
terminalActiveTabId={terminalActiveTabId}
|
||||||
|
onTerminalActiveTabIdChange={setTerminalActiveTabId}
|
||||||
|
terminalCustomNames={terminalCustomNames}
|
||||||
|
onTerminalCustomNamesChange={setTerminalCustomNames}
|
||||||
|
onOpenSettings={navigateToSettings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dragRegion = isDesktop ? (
|
const dragRegion = isDesktop ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -1310,11 +1624,11 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: t.textTertiary,
|
color: t.textPrimary,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
":hover": { color: t.textPrimary, backgroundColor: t.interactiveHover },
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarTransition = "width 200ms ease";
|
const sidebarTransition = "width 200ms ease";
|
||||||
|
|
@ -1370,15 +1684,19 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
{!leftSidebarOpen || !rightSidebarOpen ? (
|
{!leftSidebarOpen || !rightSidebarOpen ? (
|
||||||
<div style={{ display: "flex", alignItems: "center", padding: "8px 8px 0 8px" }}>
|
<div style={{ display: "flex", alignItems: "center", padding: "8px 8px 0 8px" }}>
|
||||||
{leftSidebarOpen ? null : (
|
{leftSidebarOpen ? null : (
|
||||||
<div className={collapsedToggleClass} onClick={() => setLeftSidebarOpen(true)}>
|
<Tooltip label="Toggle sidebar" placement="bottom">
|
||||||
<PanelLeft size={14} />
|
<div className={collapsedToggleClass} onClick={() => setLeftSidebarOpen(true)}>
|
||||||
</div>
|
<PanelLeft size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
{rightSidebarOpen ? null : (
|
{rightSidebarOpen ? null : (
|
||||||
<div className={collapsedToggleClass} onClick={() => setRightSidebarOpen(true)}>
|
<Tooltip label="Toggle changes" placement="bottom">
|
||||||
<PanelRight size={14} />
|
<div className={collapsedToggleClass} onClick={() => setRightSidebarOpen(true)}>
|
||||||
</div>
|
<PanelRight size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -1409,7 +1727,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={createTask}
|
onClick={() => createTask()}
|
||||||
disabled={viewModel.repos.length === 0}
|
disabled={viewModel.repos.length === 0}
|
||||||
style={{
|
style={{
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
|
|
@ -1543,6 +1861,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||||
<TranscriptPanel
|
<TranscriptPanel
|
||||||
|
workspaceId={workspaceId}
|
||||||
taskWorkbenchClient={taskWorkbenchClient}
|
taskWorkbenchClient={taskWorkbenchClient}
|
||||||
task={activeTask}
|
task={activeTask}
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
|
|
@ -1568,6 +1887,15 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
rightSidebarCollapsed={!rightSidebarOpen}
|
rightSidebarCollapsed={!rightSidebarOpen}
|
||||||
onToggleRightSidebar={() => setRightSidebarOpen(true)}
|
onToggleRightSidebar={() => setRightSidebarOpen(true)}
|
||||||
onNavigateToUsage={navigateToUsage}
|
onNavigateToUsage={navigateToUsage}
|
||||||
|
terminalTabOpen={terminalTabOpen}
|
||||||
|
onOpenTerminalTab={openTerminalTab}
|
||||||
|
onCloseTerminalTab={closeTerminalTab}
|
||||||
|
terminalProcessTabs={terminalProcessTabs}
|
||||||
|
onTerminalProcessTabsChange={setTerminalProcessTabs}
|
||||||
|
terminalActiveTabId={terminalActiveTabId}
|
||||||
|
onTerminalActiveTabIdChange={setTerminalActiveTabId}
|
||||||
|
terminalCustomNames={terminalCustomNames}
|
||||||
|
onTerminalCustomNamesChange={setTerminalCustomNames}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||||
|
|
@ -1592,6 +1920,14 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
onRevertFile={revertFile}
|
onRevertFile={revertFile}
|
||||||
onPublishPr={publishPr}
|
onPublishPr={publishPr}
|
||||||
onToggleSidebar={() => setRightSidebarOpen(false)}
|
onToggleSidebar={() => setRightSidebarOpen(false)}
|
||||||
|
onOpenTerminalTab={openTerminalTab}
|
||||||
|
terminalTabOpen={terminalTabOpen}
|
||||||
|
terminalProcessTabs={terminalProcessTabs}
|
||||||
|
onTerminalProcessTabsChange={setTerminalProcessTabs}
|
||||||
|
terminalActiveTabId={terminalActiveTabId}
|
||||||
|
onTerminalActiveTabIdChange={setTerminalActiveTabId}
|
||||||
|
terminalCustomNames={terminalCustomNames}
|
||||||
|
onTerminalCustomNamesChange={setTerminalCustomNames}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { LabelXSmall } from "baseui/typography";
|
||||||
import { History } from "lucide-react";
|
import { History } from "lucide-react";
|
||||||
|
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
|
import { Tooltip } from "./ui";
|
||||||
import { formatMessageTimestamp, type HistoryEvent } from "./view-model";
|
import { formatMessageTimestamp, type HistoryEvent } from "./view-model";
|
||||||
|
|
||||||
export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: { events: HistoryEvent[]; onSelect: (event: HistoryEvent) => void }) {
|
export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: { events: HistoryEvent[]; onSelect: (event: HistoryEvent) => void }) {
|
||||||
|
|
@ -41,29 +42,31 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
|
||||||
gap: "6px",
|
gap: "6px",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<Tooltip label="History" placement="left">
|
||||||
role="button"
|
<div
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => {
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
if (e.key === "Enter" || e.key === " ") setOpen((prev) => !prev);
|
onKeyDown={(e) => {
|
||||||
}}
|
if (e.key === "Enter" || e.key === " ") setOpen((prev) => !prev);
|
||||||
className={css({
|
}}
|
||||||
width: "26px",
|
className={css({
|
||||||
height: "26px",
|
width: "26px",
|
||||||
borderRadius: "6px",
|
height: "26px",
|
||||||
display: "flex",
|
borderRadius: "6px",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
justifyContent: "center",
|
||||||
color: open ? t.textSecondary : t.textTertiary,
|
cursor: "pointer",
|
||||||
backgroundColor: open ? t.interactiveHover : "transparent",
|
color: open ? t.textSecondary : t.textTertiary,
|
||||||
transition: "background 200ms ease, color 200ms ease",
|
backgroundColor: open ? t.interactiveHover : "transparent",
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
transition: "background 200ms ease, color 200ms ease",
|
||||||
})}
|
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||||
>
|
})}
|
||||||
<History size={14} />
|
>
|
||||||
</div>
|
<History size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{open ? (
|
{open ? (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,15 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
|
||||||
messageRefs,
|
messageRefs,
|
||||||
copiedMessageId,
|
copiedMessageId,
|
||||||
onCopyMessage,
|
onCopyMessage,
|
||||||
|
userName,
|
||||||
|
userAvatarUrl,
|
||||||
}: {
|
}: {
|
||||||
message: Message;
|
message: Message;
|
||||||
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
copiedMessageId: string | null;
|
copiedMessageId: string | null;
|
||||||
onCopyMessage: (message: Message) => void;
|
onCopyMessage: (message: Message) => void;
|
||||||
|
userName?: string | null;
|
||||||
|
userAvatarUrl?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
|
@ -81,12 +85,52 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
|
||||||
className={css({
|
className={css({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "10px",
|
gap: "6px",
|
||||||
justifyContent: isUser ? "flex-end" : "flex-start",
|
justifyContent: isUser ? "flex-end" : "flex-start",
|
||||||
minHeight: "16px",
|
minHeight: "16px",
|
||||||
paddingLeft: isUser ? undefined : "2px",
|
paddingLeft: isUser ? undefined : "2px",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{isUser && (userAvatarUrl || userName) ? (
|
||||||
|
<>
|
||||||
|
{userAvatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={userAvatarUrl}
|
||||||
|
alt=""
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : userName ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: t.borderMedium,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.textSecondary,
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{userName.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{userName ? (
|
||||||
|
<LabelXSmall color={t.textTertiary} $style={{ fontWeight: 500 }}>
|
||||||
|
{userName}
|
||||||
|
</LabelXSmall>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{displayFooter ? (
|
{displayFooter ? (
|
||||||
<LabelXSmall color={t.textTertiary} $style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}>
|
<LabelXSmall color={t.textTertiary} $style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}>
|
||||||
{displayFooter}
|
{displayFooter}
|
||||||
|
|
@ -130,6 +174,8 @@ export const MessageList = memo(function MessageList({
|
||||||
copiedMessageId,
|
copiedMessageId,
|
||||||
onCopyMessage,
|
onCopyMessage,
|
||||||
thinkingTimerLabel,
|
thinkingTimerLabel,
|
||||||
|
userName,
|
||||||
|
userAvatarUrl,
|
||||||
}: {
|
}: {
|
||||||
tab: AgentTab | null | undefined;
|
tab: AgentTab | null | undefined;
|
||||||
scrollRef: Ref<HTMLDivElement>;
|
scrollRef: Ref<HTMLDivElement>;
|
||||||
|
|
@ -139,6 +185,8 @@ export const MessageList = memo(function MessageList({
|
||||||
copiedMessageId: string | null;
|
copiedMessageId: string | null;
|
||||||
onCopyMessage: (message: Message) => void;
|
onCopyMessage: (message: Message) => void;
|
||||||
thinkingTimerLabel: string | null;
|
thinkingTimerLabel: string | null;
|
||||||
|
userName?: string | null;
|
||||||
|
userAvatarUrl?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
|
@ -238,7 +286,16 @@ export const MessageList = memo(function MessageList({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TranscriptMessageBody message={message} messageRefs={messageRefs} copiedMessageId={copiedMessageId} onCopyMessage={onCopyMessage} />;
|
return (
|
||||||
|
<TranscriptMessageBody
|
||||||
|
message={message}
|
||||||
|
messageRefs={messageRefs}
|
||||||
|
copiedMessageId={copiedMessageId}
|
||||||
|
onCopyMessage={onCopyMessage}
|
||||||
|
userName={userName}
|
||||||
|
userAvatarUrl={userAvatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
isThinking={Boolean(tab && tab.status === "running" && transcriptEntries.length > 0)}
|
isThinking={Boolean(tab && tab.status === "running" && transcriptEntries.length > 0)}
|
||||||
renderThinkingState={() => (
|
renderThinkingState={() => (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
import { memo, useCallback, useRef, useState } from "react";
|
||||||
|
import { useStyletron } from "baseui";
|
||||||
|
import { FileText, List, MessageSquare, Settings, Terminal as TerminalIcon } from "lucide-react";
|
||||||
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
|
|
||||||
|
import type { WorkbenchProjectSection, WorkbenchRepo } from "@sandbox-agent/foundry-shared";
|
||||||
|
import { RightSidebar } from "./right-sidebar";
|
||||||
|
import { Sidebar } from "./sidebar";
|
||||||
|
import { TerminalPane, type ProcessTab } from "./terminal-pane";
|
||||||
|
import type { Task } from "./view-model";
|
||||||
|
|
||||||
|
type MobileView = "tasks" | "chat" | "changes" | "terminal";
|
||||||
|
const VIEW_ORDER: MobileView[] = ["tasks", "chat", "changes", "terminal"];
|
||||||
|
|
||||||
|
const SWIPE_THRESHOLD = 50;
|
||||||
|
const SWIPE_MAX_VERTICAL = 80;
|
||||||
|
|
||||||
|
interface MobileLayoutProps {
|
||||||
|
workspaceId: string;
|
||||||
|
task: Task;
|
||||||
|
tasks: Task[];
|
||||||
|
projects: WorkbenchProjectSection[];
|
||||||
|
repos: WorkbenchRepo[];
|
||||||
|
selectedNewTaskRepoId: string;
|
||||||
|
onSelectNewTaskRepo: (id: string) => void;
|
||||||
|
onSelectTask: (id: string) => void;
|
||||||
|
onCreateTask: (repoId?: string) => void;
|
||||||
|
onMarkUnread: (id: string) => void;
|
||||||
|
onRenameTask: (id: string) => void;
|
||||||
|
onRenameBranch: (id: string) => void;
|
||||||
|
onReorderProjects: (from: number, to: number) => void;
|
||||||
|
taskOrderByProject: Record<string, string[]>;
|
||||||
|
onReorderTasks: (projectId: string, from: number, to: number) => void;
|
||||||
|
// Transcript panel (rendered by parent)
|
||||||
|
transcriptPanel: React.ReactNode;
|
||||||
|
// Diff/file actions
|
||||||
|
onOpenDiff: (path: string) => void;
|
||||||
|
onArchive: () => void;
|
||||||
|
onRevertFile: (path: string) => void;
|
||||||
|
onPublishPr: () => void;
|
||||||
|
// Tab state
|
||||||
|
activeTabId: string | null;
|
||||||
|
// Terminal state
|
||||||
|
terminalProcessTabs: ProcessTab[];
|
||||||
|
onTerminalProcessTabsChange: (tabs: ProcessTab[]) => void;
|
||||||
|
terminalActiveTabId: string | null;
|
||||||
|
onTerminalActiveTabIdChange: (id: string | null) => void;
|
||||||
|
terminalCustomNames: Record<string, string>;
|
||||||
|
onTerminalCustomNamesChange: (names: Record<string, string>) => void;
|
||||||
|
onOpenSettings?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileLayout = memo(function MobileLayout(props: MobileLayoutProps) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const [activeView, setActiveView] = useState<MobileView>("tasks");
|
||||||
|
|
||||||
|
// Swipe gesture tracking
|
||||||
|
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
if (touch) {
|
||||||
|
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
const start = touchStartRef.current;
|
||||||
|
const touch = e.changedTouches[0];
|
||||||
|
if (!start || !touch) return;
|
||||||
|
touchStartRef.current = null;
|
||||||
|
|
||||||
|
const dx = touch.clientX - start.x;
|
||||||
|
const dy = touch.clientY - start.y;
|
||||||
|
|
||||||
|
if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dy) > SWIPE_MAX_VERTICAL) return;
|
||||||
|
|
||||||
|
const currentIndex = VIEW_ORDER.indexOf(activeView);
|
||||||
|
if (dx > 0 && currentIndex > 0) {
|
||||||
|
// Swipe right -> go back
|
||||||
|
setActiveView(VIEW_ORDER[currentIndex - 1]!);
|
||||||
|
} else if (dx < 0 && currentIndex < VIEW_ORDER.length - 1) {
|
||||||
|
// Swipe left -> go forward
|
||||||
|
setActiveView(VIEW_ORDER[currentIndex + 1]!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeView],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectTask = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
props.onSelectTask(id);
|
||||||
|
setActiveView("chat");
|
||||||
|
},
|
||||||
|
[props.onSelectTask],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100dvh",
|
||||||
|
backgroundColor: t.surfacePrimary,
|
||||||
|
paddingTop: "max(var(--safe-area-top), 47px)",
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Header - show task info when not on tasks view */}
|
||||||
|
{activeView !== "tasks" && <MobileHeader task={props.task} />}
|
||||||
|
|
||||||
|
{/* Content area */}
|
||||||
|
<div
|
||||||
|
className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" })}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{activeView === "tasks" ? (
|
||||||
|
<div className={css({ flex: 1, minHeight: 0, overflow: "auto" })}>
|
||||||
|
<Sidebar
|
||||||
|
projects={props.projects}
|
||||||
|
newTaskRepos={props.repos}
|
||||||
|
selectedNewTaskRepoId={props.selectedNewTaskRepoId}
|
||||||
|
activeId={props.task.id}
|
||||||
|
onSelect={handleSelectTask}
|
||||||
|
onCreate={props.onCreateTask}
|
||||||
|
onSelectNewTaskRepo={props.onSelectNewTaskRepo}
|
||||||
|
onMarkUnread={props.onMarkUnread}
|
||||||
|
onRenameTask={props.onRenameTask}
|
||||||
|
onRenameBranch={props.onRenameBranch}
|
||||||
|
onReorderProjects={props.onReorderProjects}
|
||||||
|
taskOrderByProject={props.taskOrderByProject}
|
||||||
|
onReorderTasks={props.onReorderTasks}
|
||||||
|
onToggleSidebar={undefined}
|
||||||
|
hideSettings
|
||||||
|
panelStyle={{ backgroundColor: t.surfacePrimary }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : activeView === "chat" ? (
|
||||||
|
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" })}>{props.transcriptPanel}</div>
|
||||||
|
) : activeView === "changes" ? (
|
||||||
|
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" })}>
|
||||||
|
<RightSidebar
|
||||||
|
task={props.task}
|
||||||
|
activeTabId={props.activeTabId}
|
||||||
|
onOpenDiff={props.onOpenDiff}
|
||||||
|
onArchive={props.onArchive}
|
||||||
|
onRevertFile={props.onRevertFile}
|
||||||
|
onPublishPr={props.onPublishPr}
|
||||||
|
mobile
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" })}>
|
||||||
|
<TerminalPane
|
||||||
|
workspaceId={props.workspaceId}
|
||||||
|
taskId={props.task.id}
|
||||||
|
hideHeader
|
||||||
|
processTabs={props.terminalProcessTabs}
|
||||||
|
onProcessTabsChange={props.onTerminalProcessTabsChange}
|
||||||
|
activeProcessTabId={props.terminalActiveTabId}
|
||||||
|
onActiveProcessTabIdChange={props.onTerminalActiveTabIdChange}
|
||||||
|
customTabNames={props.terminalCustomNames}
|
||||||
|
onCustomTabNamesChange={props.onTerminalCustomNamesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom tab bar - always fixed at bottom */}
|
||||||
|
<MobileTabBar
|
||||||
|
activeView={activeView}
|
||||||
|
onViewChange={setActiveView}
|
||||||
|
changesCount={Object.keys(props.task.diffs).length}
|
||||||
|
onOpenSettings={props.onOpenSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function MobileHeader({ task }: { task: Task }) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px 12px",
|
||||||
|
gap: "8px",
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: `1px solid ${t.borderDefault}`,
|
||||||
|
minHeight: "40px",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={css({ flex: 1, minWidth: 0, overflow: "hidden" })}>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{task.title}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: "11px",
|
||||||
|
color: t.textTertiary,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{task.repoName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileTabBar({
|
||||||
|
activeView,
|
||||||
|
onViewChange,
|
||||||
|
changesCount,
|
||||||
|
onOpenSettings,
|
||||||
|
}: {
|
||||||
|
activeView: MobileView;
|
||||||
|
onViewChange: (view: MobileView) => void;
|
||||||
|
changesCount: number;
|
||||||
|
onOpenSettings?: () => void;
|
||||||
|
}) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
|
||||||
|
const tabs: { id: MobileView; icon: React.ReactNode; badge?: number }[] = [
|
||||||
|
{ id: "tasks", icon: <List size={20} /> },
|
||||||
|
{ id: "chat", icon: <MessageSquare size={20} /> },
|
||||||
|
{ id: "changes", icon: <FileText size={20} />, badge: changesCount > 0 ? changesCount : undefined },
|
||||||
|
{ id: "terminal", icon: <TerminalIcon size={20} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const iconButtonClass = css({
|
||||||
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "42px",
|
||||||
|
height: "42px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
position: "relative",
|
||||||
|
transition: "background 150ms ease, color 150ms ease",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "8px 16px",
|
||||||
|
paddingBottom: "calc(8px + var(--safe-area-bottom))",
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Pill container */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "4px",
|
||||||
|
backgroundColor: t.surfaceElevated,
|
||||||
|
borderRadius: "16px",
|
||||||
|
padding: "4px",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = activeView === tab.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onViewChange(tab.id)}
|
||||||
|
className={iconButtonClass}
|
||||||
|
style={{
|
||||||
|
background: isActive ? t.interactiveHover : "transparent",
|
||||||
|
color: isActive ? t.textPrimary : t.textTertiary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={css({ position: "relative", display: "flex" })}>
|
||||||
|
{tab.icon}
|
||||||
|
{tab.badge ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "absolute",
|
||||||
|
top: "-4px",
|
||||||
|
right: "-8px",
|
||||||
|
backgroundColor: t.accent,
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: "8px",
|
||||||
|
minWidth: "16px",
|
||||||
|
height: "16px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "0 4px",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{tab.badge}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{onOpenSettings && (
|
||||||
|
<button type="button" onClick={onOpenSettings} className={iconButtonClass} style={{ color: t.textTertiary }}>
|
||||||
|
<Settings size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { LabelSmall } from "baseui/typography";
|
||||||
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
|
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
|
||||||
|
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, Tooltip, useContextMenu } from "./ui";
|
||||||
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
|
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
|
||||||
|
|
||||||
const FileTree = memo(function FileTree({
|
const FileTree = memo(function FileTree({
|
||||||
|
|
@ -96,6 +96,7 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
onRevertFile,
|
onRevertFile,
|
||||||
onPublishPr,
|
onPublishPr,
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
|
mobile,
|
||||||
}: {
|
}: {
|
||||||
task: Task;
|
task: Task;
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
|
|
@ -104,6 +105,7 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
onRevertFile: (path: string) => void;
|
onRevertFile: (path: string) => void;
|
||||||
onPublishPr: () => void;
|
onPublishPr: () => void;
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
mobile?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
|
@ -151,128 +153,138 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SPanel $style={{ backgroundColor: t.surfacePrimary, minWidth: 0 }}>
|
<SPanel $style={{ backgroundColor: t.surfacePrimary, minWidth: 0 }}>
|
||||||
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", overflow: "hidden" }}>
|
{!mobile && (
|
||||||
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", overflow: "hidden" }}>
|
||||||
{!isTerminal ? (
|
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
||||||
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
{!isTerminal ? (
|
||||||
<button
|
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
||||||
onClick={() => {
|
<Tooltip label={pullRequestUrl ? "Open PR" : "Publish PR"} placement="bottom">
|
||||||
if (pullRequestUrl) {
|
<button
|
||||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
onClick={() => {
|
||||||
return;
|
if (pullRequestUrl) {
|
||||||
}
|
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onPublishPr();
|
onPublishPr();
|
||||||
}}
|
}}
|
||||||
className={css({
|
className={css({
|
||||||
appearance: "none",
|
appearance: "none",
|
||||||
WebkitAppearance: "none",
|
WebkitAppearance: "none",
|
||||||
background: "none",
|
background: "none",
|
||||||
border: "none",
|
border: "none",
|
||||||
margin: "0",
|
margin: "0",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "5px",
|
gap: "5px",
|
||||||
padding: compact ? "4px 6px" : "4px 10px",
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
fontSize: "11px",
|
fontSize: "11px",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
color: t.textSecondary,
|
color: t.textSecondary,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all 200ms ease",
|
transition: "all 200ms ease",
|
||||||
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||||
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
</Tooltip>
|
||||||
className={css({
|
<Tooltip label="Push" placement="bottom">
|
||||||
appearance: "none",
|
<button
|
||||||
WebkitAppearance: "none",
|
className={css({
|
||||||
background: "none",
|
appearance: "none",
|
||||||
border: "none",
|
WebkitAppearance: "none",
|
||||||
margin: "0",
|
background: "none",
|
||||||
boxSizing: "border-box",
|
border: "none",
|
||||||
display: "inline-flex",
|
margin: "0",
|
||||||
alignItems: "center",
|
boxSizing: "border-box",
|
||||||
gap: "5px",
|
display: "inline-flex",
|
||||||
padding: compact ? "4px 6px" : "4px 10px",
|
alignItems: "center",
|
||||||
borderRadius: "6px",
|
gap: "5px",
|
||||||
fontSize: "11px",
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
fontWeight: 500,
|
borderRadius: "6px",
|
||||||
lineHeight: 1,
|
fontSize: "11px",
|
||||||
whiteSpace: "nowrap",
|
fontWeight: 500,
|
||||||
flexShrink: 0,
|
lineHeight: 1,
|
||||||
color: t.textSecondary,
|
whiteSpace: "nowrap",
|
||||||
cursor: "pointer",
|
flexShrink: 0,
|
||||||
transition: "all 200ms ease",
|
color: t.textSecondary,
|
||||||
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
cursor: "pointer",
|
||||||
})}
|
transition: "all 200ms ease",
|
||||||
>
|
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
||||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
|
})}
|
||||||
{!compact && <span>Push</span>}
|
>
|
||||||
</button>
|
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
|
||||||
<button
|
{!compact && <span>Push</span>}
|
||||||
onClick={onArchive}
|
</button>
|
||||||
className={css({
|
</Tooltip>
|
||||||
appearance: "none",
|
<Tooltip label="Archive" placement="bottom">
|
||||||
WebkitAppearance: "none",
|
<button
|
||||||
background: "none",
|
onClick={onArchive}
|
||||||
border: "none",
|
className={css({
|
||||||
margin: "0",
|
appearance: "none",
|
||||||
boxSizing: "border-box",
|
WebkitAppearance: "none",
|
||||||
display: "inline-flex",
|
background: "none",
|
||||||
alignItems: "center",
|
border: "none",
|
||||||
gap: "5px",
|
margin: "0",
|
||||||
padding: compact ? "4px 6px" : "4px 10px",
|
boxSizing: "border-box",
|
||||||
borderRadius: "6px",
|
display: "inline-flex",
|
||||||
fontSize: "11px",
|
alignItems: "center",
|
||||||
fontWeight: 500,
|
gap: "5px",
|
||||||
lineHeight: 1,
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
whiteSpace: "nowrap",
|
borderRadius: "6px",
|
||||||
flexShrink: 0,
|
fontSize: "11px",
|
||||||
color: t.textSecondary,
|
fontWeight: 500,
|
||||||
cursor: "pointer",
|
lineHeight: 1,
|
||||||
transition: "all 200ms ease",
|
whiteSpace: "nowrap",
|
||||||
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
flexShrink: 0,
|
||||||
})}
|
color: t.textSecondary,
|
||||||
>
|
cursor: "pointer",
|
||||||
<Archive size={12} style={{ flexShrink: 0 }} />
|
transition: "all 200ms ease",
|
||||||
{!compact && <span>Archive</span>}
|
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
|
||||||
</button>
|
})}
|
||||||
</div>
|
>
|
||||||
) : null}
|
<Archive size={12} style={{ flexShrink: 0 }} />
|
||||||
{onToggleSidebar ? (
|
{!compact && <span>Archive</span>}
|
||||||
<div
|
</button>
|
||||||
role="button"
|
</Tooltip>
|
||||||
tabIndex={0}
|
</div>
|
||||||
onClick={onToggleSidebar}
|
) : null}
|
||||||
onKeyDown={(event) => {
|
{onToggleSidebar ? (
|
||||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
<Tooltip label="Toggle sidebar" placement="bottom">
|
||||||
}}
|
<div
|
||||||
className={css({
|
role="button"
|
||||||
width: "26px",
|
tabIndex={0}
|
||||||
height: "26px",
|
onClick={onToggleSidebar}
|
||||||
borderRadius: "6px",
|
onKeyDown={(event) => {
|
||||||
color: t.textTertiary,
|
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||||
cursor: "pointer",
|
}}
|
||||||
display: "flex",
|
className={css({
|
||||||
alignItems: "center",
|
width: "26px",
|
||||||
justifyContent: "center",
|
height: "26px",
|
||||||
flexShrink: 0,
|
borderRadius: "6px",
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
color: t.textTertiary,
|
||||||
})}
|
cursor: "pointer",
|
||||||
>
|
display: "flex",
|
||||||
<PanelRight size={14} />
|
alignItems: "center",
|
||||||
</div>
|
justifyContent: "center",
|
||||||
) : null}
|
flexShrink: 0,
|
||||||
</div>
|
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||||
</PanelHeaderBar>
|
})}
|
||||||
|
>
|
||||||
|
<PanelRight size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</PanelHeaderBar>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -280,9 +292,13 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
borderTop: `1px solid ${t.borderDefault}`,
|
...(mobile
|
||||||
borderRight: `1px solid ${t.borderDefault}`,
|
? {}
|
||||||
borderTopRightRadius: "12px",
|
: {
|
||||||
|
borderTop: `1px solid ${t.borderDefault}`,
|
||||||
|
borderRight: `1px solid ${t.borderDefault}`,
|
||||||
|
borderTopRightRadius: "12px",
|
||||||
|
}),
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -296,7 +312,7 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
height: "41px",
|
height: "41px",
|
||||||
minHeight: "41px",
|
minHeight: "41px",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderTopRightRadius: "12px",
|
...(mobile ? {} : { borderTopRightRadius: "12px" }),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
CloudUpload,
|
CloudUpload,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
GitPullRequestDraft,
|
GitPullRequestDraft,
|
||||||
ListChecks,
|
List,
|
||||||
LogOut,
|
LogOut,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
Plus,
|
Plus,
|
||||||
|
|
@ -18,14 +18,166 @@ import {
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import type { WorkbenchPresence } from "@sandbox-agent/foundry-shared";
|
||||||
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
||||||
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, Tooltip, useContextMenu } from "./ui";
|
||||||
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
|
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
import type { FoundryTokens } from "../../styles/tokens";
|
import type { FoundryTokens } from "../../styles/tokens";
|
||||||
|
|
||||||
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
||||||
|
|
||||||
|
const AWAY_THRESHOLD_MS = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
const PresenceAvatar = memo(function PresenceAvatar({ member, idx, isAway }: { member: WorkbenchPresence; idx: number; isAway: boolean }) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const label = `${member.name}${isAway ? " (away)" : ""}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "relative",
|
||||||
|
marginLeft: idx > 0 ? "-5px" : "0",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover > div:last-child": {
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translateX(-50%) translateY(0)",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: `1.5px solid ${t.surfacePrimary}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: t.interactiveHover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
opacity: isAway ? 0.35 : 1,
|
||||||
|
filter: isAway ? "grayscale(1)" : "none",
|
||||||
|
transition: "opacity 0.3s, filter 0.3s",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{member.avatarUrl ? (
|
||||||
|
<img src={member.avatarUrl} alt={member.name} className={css({ width: "100%", height: "100%", objectFit: "cover", display: "block" })} />
|
||||||
|
) : (
|
||||||
|
<span className={css({ fontSize: "9px", fontWeight: 600, color: t.textTertiary })}>{member.name.charAt(0).toUpperCase()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "calc(100% + 6px)",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%) translateY(4px)",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "rgba(32, 32, 32, 0.98)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.4)",
|
||||||
|
color: "#e0e0e0",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 500,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
pointerEvents: "none",
|
||||||
|
opacity: 0,
|
||||||
|
transition: "opacity 150ms ease, transform 150ms ease",
|
||||||
|
zIndex: 300,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PresenceAvatars = memo(function PresenceAvatars({ presence }: { presence: WorkbenchPresence[] }) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const maxShow = 3;
|
||||||
|
const visible = presence.slice(0, maxShow);
|
||||||
|
const overflow = presence.length - maxShow;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css({ display: "flex", alignItems: "center", gap: "6px", paddingLeft: "22px", paddingTop: "2px" })}>
|
||||||
|
<div className={css({ display: "flex", alignItems: "center" })}>
|
||||||
|
{visible.map((member, idx) => {
|
||||||
|
const isAway = now - member.lastSeenAtMs > AWAY_THRESHOLD_MS;
|
||||||
|
return <PresenceAvatar key={member.memberId} member={member} idx={idx} isAway={isAway} />;
|
||||||
|
})}
|
||||||
|
{overflow > 0 && (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "relative",
|
||||||
|
marginLeft: "-5px",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover > div:last-child": {
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translateX(-50%) translateY(0)",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: `1.5px solid ${t.surfacePrimary}`,
|
||||||
|
backgroundColor: t.interactiveHover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className={css({ fontSize: "8px", fontWeight: 600, color: t.textTertiary })}>+{overflow}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "calc(100% + 6px)",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%) translateY(4px)",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "rgba(32, 32, 32, 0.98)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.4)",
|
||||||
|
color: "#e0e0e0",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 500,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
pointerEvents: "none",
|
||||||
|
opacity: 0,
|
||||||
|
transition: "opacity 150ms ease, transform 150ms ease",
|
||||||
|
zIndex: 300,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{presence
|
||||||
|
.slice(maxShow)
|
||||||
|
.map((m) => m.name)
|
||||||
|
.join(", ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{presence.length <= 2 && (
|
||||||
|
<span className={css({ fontSize: "10px", color: t.textTertiary, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||||
|
{presence.map((m) => m.name).join(", ")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function projectInitial(label: string): string {
|
function projectInitial(label: string): string {
|
||||||
const parts = label.split("/");
|
const parts = label.split("/");
|
||||||
const name = parts[parts.length - 1] ?? label;
|
const name = parts[parts.length - 1] ?? label;
|
||||||
|
|
@ -55,13 +207,15 @@ export const Sidebar = memo(function Sidebar({
|
||||||
taskOrderByProject,
|
taskOrderByProject,
|
||||||
onReorderTasks,
|
onReorderTasks,
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
|
hideSettings,
|
||||||
|
panelStyle,
|
||||||
}: {
|
}: {
|
||||||
projects: ProjectSection[];
|
projects: ProjectSection[];
|
||||||
newTaskRepos: Array<{ id: string; label: string }>;
|
newTaskRepos: Array<{ id: string; label: string }>;
|
||||||
selectedNewTaskRepoId: string;
|
selectedNewTaskRepoId: string;
|
||||||
activeId: string;
|
activeId: string;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
onCreate: () => void;
|
onCreate: (repoId?: string) => void;
|
||||||
onSelectNewTaskRepo: (repoId: string) => void;
|
onSelectNewTaskRepo: (repoId: string) => void;
|
||||||
onMarkUnread: (id: string) => void;
|
onMarkUnread: (id: string) => void;
|
||||||
onRenameTask: (id: string) => void;
|
onRenameTask: (id: string) => void;
|
||||||
|
|
@ -70,6 +224,8 @@ export const Sidebar = memo(function Sidebar({
|
||||||
taskOrderByProject: Record<string, string[]>;
|
taskOrderByProject: Record<string, string[]>;
|
||||||
onReorderTasks: (projectId: string, fromIndex: number, toIndex: number) => void;
|
onReorderTasks: (projectId: string, fromIndex: number, toIndex: number) => void;
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
hideSettings?: boolean;
|
||||||
|
panelStyle?: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
|
@ -90,6 +246,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
// Attach global mousemove/mouseup when dragging
|
// Attach global mousemove/mouseup when dragging
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!drag) return;
|
if (!drag) return;
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
const onMove = (e: MouseEvent) => {
|
const onMove = (e: MouseEvent) => {
|
||||||
// Detect which element is under the cursor using data attributes
|
// Detect which element is under the cursor using data attributes
|
||||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
|
@ -132,6 +289,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
document.addEventListener("mousemove", onMove);
|
document.addEventListener("mousemove", onMove);
|
||||||
document.addEventListener("mouseup", onUp);
|
document.addEventListener("mouseup", onUp);
|
||||||
return () => {
|
return () => {
|
||||||
|
document.body.style.cursor = "";
|
||||||
document.removeEventListener("mousemove", onMove);
|
document.removeEventListener("mousemove", onMove);
|
||||||
document.removeEventListener("mouseup", onUp);
|
document.removeEventListener("mouseup", onUp);
|
||||||
};
|
};
|
||||||
|
|
@ -152,12 +310,12 @@ export const Sidebar = memo(function Sidebar({
|
||||||
}, [createMenuOpen]);
|
}, [createMenuOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SPanel>
|
<SPanel $style={panelStyle}>
|
||||||
<style>{`
|
<style>{`
|
||||||
[data-project-header]:hover [data-chevron] {
|
[data-project-header] [data-chevron] {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
}
|
}
|
||||||
[data-project-header]:hover [data-project-icon] {
|
[data-project-header] [data-project-icon] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
@ -175,6 +333,43 @@ export const Sidebar = memo(function Sidebar({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{onToggleSidebar ? (
|
{onToggleSidebar ? (
|
||||||
|
<Tooltip label="Toggle sidebar" placement="bottom">
|
||||||
|
<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: t.textTertiary,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PanelLeft size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
|
||||||
|
<LabelSmall
|
||||||
|
color={t.textPrimary}
|
||||||
|
$style={{ fontWeight: 600, flex: 1, fontSize: "16px", display: "flex", alignItems: "center", gap: "6px", lineHeight: 1 }}
|
||||||
|
>
|
||||||
|
<List size={16} />
|
||||||
|
Tasks
|
||||||
|
</LabelSmall>
|
||||||
|
{!import.meta.env.VITE_DESKTOP && onToggleSidebar ? (
|
||||||
|
<Tooltip label="Toggle sidebar" placement="bottom">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
@ -197,84 +392,53 @@ export const Sidebar = memo(function Sidebar({
|
||||||
>
|
>
|
||||||
<PanelLeft size={14} />
|
<PanelLeft size={14} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</Tooltip>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
|
|
||||||
<LabelSmall
|
|
||||||
color={t.textPrimary}
|
|
||||||
$style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px", lineHeight: 1 }}
|
|
||||||
>
|
|
||||||
<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: t.textTertiary,
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexShrink: 0,
|
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PanelLeft size={14} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
<div ref={createMenuRef} className={css({ position: "relative", flexShrink: 0 })}>
|
<div ref={createMenuRef} className={css({ position: "relative", flexShrink: 0 })}>
|
||||||
<div
|
<Tooltip label="New task" placement="bottom">
|
||||||
role="button"
|
<div
|
||||||
tabIndex={0}
|
role="button"
|
||||||
aria-disabled={newTaskRepos.length === 0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
aria-disabled={newTaskRepos.length === 0}
|
||||||
if (newTaskRepos.length === 0) return;
|
onClick={() => {
|
||||||
if (newTaskRepos.length === 1) {
|
if (newTaskRepos.length === 0) return;
|
||||||
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) {
|
if (newTaskRepos.length === 1) {
|
||||||
onSelectNewTaskRepo(newTaskRepos[0]!.id);
|
onSelectNewTaskRepo(newTaskRepos[0]!.id);
|
||||||
onCreate();
|
onCreate(newTaskRepos[0]!.id);
|
||||||
} else {
|
} else {
|
||||||
setCreateMenuOpen((prev) => !prev);
|
setCreateMenuOpen((prev) => !prev);
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(event) => {
|
||||||
className={css({
|
if (newTaskRepos.length === 0) return;
|
||||||
width: "26px",
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
height: "26px",
|
if (newTaskRepos.length === 1) {
|
||||||
borderRadius: "8px",
|
onSelectNewTaskRepo(newTaskRepos[0]!.id);
|
||||||
backgroundColor: newTaskRepos.length > 0 ? t.borderMedium : t.interactiveHover,
|
onCreate(newTaskRepos[0]!.id);
|
||||||
color: t.textPrimary,
|
} else {
|
||||||
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
|
setCreateMenuOpen((prev) => !prev);
|
||||||
display: "flex",
|
}
|
||||||
alignItems: "center",
|
}
|
||||||
justifyContent: "center",
|
}}
|
||||||
transition: "background 200ms ease",
|
className={css({
|
||||||
flexShrink: 0,
|
width: "26px",
|
||||||
opacity: newTaskRepos.length > 0 ? 1 : 0.6,
|
height: "26px",
|
||||||
":hover": newTaskRepos.length > 0 ? { backgroundColor: "rgba(255, 255, 255, 0.20)" } : undefined,
|
borderRadius: "8px",
|
||||||
})}
|
backgroundColor: newTaskRepos.length > 0 ? t.borderMedium : t.interactiveHover,
|
||||||
>
|
color: t.textPrimary,
|
||||||
<Plus size={14} style={{ display: "block" }} />
|
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
|
||||||
</div>
|
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>
|
||||||
|
</Tooltip>
|
||||||
{createMenuOpen && newTaskRepos.length > 1 ? (
|
{createMenuOpen && newTaskRepos.length > 1 ? (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
|
|
@ -303,7 +467,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectNewTaskRepo(repo.id);
|
onSelectNewTaskRepo(repo.id);
|
||||||
setCreateMenuOpen(false);
|
setCreateMenuOpen(false);
|
||||||
onCreate();
|
onCreate(repo.id);
|
||||||
}}
|
}}
|
||||||
className={css({
|
className={css({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -442,9 +606,31 @@ export const Sidebar = memo(function Sidebar({
|
||||||
>
|
>
|
||||||
{projectInitial(project.label)}
|
{projectInitial(project.label)}
|
||||||
</span>
|
</span>
|
||||||
<span className={css({ position: "absolute", inset: 0, display: "none", alignItems: "center", justifyContent: "center" })} data-chevron>
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCollapsedProjects((current) => ({
|
||||||
|
...current,
|
||||||
|
[project.id]: !current[project.id],
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className={css({
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "none",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
})}
|
||||||
|
data-chevron
|
||||||
|
>
|
||||||
{isCollapsed ? <ChevronDown size={12} color={t.textTertiary} /> : <ChevronUp size={12} color={t.textTertiary} />}
|
{isCollapsed ? <ChevronDown size={12} color={t.textTertiary} /> : <ChevronUp size={12} color={t.textTertiary} />}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<LabelSmall
|
<LabelSmall
|
||||||
color={t.textSecondary}
|
color={t.textSecondary}
|
||||||
|
|
@ -468,7 +654,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setHoveredProjectId(null);
|
setHoveredProjectId(null);
|
||||||
onSelectNewTaskRepo(project.id);
|
onSelectNewTaskRepo(project.id);
|
||||||
onCreate();
|
onCreate(project.id);
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
className={css({
|
className={css({
|
||||||
|
|
@ -543,7 +729,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
position: "relative",
|
position: "relative",
|
||||||
backgroundColor: isActive ? t.interactiveHover : "transparent",
|
backgroundColor: isActive ? t.interactiveHover : "transparent",
|
||||||
opacity: isTaskBeingDragged ? 0.4 : 1,
|
opacity: isTaskBeingDragged ? 0.4 : 1,
|
||||||
cursor: "pointer",
|
cursor: drag?.type === "task" ? "grabbing" : "pointer",
|
||||||
transition: "all 150ms ease",
|
transition: "all 150ms ease",
|
||||||
"::before": {
|
"::before": {
|
||||||
content: '""',
|
content: '""',
|
||||||
|
|
@ -607,6 +793,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
{formatRelativeAge(task.updatedAtMs)}
|
{formatRelativeAge(task.updatedAtMs)}
|
||||||
</LabelXSmall>
|
</LabelXSmall>
|
||||||
</div>
|
</div>
|
||||||
|
{task.presence.length > 0 && <PresenceAvatars presence={task.presence} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -658,7 +845,7 @@ export const Sidebar = memo(function Sidebar({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollBody>
|
</ScrollBody>
|
||||||
<SidebarFooter />
|
{!hideSettings && <SidebarFooter />}
|
||||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||||
</SPanel>
|
</SPanel>
|
||||||
);
|
);
|
||||||
|
|
@ -945,34 +1132,36 @@ function SidebarFooter() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={css({ padding: "8px" })}>
|
<div className={css({ padding: "8px" })}>
|
||||||
<button
|
<Tooltip label="Settings" placement="right">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
setOpen((prev) => {
|
onClick={() => {
|
||||||
if (prev) setWorkspaceFlyoutOpen(false);
|
setOpen((prev) => {
|
||||||
return !prev;
|
if (prev) setWorkspaceFlyoutOpen(false);
|
||||||
});
|
return !prev;
|
||||||
}}
|
});
|
||||||
className={css({
|
}}
|
||||||
display: "flex",
|
className={css({
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
width: "28px",
|
justifyContent: "center",
|
||||||
height: "28px",
|
width: "28px",
|
||||||
borderRadius: "6px",
|
height: "28px",
|
||||||
border: "none",
|
borderRadius: "6px",
|
||||||
background: open ? t.interactiveHover : "transparent",
|
border: "none",
|
||||||
color: open ? t.textPrimary : t.textTertiary,
|
background: open ? t.interactiveHover : "transparent",
|
||||||
cursor: "pointer",
|
color: open ? t.textPrimary : t.textTertiary,
|
||||||
transition: "all 160ms ease",
|
cursor: "pointer",
|
||||||
":hover": {
|
transition: "all 160ms ease",
|
||||||
backgroundColor: t.interactiveHover,
|
":hover": {
|
||||||
color: t.textSecondary,
|
backgroundColor: t.interactiveHover,
|
||||||
},
|
color: t.textSecondary,
|
||||||
})}
|
},
|
||||||
>
|
})}
|
||||||
<Settings size={14} />
|
>
|
||||||
</button>
|
<Settings size={14} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useStyletron } from "baseui";
|
import { useStyletron } from "baseui";
|
||||||
import { LabelXSmall } from "baseui/typography";
|
import { LabelXSmall } from "baseui/typography";
|
||||||
import { FileCode, Plus, X } from "lucide-react";
|
import { FileCode, Plus, SquareTerminal, X } from "lucide-react";
|
||||||
|
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
import { ContextMenuOverlay, TabAvatar, useContextMenu } from "./ui";
|
import { ContextMenuOverlay, TabAvatar, Tooltip, useContextMenu } from "./ui";
|
||||||
import { diffTabId, fileName, type Task } from "./view-model";
|
import { diffTabId, fileName, terminalTabId, type Task } from "./view-model";
|
||||||
|
|
||||||
export const TabStrip = memo(function TabStrip({
|
export const TabStrip = memo(function TabStrip({
|
||||||
task,
|
task,
|
||||||
|
|
@ -22,6 +22,8 @@ export const TabStrip = memo(function TabStrip({
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseDiffTab,
|
onCloseDiffTab,
|
||||||
onAddTab,
|
onAddTab,
|
||||||
|
terminalTabOpen,
|
||||||
|
onCloseTerminalTab,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
}: {
|
}: {
|
||||||
task: Task;
|
task: Task;
|
||||||
|
|
@ -38,6 +40,8 @@ export const TabStrip = memo(function TabStrip({
|
||||||
onCloseTab: (tabId: string) => void;
|
onCloseTab: (tabId: string) => void;
|
||||||
onCloseDiffTab: (path: string) => void;
|
onCloseDiffTab: (path: string) => void;
|
||||||
onAddTab: () => void;
|
onAddTab: () => void;
|
||||||
|
terminalTabOpen?: boolean;
|
||||||
|
onCloseTerminalTab?: () => void;
|
||||||
sidebarCollapsed?: boolean;
|
sidebarCollapsed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
|
|
@ -216,21 +220,71 @@ export const TabStrip = memo(function TabStrip({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div
|
{terminalTabOpen
|
||||||
onClick={onAddTab}
|
? (() => {
|
||||||
className={css({
|
const tabId = terminalTabId();
|
||||||
display: "flex",
|
const isActive = tabId === activeTabId;
|
||||||
alignItems: "center",
|
return (
|
||||||
padding: "0 10px",
|
<div
|
||||||
cursor: "pointer",
|
key={tabId}
|
||||||
opacity: 0.4,
|
onClick={() => onSwitchTab(tabId)}
|
||||||
lineHeight: 0,
|
onMouseDown={(event) => {
|
||||||
":hover": { opacity: 0.7 },
|
if (event.button === 1) {
|
||||||
flexShrink: 0,
|
event.preventDefault();
|
||||||
})}
|
onCloseTerminalTab?.();
|
||||||
>
|
}
|
||||||
<Plus size={14} color={t.textTertiary} />
|
}}
|
||||||
</div>
|
data-tab
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
padding: "4px 12px",
|
||||||
|
marginTop: "6px",
|
||||||
|
marginBottom: "6px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundColor: isActive ? t.interactiveHover : "transparent",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "color 200ms ease, background-color 200ms ease",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover": { color: t.textPrimary, backgroundColor: isActive ? t.interactiveHover : t.interactiveSubtle },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SquareTerminal size={12} color={isActive ? t.textPrimary : t.textSecondary} />
|
||||||
|
<LabelXSmall color={isActive ? t.textPrimary : t.textSecondary} $style={{ fontWeight: 500 }}>
|
||||||
|
Terminal
|
||||||
|
</LabelXSmall>
|
||||||
|
<X
|
||||||
|
size={11}
|
||||||
|
color={t.textTertiary}
|
||||||
|
data-tab-close
|
||||||
|
className={css({ cursor: "pointer", opacity: 0 })}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onCloseTerminalTab?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: null}
|
||||||
|
<Tooltip label="New session" placement="bottom">
|
||||||
|
<div
|
||||||
|
onClick={onAddTab}
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 10px",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: 0.4,
|
||||||
|
lineHeight: 0,
|
||||||
|
":hover": { opacity: 0.7 },
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Plus size={14} color={t.textTertiary} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ProcessTerminal } from "@sandbox-agent/react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useStyletron } from "baseui";
|
import { useStyletron } from "baseui";
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
import { ChevronDown, ChevronUp, Plus, SquareTerminal, Trash2 } from "lucide-react";
|
import { ArrowUpLeft, ChevronDown, ChevronUp, Plus, SquareTerminal, Trash2 } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { backendClient } from "../../lib/backend";
|
import { backendClient } from "../../lib/backend";
|
||||||
|
|
@ -12,11 +12,21 @@ interface TerminalPaneProps {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
taskId: string | null;
|
taskId: string | null;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
|
hideHeader?: boolean;
|
||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
onCollapse?: () => void;
|
onCollapse?: () => void;
|
||||||
onStartResize?: (e: React.PointerEvent) => void;
|
onStartResize?: (e: React.PointerEvent) => void;
|
||||||
|
onOpenTerminalTab?: () => void;
|
||||||
|
processTabs?: ProcessTab[];
|
||||||
|
onProcessTabsChange?: (tabs: ProcessTab[]) => void;
|
||||||
|
activeProcessTabId?: string | null;
|
||||||
|
onActiveProcessTabIdChange?: (id: string | null) => void;
|
||||||
|
customTabNames?: Record<string, string>;
|
||||||
|
onCustomTabNamesChange?: (names: Record<string, string>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { ProcessTab };
|
||||||
|
|
||||||
interface ProcessTab {
|
interface ProcessTab {
|
||||||
id: string;
|
id: string;
|
||||||
processId: string;
|
processId: string;
|
||||||
|
|
@ -94,15 +104,66 @@ function HeaderIconButton({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onCollapse, onStartResize }: TerminalPaneProps) {
|
export function TerminalPane({
|
||||||
|
workspaceId,
|
||||||
|
taskId,
|
||||||
|
isExpanded,
|
||||||
|
hideHeader,
|
||||||
|
onExpand,
|
||||||
|
onCollapse,
|
||||||
|
onStartResize,
|
||||||
|
onOpenTerminalTab,
|
||||||
|
processTabs: controlledProcessTabs,
|
||||||
|
onProcessTabsChange,
|
||||||
|
activeProcessTabId: controlledActiveTabId,
|
||||||
|
onActiveProcessTabIdChange,
|
||||||
|
customTabNames: controlledCustomTabNames,
|
||||||
|
onCustomTabNamesChange,
|
||||||
|
}: TerminalPaneProps) {
|
||||||
const [css] = useStyletron();
|
const [css] = useStyletron();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
const [internalActiveTabId, setInternalActiveTabId] = useState<string | null>(null);
|
||||||
const [processTabs, setProcessTabs] = useState<ProcessTab[]>([]);
|
const [internalProcessTabs, setInternalProcessTabs] = useState<ProcessTab[]>([]);
|
||||||
const [creatingProcess, setCreatingProcess] = useState(false);
|
const [creatingProcess, setCreatingProcess] = useState(false);
|
||||||
const [hoveredTabId, setHoveredTabId] = useState<string | null>(null);
|
const [hoveredTabId, setHoveredTabId] = useState<string | null>(null);
|
||||||
const [terminalClient, setTerminalClient] = useState<SandboxAgent | null>(null);
|
const [terminalClient, setTerminalClient] = useState<SandboxAgent | null>(null);
|
||||||
const [customTabNames, setCustomTabNames] = useState<Record<string, string>>({});
|
const [internalCustomTabNames, setInternalCustomTabNames] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const processTabs = controlledProcessTabs ?? internalProcessTabs;
|
||||||
|
const setProcessTabs = useCallback(
|
||||||
|
(update: ProcessTab[] | ((prev: ProcessTab[]) => ProcessTab[])) => {
|
||||||
|
if (onProcessTabsChange) {
|
||||||
|
const next = typeof update === "function" ? update(controlledProcessTabs ?? []) : update;
|
||||||
|
onProcessTabsChange(next);
|
||||||
|
} else {
|
||||||
|
setInternalProcessTabs(update);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onProcessTabsChange, controlledProcessTabs],
|
||||||
|
);
|
||||||
|
const activeTabId = controlledActiveTabId !== undefined ? controlledActiveTabId : internalActiveTabId;
|
||||||
|
const setActiveTabId = useCallback(
|
||||||
|
(id: string | null) => {
|
||||||
|
if (onActiveProcessTabIdChange) {
|
||||||
|
onActiveProcessTabIdChange(id);
|
||||||
|
} else {
|
||||||
|
setInternalActiveTabId(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onActiveProcessTabIdChange],
|
||||||
|
);
|
||||||
|
const customTabNames = controlledCustomTabNames ?? internalCustomTabNames;
|
||||||
|
const setCustomTabNames = useCallback(
|
||||||
|
(update: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)) => {
|
||||||
|
if (onCustomTabNamesChange) {
|
||||||
|
const next = typeof update === "function" ? update(controlledCustomTabNames ?? {}) : update;
|
||||||
|
onCustomTabNamesChange(next);
|
||||||
|
} else {
|
||||||
|
setInternalCustomTabNames(update);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onCustomTabNamesChange, controlledCustomTabNames],
|
||||||
|
);
|
||||||
const [editingTabId, setEditingTabId] = useState<string | null>(null);
|
const [editingTabId, setEditingTabId] = useState<string | null>(null);
|
||||||
const editInputRef = useRef<HTMLInputElement>(null);
|
const editInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -135,7 +196,7 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl
|
||||||
setProcessTabs((prev) => {
|
setProcessTabs((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
const [moved] = next.splice(d.fromIdx, 1);
|
const [moved] = next.splice(d.fromIdx, 1);
|
||||||
next.splice(d.overIdx!, 0, moved);
|
if (moved) next.splice(d.overIdx!, 0, moved);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -306,43 +367,48 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl
|
||||||
};
|
};
|
||||||
}, [terminalClient]);
|
}, [terminalClient]);
|
||||||
|
|
||||||
|
// Only reset on taskId change when using internal (uncontrolled) state.
|
||||||
|
// When controlled, the parent (MockLayout) owns per-task state via keyed records.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTabId(null);
|
if (!controlledProcessTabs) {
|
||||||
setProcessTabs([]);
|
setActiveTabId(null);
|
||||||
}, [taskId]);
|
setProcessTabs([]);
|
||||||
|
}
|
||||||
|
}, [taskId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const processes = processesQuery.data?.processes ?? [];
|
const processes = processesQuery.data?.processes ?? [];
|
||||||
|
|
||||||
const openTerminalTab = useCallback((process: SandboxProcessRecord) => {
|
const openTerminalTab = useCallback(
|
||||||
setProcessTabs((current) => {
|
(process: SandboxProcessRecord) => {
|
||||||
const existing = current.find((tab) => tab.processId === process.id);
|
setProcessTabs((current) => {
|
||||||
if (existing) {
|
const existing = current.find((tab) => tab.processId === process.id);
|
||||||
setActiveTabId(existing.id);
|
if (existing) {
|
||||||
return current;
|
setActiveTabId(existing.id);
|
||||||
}
|
return current;
|
||||||
|
|
||||||
const nextTab: ProcessTab = {
|
|
||||||
id: `terminal:${process.id}`,
|
|
||||||
processId: process.id,
|
|
||||||
title: formatProcessTabTitle(process, current.length + 1),
|
|
||||||
};
|
|
||||||
setActiveTabId(nextTab.id);
|
|
||||||
return [...current, nextTab];
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeTerminalTab = useCallback((tabId: string) => {
|
|
||||||
setProcessTabs((current) => {
|
|
||||||
const next = current.filter((tab) => tab.id !== tabId);
|
|
||||||
setActiveTabId((currentActive) => {
|
|
||||||
if (currentActive === tabId) {
|
|
||||||
return next.length > 0 ? next[next.length - 1]!.id : null;
|
|
||||||
}
|
}
|
||||||
return currentActive;
|
|
||||||
|
const nextTab: ProcessTab = {
|
||||||
|
id: `terminal:${process.id}`,
|
||||||
|
processId: process.id,
|
||||||
|
title: formatProcessTabTitle(process, current.length + 1),
|
||||||
|
};
|
||||||
|
setActiveTabId(nextTab.id);
|
||||||
|
return [...current, nextTab];
|
||||||
});
|
});
|
||||||
return next;
|
},
|
||||||
});
|
[setProcessTabs, setActiveTabId],
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
|
const closeTerminalTab = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
const next = processTabs.filter((tab) => tab.id !== tabId);
|
||||||
|
setProcessTabs(next);
|
||||||
|
if (activeTabId === tabId) {
|
||||||
|
setActiveTabId(next.length > 0 ? next[next.length - 1]!.id : null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[processTabs, activeTabId, setProcessTabs, setActiveTabId],
|
||||||
|
);
|
||||||
|
|
||||||
const spawnTerminal = useCallback(async () => {
|
const spawnTerminal = useCallback(async () => {
|
||||||
if (!activeSandbox?.sandboxId) {
|
if (!activeSandbox?.sandboxId) {
|
||||||
|
|
@ -527,25 +593,27 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Resize handle */}
|
{/* Resize handle — hidden when in tab view */}
|
||||||
<div
|
{!hideHeader && (
|
||||||
onPointerDown={onStartResize}
|
<div
|
||||||
className={css({
|
onPointerDown={onStartResize}
|
||||||
height: "3px",
|
className={css({
|
||||||
flexShrink: 0,
|
height: "3px",
|
||||||
cursor: "ns-resize",
|
flexShrink: 0,
|
||||||
position: "relative",
|
cursor: "ns-resize",
|
||||||
"::before": {
|
position: "relative",
|
||||||
content: '""',
|
"::before": {
|
||||||
position: "absolute",
|
content: '""',
|
||||||
top: "-2px",
|
position: "absolute",
|
||||||
left: 0,
|
top: "-2px",
|
||||||
right: 0,
|
left: 0,
|
||||||
height: "7px",
|
right: 0,
|
||||||
},
|
height: "7px",
|
||||||
})}
|
},
|
||||||
/>
|
})}
|
||||||
{/* Full-width header bar */}
|
/>
|
||||||
|
)}
|
||||||
|
{/* Header bar — in tab view, only show action buttons (no title/expand/chevron) */}
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -554,13 +622,17 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl
|
||||||
minHeight: "39px",
|
minHeight: "39px",
|
||||||
maxHeight: "39px",
|
maxHeight: "39px",
|
||||||
padding: "0 14px",
|
padding: "0 14px",
|
||||||
borderTop: `1px solid ${t.borderDefault}`,
|
borderTop: hideHeader ? "none" : `1px solid ${t.borderDefault}`,
|
||||||
backgroundColor: t.surfacePrimary,
|
backgroundColor: t.surfacePrimary,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<SquareTerminal size={14} color={t.textTertiary} />
|
{!hideHeader && (
|
||||||
<span className={css({ fontSize: "12px", fontWeight: 600, color: t.textSecondary })}>Terminal</span>
|
<>
|
||||||
|
<SquareTerminal size={14} color={t.textTertiary} />
|
||||||
|
<span className={css({ fontSize: "12px", fontWeight: 600, color: t.textSecondary })}>Terminal</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className={css({ flex: 1 })} />
|
<div className={css({ flex: 1 })} />
|
||||||
<div className={css({ display: "flex", alignItems: "center", gap: "2px" })}>
|
<div className={css({ display: "flex", alignItems: "center", gap: "2px" })}>
|
||||||
<HeaderIconButton
|
<HeaderIconButton
|
||||||
|
|
@ -585,14 +657,21 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl
|
||||||
>
|
>
|
||||||
<Trash2 size={13} />
|
<Trash2 size={13} />
|
||||||
</HeaderIconButton>
|
</HeaderIconButton>
|
||||||
<HeaderIconButton css={css} t={t} label={isExpanded ? "Collapse terminal" : "Expand terminal"} onClick={isExpanded ? onCollapse : onExpand}>
|
{!hideHeader && onOpenTerminalTab ? (
|
||||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
<HeaderIconButton css={css} t={t} label="Open terminal in tab" onClick={onOpenTerminalTab}>
|
||||||
</HeaderIconButton>
|
<ArrowUpLeft size={13} />
|
||||||
|
</HeaderIconButton>
|
||||||
|
) : null}
|
||||||
|
{!hideHeader && (
|
||||||
|
<HeaderIconButton css={css} t={t} label={isExpanded ? "Collapse terminal" : "Expand terminal"} onClick={isExpanded ? onCollapse : onExpand}>
|
||||||
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
||||||
|
</HeaderIconButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Two-column body: terminal left, list right — hidden when no tabs */}
|
{/* Two-column body: terminal left, list right — visible when expanded or when tabs exist */}
|
||||||
{processTabs.length > 0 && (
|
{(processTabs.length > 0 || hideHeader) && (
|
||||||
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "row" })}>
|
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "row" })}>
|
||||||
{/* Left: terminal content */}
|
{/* Left: terminal content */}
|
||||||
<div className={css({ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" })}>{renderBody()}</div>
|
<div className={css({ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" })}>{renderBody()}</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { LabelSmall } from "baseui/typography";
|
||||||
import { Clock, PanelLeft, PanelRight } from "lucide-react";
|
import { Clock, PanelLeft, PanelRight } from "lucide-react";
|
||||||
|
|
||||||
import { useFoundryTokens } from "../../app/theme";
|
import { useFoundryTokens } from "../../app/theme";
|
||||||
import { PanelHeaderBar } from "./ui";
|
import { PanelHeaderBar, Tooltip } from "./ui";
|
||||||
import { type AgentTab, type Task } from "./view-model";
|
import { type AgentTab, type Task } from "./view-model";
|
||||||
|
|
||||||
export const TranscriptHeader = memo(function TranscriptHeader({
|
export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
|
|
@ -50,25 +50,27 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
return (
|
return (
|
||||||
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
|
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
|
||||||
{sidebarCollapsed && onToggleSidebar ? (
|
{sidebarCollapsed && onToggleSidebar ? (
|
||||||
<div
|
<Tooltip label="Toggle sidebar" placement="bottom">
|
||||||
className={css({
|
<div
|
||||||
width: "26px",
|
className={css({
|
||||||
height: "26px",
|
width: "26px",
|
||||||
borderRadius: "6px",
|
height: "26px",
|
||||||
display: "flex",
|
borderRadius: "6px",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
justifyContent: "center",
|
||||||
color: t.textTertiary,
|
cursor: "pointer",
|
||||||
flexShrink: 0,
|
color: t.textTertiary,
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
flexShrink: 0,
|
||||||
})}
|
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||||
onClick={onToggleSidebar}
|
})}
|
||||||
onMouseEnter={onSidebarPeekStart}
|
onClick={onToggleSidebar}
|
||||||
onMouseLeave={onSidebarPeekEnd}
|
onMouseEnter={onSidebarPeekStart}
|
||||||
>
|
onMouseLeave={onSidebarPeekEnd}
|
||||||
<PanelLeft size={14} />
|
>
|
||||||
</div>
|
<PanelLeft size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
{editingField === "title" ? (
|
{editingField === "title" ? (
|
||||||
<input
|
<input
|
||||||
|
|
@ -190,23 +192,25 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
<span>{task.minutesUsed ?? 0} min used</span>
|
<span>{task.minutesUsed ?? 0} min used</span>
|
||||||
</div>
|
</div>
|
||||||
{rightSidebarCollapsed && onToggleRightSidebar ? (
|
{rightSidebarCollapsed && onToggleRightSidebar ? (
|
||||||
<div
|
<Tooltip label="Toggle changes" placement="bottom">
|
||||||
className={css({
|
<div
|
||||||
width: "26px",
|
className={css({
|
||||||
height: "26px",
|
width: "26px",
|
||||||
borderRadius: "6px",
|
height: "26px",
|
||||||
display: "flex",
|
borderRadius: "6px",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
justifyContent: "center",
|
||||||
color: t.textTertiary,
|
cursor: "pointer",
|
||||||
flexShrink: 0,
|
color: t.textTertiary,
|
||||||
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
flexShrink: 0,
|
||||||
})}
|
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
|
||||||
onClick={onToggleRightSidebar}
|
})}
|
||||||
>
|
onClick={onToggleRightSidebar}
|
||||||
<PanelRight size={14} />
|
>
|
||||||
</div>
|
<PanelRight size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</PanelHeaderBar>
|
</PanelHeaderBar>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo, useCallback, useEffect, useState, type MouseEvent } from "react";
|
import { memo, useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { styled, useStyletron } from "baseui";
|
import { styled, useStyletron } from "baseui";
|
||||||
import { GitPullRequest, GitPullRequestDraft } from "lucide-react";
|
import { GitPullRequest, GitPullRequestDraft } from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -210,6 +211,115 @@ export const ScrollBody = styled("div", () => ({
|
||||||
flexDirection: "column" as const,
|
flexDirection: "column" as const,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const Tooltip = memo(function Tooltip({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
placement = "bottom",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: ReactNode;
|
||||||
|
placement?: "top" | "bottom" | "left" | "right";
|
||||||
|
}) {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
const show = useCallback(() => {
|
||||||
|
const el = triggerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
let top: number;
|
||||||
|
let left: number;
|
||||||
|
if (placement === "bottom") {
|
||||||
|
top = rect.bottom + 6;
|
||||||
|
left = rect.left + rect.width / 2;
|
||||||
|
} else if (placement === "top") {
|
||||||
|
top = rect.top - 6;
|
||||||
|
left = rect.left + rect.width / 2;
|
||||||
|
} else if (placement === "left") {
|
||||||
|
top = rect.top + rect.height / 2;
|
||||||
|
left = rect.left - 6;
|
||||||
|
} else {
|
||||||
|
top = rect.top + rect.height / 2;
|
||||||
|
left = rect.right + 6;
|
||||||
|
}
|
||||||
|
setPos({ top, left });
|
||||||
|
}, [placement]);
|
||||||
|
|
||||||
|
const hide = useCallback(() => setPos(null), []);
|
||||||
|
|
||||||
|
// Clamp tooltip position after it renders so it stays within the viewport
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pos) return;
|
||||||
|
const tip = tooltipRef.current;
|
||||||
|
if (!tip) return;
|
||||||
|
const tipRect = tip.getBoundingClientRect();
|
||||||
|
const pad = 8;
|
||||||
|
let adjustLeft = 0;
|
||||||
|
let adjustTop = 0;
|
||||||
|
if (tipRect.right > window.innerWidth - pad) {
|
||||||
|
adjustLeft = window.innerWidth - pad - tipRect.right;
|
||||||
|
}
|
||||||
|
if (tipRect.left < pad) {
|
||||||
|
adjustLeft = pad - tipRect.left;
|
||||||
|
}
|
||||||
|
if (tipRect.bottom > window.innerHeight - pad) {
|
||||||
|
adjustTop = window.innerHeight - pad - tipRect.bottom;
|
||||||
|
}
|
||||||
|
if (tipRect.top < pad) {
|
||||||
|
adjustTop = pad - tipRect.top;
|
||||||
|
}
|
||||||
|
if (adjustLeft !== 0 || adjustTop !== 0) {
|
||||||
|
setPos((prev) => prev && { top: prev.top + adjustTop, left: prev.left + adjustLeft });
|
||||||
|
}
|
||||||
|
}, [pos]);
|
||||||
|
|
||||||
|
const transform =
|
||||||
|
placement === "bottom"
|
||||||
|
? "translateX(-50%)"
|
||||||
|
: placement === "top"
|
||||||
|
? "translateX(-50%) translateY(-100%)"
|
||||||
|
: placement === "left"
|
||||||
|
? "translateX(-100%) translateY(-50%)"
|
||||||
|
: "translateY(-50%)";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={triggerRef} onMouseEnter={show} onMouseLeave={hide} className={css({ display: "inline-flex" })}>
|
||||||
|
{children}
|
||||||
|
{pos &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
className={css({
|
||||||
|
position: "fixed",
|
||||||
|
top: `${pos.top}px`,
|
||||||
|
left: `${pos.left}px`,
|
||||||
|
transform,
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: "none",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "rgba(32, 32, 32, 0.98)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.4)",
|
||||||
|
color: "#e0e0e0",
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "1.3",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const HEADER_HEIGHT = "42px";
|
export const HEADER_HEIGHT = "42px";
|
||||||
export const PROMPT_TEXTAREA_MIN_HEIGHT = 56;
|
export const PROMPT_TEXTAREA_MIN_HEIGHT = 56;
|
||||||
export const PROMPT_TEXTAREA_MAX_HEIGHT = 100;
|
export const PROMPT_TEXTAREA_MAX_HEIGHT = 100;
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export function providerAgent(provider: string): AgentKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIFF_PREFIX = "diff:";
|
const DIFF_PREFIX = "diff:";
|
||||||
|
const TERMINAL_PREFIX = "terminal:";
|
||||||
|
|
||||||
export function isDiffTab(id: string): boolean {
|
export function isDiffTab(id: string): boolean {
|
||||||
return id.startsWith(DIFF_PREFIX);
|
return id.startsWith(DIFF_PREFIX);
|
||||||
|
|
@ -115,6 +116,14 @@ export function diffTabId(path: string): string {
|
||||||
return `${DIFF_PREFIX}${path}`;
|
return `${DIFF_PREFIX}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTerminalTab(id: string): boolean {
|
||||||
|
return id.startsWith(TERMINAL_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function terminalTabId(): string {
|
||||||
|
return `${TERMINAL_PREFIX}main`;
|
||||||
|
}
|
||||||
|
|
||||||
export function fileName(path: string): string {
|
export function fileName(path: string): string {
|
||||||
return path.split("/").pop() ?? path;
|
return path.split("/").pop() ?? path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { type FoundryBillingPlanId, type FoundryOrganization, type FoundryOrganizationMember, type FoundryUser } from "@sandbox-agent/foundry-shared";
|
import { type FoundryBillingPlanId, type FoundryOrganization, type FoundryOrganizationMember, type FoundryUser } from "@sandbox-agent/foundry-shared";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, Clock, CreditCard, FileText, Github, LogOut, Moon, Settings, Sun, Users } from "lucide-react";
|
import { ArrowLeft, Clock, CreditCard, FileText, Github, LogOut, Moon, Settings, Sun, Users, Volume2 } from "lucide-react";
|
||||||
|
import { NOTIFICATION_SOUND_OPTIONS, previewNotificationSound, useNotificationSound } from "../lib/notification-sound";
|
||||||
import { activeMockUser, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../lib/mock-app";
|
import { activeMockUser, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../lib/mock-app";
|
||||||
import { isMockFrontendClient } from "../lib/env";
|
import { isMockFrontendClient } from "../lib/env";
|
||||||
|
import { useIsMobile } from "../lib/platform";
|
||||||
import { useColorMode, useFoundryTokens } from "../app/theme";
|
import { useColorMode, useFoundryTokens } from "../app/theme";
|
||||||
import type { FoundryTokens } from "../styles/tokens";
|
import type { FoundryTokens } from "../styles/tokens";
|
||||||
import { appSurfaceStyle, primaryButtonStyle, secondaryButtonStyle, subtleButtonStyle, cardStyle, badgeStyle, inputStyle } from "../styles/shared-styles";
|
import { appSurfaceStyle, primaryButtonStyle, secondaryButtonStyle, subtleButtonStyle, cardStyle, badgeStyle, inputStyle } from "../styles/shared-styles";
|
||||||
|
|
@ -124,7 +126,7 @@ function statusBadge(t: FoundryTokens, organization: FoundryOrganization) {
|
||||||
|
|
||||||
function githubBadge(t: FoundryTokens, organization: FoundryOrganization) {
|
function githubBadge(t: FoundryTokens, organization: FoundryOrganization) {
|
||||||
if (organization.github.installationStatus === "connected") {
|
if (organization.github.installationStatus === "connected") {
|
||||||
return <span style={badgeStyle(t, "rgba(46, 160, 67, 0.16)", "#b7f0c3")}>GitHub connected</span>;
|
return <span style={badgeStyle(t, "rgba(46, 160, 67, 0.16)", "#1a7f37")}>GitHub connected</span>;
|
||||||
}
|
}
|
||||||
if (organization.github.installationStatus === "reconnect_required") {
|
if (organization.github.installationStatus === "reconnect_required") {
|
||||||
return <span style={badgeStyle(t, "rgba(255, 193, 7, 0.18)", "#ffe6a6")}>Reconnect required</span>;
|
return <span style={badgeStyle(t, "rgba(255, 193, 7, 0.18)", "#ffe6a6")}>Reconnect required</span>;
|
||||||
|
|
@ -164,9 +166,42 @@ function MemberRow({ member }: { member: FoundryOrganizationMember }) {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div style={{ display: "flex", alignItems: "center", gap: "8px", overflow: "hidden" }}>
|
||||||
<div style={{ fontWeight: 500, fontSize: "12px" }}>{member.name}</div>
|
{member.avatarUrl ? (
|
||||||
<div style={{ color: t.textSecondary, fontSize: "11px" }}>{member.email}</div>
|
<img
|
||||||
|
src={member.avatarUrl}
|
||||||
|
alt={member.name}
|
||||||
|
style={{
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
flexShrink: 0,
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: t.interactiveHover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.textSecondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{member.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ overflow: "hidden" }}>
|
||||||
|
<div style={{ fontWeight: 500, fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{member.name}</div>
|
||||||
|
<div style={{ color: t.textSecondary, fontSize: "11px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{member.email}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: t.textSecondary, fontSize: "12px", textTransform: "capitalize" }}>{member.role}</div>
|
<div style={{ color: t.textSecondary, fontSize: "12px", textTransform: "capitalize" }}>{member.role}</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -551,16 +586,130 @@ function SettingsLayout({
|
||||||
const user = activeMockUser(snapshot);
|
const user = activeMockUser(snapshot);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const navSections: Array<{ section: SettingsSection; icon: React.ReactNode; label: string }> = [
|
const navSections: Array<{ section: SettingsSection; icon: React.ReactNode; label: string }> = [
|
||||||
{ section: "settings", icon: <Settings size={13} />, label: "Settings" },
|
{ section: "settings", icon: <Settings size={13} />, label: "Settings" },
|
||||||
{ section: "members", icon: <Users size={13} />, label: "Members" },
|
{ section: "members", icon: <Users size={13} />, label: "Members" },
|
||||||
{ section: "billing", icon: <CreditCard size={13} />, label: "Billing & Invoices" },
|
{ section: "billing", icon: <CreditCard size={13} />, label: "Billing" },
|
||||||
{ section: "docs", icon: <FileText size={13} />, label: "Docs" },
|
{ section: "docs", icon: <FileText size={13} />, label: "Docs" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
void (async () => {
|
||||||
|
await client.selectOrganization(organization.id);
|
||||||
|
await navigate({ to: workspacePath(organization) });
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavClick = (item: (typeof navSections)[0]) => {
|
||||||
|
if (item.section === "billing") {
|
||||||
|
void navigate({ to: billingPath(organization) });
|
||||||
|
} else if (onSectionChange) {
|
||||||
|
onSectionChange(item.section);
|
||||||
|
} else {
|
||||||
|
void navigate({ to: settingsPath(organization) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...appSurfaceStyle(t),
|
||||||
|
height: "100dvh",
|
||||||
|
maxHeight: "100dvh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
paddingTop: "max(var(--safe-area-top), 47px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mobile header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
padding: "8px 12px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
borderBottom: `1px solid ${t.borderSubtle}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={goBack}
|
||||||
|
style={{
|
||||||
|
...subtleButtonStyle(t),
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: "8px",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: "15px", fontWeight: 600 }}>{organization.settings.displayName}</div>
|
||||||
|
<div style={{ fontSize: "11px", color: t.textMuted }}>{planCatalog[organization.billing.planId]?.label ?? "Free"} Plan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile tab strip */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
gap: "2px",
|
||||||
|
padding: "6px 12px",
|
||||||
|
overflowX: "auto",
|
||||||
|
borderBottom: `1px solid ${t.borderSubtle}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{navSections.map((item) => {
|
||||||
|
const isActive = activeSection === item.section;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.section}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleNavClick(item)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: isActive ? t.interactiveHover : "transparent",
|
||||||
|
color: isActive ? t.textPrimary : t.textMuted,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: isActive ? 500 : 400,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", padding: "20px 16px 40px" }}>
|
||||||
|
<div style={{ maxWidth: "560px" }}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={appSurfaceStyle(t)}>
|
<div style={{ ...appSurfaceStyle(t), height: "100dvh", maxHeight: "100dvh" }}>
|
||||||
<DesktopDragRegion />
|
<DesktopDragRegion />
|
||||||
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
|
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
|
||||||
{/* Left nav */}
|
{/* Left nav */}
|
||||||
|
|
@ -579,12 +728,7 @@ function SettingsLayout({
|
||||||
{/* Back to workspace */}
|
{/* Back to workspace */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={goBack}
|
||||||
void (async () => {
|
|
||||||
await client.selectOrganization(organization.id);
|
|
||||||
await navigate({ to: workspacePath(organization) });
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
...subtleButtonStyle(t),
|
...subtleButtonStyle(t),
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -612,15 +756,7 @@ function SettingsLayout({
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
active={activeSection === item.section}
|
active={activeSection === item.section}
|
||||||
onClick={() => {
|
onClick={() => handleNavClick(item)}
|
||||||
if (item.section === "billing") {
|
|
||||||
void navigate({ to: billingPath(organization) });
|
|
||||||
} else if (onSectionChange) {
|
|
||||||
onSectionChange(item.section);
|
|
||||||
} else {
|
|
||||||
void navigate({ to: settingsPath(organization) });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -692,6 +828,8 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
|
||||||
|
|
||||||
<AppearanceSection />
|
<AppearanceSection />
|
||||||
|
|
||||||
|
<NotificationSoundSection />
|
||||||
|
|
||||||
<SettingsContentSection
|
<SettingsContentSection
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
description={`Connected as ${organization.github.connectedAccount}. ${organization.github.importedRepoCount} repos imported.`}
|
description={`Connected as ${organization.github.connectedAccount}. ${organization.github.importedRepoCount} repos imported.`}
|
||||||
|
|
@ -1090,6 +1228,7 @@ export function MockAccountSettingsPage() {
|
||||||
const user = activeMockUser(snapshot);
|
const user = activeMockUser(snapshot);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const t = useFoundryTokens();
|
const t = useFoundryTokens();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [name, setName] = useState(user?.name ?? "");
|
const [name, setName] = useState(user?.name ?? "");
|
||||||
const [email, setEmail] = useState(user?.email ?? "");
|
const [email, setEmail] = useState(user?.email ?? "");
|
||||||
|
|
||||||
|
|
@ -1098,8 +1237,168 @@ export function MockAccountSettingsPage() {
|
||||||
setEmail(user?.email ?? "");
|
setEmail(user?.email ?? "");
|
||||||
}, [user?.name, user?.email]);
|
}, [user?.name, user?.email]);
|
||||||
|
|
||||||
|
const accountContent = (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
||||||
|
<div>
|
||||||
|
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Account</h1>
|
||||||
|
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Manage your personal account settings.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsContentSection title="Profile">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "12px", marginBottom: "4px" }}>
|
||||||
|
{user?.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={user.avatarUrl}
|
||||||
|
alt={user.name}
|
||||||
|
style={{
|
||||||
|
width: "48px",
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: `1px solid ${t.borderSubtle}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "48px",
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: t.interactiveHover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.textSecondary,
|
||||||
|
border: `1px solid ${t.borderSubtle}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(user?.name ?? "U").charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "13px", fontWeight: 600 }}>{user?.name ?? "User"}</div>
|
||||||
|
<div style={{ fontSize: "11px", color: t.textMuted }}>@{user?.githubLogin ?? ""}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label style={{ display: "grid", gap: "4px" }}>
|
||||||
|
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Display name</span>
|
||||||
|
<input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle(t)} />
|
||||||
|
</label>
|
||||||
|
<label style={{ display: "grid", gap: "4px" }}>
|
||||||
|
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Email</span>
|
||||||
|
<input value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle(t)} />
|
||||||
|
</label>
|
||||||
|
<label style={{ display: "grid", gap: "4px" }}>
|
||||||
|
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>GitHub</span>
|
||||||
|
<input value={`@${user?.githubLogin ?? ""}`} readOnly style={{ ...inputStyle(t), color: t.textMuted }} />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button type="button" style={primaryButtonStyle(t)}>
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingsContentSection>
|
||||||
|
|
||||||
|
<SettingsContentSection title="Sessions" description="Manage your active sessions across devices.">
|
||||||
|
<SettingsRow label="Current session" description="This device — signed in via GitHub OAuth." />
|
||||||
|
</SettingsContentSection>
|
||||||
|
|
||||||
|
<SettingsContentSection title="Sign out" description="Sign out of Foundry on this device.">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void (async () => {
|
||||||
|
await client.signOut();
|
||||||
|
await navigate({ to: "/signin" });
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
style={{ ...secondaryButtonStyle(t), display: "inline-flex", alignItems: "center", gap: "6px" }}
|
||||||
|
>
|
||||||
|
<LogOut size={12} />
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingsContentSection>
|
||||||
|
|
||||||
|
<SettingsContentSection title="Danger zone">
|
||||||
|
<SettingsRow
|
||||||
|
label="Delete account"
|
||||||
|
description="Permanently delete your account and all data."
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
...secondaryButtonStyle(t),
|
||||||
|
borderColor: "rgba(255, 110, 110, 0.24)",
|
||||||
|
color: t.statusError,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsContentSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...appSurfaceStyle(t),
|
||||||
|
height: "100dvh",
|
||||||
|
maxHeight: "100dvh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
paddingTop: "max(var(--safe-area-top), 47px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mobile header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
padding: "8px 12px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
borderBottom: `1px solid ${t.borderSubtle}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void navigate({ to: "/" })}
|
||||||
|
style={{
|
||||||
|
...subtleButtonStyle(t),
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: "8px",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<div style={{ fontSize: "15px", fontWeight: 600 }}>Account</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", padding: "20px 16px 40px" }}>
|
||||||
|
<div style={{ maxWidth: "560px" }}>{accountContent}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={appSurfaceStyle(t)}>
|
<div style={{ ...appSurfaceStyle(t), height: "100dvh", maxHeight: "100dvh" }}>
|
||||||
<DesktopDragRegion />
|
<DesktopDragRegion />
|
||||||
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
|
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
|
||||||
{/* Left nav */}
|
{/* Left nav */}
|
||||||
|
|
@ -1131,9 +1430,32 @@ export function MockAccountSettingsPage() {
|
||||||
Back to workspace
|
Back to workspace
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style={{ padding: "2px 10px 12px", display: "flex", flexDirection: "column", gap: "1px" }}>
|
<div style={{ padding: "2px 10px 12px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
<span style={{ fontSize: "12px", fontWeight: 600 }}>{user?.name ?? "User"}</span>
|
{user?.avatarUrl ? (
|
||||||
<span style={{ fontSize: "10px", color: t.textMuted }}>{user?.email ?? ""}</span>
|
<img src={user.avatarUrl} alt={user.name} style={{ width: "24px", height: "24px", borderRadius: "50%", objectFit: "cover", flexShrink: 0 }} />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: t.interactiveHover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.textSecondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(user?.name ?? "U").charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "1px", overflow: "hidden" }}>
|
||||||
|
<span style={{ fontSize: "12px", fontWeight: 600 }}>{user?.name ?? "User"}</span>
|
||||||
|
<span style={{ fontSize: "10px", color: t.textMuted }}>{user?.email ?? ""}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsNavItem icon={<Settings size={13} />} label="General" active onClick={() => {}} />
|
<SettingsNavItem icon={<Settings size={13} />} label="General" active onClick={() => {}} />
|
||||||
|
|
@ -1141,77 +1463,7 @@ export function MockAccountSettingsPage() {
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div style={{ flex: 1, overflowY: "auto", padding: "80px 36px 40px" }}>
|
<div style={{ flex: 1, overflowY: "auto", padding: "80px 36px 40px" }}>
|
||||||
<div style={{ maxWidth: "560px" }}>
|
<div style={{ maxWidth: "560px" }}>{accountContent}</div>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
|
||||||
<div>
|
|
||||||
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Account</h1>
|
|
||||||
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Manage your personal account settings.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SettingsContentSection title="Profile">
|
|
||||||
<label style={{ display: "grid", gap: "4px" }}>
|
|
||||||
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Display name</span>
|
|
||||||
<input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle(t)} />
|
|
||||||
</label>
|
|
||||||
<label style={{ display: "grid", gap: "4px" }}>
|
|
||||||
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Email</span>
|
|
||||||
<input value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle(t)} />
|
|
||||||
</label>
|
|
||||||
<label style={{ display: "grid", gap: "4px" }}>
|
|
||||||
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>GitHub</span>
|
|
||||||
<input value={`@${user?.githubLogin ?? ""}`} readOnly style={{ ...inputStyle(t), color: t.textMuted }} />
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<button type="button" style={primaryButtonStyle(t)}>
|
|
||||||
Save changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</SettingsContentSection>
|
|
||||||
|
|
||||||
<SettingsContentSection title="Sessions" description="Manage your active sessions across devices.">
|
|
||||||
<SettingsRow label="Current session" description="This device — signed in via GitHub OAuth." />
|
|
||||||
</SettingsContentSection>
|
|
||||||
|
|
||||||
<SettingsContentSection title="Sign out" description="Sign out of Foundry on this device.">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
void (async () => {
|
|
||||||
await client.signOut();
|
|
||||||
await navigate({ to: "/signin" });
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
style={{ ...secondaryButtonStyle(t), display: "inline-flex", alignItems: "center", gap: "6px" }}
|
|
||||||
>
|
|
||||||
<LogOut size={12} />
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</SettingsContentSection>
|
|
||||||
|
|
||||||
<SettingsContentSection title="Danger zone">
|
|
||||||
<SettingsRow
|
|
||||||
label="Delete account"
|
|
||||||
description="Permanently delete your account and all data."
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
...secondaryButtonStyle(t),
|
|
||||||
borderColor: "rgba(255, 110, 110, 0.24)",
|
|
||||||
color: t.statusError,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SettingsContentSection>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1238,7 +1490,7 @@ function AppearanceSection() {
|
||||||
height: "20px",
|
height: "20px",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
border: "1px solid rgba(128, 128, 128, 0.3)",
|
border: "1px solid rgba(128, 128, 128, 0.3)",
|
||||||
background: isDark ? t.borderDefault : t.accent,
|
background: isDark ? t.borderDefault : t.textPrimary,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
transition: "background 0.2s",
|
transition: "background 0.2s",
|
||||||
|
|
@ -1260,7 +1512,7 @@ function AppearanceSection() {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDark ? <Moon size={8} /> : <Sun size={8} color={t.accent} />}
|
{isDark ? <Moon size={8} /> : <Sun size={8} color={t.textPrimary} />}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
@ -1268,3 +1520,130 @@ function AppearanceSection() {
|
||||||
</SettingsContentSection>
|
</SettingsContentSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NotificationSoundSection() {
|
||||||
|
const t = useFoundryTokens();
|
||||||
|
const [selected, setSelected] = useNotificationSound();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const selectedLabel = NOTIFICATION_SOUND_OPTIONS.find((o) => o.id === selected)?.label ?? "None";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handler);
|
||||||
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContentSection title="Notifications" description="Play a sound when the agent finishes and needs your input.">
|
||||||
|
<SettingsRow
|
||||||
|
label="Completion sound"
|
||||||
|
description={selected === "none" ? "No sound will play." : `"${selectedLabel}" will play when the agent is done.`}
|
||||||
|
action={
|
||||||
|
<div ref={containerRef} style={{ position: "relative", display: "flex", alignItems: "center", gap: "6px" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
padding: "5px 10px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
background: t.interactiveSubtle,
|
||||||
|
color: t.textPrimary,
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
cursor: "pointer",
|
||||||
|
minWidth: "90px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{selectedLabel}</span>
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={t.textTertiary} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d={open ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6"} />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "calc(100% + 4px)",
|
||||||
|
right: 0,
|
||||||
|
minWidth: "160px",
|
||||||
|
backgroundColor: "rgba(32, 32, 32, 0.98)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
borderRadius: "10px",
|
||||||
|
border: `1px solid ${t.borderDefault}`,
|
||||||
|
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${t.interactiveSubtle}`,
|
||||||
|
padding: "4px 0",
|
||||||
|
zIndex: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{NOTIFICATION_SOUND_OPTIONS.map((option) => {
|
||||||
|
const isActive = option.id === selected;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(option.id);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLElement).style.backgroundColor = t.borderSubtle;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLElement).style.backgroundColor = "transparent";
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "8px",
|
||||||
|
width: "calc(100% - 8px)",
|
||||||
|
padding: "6px 12px",
|
||||||
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
color: isActive ? t.textPrimary : t.textSecondary,
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: isActive ? 600 : 400,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: "6px",
|
||||||
|
margin: "0 4px",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{option.id !== "none" && (
|
||||||
|
<Volume2
|
||||||
|
size={11}
|
||||||
|
color={isActive ? t.accent : t.textTertiary}
|
||||||
|
style={{ cursor: "pointer", flexShrink: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
previewNotificationSound(option.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsContentSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
77
foundry/packages/frontend/src/lib/notification-sound.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
export type NotificationSoundId = "none" | "chime" | "ping";
|
||||||
|
|
||||||
|
const SOUNDS: Record<Exclude<NotificationSoundId, "none">, { label: string; src: string }> = {
|
||||||
|
chime: { label: "Chime", src: "/sounds/notification-1.mp3" },
|
||||||
|
ping: { label: "Ping", src: "/sounds/notification-2.mp3" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORAGE_KEY = "foundry:notification-sound";
|
||||||
|
|
||||||
|
let currentValue: NotificationSoundId = (localStorage.getItem(STORAGE_KEY) as NotificationSoundId) || "none";
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function notify() {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot(): NotificationSoundId {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(listener: () => void): () => void {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNotificationSound(id: NotificationSoundId) {
|
||||||
|
currentValue = id;
|
||||||
|
localStorage.setItem(STORAGE_KEY, id);
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotificationSound(): [NotificationSoundId, (id: NotificationSoundId) => void] {
|
||||||
|
const value = useSyncExternalStore(subscribe, getSnapshot);
|
||||||
|
return [value, setNotificationSound];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playNotificationSound() {
|
||||||
|
const id = getSnapshot();
|
||||||
|
if (id === "none") return;
|
||||||
|
const sound = SOUNDS[id];
|
||||||
|
if (!sound) return;
|
||||||
|
const audio = new Audio(sound.src);
|
||||||
|
audio.volume = 0.6;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewNotificationSound(id: NotificationSoundId) {
|
||||||
|
if (id === "none") return;
|
||||||
|
const sound = SOUNDS[id];
|
||||||
|
if (!sound) return;
|
||||||
|
const audio = new Audio(sound.src);
|
||||||
|
audio.volume = 0.6;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAgentDoneNotification(status: "running" | "idle" | "error" | undefined) {
|
||||||
|
const prevStatus = useRef(status);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevStatus.current;
|
||||||
|
prevStatus.current = status;
|
||||||
|
|
||||||
|
if (prev === "running" && status === "idle") {
|
||||||
|
playNotificationSound();
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFICATION_SOUND_OPTIONS: { id: NotificationSoundId; label: string }[] = [
|
||||||
|
{ id: "none", label: "None" },
|
||||||
|
{ id: "chime", label: "Chime" },
|
||||||
|
{ id: "ping", label: "Ping" },
|
||||||
|
];
|
||||||
55
foundry/packages/frontend/src/lib/platform.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
/** True when built with VITE_MOBILE=1 (Tauri mobile build) */
|
||||||
|
export const isNativeMobile = !!import.meta.env.VITE_MOBILE;
|
||||||
|
|
||||||
|
/** True when built with VITE_DESKTOP=1 (Tauri desktop build) */
|
||||||
|
export const isNativeDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||||
|
|
||||||
|
/** True when running inside any Tauri shell */
|
||||||
|
export const isNativeApp = isNativeMobile || isNativeDesktop;
|
||||||
|
|
||||||
|
function getIsMobileViewport(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
return window.innerWidth < MOBILE_BREAKPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentIsMobile = isNativeMobile || getIsMobileViewport();
|
||||||
|
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
|
mql.addEventListener("change", (e) => {
|
||||||
|
const next = isNativeMobile || e.matches;
|
||||||
|
if (next !== currentIsMobile) {
|
||||||
|
currentIsMobile = next;
|
||||||
|
for (const fn of listeners) fn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(cb: () => void) {
|
||||||
|
listeners.add(cb);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(cb);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot() {
|
||||||
|
return currentIsMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the app should render in mobile layout.
|
||||||
|
* This is true when:
|
||||||
|
* - Built with VITE_MOBILE=1 (always mobile), OR
|
||||||
|
* - Viewport width is below 768px (responsive web)
|
||||||
|
*
|
||||||
|
* Re-renders when the viewport crosses the breakpoint.
|
||||||
|
*/
|
||||||
|
export function useIsMobile(): boolean {
|
||||||
|
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
background: var(--f-surface-primary, #000000);
|
background: var(--f-surface-primary, #000000);
|
||||||
color: var(--f-text-primary, #ffffff);
|
color: var(--f-text-primary, #ffffff);
|
||||||
|
--safe-area-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-area-right: env(safe-area-inset-right, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|
@ -44,6 +48,41 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes hf-dot-fade {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hf-typing-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 40px;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hf-typing-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 40px;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface FoundryUser {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
eligibleOrganizationIds: string[];
|
eligibleOrganizationIds: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +21,8 @@ export interface FoundryOrganizationMember {
|
||||||
email: string;
|
email: string;
|
||||||
role: "owner" | "admin" | "member";
|
role: "owner" | "admin" | "member";
|
||||||
state: "active" | "invited";
|
state: "active" | "invited";
|
||||||
|
avatarUrl: string | null;
|
||||||
|
githubLogin: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FoundryInvoice {
|
export interface FoundryInvoice {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,14 @@ export interface WorkbenchPullRequestSummary {
|
||||||
status: "draft" | "ready";
|
status: "draft" | "ready";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkbenchPresence {
|
||||||
|
memberId: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
lastSeenAtMs: number;
|
||||||
|
typing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkbenchTask {
|
export interface WorkbenchTask {
|
||||||
id: string;
|
id: string;
|
||||||
repoId: string;
|
repoId: string;
|
||||||
|
|
@ -90,6 +98,7 @@ export interface WorkbenchTask {
|
||||||
diffs: Record<string, string>;
|
diffs: Record<string, string>;
|
||||||
fileTree: WorkbenchFileTreeNode[];
|
fileTree: WorkbenchFileTreeNode[];
|
||||||
minutesUsed: number;
|
minutesUsed: number;
|
||||||
|
presence: WorkbenchPresence[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchRepo {
|
export interface WorkbenchRepo {
|
||||||
|
|
|
||||||