mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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:
parent
699c519c97
commit
3fe77b6d5f
4 changed files with 60 additions and 21 deletions
|
|
@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
[api]
|
||||
base_url = "http://api:3000"
|
||||
|
||||
[feature_flags]
|
||||
session_sandboxes = false
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue