Studio: auto-detect server GH_TOKEN and widen Easy-mode detection

The GitHub seed form now fetches /api/data-recipe/seed/github/env-token
on mount and, when the server exposes a GH_TOKEN / GITHUB_TOKEN env var
and the token field is blank, shows a small 'Using server env var' badge
and swaps the placeholder text. The token value itself is never returned
to the UI.

Widens Easy-mode detection in recipe-studio-page.tsx so that recipes
saved before ui.seed_source_type was persisted also get the Easy tab:
falls back to recipe.seed_config.source.seed_type, which is always
present for github_repo seeds.
This commit is contained in:
Daniel Han 2026-04-24 16:01:28 +00:00
commit 6ced30cdaa
4 changed files with 60 additions and 7 deletions

View file

@ -8,6 +8,7 @@ from __future__ import annotations
import base64
import binascii
import json
import os
import re
from itertools import islice
from pathlib import Path
@ -627,3 +628,16 @@ def inspect_seed_upload(payload: SeedInspectUploadRequest) -> SeedInspectRespons
split = None,
subset = None,
)
@router.get("/seed/github/env-token")
def get_github_env_token_status() -> dict:
"""Report whether the server has a GH_TOKEN / GITHUB_TOKEN env var.
The value is never returned; the UI uses this to tell the user they
can leave the token field blank.
"""
has_token = bool(
os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
)
return {"has_token": has_token}

View file

@ -315,6 +315,13 @@ export async function inspectSeedUpload(
return postJson<SeedInspectResponse>("/seed/inspect-upload", payload);
}
// biome-ignore lint/style/useNamingConvention: api schema
export type GithubEnvTokenStatus = { has_token: boolean };
export async function getGithubEnvTokenStatus(): Promise<GithubEnvTokenStatus> {
return getJson<GithubEnvTokenStatus>("/seed/github/env-token");
}
export async function listMcpTools(
payload: McpToolsListRequest,
): Promise<McpToolsListResponse> {

View file

@ -40,7 +40,7 @@ import {
import { type ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { UnstructuredDropZone, type FileEntry } from "./unstructured-drop-zone";
import { inspectSeedDataset, inspectSeedUpload } from "../../api";
import { getGithubEnvTokenStatus, inspectSeedDataset, inspectSeedUpload } from "../../api";
import { resolveImagePreview } from "../../utils/image-preview";
import type {
SeedConfig,
@ -83,8 +83,26 @@ export function GithubRepoSeedForm({
onUpdate: (patch: Partial<SeedConfig>) => void;
}): ReactElement {
const [advancedOpen, setAdvancedOpen] = useState(false);
const [serverHasEnvToken, setServerHasEnvToken] = useState<boolean | null>(
null,
);
const limitStr = (config.github_limit ?? "100").trim();
const allMode = limitStr === "" || limitStr === "0";
useEffect(() => {
let cancelled = false;
void getGithubEnvTokenStatus()
.then((status) => {
if (!cancelled) setServerHasEnvToken(status.has_token);
})
.catch(() => {
if (!cancelled) setServerHasEnvToken(false);
});
return () => {
cancelled = true;
};
}, []);
const userTokenEmpty = !(config.github_token ?? "").trim();
const usingEnvToken = userTokenEmpty && serverHasEnvToken === true;
return (
<div className="space-y-3 rounded-xl corner-squircle border border-border/60 p-3">
<div className="grid gap-1.5">
@ -100,16 +118,27 @@ export function GithubRepoSeedForm({
/>
</div>
<div className="grid gap-1.5">
<FieldLabel
label="GitHub token"
hint="Personal access token with repo scope. Leave blank to use the server's GH_TOKEN / GITHUB_TOKEN env var."
/>
<div className="flex items-start justify-between gap-2">
<FieldLabel
label="GitHub token"
hint="Personal access token with repo scope. Leave blank to use the server's GH_TOKEN / GITHUB_TOKEN env var."
/>
{usingEnvToken && (
<span className="shrink-0 rounded-md border border-emerald-500/40 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700 dark:text-emerald-300">
Using server env var
</span>
)}
</div>
<Input
type="password"
className="nodrag"
value={config.github_token ?? ""}
onChange={(e) => onUpdate({ github_token: e.target.value })}
placeholder="ghp_... (optional if server has GH_TOKEN)"
placeholder={
usingEnvToken
? "using server's GH_TOKEN env var"
: "ghp_... (optional if server has GH_TOKEN)"
}
/>
</div>
<div className="grid gap-1.5">

View file

@ -202,7 +202,10 @@ export function RecipeStudioPage({
null,
);
const flowContainerRef = useRef<HTMLDivElement | null>(null);
const supportsEasyMode = initialPayload?.ui?.seed_source_type === "github_repo";
const supportsEasyMode =
initialPayload?.ui?.seed_source_type === "github_repo" ||
(initialPayload?.recipe?.seed_config as { source?: { seed_type?: string } } | undefined)
?.source?.seed_type === "github_repo";
const viewModeStorageKey = `recipe-studio:view-mode:${recipeId}`;
const [activeView, setActiveViewState] = useState<RecipeStudioView>(() => {
if (typeof window !== "undefined") {