@@ -105,6 +105,14 @@ import {
105105import { getCachedThemeSetting, useThemeController, writeThemeSettingCache, type ThemeMode, type ThemeSetting } from "@/lib/theme";
106106import { loadHiddenProjectIds, loadShowHiddenProjects, saveHiddenProjectIds, saveShowHiddenProjects } from "@/lib/projects-hidden";
107107import { loadConsoleSession, saveConsoleSession, type PersistedConsoleTab } from "@/lib/console-session";
108+ import {
109+ cloneBuildRunCommandConfig,
110+ createEmptyBuildRunCommandConfig,
111+ hasBuildRunCommand,
112+ normalizeBuildRunCommandDraft,
113+ removeBuildRunCommandConfig,
114+ upsertBuildRunCommandConfig,
115+ } from "@/lib/build-run-config";
108116import {
109117 clearWorktreeCreateTransientPrefs,
110118 loadWorktreeCreatePrefs,
@@ -558,15 +566,15 @@ export default function CodexFlowManagerUI() {
558566 const [buildRunCfgByDirKey, setBuildRunCfgByDirKey] = useState<Record<string, DirBuildRunConfig | null>>({});
559567 const buildRunCfgByDirKeyRef = useRef<Record<string, DirBuildRunConfig | null>>({});
560568 const [dirDrag, setDirDrag] = useState<{ draggingId: string; overId?: string; position?: "before" | "after" | "asChild" | "root-end" } | null>(null);
561- const [buildRunDialog, setBuildRunDialog] = useState<BuildRunDialogState>(() => ({
562- open: false,
563- action: "build",
564- projectId: "",
565- saveScope: "self",
566- parentProjectId: undefined,
567- draft: { mode: "simple", commandText: "", cwd: "", env: [], backend: { kind: "system" } } as any,
568- advanced: false,
569- }));
569+ const [buildRunDialog, setBuildRunDialog] = useState<BuildRunDialogState>(() => ({
570+ open: false,
571+ action: "build",
572+ projectId: "",
573+ saveScope: "self",
574+ parentProjectId: undefined,
575+ draft: createEmptyBuildRunCommandConfig(),
576+ advanced: false,
577+ }));
570578 const [dirLabelDialog, setDirLabelDialog] = useState<DirLabelDialogState>(() => ({ open: false, projectId: "", draft: "" }));
571579 const [gitWorktreeAutoCommitEnabled, setGitWorktreeAutoCommitEnabled] = useState<boolean>(true);
572580 const [gitWorktreeCopyRulesOnCreate, setGitWorktreeCopyRulesOnCreate] = useState<boolean>(true);
@@ -4752,31 +4760,98 @@ export default function CodexFlowManagerUI() {
47524760 * - 先读自身配置
47534761 * - 若自身无配置且为子节点,则继承父节点配置
47544762 */
4755- const resolveEffectiveBuildRunCommand = useCallback(async (project: Project, action: BuildRunAction): Promise<{
4756- effective: BuildRunCommandConfig | null;
4763+ const resolveEffectiveBuildRunCommand = useCallback(async (project: Project, action: BuildRunAction): Promise<{
4764+ effective: BuildRunCommandConfig | null;
47574765 inherited: boolean;
47584766 parentProjectId?: string;
47594767 defaultSaveScope: "self" | "parent";
4760- }> => {
4761- const selfCfg = await ensureBuildRunConfigLoaded(project.winPath);
4762- const selfCmd = (selfCfg as any)?.[action] as BuildRunCommandConfig | undefined;
4763- if (selfCmd) return { effective: selfCmd, inherited: false, defaultSaveScope: "self" };
4764-
4765- const parentId = String(dirTreeStore.parentById[project.id] || "").trim();
4766- if (parentId) {
4767- const parent = projectsRef.current.find((x) => x.id === parentId) || null;
4768- if (parent) {
4769- const parentCfg = await ensureBuildRunConfigLoaded(parent.winPath);
4770- const parentCmd = (parentCfg as any)?.[action] as BuildRunCommandConfig | undefined;
4771- if (parentCmd) return { effective: parentCmd, inherited: true, parentProjectId: parentId, defaultSaveScope: "parent" };
4772- return { effective: null, inherited: false, parentProjectId: parentId, defaultSaveScope: "parent" };
4773- }
4774- }
4775- return { effective: null, inherited: false, defaultSaveScope: "self" };
4776- }, [dirTreeStore.parentById, ensureBuildRunConfigLoaded]);
4777-
4778- /**
4779- * 触发 Build/Run:
4768+ }> => {
4769+ const selfCfg = await ensureBuildRunConfigLoaded(project.winPath);
4770+ const selfCmd = (selfCfg as any)?.[action] as BuildRunCommandConfig | undefined;
4771+ if (hasBuildRunCommand(selfCmd)) return { effective: selfCmd, inherited: false, defaultSaveScope: "self" };
4772+
4773+ const parentId = String(dirTreeStore.parentById[project.id] || "").trim();
4774+ if (parentId) {
4775+ const parent = projectsRef.current.find((x) => x.id === parentId) || null;
4776+ if (parent) {
4777+ const parentCfg = await ensureBuildRunConfigLoaded(parent.winPath);
4778+ const parentCmd = (parentCfg as any)?.[action] as BuildRunCommandConfig | undefined;
4779+ if (hasBuildRunCommand(parentCmd)) return { effective: parentCmd, inherited: true, parentProjectId: parentId, defaultSaveScope: "parent" };
4780+ return { effective: null, inherited: false, parentProjectId: parentId, defaultSaveScope: "parent" };
4781+ }
4782+ }
4783+ return { effective: null, inherited: false, defaultSaveScope: "self" };
4784+ }, [dirTreeStore.parentById, ensureBuildRunConfigLoaded]);
4785+
4786+ /**
4787+ * 中文说明:统一使用应用内提示弹窗展示 Build/Run 相关错误,避免原生 alert 破坏弹窗交互与焦点状态。
4788+ */
4789+ const showBuildRunNotice = useCallback((action: BuildRunAction, message: string) => {
4790+ const text = String(message || "").trim();
4791+ if (!text) return;
4792+ setNoticeDialog({
4793+ open: true,
4794+ title: action === "build"
4795+ ? (t("projects:buildCommandTitle", "配置 Build 命令") as string)
4796+ : (t("projects:runCommandTitle", "配置 Run 命令") as string),
4797+ message: text,
4798+ });
4799+ }, [t]);
4800+
4801+ /**
4802+ * 中文说明:切换 Build/Run 配置保存范围,并在切回“继承父项目”时同步对齐草稿内容。
4803+ */
4804+ const switchBuildRunDialogSaveScope = useCallback(async (nextScope: "self" | "parent") => {
4805+ const snapshot = buildRunDialog;
4806+ if (!snapshot.open) return;
4807+ if (snapshot.saveScope === nextScope) return;
4808+
4809+ const target = projectsRef.current.find((item) => item.id === snapshot.projectId) || null;
4810+ if (!target) return;
4811+
4812+ const parentIdFromTree = String(dirTreeStore.parentById[target.id] || "").trim();
4813+ const parentId = String(snapshot.parentProjectId || parentIdFromTree || "").trim();
4814+ const parent = parentId ? (projectsRef.current.find((item) => item.id === parentId) || null) : null;
4815+
4816+ if (nextScope === "parent") {
4817+ if (!parentId || !parent) return;
4818+ const parentCfg = await ensureBuildRunConfigLoaded(parent.winPath);
4819+ const parentCmd = (parentCfg as any)?.[snapshot.action] as BuildRunCommandConfig | undefined;
4820+ const nextDraft = hasBuildRunCommand(parentCmd)
4821+ ? cloneBuildRunCommandConfig(parentCmd)
4822+ : createEmptyBuildRunCommandConfig();
4823+ setBuildRunDialog((prev) => {
4824+ if (!prev.open || prev.projectId !== snapshot.projectId || prev.action !== snapshot.action)
4825+ return prev;
4826+ return {
4827+ ...prev,
4828+ saveScope: "parent",
4829+ parentProjectId: parentId,
4830+ draft: nextDraft,
4831+ advanced: nextDraft.mode === "advanced",
4832+ };
4833+ });
4834+ return;
4835+ }
4836+
4837+ const selfCfg = await ensureBuildRunConfigLoaded(target.winPath);
4838+ const selfCmd = (selfCfg as any)?.[snapshot.action] as BuildRunCommandConfig | undefined;
4839+ setBuildRunDialog((prev) => {
4840+ if (!prev.open || prev.projectId !== snapshot.projectId || prev.action !== snapshot.action)
4841+ return prev;
4842+ const nextDraft = hasBuildRunCommand(selfCmd) ? cloneBuildRunCommandConfig(selfCmd) : prev.draft;
4843+ return {
4844+ ...prev,
4845+ saveScope: "self",
4846+ parentProjectId: parentId || prev.parentProjectId,
4847+ draft: nextDraft,
4848+ advanced: hasBuildRunCommand(selfCmd) ? nextDraft.mode === "advanced" : prev.advanced,
4849+ };
4850+ });
4851+ }, [buildRunDialog, dirTreeStore.parentById, ensureBuildRunConfigLoaded]);
4852+
4853+ /**
4854+ * 触发 Build/Run:
47804855 * - 若无配置:打开配置对话框
47814856 * - 否则:直接外部终端执行
47824857 * - edit=true:强制进入“编辑命令”
@@ -4785,14 +4860,12 @@ export default function CodexFlowManagerUI() {
47854860 const p = project;
47864861 if (!p?.id || !p?.winPath) return;
47874862 const resolved = await resolveEffectiveBuildRunCommand(p, action);
4788- const effective = resolved.effective;
4789- if (edit || !effective) {
4790- const draft = effective
4791- ? ({ ...effective, env: Array.isArray(effective.env) ? effective.env : [] } as BuildRunCommandConfig)
4792- : ({ mode: "simple", commandText: "", cwd: "", env: [], backend: { kind: "system" } } as BuildRunCommandConfig);
4793- const advanced = draft.mode === "advanced";
4794- setBuildRunDialog({
4795- open: true,
4863+ const effective = resolved.effective;
4864+ if (edit || !effective) {
4865+ const draft = effective ? cloneBuildRunCommandConfig(effective) : createEmptyBuildRunCommandConfig();
4866+ const advanced = draft.mode === "advanced";
4867+ setBuildRunDialog({
4868+ open: true,
47964869 action,
47974870 projectId: p.id,
47984871 saveScope: resolved.defaultSaveScope,
@@ -4805,16 +4878,19 @@ export default function CodexFlowManagerUI() {
48054878
48064879 const cwd = String(effective.cwd || "").trim() || p.winPath;
48074880 const title = `${getDirNodeLabel(p)} ${action === "build" ? "Build" : "Run"}`;
4808- try {
4809- const res: any = await (window as any).host?.buildRun?.exec?.({ dir: p.winPath, cwd, title, command: effective });
4810- if (!(res && res.ok)) throw new Error(res?.error || "failed");
4811- } catch (e: any) {
4812- alert(String((t("projects:buildRunFailed", "执行失败:{{error}}") as any) || "").replace("{{error}}", String(e?.message || e)));
4813- }
4814- }, [getDirNodeLabel, resolveEffectiveBuildRunCommand, t]);
4815-
4816- /**
4817- * 保存 Build/Run 配置对话框的草稿到本地持久化存储。
4881+ try {
4882+ const res: any = await (window as any).host?.buildRun?.exec?.({ dir: p.winPath, cwd, title, command: effective });
4883+ if (!(res && res.ok)) throw new Error(res?.error || "failed");
4884+ } catch (e: any) {
4885+ showBuildRunNotice(
4886+ action,
4887+ String((t("projects:buildRunFailed", "执行失败:{{error}}") as any) || "").replace("{{error}}", String(e?.message || e)),
4888+ );
4889+ }
4890+ }, [createEmptyBuildRunCommandConfig, getDirNodeLabel, resolveEffectiveBuildRunCommand, showBuildRunNotice]);
4891+
4892+ /**
4893+ * 保存 Build/Run 配置对话框的草稿到本地持久化存储。
48184894 */
48194895 const saveBuildRunDialog = useCallback(async () => {
48204896 const dlg = buildRunDialog;
@@ -4826,50 +4902,56 @@ export default function CodexFlowManagerUI() {
48264902 const parentIdFromTree = String(dirTreeStore.parentById[target.id] || "").trim();
48274903 const parentId = String(dlg.parentProjectId || parentIdFromTree || "").trim();
48284904
4829- const saveProject = (() => {
4830- if (dlg.saveScope !== "parent") return target;
4831- if (!parentId) return target;
4832- return projectsRef.current.find((x) => x.id === parentId) || target;
4833- })();
4834-
4835- const draft = dlg.draft || ({} as any);
4836- const nextCmd: BuildRunCommandConfig = { ...draft } as any;
4837- nextCmd.cwd = String(draft.cwd || "").trim();
4838- nextCmd.backend = (draft.backend && typeof draft.backend === "object") ? draft.backend : { kind: "system" };
4839- nextCmd.env = Array.isArray(draft.env)
4840- ? draft.env.map((r: any) => ({ key: String(r?.key || ""), value: String(r?.value ?? "") }))
4841- : [];
4842-
4843- if (dlg.advanced) {
4844- nextCmd.mode = "advanced";
4845- nextCmd.commandText = undefined;
4846- nextCmd.cmd = String(draft.cmd || "").trim();
4847- nextCmd.args = Array.isArray(draft.args) ? draft.args.map((x: any) => String(x ?? "")).filter((x: string) => x.trim().length > 0) : [];
4848- if (!nextCmd.cmd) {
4849- alert(t("projects:buildRunMissingCmd", "请输入命令") as string);
4850- return;
4851- }
4852- } else {
4853- nextCmd.mode = "simple";
4854- nextCmd.cmd = undefined;
4855- nextCmd.args = undefined;
4856- nextCmd.commandText = String(draft.commandText || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
4857- if (!nextCmd.commandText) {
4858- alert(t("projects:buildRunMissingCmd", "请输入命令") as string);
4859- return;
4860- }
4861- }
4862-
4863- const existing = (await ensureBuildRunConfigLoaded(saveProject.winPath)) || {};
4864- const nextCfg: DirBuildRunConfig = { ...(existing as any), [dlg.action]: nextCmd } as any;
4865- const ok = await persistBuildRunConfig(saveProject.winPath, nextCfg);
4866- if (!ok) {
4867- alert(t("projects:buildRunSaveFailed", "保存失败") as string);
4868- return;
4869- }
4870-
4871- setBuildRunDialog((prev) => ({ ...prev, open: false }));
4872- }, [buildRunDialog, dirTreeStore.parentById, ensureBuildRunConfigLoaded, persistBuildRunConfig, t]);
4905+ const saveProject = (() => {
4906+ if (dlg.saveScope !== "parent") return target;
4907+ if (!parentId) return target;
4908+ return projectsRef.current.find((x) => x.id === parentId) || target;
4909+ })();
4910+
4911+ const nextCmd = normalizeBuildRunCommandDraft(dlg.draft, dlg.advanced);
4912+ const hasCommand = hasBuildRunCommand(nextCmd);
4913+ const selfExisting = (await ensureBuildRunConfigLoaded(target.winPath)) || {};
4914+ const parentExisting = saveProject.id === target.id
4915+ ? selfExisting
4916+ : ((await ensureBuildRunConfigLoaded(saveProject.winPath)) || {});
4917+ const parentCurrentCmd = (parentExisting as any)?.[dlg.action] as BuildRunCommandConfig | undefined;
4918+
4919+ if (dlg.saveScope === "self" && !hasCommand) {
4920+ showBuildRunNotice(dlg.action, t("projects:buildRunMissingCmd", "请输入命令") as string);
4921+ return;
4922+ }
4923+
4924+ if (dlg.saveScope === "parent" && saveProject.id !== target.id && !hasCommand && hasBuildRunCommand(parentCurrentCmd)) {
4925+ showBuildRunNotice(dlg.action, t("projects:buildRunMissingCmd", "请输入命令") as string);
4926+ return;
4927+ }
4928+
4929+ if (dlg.saveScope === "self") {
4930+ const nextCfg = upsertBuildRunCommandConfig(selfExisting, dlg.action, nextCmd);
4931+ const ok = await persistBuildRunConfig(target.winPath, nextCfg);
4932+ if (!ok) {
4933+ showBuildRunNotice(dlg.action, t("projects:buildRunSaveFailed", "保存失败") as string);
4934+ return;
4935+ }
4936+ } else {
4937+ let parentOk = true;
4938+ let selfOk = true;
4939+ if (saveProject.id !== target.id && hasCommand) {
4940+ const nextParentCfg = upsertBuildRunCommandConfig(parentExisting, dlg.action, nextCmd);
4941+ parentOk = await persistBuildRunConfig(saveProject.winPath, nextParentCfg);
4942+ }
4943+ if (saveProject.id !== target.id && parentOk) {
4944+ const nextSelfCfg = removeBuildRunCommandConfig(selfExisting, dlg.action);
4945+ selfOk = await persistBuildRunConfig(target.winPath, nextSelfCfg);
4946+ }
4947+ if (!(parentOk && selfOk)) {
4948+ showBuildRunNotice(dlg.action, t("projects:buildRunSaveFailed", "保存失败") as string);
4949+ return;
4950+ }
4951+ }
4952+
4953+ setBuildRunDialog((prev) => ({ ...prev, open: false }));
4954+ }, [buildRunDialog, dirTreeStore.parentById, ensureBuildRunConfigLoaded, persistBuildRunConfig, showBuildRunNotice, t]);
48734955
48744956 /**
48754957 * 关闭 Build/Run 配置对话框(不保存)。
@@ -8812,14 +8894,10 @@ export default function CodexFlowManagerUI() {
88128894 ? "border-[var(--cf-accent)]/30 bg-[var(--cf-accent)]/5 dark:border-[var(--cf-accent)]/40 dark:bg-[var(--cf-accent)]/10"
88138895 : "border-slate-200/70 bg-white/60 dark:border-[var(--cf-border)] dark:bg-[var(--cf-surface-muted)]"
88148896 }`}
8815- onClick={() => {
8816- setBuildRunDialog((prev) => ({
8817- ...prev,
8818- saveScope: override ? "parent" : "self",
8819- parentProjectId: parentId || prev.parentProjectId,
8820- }));
8821- }}
8822- >
8897+ onClick={() => {
8898+ void switchBuildRunDialogSaveScope(override ? "parent" : "self");
8899+ }}
8900+ >
88238901 <div className="pt-0.5 shrink-0">
88248902 <div className={`h-3.5 w-3.5 rounded-full border flex items-center justify-center transition-colors ${
88258903 override ? "border-[var(--cf-accent)] bg-[var(--cf-accent)]" : "border-slate-400 bg-transparent"
0 commit comments