Add session_sandboxes feature flag to gate project/branch pickers

Add a FeatureFlags config section with a session_sandboxes boolean
(default false) to both the Rust server config and web app config.
Gate the project/branch picker UI behind this flag. Set it to false
in the Docker demo config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-06 07:34:32 -05:00
parent 699c519c97
commit 3fe77b6d5f
4 changed files with 60 additions and 21 deletions

View file

@ -19,6 +19,10 @@ interface GitConfig {
client_id: string | null;
}
interface FeatureFlags {
session_sandboxes: boolean;
}
interface WebConfig {
url: string;
auth: AuthConfig;
@ -28,6 +32,7 @@ export interface AppConfig {
web: WebConfig;
api: ApiConfig;
git: GitConfig;
feature_flags: FeatureFlags;
}
const AUTH_DEFAULTS: AuthConfig = {
@ -51,6 +56,10 @@ const GIT_DEFAULTS: GitConfig = {
client_id: null,
};
const FEATURE_FLAGS_DEFAULTS: FeatureFlags = {
session_sandboxes: false,
};
export const ARC_CONFIG_PATH = join(homedir(), ".arc", "server.toml");
function loadAppConfig(): AppConfig {
@ -67,6 +76,7 @@ function loadAppConfig(): AppConfig {
const rawWebAuth = (rawWeb.auth ?? {}) as Partial<AuthConfig>;
const rawApi = (raw.api ?? {}) as Partial<ApiConfig>;
const rawGit = (raw.git ?? {}) as Partial<GitConfig>;
const rawFeatureFlags = (raw.feature_flags ?? {}) as Partial<FeatureFlags>;
const demo = process.env.ARC_DEMO === "1";
@ -82,6 +92,7 @@ function loadAppConfig(): AppConfig {
? { ...API_DEFAULTS, ...rawApi, authentication_strategy: "insecure_disabled" }
: { ...API_DEFAULTS, ...rawApi },
git: { ...GIT_DEFAULTS, ...rawGit },
feature_flags: { ...FEATURE_FLAGS_DEFAULTS, ...rawFeatureFlags },
};
}

View file

@ -20,7 +20,9 @@ import {
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Link } from "react-router";
import { timeAgo } from "../lib/time";
import { apiJson } from "../api-client";
import { getAppConfig } from "../lib/config.server";
import type { PaginatedProjectList, PaginatedSessionGroupList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/start";
@ -31,6 +33,7 @@ export function meta({}: Route.MetaArgs) {
}
export async function loader({ request }: Route.LoaderArgs) {
const { feature_flags } = getAppConfig();
const [{ data: apiProjects }, { data: apiSessions }] = await Promise.all([
apiJson<PaginatedProjectList>("/projects", { request }),
apiJson<PaginatedSessionGroupList>("/sessions", { request }),
@ -41,11 +44,10 @@ export async function loader({ request }: Route.LoaderArgs) {
sessions: g.sessions.map((s) => ({
id: s.id,
title: s.title,
repo: s.repo,
time: s.time,
created_at: s.created_at,
})),
}));
return { projects, sessionGroups };
return { projects, sessionGroups, feature_flags };
}
const branches = [
@ -62,7 +64,7 @@ function BranchIcon({ className }: { className?: string }) {
);
}
function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: string; title: string; repo: string; time: string }[] }[] }) {
function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: string; title: string; created_at: string }[] }[] }) {
return (
<aside className="w-64 shrink-0 border-r border-line flex flex-col h-[calc(100vh-4rem)]">
<div className="p-3">
@ -86,8 +88,7 @@ function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: s
>
<span className="truncate text-sm">{session.title}</span>
<span className="flex items-center gap-1.5 mt-0.5">
<span className="font-mono text-[11px] text-teal-500">{session.repo}</span>
<span className="text-[11px] text-fg-muted">{session.time}</span>
<span className="text-[11px] text-fg-muted">{timeAgo(session.created_at)}</span>
</span>
</Link>
</li>
@ -101,7 +102,7 @@ function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: s
}
export default function Start({ loaderData }: Route.ComponentProps) {
const { projects, sessionGroups } = loaderData;
const { projects, sessionGroups, feature_flags } = loaderData;
const [prompt, setPrompt] = useState("");
const [project, setProject] = useState(projects[0]);
const [branch, setBranch] = useState(branches[0]);
@ -160,20 +161,22 @@ export default function Start({ loaderData }: Route.ComponentProps) {
/>
<div className="absolute bottom-3 inset-x-3 flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Picker
value={project}
onChange={setProject}
options={projects}
icon={<FolderIcon className="size-3.5 text-fg-muted" />}
/>
<Picker
value={branch}
onChange={setBranch}
options={branches}
icon={<BranchIcon className="size-3.5 text-fg-muted" />}
/>
</div>
{feature_flags.session_sandboxes && (
<div className="flex items-center gap-1.5">
<Picker
value={project}
onChange={setProject}
options={projects}
icon={<FolderIcon className="size-3.5 text-fg-muted" />}
/>
<Picker
value={branch}
onChange={setBranch}
options={branches}
icon={<BranchIcon className="size-3.5 text-fg-muted" />}
/>
</div>
)}
<div className="flex items-center gap-3">
<span className="text-xs text-fg-muted select-none">

View file

@ -94,6 +94,12 @@ impl Default for WebConfig {
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct FeatureFlags {
#[serde(default)]
pub session_sandboxes: bool,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct ServerConfig {
pub data_dir: Option<PathBuf>,
@ -104,6 +110,8 @@ pub struct ServerConfig {
pub api: ApiConfig,
#[serde(default)]
pub git: GitConfig,
#[serde(default)]
pub feature_flags: FeatureFlags,
#[serde(flatten)]
pub run_defaults: RunDefaults,
#[serde(flatten)]
@ -417,6 +425,20 @@ matcher = "agent_loop"
);
}
#[test]
fn parse_feature_flags() {
let toml = "[feature_flags]\nsession_sandboxes = true";
let config: ServerConfig = toml::from_str(toml).unwrap();
assert_eq!(config.feature_flags.session_sandboxes, true);
}
#[test]
fn parse_feature_flags_defaults() {
let toml = "";
let config: ServerConfig = toml::from_str(toml).unwrap();
assert_eq!(config.feature_flags.session_sandboxes, false);
}
#[test]
fn parse_config_without_hooks_defaults_empty() {
let toml = "";

View file

@ -1,2 +1,5 @@
[api]
base_url = "http://api:3000"
[feature_flags]
session_sandboxes = false