FN-6771: localize settings section copy
Localize dashboard settings section text and restore i18n lint coverage for those files. - Replace hardcoded settings section labels, help text, placeholders, and status copy with i18n lookups. - Add settings translation keys across supported app and common locale catalogs and refresh generated resource types. - Remove the settings sections from the i18n lint deferral list and document that coverage requirement. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-6771-localize-settings-sections.md | 5 + docs/i18n-contributing.md | 3 +- i18next.config.ts | 26 +- .../settings/sections/AgentPermissionsSection.tsx | 55 +- .../settings/sections/AppearanceSection.tsx | 91 +- .../settings/sections/AuthenticationSection.tsx | 457 +++------- .../settings/sections/BackupsSection.tsx | 229 ++--- .../settings/sections/CommandsSection.tsx | 49 +- .../settings/sections/ExperimentalSection.tsx | 109 +-- .../settings/sections/GeneralSection.tsx | 487 ++++------- .../settings/sections/GlobalGeneralSection.tsx | 183 +--- .../settings/sections/GlobalModelsSection.tsx | 525 ++++------- .../components/settings/sections/MemorySection.tsx | 458 +++------- .../components/settings/sections/MergeSection.tsx | 863 ++++++------------ .../settings/sections/NodeRoutingSection.tsx | 108 +-- .../settings/sections/NodeSyncSection.tsx | 105 +-- .../settings/sections/NotificationsSection.tsx | 484 ++++------ .../settings/sections/PluginsSection.tsx | 105 +-- .../settings/sections/ProjectModelsSection.tsx | 971 ++++++++------------- .../components/settings/sections/RemoteSection.tsx | 560 +++++------- .../settings/sections/ResearchGlobalSection.tsx | 314 ++----- .../settings/sections/ResearchProjectSection.tsx | 226 ++--- .../settings/sections/RuntimesSections.tsx | 40 +- .../settings/sections/ScheduledEvalsSection.tsx | 169 ++-- .../settings/sections/SchedulingSection.tsx | 381 ++------ .../settings/sections/WorktreesSection.tsx | 330 ++----- packages/i18n/locales/en/app.json | 698 ++++++++++++++- packages/i18n/locales/en/common.json | 8 + packages/i18n/locales/es/app.json | 698 ++++++++++++++- packages/i18n/locales/es/common.json | 8 + packages/i18n/locales/fr/app.json | 698 ++++++++++++++- packages/i18n/locales/fr/common.json | 8 + packages/i18n/locales/ko/app.json | 698 ++++++++++++++- packages/i18n/locales/ko/common.json | 8 + packages/i18n/locales/zh-CN/app.json | 698 ++++++++++++++- packages/i18n/locales/zh-CN/common.json | 8 + packages/i18n/locales/zh-TW/app.json | 698 ++++++++++++++- packages/i18n/locales/zh-TW/common.json | 8 + packages/i18n/src/resources.d.ts | 706 ++++++++++++++- 39 files changed, 7178 insertions(+), 5097 deletions(-) Fusion-Task-Id: FN-6771 Fusion-Task-Lineage: cdf9b36f-414e-4084-b661-b578a4c27314
This commit is contained in:
5
.changeset/fn-6771-localize-settings-sections.md
Normal file
5
.changeset/fn-6771-localize-settings-sections.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Keep settings section dashboard copy covered by i18n lint by removing the settings/sections deferral and regenerating i18n resource types.
|
||||||
@@ -52,7 +52,8 @@ content in `<kbd>`, instead of hiding source directories.
|
|||||||
Any remaining user-facing copy must be localized with `t()` / `<Trans>` and an
|
Any remaining user-facing copy must be localized with `t()` / `<Trans>` and an
|
||||||
`en` catalog entry. A temporary deferral is only acceptable when it is scoped to
|
`en` catalog entry. A temporary deferral is only acceptable when it is scoped to
|
||||||
specific files or a small cluster in `lint.ignore`, includes an `FNXC` rationale,
|
specific files or a small cluster in `lint.ignore`, includes an `FNXC` rationale,
|
||||||
and has a filed follow-up task that removes the ignore.
|
and has a filed follow-up task that removes the ignore. The settings sections
|
||||||
|
cluster is no longer deferred as of FN-6771; keep those files covered by lint.
|
||||||
|
|
||||||
## Translating an existing language
|
## Translating an existing language
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
const DEFERRED_I18N_LINT_FILES = [
|
const DEFERRED_I18N_LINT_FILES = [
|
||||||
// FNXC:i18n-LintBaseline 2026-06-19-00:00:
|
// FNXC:i18n-LintBaseline 2026-06-19-00:00:
|
||||||
// These exact files still carry pre-existing user-facing copy debt after FN-6749 restored the guardrail scope and token suppression.
|
// These exact files still carry pre-existing user-facing copy debt after FN-6749 restored the guardrail scope and token suppression.
|
||||||
// Deferral split after FN-6769: 23 settings section files -> FN-6771 and workflow/task/setup/PR files -> FN-6770.
|
|
||||||
// Keep the deferral file-scoped and remove entries as those follow-ups localize each cluster.
|
// Keep the deferral file-scoped and remove entries as those follow-ups localize each cluster.
|
||||||
|
// FNXC:i18n-LintBaseline 2026-06-20-00:00:
|
||||||
|
// FN-6771 localized the settings/sections cluster, so those files are no longer deferred and must stay covered by i18n lint.
|
||||||
"packages/dashboard/app/components/WorkflowSelector.tsx",
|
"packages/dashboard/app/components/WorkflowSelector.tsx",
|
||||||
"packages/dashboard/app/components/WorkflowResultsTab.tsx",
|
"packages/dashboard/app/components/WorkflowResultsTab.tsx",
|
||||||
"packages/dashboard/app/components/WorkflowNodeEditor.tsx",
|
"packages/dashboard/app/components/WorkflowNodeEditor.tsx",
|
||||||
@@ -22,29 +23,6 @@ const DEFERRED_I18N_LINT_FILES = [
|
|||||||
"packages/dashboard/app/components/PrPanel.tsx",
|
"packages/dashboard/app/components/PrPanel.tsx",
|
||||||
"packages/dashboard/app/components/PrCreateModal.tsx",
|
"packages/dashboard/app/components/PrCreateModal.tsx",
|
||||||
"packages/dashboard/app/components/Board.tsx",
|
"packages/dashboard/app/components/Board.tsx",
|
||||||
"packages/dashboard/app/components/settings/sections/WorktreesSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/SchedulingSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/ScheduledEvalsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/RuntimesSections.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/ResearchProjectSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/ResearchGlobalSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/RemoteSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/PluginsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/NotificationsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/NodeSyncSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/NodeRoutingSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/MergeSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/MemorySection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/GeneralSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/ExperimentalSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/CommandsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/BackupsSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/AppearanceSection.tsx",
|
|
||||||
"packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,60 +1,37 @@
|
|||||||
/**
|
|
||||||
* Agent Permissions section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-default agent permission policy editor plus the agent provisioning
|
|
||||||
* approval policy editor. The rule-completion helper is co-located (pure, used
|
|
||||||
* only here). Keys and editor wiring preserved verbatim from the original inline
|
|
||||||
* JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { AGENT_PERMISSION_POLICY_ACTION_CATEGORIES } from "@fusion/core";
|
import { AGENT_PERMISSION_POLICY_ACTION_CATEGORIES } from "@fusion/core";
|
||||||
import type { AgentPermissionPolicyRules } from "@fusion/core";
|
import type { AgentPermissionPolicyRules } from "@fusion/core";
|
||||||
import { AgentPermissionPolicyEditor } from "../../AgentPermissionPolicyEditor";
|
import { AgentPermissionPolicyEditor } from "../../AgentPermissionPolicyEditor";
|
||||||
import { AgentProvisioningPolicyEditor } from "../../AgentProvisioningPolicyEditor";
|
import { AgentProvisioningPolicyEditor } from "../../AgentProvisioningPolicyEditor";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
|
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
|
||||||
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
|
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
|
||||||
acc[category] = rules?.[category] ?? "allow";
|
acc[category] = rules?.[category] ?? "allow";
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as AgentPermissionPolicyRules);
|
}, {} as AgentPermissionPolicyRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentPermissionsSectionProps extends SectionBaseProps {
|
export interface AgentPermissionsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentPermissionsSection({ scopeBanner, form, setForm }: AgentPermissionsSectionProps) {
|
export function AgentPermissionsSection({ scopeBanner, form, setForm }: AgentPermissionsSectionProps) {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Agent Permissions</h4>
|
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<small className="settings-muted">Per-agent settings override project defaults. Each category controls a separate approval gate.</small>
|
<small className="settings-muted">{t("settings.agentPermissions.perAgentSettingsOverrideProjectDefaultsEachCategory", "Per-agent settings override project defaults. Each category controls a separate approval gate.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<AgentPermissionPolicyEditor
|
<AgentPermissionPolicyEditor mode="project-default" value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: toCompleteAgentPermissionRules(form.defaultAgentPermissionPolicy.rules) } : { presetId: "custom", rules: toCompleteAgentPermissionRules() }} onChange={(next) => setForm((f) => ({
|
||||||
mode="project-default"
|
|
||||||
value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: toCompleteAgentPermissionRules(form.defaultAgentPermissionPolicy.rules) } : { presetId: "custom", rules: toCompleteAgentPermissionRules() }}
|
|
||||||
onChange={(next) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules) },
|
defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules) },
|
||||||
}))
|
}))}/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h4 className="settings-section-heading">Agent Provisioning Approvals</h4>
|
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentProvisioningApprovals", "Agent Provisioning Approvals")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<small className="settings-muted">
|
<small className="settings-muted">{t("settings.agentPermissions.configureProjectLevelApprovalBehaviorForDurableProvisioning", " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). ")}</small>
|
||||||
Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete).
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<AgentProvisioningPolicyEditor
|
<AgentProvisioningPolicyEditor value={form.agentProvisioning} onChange={(next) => setForm((f) => ({ ...f, agentProvisioning: next }))}/>
|
||||||
value={form.agentProvisioning}
|
</>);
|
||||||
onChange={(next) => setForm((f) => ({ ...f, agentProvisioning: next }))}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AgentPermissionsSection;
|
export default AgentPermissionsSection;
|
||||||
|
|||||||
@@ -1,82 +1,43 @@
|
|||||||
/**
|
|
||||||
* Appearance section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Theme mode, color theme, dashboard font scale, language, and the
|
|
||||||
* session-banner suppression toggle. The three-tier device-local prefs
|
|
||||||
* (theme/language/font scale) keep their hooks in the shell — this section only
|
|
||||||
* relays their current values and change callbacks, mirroring the original
|
|
||||||
* inline JSX exactly (it both writes the modal form AND calls the write-through
|
|
||||||
* callback so the live UI updates immediately).
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ThemeMode, ColorTheme } from "@fusion/core";
|
import type { ThemeMode, ColorTheme } from "@fusion/core";
|
||||||
import { ThemeSelector } from "../../ThemeSelector";
|
import { ThemeSelector } from "../../ThemeSelector";
|
||||||
import { LanguageSelector } from "../../LanguageSelector";
|
import { LanguageSelector } from "../../LanguageSelector";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
export interface AppearanceSectionProps extends SectionBaseProps {
|
export interface AppearanceSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
colorTheme: ColorTheme;
|
colorTheme: ColorTheme;
|
||||||
dashboardFontScalePct: number;
|
dashboardFontScalePct: number;
|
||||||
onThemeModeChange?: (mode: ThemeMode) => void;
|
onThemeModeChange?: (mode: ThemeMode) => void;
|
||||||
onColorThemeChange?: (theme: ColorTheme) => void;
|
onColorThemeChange?: (theme: ColorTheme) => void;
|
||||||
onDashboardFontScaleChange?: (scalePct: number) => void;
|
onDashboardFontScaleChange?: (scalePct: number) => void;
|
||||||
sessionBannersHidden: boolean;
|
sessionBannersHidden: boolean;
|
||||||
setSessionBannersHidden: (hidden: boolean) => void;
|
setSessionBannersHidden: (hidden: boolean) => void;
|
||||||
}
|
}
|
||||||
|
export function AppearanceSection({ scopeBanner, setForm, themeMode, colorTheme, dashboardFontScalePct, onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, sessionBannersHidden, setSessionBannersHidden, }: AppearanceSectionProps) {
|
||||||
export function AppearanceSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
setForm,
|
|
||||||
themeMode,
|
|
||||||
colorTheme,
|
|
||||||
dashboardFontScalePct,
|
|
||||||
onThemeModeChange,
|
|
||||||
onColorThemeChange,
|
|
||||||
onDashboardFontScaleChange,
|
|
||||||
sessionBannersHidden,
|
|
||||||
setSessionBannersHidden,
|
|
||||||
}: AppearanceSectionProps) {
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4>
|
<h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4>
|
||||||
<ThemeSelector
|
<ThemeSelector themeMode={themeMode} colorTheme={colorTheme} dashboardFontScalePct={dashboardFontScalePct} onThemeModeChange={(mode) => {
|
||||||
themeMode={themeMode}
|
setForm((f) => ({ ...f, themeMode: mode }));
|
||||||
colorTheme={colorTheme}
|
onThemeModeChange?.(mode);
|
||||||
dashboardFontScalePct={dashboardFontScalePct}
|
}} onColorThemeChange={(theme) => {
|
||||||
onThemeModeChange={(mode) => {
|
setForm((f) => ({ ...f, colorTheme: theme }));
|
||||||
setForm((f) => ({ ...f, themeMode: mode }));
|
onColorThemeChange?.(theme);
|
||||||
onThemeModeChange?.(mode);
|
}} onDashboardFontScaleChange={(scalePct) => {
|
||||||
}}
|
setForm((f) => ({ ...f, dashboardFontScalePct: scalePct }));
|
||||||
onColorThemeChange={(theme) => {
|
onDashboardFontScaleChange?.(scalePct);
|
||||||
setForm((f) => ({ ...f, colorTheme: theme }));
|
}}/>
|
||||||
onColorThemeChange?.(theme);
|
|
||||||
}}
|
|
||||||
onDashboardFontScaleChange={(scalePct) => {
|
|
||||||
setForm((f) => ({ ...f, dashboardFontScalePct: scalePct }));
|
|
||||||
onDashboardFontScaleChange?.(scalePct);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
<input
|
<input type="checkbox" checked={sessionBannersHidden} onChange={(e) => setSessionBannersHidden(e.target.checked)}/>
|
||||||
type="checkbox"
|
<span>{t("settings.appearance.hideAISessionNotificationBanners", "Hide AI session notification banners")}</span>
|
||||||
checked={sessionBannersHidden}
|
|
||||||
onChange={(e) => setSessionBannersHidden(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Hide AI session notification banners</span>
|
|
||||||
</label>
|
</label>
|
||||||
<small className="form-text text-muted">
|
<small className="form-text text-muted">{t("settings.appearance.suppressTheLdquoNeedsYourInputRdquoBanner", " Suppress the “needs your input” banner that appears when AI sessions are awaiting input or have failed. ")}</small>
|
||||||
Suppress the “needs your input” banner that appears when AI sessions are awaiting input or have failed.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppearanceSection;
|
export default AppearanceSection;
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
/**
|
|
||||||
* Authentication section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Provider sign-in surface: CLI-backed provider cards, OAuth login/logout
|
|
||||||
* flows (device codes, manual code entry, login instructions), API-key
|
|
||||||
* entry/clear, plugin-contributed provider/integration cards, and the custom
|
|
||||||
* providers manager. This section is scope-less (auth changes apply
|
|
||||||
* immediately, not via the modal save), so it owns no form state — but it has a
|
|
||||||
* large set of shell-owned auth state and handlers, relayed via the `auth` prop
|
|
||||||
* bag. Component imports and pure utilities (clipboard, token-query) are
|
|
||||||
* imported directly. Behavior, test ids, and i18n keys are preserved verbatim.
|
|
||||||
*/
|
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import type { AuthProvider, ManualOAuthCodeInfo, OAuthDeviceCodeInfo } from "../../../api";
|
import type { AuthProvider, ManualOAuthCodeInfo, OAuthDeviceCodeInfo } from "../../../api";
|
||||||
import type { ToastType } from "../../../hooks/useToast";
|
import type { ToastType } from "../../../hooks/useToast";
|
||||||
@@ -24,405 +12,214 @@ import { OAuthManualCodeForm } from "../../OAuthManualCodeForm";
|
|||||||
import { CustomProvidersSection } from "../../CustomProvidersSection";
|
import { CustomProvidersSection } from "../../CustomProvidersSection";
|
||||||
import { copyTextToClipboard } from "../../../utils/copyToClipboard";
|
import { copyTextToClipboard } from "../../../utils/copyToClipboard";
|
||||||
import { appendTokenQuery } from "../../../auth";
|
import { appendTokenQuery } from "../../../auth";
|
||||||
|
|
||||||
export interface AuthenticationSectionData {
|
export interface AuthenticationSectionData {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
authProviders: AuthProvider[];
|
authProviders: AuthProvider[];
|
||||||
authLoading: boolean;
|
authLoading: boolean;
|
||||||
authActionInProgress: string | null;
|
authActionInProgress: string | null;
|
||||||
apiKeyInputs: Record<string, string>;
|
apiKeyInputs: Record<string, string>;
|
||||||
setApiKeyInputs: Dispatch<SetStateAction<Record<string, string>>>;
|
setApiKeyInputs: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
apiKeyErrors: Record<string, string>;
|
apiKeyErrors: Record<string, string>;
|
||||||
opencodeApiKeyRefreshStatus: Record<string, { tone: "success" | "error"; message: string }>;
|
opencodeApiKeyRefreshStatus: Record<string, {
|
||||||
deviceCodes: Record<string, OAuthDeviceCodeInfo>;
|
tone: "success" | "error";
|
||||||
loginInstructions: Record<string, string>;
|
message: string;
|
||||||
manualCodeConfigs: Record<string, ManualOAuthCodeInfo>;
|
}>;
|
||||||
manualCodeInputs: Record<string, string>;
|
deviceCodes: Record<string, OAuthDeviceCodeInfo>;
|
||||||
setManualCodeInputs: Dispatch<SetStateAction<Record<string, string>>>;
|
loginInstructions: Record<string, string>;
|
||||||
manualCodeSubmitInProgress: string | null;
|
manualCodeConfigs: Record<string, ManualOAuthCodeInfo>;
|
||||||
loadAuthStatus: () => void | Promise<void>;
|
manualCodeInputs: Record<string, string>;
|
||||||
handleLogin: (providerId: string) => void;
|
setManualCodeInputs: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
handleLogout: (providerId: string) => void;
|
manualCodeSubmitInProgress: string | null;
|
||||||
handleCancelLogin: (providerId: string) => void;
|
loadAuthStatus: () => void | Promise<void>;
|
||||||
handleSaveApiKey: (providerId: string) => void;
|
handleLogin: (providerId: string) => void;
|
||||||
handleClearApiKey: (providerId: string) => void;
|
handleLogout: (providerId: string) => void;
|
||||||
handleSubmitManualCode: (providerId: string) => void | Promise<void>;
|
handleCancelLogin: (providerId: string) => void;
|
||||||
onReopenOnboarding?: () => void;
|
handleSaveApiKey: (providerId: string) => void;
|
||||||
|
handleClearApiKey: (providerId: string) => void;
|
||||||
|
handleSubmitManualCode: (providerId: string) => void | Promise<void>;
|
||||||
|
onReopenOnboarding?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationSectionProps {
|
export interface AuthenticationSectionProps {
|
||||||
auth: AuthenticationSectionData;
|
auth: AuthenticationSectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthenticationSection({ auth }: AuthenticationSectionProps) {
|
export function AuthenticationSection({ auth }: AuthenticationSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const {
|
const { projectId, addToast, authProviders, authLoading, authActionInProgress, apiKeyInputs, setApiKeyInputs, apiKeyErrors, opencodeApiKeyRefreshStatus, deviceCodes, loginInstructions, manualCodeConfigs, manualCodeInputs, setManualCodeInputs, manualCodeSubmitInProgress, loadAuthStatus, handleLogin, handleLogout, handleCancelLogin, handleSaveApiKey, handleClearApiKey, handleSubmitManualCode, onReopenOnboarding, } = auth;
|
||||||
projectId,
|
// CLI-backed providers render their own compact card; filter them out of the
|
||||||
addToast,
|
// standard OAuth/API-key sort and render alongside.
|
||||||
authProviders,
|
const cliAuthProviders = authProviders.filter((p) => p.type === "cli");
|
||||||
authLoading,
|
const nonCliProviders = authProviders.filter((p) => p.type !== "cli");
|
||||||
authActionInProgress,
|
const sortedProviders = [...nonCliProviders].sort((a, b) => {
|
||||||
apiKeyInputs,
|
if (a.authenticated !== b.authenticated) {
|
||||||
setApiKeyInputs,
|
return a.authenticated ? -1 : 1;
|
||||||
apiKeyErrors,
|
}
|
||||||
opencodeApiKeyRefreshStatus,
|
return a.name.localeCompare(b.name);
|
||||||
deviceCodes,
|
});
|
||||||
loginInstructions,
|
const authenticatedProviders = sortedProviders.filter((p) => p.authenticated);
|
||||||
manualCodeConfigs,
|
const unauthenticatedProviders = sortedProviders.filter((p) => !p.authenticated);
|
||||||
manualCodeInputs,
|
const claudeCliProvider = cliAuthProviders.find((p) => p.id === "claude-cli");
|
||||||
setManualCodeInputs,
|
const cursorCliProvider = cliAuthProviders.find((p) => p.id === "cursor-cli");
|
||||||
manualCodeSubmitInProgress,
|
const llamaCppProvider = cliAuthProviders.find((p) => p.id === "llama-cpp");
|
||||||
loadAuthStatus,
|
const claudeCliCard = claudeCliProvider ? (<ClaudeCliProviderCard compact authenticated={claudeCliProvider.authenticated} onToggled={() => {
|
||||||
handleLogin,
|
void loadAuthStatus();
|
||||||
handleLogout,
|
}}/>) : null;
|
||||||
handleCancelLogin,
|
const cursorCliCard = cursorCliProvider ? (<CursorCliProviderCard compact authenticated={cursorCliProvider.authenticated} onToggled={() => {
|
||||||
handleSaveApiKey,
|
void loadAuthStatus();
|
||||||
handleClearApiKey,
|
}}/>) : null;
|
||||||
handleSubmitManualCode,
|
const llamaCppCard = llamaCppProvider ? (<LlamaCppProviderCard compact authenticated={llamaCppProvider.authenticated} onToggled={() => {
|
||||||
onReopenOnboarding,
|
void loadAuthStatus();
|
||||||
} = auth;
|
}}/>) : null;
|
||||||
|
const showAuthenticatedGroup = authenticatedProviders.length > 0 ||
|
||||||
// CLI-backed providers render their own compact card; filter them out of the
|
(claudeCliProvider?.authenticated ?? false) ||
|
||||||
// standard OAuth/API-key sort and render alongside.
|
(cursorCliProvider?.authenticated ?? false) ||
|
||||||
const cliAuthProviders = authProviders.filter((p) => p.type === "cli");
|
(llamaCppProvider?.authenticated ?? false);
|
||||||
const nonCliProviders = authProviders.filter((p) => p.type !== "cli");
|
const showAvailableGroup = unauthenticatedProviders.length > 0 ||
|
||||||
const sortedProviders = [...nonCliProviders].sort((a, b) => {
|
(claudeCliProvider && !claudeCliProvider.authenticated) ||
|
||||||
if (a.authenticated !== b.authenticated) {
|
(cursorCliProvider && !cursorCliProvider.authenticated) ||
|
||||||
return a.authenticated ? -1 : 1;
|
(llamaCppProvider && !llamaCppProvider.authenticated);
|
||||||
}
|
return (<>
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
const authenticatedProviders = sortedProviders.filter((p) => p.authenticated);
|
|
||||||
const unauthenticatedProviders = sortedProviders.filter((p) => !p.authenticated);
|
|
||||||
|
|
||||||
const claudeCliProvider = cliAuthProviders.find((p) => p.id === "claude-cli");
|
|
||||||
const cursorCliProvider = cliAuthProviders.find((p) => p.id === "cursor-cli");
|
|
||||||
const llamaCppProvider = cliAuthProviders.find((p) => p.id === "llama-cpp");
|
|
||||||
const claudeCliCard = claudeCliProvider ? (
|
|
||||||
<ClaudeCliProviderCard
|
|
||||||
compact
|
|
||||||
authenticated={claudeCliProvider.authenticated}
|
|
||||||
onToggled={() => {
|
|
||||||
void loadAuthStatus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
const cursorCliCard = cursorCliProvider ? (
|
|
||||||
<CursorCliProviderCard
|
|
||||||
compact
|
|
||||||
authenticated={cursorCliProvider.authenticated}
|
|
||||||
onToggled={() => {
|
|
||||||
void loadAuthStatus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
const llamaCppCard = llamaCppProvider ? (
|
|
||||||
<LlamaCppProviderCard
|
|
||||||
compact
|
|
||||||
authenticated={llamaCppProvider.authenticated}
|
|
||||||
onToggled={() => {
|
|
||||||
void loadAuthStatus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
const showAuthenticatedGroup =
|
|
||||||
authenticatedProviders.length > 0 ||
|
|
||||||
(claudeCliProvider?.authenticated ?? false) ||
|
|
||||||
(cursorCliProvider?.authenticated ?? false) ||
|
|
||||||
(llamaCppProvider?.authenticated ?? false);
|
|
||||||
const showAvailableGroup =
|
|
||||||
unauthenticatedProviders.length > 0 ||
|
|
||||||
(claudeCliProvider && !claudeCliProvider.authenticated) ||
|
|
||||||
(cursorCliProvider && !cursorCliProvider.authenticated) ||
|
|
||||||
(llamaCppProvider && !llamaCppProvider.authenticated);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4 className="settings-section-heading">{t("settings.auth.title", "Authentication")}</h4>
|
<h4 className="settings-section-heading">{t("settings.auth.title", "Authentication")}</h4>
|
||||||
{authLoading ? (
|
{authLoading ? (<div className="settings-empty-state">{t("settings.auth.loadingStatus", "Loading authentication status…")}</div>) : authProviders.length === 0 ? (<div className="settings-empty-state settings-muted">
|
||||||
<div className="settings-empty-state">{t("settings.auth.loadingStatus", "Loading authentication status…")}</div>
|
|
||||||
) : authProviders.length === 0 ? (
|
|
||||||
<div className="settings-empty-state settings-muted">
|
|
||||||
{t("settings.auth.noProviders", "No providers available")}
|
{t("settings.auth.noProviders", "No providers available")}
|
||||||
</div>
|
</div>) : (<div className="auth-panel-body">
|
||||||
) : (
|
<PluginSlot slotId="settings-provider-card" projectId={projectId} renderPlaceholder={false} actions={{ refreshAuthProviders: () => { void loadAuthStatus(); } }}/>
|
||||||
<div className="auth-panel-body">
|
<PluginSlot slotId="settings-integration-card" projectId={projectId} renderPlaceholder={false} actions={{ refreshAuthProviders: () => { void loadAuthStatus(); } }}/>
|
||||||
<PluginSlot
|
{!showAuthenticatedGroup && (<div className="auth-section-hint">
|
||||||
slotId="settings-provider-card"
|
|
||||||
projectId={projectId}
|
|
||||||
renderPlaceholder={false}
|
|
||||||
actions={{ refreshAuthProviders: () => { void loadAuthStatus(); } }}
|
|
||||||
/>
|
|
||||||
<PluginSlot
|
|
||||||
slotId="settings-integration-card"
|
|
||||||
projectId={projectId}
|
|
||||||
renderPlaceholder={false}
|
|
||||||
actions={{ refreshAuthProviders: () => { void loadAuthStatus(); } }}
|
|
||||||
/>
|
|
||||||
{!showAuthenticatedGroup && (
|
|
||||||
<div className="auth-section-hint">
|
|
||||||
{t("settings.auth.signInHint", "Sign in to at least one provider to get started with AI models.")}
|
{t("settings.auth.signInHint", "Sign in to at least one provider to get started with AI models.")}
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
{showAuthenticatedGroup && (<div className="auth-provider-group">
|
||||||
{showAuthenticatedGroup && (
|
|
||||||
<div className="auth-provider-group">
|
|
||||||
<div className="auth-group-label">{t("settings.auth.groupAuthenticated", "Authenticated")}</div>
|
<div className="auth-group-label">{t("settings.auth.groupAuthenticated", "Authenticated")}</div>
|
||||||
{claudeCliProvider?.authenticated && claudeCliCard}
|
{claudeCliProvider?.authenticated && claudeCliCard}
|
||||||
{cursorCliProvider?.authenticated && cursorCliCard}
|
{cursorCliProvider?.authenticated && cursorCliCard}
|
||||||
{llamaCppProvider?.authenticated && llamaCppCard}
|
{llamaCppProvider?.authenticated && llamaCppCard}
|
||||||
{authenticatedProviders.map((provider) => (
|
{authenticatedProviders.map((provider) => (<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
|
||||||
<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
|
|
||||||
<div className="auth-provider-header">
|
<div className="auth-provider-header">
|
||||||
<div className="auth-provider-info">
|
<div className="auth-provider-info">
|
||||||
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
||||||
<span
|
<span className="auth-provider-icon-slot" data-testid={`auth-provider-icon-${provider.id}`} aria-hidden="true">
|
||||||
className="auth-provider-icon-slot"
|
<ProviderIcon provider={provider.id} size="md"/>
|
||||||
data-testid={`auth-provider-icon-${provider.id}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<ProviderIcon provider={provider.id} size="md" />
|
|
||||||
</span>
|
</span>
|
||||||
<strong>{provider.name}</strong>
|
<strong>{provider.name}</strong>
|
||||||
<span
|
<span data-testid={`auth-status-${provider.id}`} className={`auth-status-badge ${provider.authenticated ? "authenticated" : "not-authenticated"}`}>
|
||||||
data-testid={`auth-status-${provider.id}`}
|
|
||||||
className={`auth-status-badge ${provider.authenticated ? "authenticated" : "not-authenticated"}`}
|
|
||||||
>
|
|
||||||
{t("settings.auth.statusActive", "✓ Active")}
|
{t("settings.auth.statusActive", "✓ Active")}
|
||||||
</span>
|
</span>
|
||||||
{provider.authenticated && provider.keyHint && (
|
{provider.authenticated && provider.keyHint && (<span className="auth-key-hint">{t("settings.authentication.key", "Key: ")}{provider.keyHint}</span>)}
|
||||||
<span className="auth-key-hint">Key: {provider.keyHint}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{provider.type === "api_key" ? (
|
{provider.type === "api_key" ? (<div className="auth-apikey-section">
|
||||||
<div className="auth-apikey-section">
|
|
||||||
<div className="auth-apikey-input-row">
|
<div className="auth-apikey-input-row">
|
||||||
<input
|
<input type="password" className="auth-apikey-input" placeholder={t("settings.authentication.enterAPIKey", "Enter API key")} value={apiKeyInputs[provider.id] ?? ""} onChange={(e) => setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))} disabled={authActionInProgress === provider.id}/>
|
||||||
type="password"
|
{provider.authenticated && !apiKeyInputs[provider.id] ? (<button className="btn btn-sm" onClick={() => handleClearApiKey(provider.id)} disabled={authActionInProgress === provider.id}>
|
||||||
className="auth-apikey-input"
|
|
||||||
placeholder="Enter API key"
|
|
||||||
value={apiKeyInputs[provider.id] ?? ""}
|
|
||||||
onChange={(e) => setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))}
|
|
||||||
disabled={authActionInProgress === provider.id}
|
|
||||||
/>
|
|
||||||
{provider.authenticated && !apiKeyInputs[provider.id] ? (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => handleClearApiKey(provider.id)}
|
|
||||||
disabled={authActionInProgress === provider.id}
|
|
||||||
>
|
|
||||||
{t("settings.auth.clearKey", "Clear")}
|
{t("settings.auth.clearKey", "Clear")}
|
||||||
</button>
|
</button>) : (<button className="btn btn-primary btn-sm" onClick={() => handleSaveApiKey(provider.id)} disabled={authActionInProgress === provider.id}>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={() => handleSaveApiKey(provider.id)}
|
|
||||||
disabled={authActionInProgress === provider.id}
|
|
||||||
>
|
|
||||||
{t("settings.actions.save", "Save")}
|
{t("settings.actions.save", "Save")}
|
||||||
</button>
|
</button>)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{authActionInProgress === provider.id && (
|
{authActionInProgress === provider.id && (<small className="auth-apikey-progress">{t("settings.auth.savingKey", "Saving…")}</small>)}
|
||||||
<small className="auth-apikey-progress">{t("settings.auth.savingKey", "Saving…")}</small>
|
{apiKeyErrors[provider.id] && (<small className="auth-apikey-error">{apiKeyErrors[provider.id]}</small>)}
|
||||||
)}
|
{(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && (<small className={opencodeApiKeyRefreshStatus[provider.id].tone === "error" ? "form-error" : "text-muted"}>
|
||||||
{apiKeyErrors[provider.id] && (
|
|
||||||
<small className="auth-apikey-error">{apiKeyErrors[provider.id]}</small>
|
|
||||||
)}
|
|
||||||
{(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && (
|
|
||||||
<small className={opencodeApiKeyRefreshStatus[provider.id].tone === "error" ? "form-error" : "text-muted"}>
|
|
||||||
{opencodeApiKeyRefreshStatus[provider.id].message}
|
{opencodeApiKeyRefreshStatus[provider.id].message}
|
||||||
</small>
|
</small>)}
|
||||||
)}
|
</div>) : (<div>
|
||||||
</div>
|
{authActionInProgress === provider.id ? (<button className="btn btn-sm" disabled>
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
{authActionInProgress === provider.id ? (
|
|
||||||
<button className="btn btn-sm" disabled>
|
|
||||||
{t("settings.auth.loggingOut", "Logging out…")}
|
{t("settings.auth.loggingOut", "Logging out…")}
|
||||||
</button>
|
</button>) : provider.loginInProgress ? (<div className="auth-provider-actions-row">
|
||||||
) : provider.loginInProgress ? (
|
|
||||||
<div className="auth-provider-actions-row">
|
|
||||||
<button className="btn btn-sm" disabled>
|
<button className="btn btn-sm" disabled>
|
||||||
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-sm" onClick={() => handleCancelLogin(provider.id)}>
|
<button className="btn btn-sm" onClick={() => handleCancelLogin(provider.id)}>
|
||||||
{t("settings.actions.cancel", "Cancel")}
|
{t("settings.actions.cancel", "Cancel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>) : (<button className="btn btn-sm" onClick={() => handleLogout(provider.id)}>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => handleLogout(provider.id)}
|
|
||||||
>
|
|
||||||
{t("settings.auth.logout", "Logout")}
|
{t("settings.auth.logout", "Logout")}
|
||||||
</button>
|
</button>)}
|
||||||
)}
|
</div>)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>))}
|
||||||
))}
|
</div>)}
|
||||||
</div>
|
{showAvailableGroup && (<div className="auth-provider-group">
|
||||||
)}
|
|
||||||
{showAvailableGroup && (
|
|
||||||
<div className="auth-provider-group">
|
|
||||||
<div className="auth-group-label">{t("settings.auth.groupAvailable", "Available")}</div>
|
<div className="auth-group-label">{t("settings.auth.groupAvailable", "Available")}</div>
|
||||||
{claudeCliProvider && !claudeCliProvider.authenticated && claudeCliCard}
|
{claudeCliProvider && !claudeCliProvider.authenticated && claudeCliCard}
|
||||||
{cursorCliProvider && !cursorCliProvider.authenticated && cursorCliCard}
|
{cursorCliProvider && !cursorCliProvider.authenticated && cursorCliCard}
|
||||||
{llamaCppProvider && !llamaCppProvider.authenticated && llamaCppCard}
|
{llamaCppProvider && !llamaCppProvider.authenticated && llamaCppCard}
|
||||||
{unauthenticatedProviders.map((provider) => (
|
{unauthenticatedProviders.map((provider) => (<div key={provider.id} className="auth-provider-card">
|
||||||
<div key={provider.id} className="auth-provider-card">
|
|
||||||
<div className="auth-provider-header">
|
<div className="auth-provider-header">
|
||||||
<div className="auth-provider-info">
|
<div className="auth-provider-info">
|
||||||
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
||||||
<span
|
<span className="auth-provider-icon-slot" data-testid={`auth-provider-icon-${provider.id}`} aria-hidden="true">
|
||||||
className="auth-provider-icon-slot"
|
<ProviderIcon provider={provider.id} size="md"/>
|
||||||
data-testid={`auth-provider-icon-${provider.id}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<ProviderIcon provider={provider.id} size="md" />
|
|
||||||
</span>
|
</span>
|
||||||
<strong>{provider.name}</strong>
|
<strong>{provider.name}</strong>
|
||||||
<span
|
<span data-testid={`auth-status-${provider.id}`} className={`auth-status-badge ${provider.authenticated ? "authenticated" : "not-authenticated"}`}>
|
||||||
data-testid={`auth-status-${provider.id}`}
|
|
||||||
className={`auth-status-badge ${provider.authenticated ? "authenticated" : "not-authenticated"}`}
|
|
||||||
>
|
|
||||||
{t("settings.auth.statusNotConnected", "✗ Not connected")}
|
{t("settings.auth.statusNotConnected", "✗ Not connected")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{provider.type === "api_key" ? (
|
{provider.type === "api_key" ? (<div className="auth-apikey-section">
|
||||||
<div className="auth-apikey-section">
|
|
||||||
<div className="auth-apikey-input-row">
|
<div className="auth-apikey-input-row">
|
||||||
<input
|
<input type="password" className="auth-apikey-input" placeholder={t("settings.authentication.enterAPIKey", "Enter API key")} value={apiKeyInputs[provider.id] ?? ""} onChange={(e) => setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))} disabled={authActionInProgress === provider.id}/>
|
||||||
type="password"
|
<button className="btn btn-primary btn-sm" onClick={() => handleSaveApiKey(provider.id)} disabled={authActionInProgress === provider.id}>
|
||||||
className="auth-apikey-input"
|
|
||||||
placeholder="Enter API key"
|
|
||||||
value={apiKeyInputs[provider.id] ?? ""}
|
|
||||||
onChange={(e) => setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))}
|
|
||||||
disabled={authActionInProgress === provider.id}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={() => handleSaveApiKey(provider.id)}
|
|
||||||
disabled={authActionInProgress === provider.id}
|
|
||||||
>
|
|
||||||
{t("settings.actions.save", "Save")}
|
{t("settings.actions.save", "Save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{authActionInProgress === provider.id && (
|
{authActionInProgress === provider.id && (<small className="auth-apikey-progress">{t("settings.auth.savingKey", "Saving…")}</small>)}
|
||||||
<small className="auth-apikey-progress">{t("settings.auth.savingKey", "Saving…")}</small>
|
{apiKeyErrors[provider.id] && (<small className="auth-apikey-error">{apiKeyErrors[provider.id]}</small>)}
|
||||||
)}
|
{(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && (<small className={opencodeApiKeyRefreshStatus[provider.id].tone === "error" ? "form-error" : "text-muted"}>
|
||||||
{apiKeyErrors[provider.id] && (
|
|
||||||
<small className="auth-apikey-error">{apiKeyErrors[provider.id]}</small>
|
|
||||||
)}
|
|
||||||
{(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && (
|
|
||||||
<small className={opencodeApiKeyRefreshStatus[provider.id].tone === "error" ? "form-error" : "text-muted"}>
|
|
||||||
{opencodeApiKeyRefreshStatus[provider.id].message}
|
{opencodeApiKeyRefreshStatus[provider.id].message}
|
||||||
</small>
|
</small>)}
|
||||||
)}
|
</div>) : (<div>
|
||||||
</div>
|
{authActionInProgress === provider.id ? (<button className="btn btn-sm" disabled>
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
{authActionInProgress === provider.id ? (
|
|
||||||
<button className="btn btn-sm" disabled>
|
|
||||||
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
||||||
</button>
|
</button>) : provider.loginInProgress ? (<div className="auth-provider-actions-row">
|
||||||
) : provider.loginInProgress ? (
|
|
||||||
<div className="auth-provider-actions-row">
|
|
||||||
<button className="btn btn-sm" disabled>
|
<button className="btn btn-sm" disabled>
|
||||||
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
{t("settings.auth.waitingForLogin", "Waiting for login…")}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-sm" onClick={() => handleCancelLogin(provider.id)}>
|
<button className="btn btn-sm" onClick={() => handleCancelLogin(provider.id)}>
|
||||||
{t("settings.actions.cancel", "Cancel")}
|
{t("settings.actions.cancel", "Cancel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>) : (<button className="btn btn-primary btn-sm" onClick={() => handleLogin(provider.id)}>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={() => handleLogin(provider.id)}
|
|
||||||
>
|
|
||||||
{t("settings.auth.login", "Login")}
|
{t("settings.auth.login", "Login")}
|
||||||
</button>
|
</button>)}
|
||||||
)}
|
{provider.id === "github-copilot" && deviceCodes[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (<div className="auth-device-code-panel" data-testid={`auth-device-code-${provider.id}`}>
|
||||||
{provider.id === "github-copilot" && deviceCodes[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (
|
|
||||||
<div className="auth-device-code-panel" data-testid={`auth-device-code-${provider.id}`}>
|
|
||||||
<strong>{t("settings.auth.enterCodeOnGitHub", "Enter this code on GitHub")}</strong>
|
<strong>{t("settings.auth.enterCodeOnGitHub", "Enter this code on GitHub")}</strong>
|
||||||
<div className="auth-device-code-pill">{deviceCodes[provider.id].userCode}</div>
|
<div className="auth-device-code-pill">{deviceCodes[provider.id].userCode}</div>
|
||||||
<div className="auth-provider-actions-row">
|
<div className="auth-provider-actions-row">
|
||||||
<button
|
<button className="btn btn-sm" onClick={() => {
|
||||||
className="btn btn-sm"
|
void (async () => {
|
||||||
onClick={() => {
|
|
||||||
void (async () => {
|
|
||||||
const copied = await copyTextToClipboard(deviceCodes[provider.id].userCode);
|
const copied = await copyTextToClipboard(deviceCodes[provider.id].userCode);
|
||||||
if (copied) {
|
if (copied) {
|
||||||
addToast(t("settings.auth.copiedCodeToClipboard", "Copied code to clipboard"), "success");
|
addToast(t("settings.auth.copiedCodeToClipboard", "Copied code to clipboard"), "success");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addToast(t("settings.auth.failedToCopyCode", "Failed to copy code — copy it manually from the box above"), "error");
|
addToast(t("settings.auth.failedToCopyCode", "Failed to copy code — copy it manually from the box above"), "error");
|
||||||
})();
|
})();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{t("settings.auth.copyCode", "Copy code")}
|
{t("settings.auth.copyCode", "Copy code")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="btn btn-sm" onClick={() => window.open(appendTokenQuery(deviceCodes[provider.id].verificationUri), "_blank")}>
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => window.open(appendTokenQuery(deviceCodes[provider.id].verificationUri), "_blank")}
|
|
||||||
>
|
|
||||||
{t("settings.auth.openGitHub", "Open GitHub")}
|
{t("settings.auth.openGitHub", "Open GitHub")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
{loginInstructions[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (<LoginInstructions instructions={loginInstructions[provider.id]} data-testid={`auth-login-instructions-${provider.id}`}/>)}
|
||||||
{loginInstructions[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (
|
{manualCodeConfigs[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (<OAuthManualCodeForm value={manualCodeInputs[provider.id] ?? ""} onChange={(value) => setManualCodeInputs((prev) => ({ ...prev, [provider.id]: value }))} onSubmit={() => void handleSubmitManualCode(provider.id)} prompt={manualCodeConfigs[provider.id].prompt} placeholder={manualCodeConfigs[provider.id].placeholder} helpText={manualCodeConfigs[provider.id].helpText} disabled={manualCodeSubmitInProgress === provider.id} submitLabel={manualCodeSubmitInProgress === provider.id ? "Submitting…" : "Submit code"} data-testid={`auth-manual-code-${provider.id}`}/>)}
|
||||||
<LoginInstructions
|
</div>)}
|
||||||
instructions={loginInstructions[provider.id]}
|
|
||||||
data-testid={`auth-login-instructions-${provider.id}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{manualCodeConfigs[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (
|
|
||||||
<OAuthManualCodeForm
|
|
||||||
value={manualCodeInputs[provider.id] ?? ""}
|
|
||||||
onChange={(value) => setManualCodeInputs((prev) => ({ ...prev, [provider.id]: value }))}
|
|
||||||
onSubmit={() => void handleSubmitManualCode(provider.id)}
|
|
||||||
prompt={manualCodeConfigs[provider.id].prompt}
|
|
||||||
placeholder={manualCodeConfigs[provider.id].placeholder}
|
|
||||||
helpText={manualCodeConfigs[provider.id].helpText}
|
|
||||||
disabled={manualCodeSubmitInProgress === provider.id}
|
|
||||||
submitLabel={manualCodeSubmitInProgress === provider.id ? "Submitting…" : "Submit code"}
|
|
||||||
data-testid={`auth-manual-code-${provider.id}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>))}
|
||||||
))}
|
</div>)}
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<small className="auth-hint">
|
<small className="auth-hint">
|
||||||
{t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")}
|
{t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")}
|
||||||
</small>
|
</small>
|
||||||
{onReopenOnboarding && (
|
{onReopenOnboarding && (<div className="form-group" style={{ marginTop: "var(--space-md)" }}>
|
||||||
<div className="form-group" style={{ marginTop: "var(--space-md)" }}>
|
<button type="button" className="btn btn-sm" onClick={onReopenOnboarding}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={onReopenOnboarding}
|
|
||||||
>
|
|
||||||
{t("settings.auth.reopenOnboarding", "Reopen onboarding guide")}
|
{t("settings.auth.reopenOnboarding", "Reopen onboarding guide")}
|
||||||
</button>
|
</button>
|
||||||
<small className="settings-muted">
|
<small className="settings-muted">
|
||||||
{t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")}
|
{t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomProvidersSection />
|
<CustomProvidersSection />
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuthenticationSection;
|
export default AuthenticationSection;
|
||||||
|
|||||||
@@ -1,229 +1,116 @@
|
|||||||
/**
|
|
||||||
* Backups section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped database-backup and memory-backup schedules/retention/dirs plus
|
|
||||||
* the current-backups summary and the manual "Backup Now" action. The backup
|
|
||||||
* info fetch and the backup-now handler live in the shell (they touch the API and
|
|
||||||
* toast) and are relayed as props. Keys, validation regexes, and conditional
|
|
||||||
* disabling preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { BackupListResponse } from "../../../api";
|
import type { BackupListResponse } from "../../../api";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
export interface BackupsSectionProps extends SectionBaseProps {
|
export interface BackupsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
backupInfo: BackupListResponse | null;
|
backupInfo: BackupListResponse | null;
|
||||||
backupLoading: boolean;
|
backupLoading: boolean;
|
||||||
onBackupNow: () => void;
|
onBackupNow: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BackupsSection({ scopeBanner, form, setForm, backupInfo, backupLoading, onBackupNow }: BackupsSectionProps) {
|
export function BackupsSection({ scopeBanner, form, setForm, backupInfo, backupLoading, onBackupNow }: BackupsSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
return (
|
return (<>
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Database Backups</h4>
|
<h4 className="settings-section-heading">{t("settings.backups.databaseBackups", "Database Backups")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoBackupEnabled" className="checkbox-label">
|
<label htmlFor="autoBackupEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="autoBackupEnabled" type="checkbox" checked={form.autoBackupEnabled || false} onChange={(e) => setForm((f) => ({ ...f, autoBackupEnabled: e.target.checked }))}/>{t("settings.backups.enableAutomaticDatabaseBackups", " Enable automatic database backups ")}</label>
|
||||||
id="autoBackupEnabled"
|
<small>{t("settings.backups.whenEnabledTheDatabaseIsBackedUpAutomatically", "When enabled, the database is backed up automatically on a schedule")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.autoBackupEnabled || false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, autoBackupEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable automatic database backups
|
|
||||||
</label>
|
|
||||||
<small>When enabled, the database is backed up automatically on a schedule</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoBackupSchedule">Backup Schedule (Cron)</label>
|
<label htmlFor="autoBackupSchedule">{t("settings.backups.backupScheduleCron", "Backup Schedule (Cron)")}</label>
|
||||||
<input
|
<input id="autoBackupSchedule" type="text" placeholder={t("settings.backups.02", "0 2 * * *")} value={form.autoBackupSchedule || "0 2 * * *"} onChange={(e) => setForm((f) => ({ ...f, autoBackupSchedule: e.target.value }))} disabled={!form.autoBackupEnabled}/>
|
||||||
id="autoBackupSchedule"
|
<small>{t("settings.backups.cronExpressionForBackupTimingDefault02", " Cron expression for backup timing. Default: 0 2 * * * (daily at 2 AM). Examples: 0 * * * * (hourly), 0 0 * * 0 (weekly), */15 * * * * (every 15 min) ")}</small>
|
||||||
type="text"
|
{form.autoBackupSchedule && !/^[\s\d*,/-]+$/.test(form.autoBackupSchedule) && (<small className="field-error">{t("settings.backups.invalidCronExpressionFormat", "Invalid cron expression format")}</small>)}
|
||||||
placeholder="0 2 * * *"
|
|
||||||
value={form.autoBackupSchedule || "0 2 * * *"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, autoBackupSchedule: e.target.value }))
|
|
||||||
}
|
|
||||||
disabled={!form.autoBackupEnabled}
|
|
||||||
/>
|
|
||||||
<small>
|
|
||||||
Cron expression for backup timing. Default: 0 2 * * * (daily at 2 AM).
|
|
||||||
Examples: 0 * * * * (hourly), 0 0 * * 0 (weekly), */15 * * * * (every 15 min)
|
|
||||||
</small>
|
|
||||||
{form.autoBackupSchedule && !/^[\s\d*,/-]+$/.test(form.autoBackupSchedule) && (
|
|
||||||
<small className="field-error">Invalid cron expression format</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoBackupRetention">Retention Count</label>
|
<label htmlFor="autoBackupRetention">{t("settings.backups.retentionCount", "Retention Count")}</label>
|
||||||
<input
|
<input id="autoBackupRetention" type="number" min={1} max={100} value={form.autoBackupRetention ?? ""} onChange={(e) => {
|
||||||
id="autoBackupRetention"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={form.autoBackupRetention ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, autoBackupRetention: val === "" ? undefined : Number(val) }));
|
setForm((f) => ({ ...f, autoBackupRetention: val === "" ? undefined : Number(val) }));
|
||||||
}}
|
}} disabled={!form.autoBackupEnabled}/>
|
||||||
disabled={!form.autoBackupEnabled}
|
<small>{t("settings.backups.numberOfBackupFilesToKeepOldestAre", "Number of backup files to keep (oldest are deleted first). Range: 1-100.")}</small>
|
||||||
/>
|
{form.autoBackupRetention !== undefined && (form.autoBackupRetention < 1 || form.autoBackupRetention > 100) && (<small className="field-error">{t("settings.backups.mustBeBetween1And100", "Must be between 1 and 100")}</small>)}
|
||||||
<small>Number of backup files to keep (oldest are deleted first). Range: 1-100.</small>
|
|
||||||
{form.autoBackupRetention !== undefined && (form.autoBackupRetention < 1 || form.autoBackupRetention > 100) && (
|
|
||||||
<small className="field-error">Must be between 1 and 100</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoBackupDir">Backup Directory</label>
|
<label htmlFor="autoBackupDir">{t("settings.backups.backupDirectory", "Backup Directory")}</label>
|
||||||
<input
|
<input id="autoBackupDir" type="text" placeholder={t("settings.backups.fusionBackups", ".fusion/backups")} value={form.autoBackupDir || ".fusion/backups"} onChange={(e) => setForm((f) => ({ ...f, autoBackupDir: e.target.value }))} disabled={!form.autoBackupEnabled}/>
|
||||||
id="autoBackupDir"
|
<small>{t("settings.backups.directoryForBackupFilesRelativeToProjectRoot", "Directory for backup files, relative to project root")}</small>
|
||||||
type="text"
|
{form.autoBackupDir && form.autoBackupDir.includes("..") && (<small className="field-error">{t("settings.backups.pathCannotContainParentDirectoryTraversal", "Path cannot contain parent directory traversal (..)")}</small>)}
|
||||||
placeholder=".fusion/backups"
|
|
||||||
value={form.autoBackupDir || ".fusion/backups"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, autoBackupDir: e.target.value }))
|
|
||||||
}
|
|
||||||
disabled={!form.autoBackupEnabled}
|
|
||||||
/>
|
|
||||||
<small>Directory for backup files, relative to project root</small>
|
|
||||||
{form.autoBackupDir && form.autoBackupDir.includes("..") && (
|
|
||||||
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="settings-section-heading">Memory Backups</h4>
|
<h4 className="settings-section-heading">{t("settings.backups.memoryBackups", "Memory Backups")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryBackupEnabled" className="checkbox-label">
|
<label htmlFor="memoryBackupEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="memoryBackupEnabled" type="checkbox" checked={form.memoryBackupEnabled || false} onChange={(e) => setForm((f) => ({ ...f, memoryBackupEnabled: e.target.checked }))}/>{t("settings.backups.enableAutomaticMemoryBackups", " Enable automatic memory backups ")}</label>
|
||||||
id="memoryBackupEnabled"
|
<small>{t("settings.backups.whenEnabledProjectAndAgentMemoryFilesAre", "When enabled, project and agent memory files are backed up automatically on a schedule.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.memoryBackupEnabled || false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Enable automatic memory backups
|
|
||||||
</label>
|
|
||||||
<small>When enabled, project and agent memory files are backed up automatically on a schedule.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryBackupSchedule">Memory Backup Schedule (Cron)</label>
|
<label htmlFor="memoryBackupSchedule">{t("settings.backups.memoryBackupScheduleCron", "Memory Backup Schedule (Cron)")}</label>
|
||||||
<input
|
<input id="memoryBackupSchedule" type="text" placeholder={t("settings.backups.03", "0 3 * * *")} value={form.memoryBackupSchedule || "0 3 * * *"} onChange={(e) => setForm((f) => ({ ...f, memoryBackupSchedule: e.target.value }))} disabled={!form.memoryBackupEnabled}/>
|
||||||
id="memoryBackupSchedule"
|
<small>{t("settings.backups.cronExpressionForMemoryBackupTimingDefault0", "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).")}</small>
|
||||||
type="text"
|
{form.memoryBackupSchedule && !/^[\s\d*,/-]+$/.test(form.memoryBackupSchedule) && (<small className="field-error">{t("settings.backups.invalidCronExpressionFormat", "Invalid cron expression format")}</small>)}
|
||||||
placeholder="0 3 * * *"
|
|
||||||
value={form.memoryBackupSchedule || "0 3 * * *"}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupSchedule: e.target.value }))}
|
|
||||||
disabled={!form.memoryBackupEnabled}
|
|
||||||
/>
|
|
||||||
<small>Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).</small>
|
|
||||||
{form.memoryBackupSchedule && !/^[\s\d*,/-]+$/.test(form.memoryBackupSchedule) && (
|
|
||||||
<small className="field-error">Invalid cron expression format</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryBackupRetention">Memory Retention Count</label>
|
<label htmlFor="memoryBackupRetention">{t("settings.backups.memoryRetentionCount", "Memory Retention Count")}</label>
|
||||||
<input
|
<input id="memoryBackupRetention" type="number" min={1} max={100} value={form.memoryBackupRetention ?? ""} onChange={(e) => {
|
||||||
id="memoryBackupRetention"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={form.memoryBackupRetention ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, memoryBackupRetention: val === "" ? undefined : Number(val) }));
|
setForm((f) => ({ ...f, memoryBackupRetention: val === "" ? undefined : Number(val) }));
|
||||||
}}
|
}} disabled={!form.memoryBackupEnabled}/>
|
||||||
disabled={!form.memoryBackupEnabled}
|
<small>{t("settings.backups.numberOfMemoryBackupsToKeepOldestAre", "Number of memory backups to keep (oldest are deleted first). Range: 1-100.")}</small>
|
||||||
/>
|
{form.memoryBackupRetention !== undefined && (form.memoryBackupRetention < 1 || form.memoryBackupRetention > 100) && (<small className="field-error">{t("settings.backups.mustBeBetween1And100", "Must be between 1 and 100")}</small>)}
|
||||||
<small>Number of memory backups to keep (oldest are deleted first). Range: 1-100.</small>
|
|
||||||
{form.memoryBackupRetention !== undefined && (form.memoryBackupRetention < 1 || form.memoryBackupRetention > 100) && (
|
|
||||||
<small className="field-error">Must be between 1 and 100</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryBackupDir">Memory Backup Directory</label>
|
<label htmlFor="memoryBackupDir">{t("settings.backups.memoryBackupDirectory", "Memory Backup Directory")}</label>
|
||||||
<input
|
<input id="memoryBackupDir" type="text" placeholder={t("settings.backups.fusionBackupsMemory", ".fusion/backups/memory")} value={form.memoryBackupDir || ".fusion/backups/memory"} onChange={(e) => setForm((f) => ({ ...f, memoryBackupDir: e.target.value }))} disabled={!form.memoryBackupEnabled}/>
|
||||||
id="memoryBackupDir"
|
<small>{t("settings.backups.directoryForMemoryBackupsRelativeToProjectRoot", "Directory for memory backups, relative to project root.")}</small>
|
||||||
type="text"
|
{form.memoryBackupDir && form.memoryBackupDir.includes("..") && (<small className="field-error">{t("settings.backups.pathCannotContainParentDirectoryTraversal", "Path cannot contain parent directory traversal (..)")}</small>)}
|
||||||
placeholder=".fusion/backups/memory"
|
|
||||||
value={form.memoryBackupDir || ".fusion/backups/memory"}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupDir: e.target.value }))}
|
|
||||||
disabled={!form.memoryBackupEnabled}
|
|
||||||
/>
|
|
||||||
<small>Directory for memory backups, relative to project root.</small>
|
|
||||||
{form.memoryBackupDir && form.memoryBackupDir.includes("..") && (
|
|
||||||
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryBackupScope">Memory Backup Scope</label>
|
<label htmlFor="memoryBackupScope">{t("settings.backups.memoryBackupScope", "Memory Backup Scope")}</label>
|
||||||
<select
|
<select id="memoryBackupScope" value={form.memoryBackupScope || "all"} onChange={(e) => setForm((f) => ({ ...f, memoryBackupScope: e.target.value as "project" | "agents" | "all" }))} disabled={!form.memoryBackupEnabled}>
|
||||||
id="memoryBackupScope"
|
<option value="all">{t("settings.backups.allProjectAgents", "All (project + agents)")}</option>
|
||||||
value={form.memoryBackupScope || "all"}
|
<option value="project">{t("settings.backups.projectOnlyFusionMemory", "Project only (.fusion/memory)")}</option>
|
||||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupScope: e.target.value as "project" | "agents" | "all" }))}
|
<option value="agents">{t("settings.backups.agentsOnlyFusionAgentMemory", "Agents only (.fusion/agent-memory)")}</option>
|
||||||
disabled={!form.memoryBackupEnabled}
|
|
||||||
>
|
|
||||||
<option value="all">All (project + agents)</option>
|
|
||||||
<option value="project">Project only (.fusion/memory)</option>
|
|
||||||
<option value="agents">Agents only (.fusion/agent-memory)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{backupLoading ? (
|
{backupLoading ? (<div className="settings-empty-state">{t("settings.backups.loadingBackupInfo", "Loading backup info\u2026")}</div>) : backupInfo ? (<div className="form-group">
|
||||||
<div className="settings-empty-state">Loading backup info…</div>
|
<label>{t("settings.backups.currentBackups", "Current Backups")}</label>
|
||||||
) : backupInfo ? (
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Current Backups</label>
|
|
||||||
<div className="backup-stats">
|
<div className="backup-stats">
|
||||||
<div className="backup-stat">
|
<div className="backup-stat">
|
||||||
<span className="backup-stat-value">{backupInfo.count}</span>
|
<span className="backup-stat-value">{backupInfo.count}</span>
|
||||||
<span className="backup-stat-label">backups</span>
|
<span className="backup-stat-label">{t("settings.backups.backups", "backups")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-stat">
|
<div className="backup-stat">
|
||||||
<span className="backup-stat-value">
|
<span className="backup-stat-value">
|
||||||
{backupInfo.totalSize > 1024 * 1024
|
{backupInfo.totalSize > 1024 * 1024
|
||||||
? `${(backupInfo.totalSize / (1024 * 1024)).toFixed(1)} MB`
|
? `${(backupInfo.totalSize / (1024 * 1024)).toFixed(1)} MB`
|
||||||
: `${(backupInfo.totalSize / 1024).toFixed(1)} KB`}
|
: `${(backupInfo.totalSize / 1024).toFixed(1)} KB`}
|
||||||
</span>
|
</span>
|
||||||
<span className="backup-stat-label">total size</span>
|
<span className="backup-stat-label">{t("settings.backups.totalSize", "total size")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{backupInfo.backups.length > 0 && (
|
{backupInfo.backups.length > 0 && (<details className="backup-list">
|
||||||
<details className="backup-list">
|
<summary>{t("settings.backups.view", "View ")}{backupInfo.backups.length}{t("settings.backups.backupS", " backup(s)")}</summary>
|
||||||
<summary>View {backupInfo.backups.length} backup(s)</summary>
|
|
||||||
<ul>
|
<ul>
|
||||||
{backupInfo.backups.slice(0, 10).map((backup) => (
|
{backupInfo.backups.slice(0, 10).map((backup) => (<li key={backup.filename}>
|
||||||
<li key={backup.filename}>
|
|
||||||
<code>{backup.filename}</code>
|
<code>{backup.filename}</code>
|
||||||
<span className="backup-size">
|
<span className="backup-size">
|
||||||
{backup.size > 1024 * 1024
|
{backup.size > 1024 * 1024
|
||||||
? `${(backup.size / (1024 * 1024)).toFixed(1)} MB`
|
? `${(backup.size / (1024 * 1024)).toFixed(1)} MB`
|
||||||
: `${(backup.size / 1024).toFixed(1)} KB`}
|
: `${(backup.size / 1024).toFixed(1)} KB`}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>))}
|
||||||
))}
|
{backupInfo.backups.length > 10 && (<li><em>{t("settings.backups.and", "...and ")}{backupInfo.backups.length - 10}{t("settings.backups.more", " more")}</em></li>)}
|
||||||
{backupInfo.backups.length > 10 && (
|
|
||||||
<li><em>...and {backupInfo.backups.length - 10} more</em></li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>)}
|
||||||
)}
|
</div>) : null}
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={onBackupNow} disabled={backupLoading}>
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={onBackupNow}
|
|
||||||
disabled={backupLoading}
|
|
||||||
>
|
|
||||||
{backupLoading ? t("settings.backups.creating", "Creating…") : t("settings.backups.backupNow", "Backup Now")}
|
{backupLoading ? t("settings.backups.creating", "Creating…") : t("settings.backups.backupNow", "Backup Now")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BackupsSection;
|
export default BackupsSection;
|
||||||
|
|||||||
@@ -1,49 +1,24 @@
|
|||||||
/**
|
|
||||||
* Commands section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped test/build command inputs injected into generated task specs.
|
|
||||||
* Behavior and keys preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface CommandsSectionProps extends SectionBaseProps {
|
export interface CommandsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommandsSection({ scopeBanner, form, setForm }: CommandsSectionProps) {
|
export function CommandsSection({ scopeBanner, form, setForm }: CommandsSectionProps) {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Commands</h4>
|
<h4 className="settings-section-heading">{t("settings.commands.commands", "Commands")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="testCommand">Test Command</label>
|
<label htmlFor="testCommand">{t("settings.commands.testCommand", "Test Command")}</label>
|
||||||
<input
|
<input id="testCommand" type="text" placeholder={t("settings.commands.eGPnpmTest", "e.g. pnpm test")} value={form.testCommand || ""} onChange={(e) => setForm((f) => ({ ...f, testCommand: e.target.value || undefined }))}/>
|
||||||
id="testCommand"
|
<small>{t("settings.commands.commandUsedToRunTestsInjectedIntoGenerated", "Command used to run tests \u2014 injected into generated task specs")}</small>
|
||||||
type="text"
|
|
||||||
placeholder="e.g. pnpm test"
|
|
||||||
value={form.testCommand || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, testCommand: e.target.value || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Command used to run tests — injected into generated task specs</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="buildCommand">Build Command</label>
|
<label htmlFor="buildCommand">{t("settings.commands.buildCommand", "Build Command")}</label>
|
||||||
<input
|
<input id="buildCommand" type="text" placeholder={t("settings.commands.eGPnpmBuild", "e.g. pnpm build")} value={form.buildCommand || ""} onChange={(e) => setForm((f) => ({ ...f, buildCommand: e.target.value || undefined }))}/>
|
||||||
id="buildCommand"
|
<small>{t("settings.commands.commandUsedToBuildTheProjectInjectedInto", "Command used to build the project \u2014 injected into generated task specs")}</small>
|
||||||
type="text"
|
|
||||||
placeholder="e.g. pnpm build"
|
|
||||||
value={form.buildCommand || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, buildCommand: e.target.value || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Command used to build the project — injected into generated task specs</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommandsSection;
|
export default CommandsSection;
|
||||||
|
|||||||
@@ -1,94 +1,57 @@
|
|||||||
/**
|
|
||||||
* Experimental Features section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Renders the union of well-known experimental flags (always shown) and any
|
|
||||||
* custom flags present in settings, canonicalizing legacy aliases so each
|
|
||||||
* feature renders exactly one row. Toggling writes the canonical key and clears
|
|
||||||
* its legacy alias. The known-feature catalog and alias helpers live in the
|
|
||||||
* shell module and are passed in so this section stays presentational.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface ExperimentalSectionProps extends SectionBaseProps {
|
export interface ExperimentalSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
/** Display labels for well-known features (always rendered). */
|
/** Display labels for well-known features (always rendered). */
|
||||||
knownFeatures: Record<string, string>;
|
knownFeatures: Record<string, string>;
|
||||||
/** Map of legacy alias key -> canonical key. */
|
/** Map of legacy alias key -> canonical key. */
|
||||||
legacyAliases: Record<string, string>;
|
legacyAliases: Record<string, string>;
|
||||||
/** Canonicalize a possibly-legacy feature key. */
|
/** Canonicalize a possibly-legacy feature key. */
|
||||||
getCanonicalKey: (key: string) => string;
|
getCanonicalKey: (key: string) => string;
|
||||||
/** Whether a feature is enabled, honoring legacy aliases. */
|
/** Whether a feature is enabled, honoring legacy aliases. */
|
||||||
isFeatureEnabled: (features: Record<string, boolean>, key: string) => boolean;
|
isFeatureEnabled: (features: Record<string, boolean>, key: string) => boolean;
|
||||||
}
|
}
|
||||||
|
export function ExperimentalSection({ scopeBanner, form, setForm, knownFeatures, legacyAliases, getCanonicalKey, isFeatureEnabled, }: ExperimentalSectionProps) {
|
||||||
export function ExperimentalSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||||
form,
|
const allFeatureKeys = Array.from(new Set([
|
||||||
setForm,
|
...Object.keys(knownFeatures),
|
||||||
knownFeatures,
|
...Object.keys(experimentalFeatures).map(getCanonicalKey),
|
||||||
legacyAliases,
|
])).sort((a, b) => a.localeCompare(b));
|
||||||
getCanonicalKey,
|
const featureFlags = allFeatureKeys.map((key) => [key, isFeatureEnabled(experimentalFeatures, key)] as const);
|
||||||
isFeatureEnabled,
|
return (<>
|
||||||
}: ExperimentalSectionProps) {
|
|
||||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
|
||||||
const allFeatureKeys = Array.from(
|
|
||||||
new Set([
|
|
||||||
...Object.keys(knownFeatures),
|
|
||||||
...Object.keys(experimentalFeatures).map(getCanonicalKey),
|
|
||||||
]),
|
|
||||||
).sort((a, b) => a.localeCompare(b));
|
|
||||||
const featureFlags = allFeatureKeys.map(
|
|
||||||
(key) => [key, isFeatureEnabled(experimentalFeatures, key)] as const,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Experimental Features</h4>
|
<h4 className="settings-section-heading">{t("settings.experimental.experimentalFeatures", "Experimental Features")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<small>
|
<small>{t("settings.experimental.experimentalFeaturesAreEarlyCapabilitiesThatAreNot", " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. ")}</small>
|
||||||
Experimental features are early capabilities that are not yet fully stable.
|
|
||||||
Enable them to test new functionality, but be aware they may change or be removed.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Feature Flags</label>
|
<label>{t("settings.experimental.featureFlags", "Feature Flags")}</label>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||||
{featureFlags.map(([key, enabled]) => (
|
{featureFlags.map(([key, enabled]) => (<label key={key} htmlFor={`experimental-${key}`} className="checkbox-label">
|
||||||
<label key={key} htmlFor={`experimental-${key}`} className="checkbox-label">
|
<input id={`experimental-${key}`} type="checkbox" checked={enabled} onChange={(e) => {
|
||||||
<input
|
setForm((f) => {
|
||||||
id={`experimental-${key}`}
|
|
||||||
type="checkbox"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
setForm((f) => {
|
|
||||||
const nextExperimentalFeatures = {
|
const nextExperimentalFeatures = {
|
||||||
...(f.experimentalFeatures ?? {}),
|
...(f.experimentalFeatures ?? {}),
|
||||||
[key]: e.target.checked,
|
[key]: e.target.checked,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [legacyKey, canonicalKey] of Object.entries(legacyAliases)) {
|
for (const [legacyKey, canonicalKey] of Object.entries(legacyAliases)) {
|
||||||
if (canonicalKey === key) {
|
if (canonicalKey === key) {
|
||||||
delete nextExperimentalFeatures[legacyKey];
|
delete nextExperimentalFeatures[legacyKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...f,
|
...f,
|
||||||
experimentalFeatures: nextExperimentalFeatures,
|
experimentalFeatures: nextExperimentalFeatures,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
<span>{knownFeatures[key] ?? key}</span>
|
<span>{knownFeatures[key] ?? key}</span>
|
||||||
</label>
|
</label>))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExperimentalSection;
|
export default ExperimentalSection;
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
/**
|
|
||||||
* Project General section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped general settings: task prefix, default workflow, ephemeral
|
|
||||||
* agents, completion-documentation mode, quick-chat FAB, chat-history/mail/log
|
|
||||||
* retention, chat-room compaction tuning, capacity-risk banner, and GitHub
|
|
||||||
* tracking defaults. The prefix-validation error and the project tracking-repo
|
|
||||||
* options are owned by the shell (the prefix error gates Save; the repo options
|
|
||||||
* are fetched once) and relayed as props. Keys, validation regexes, and the
|
|
||||||
* cross-field summarizer hint are preserved verbatim from the original inline
|
|
||||||
* JSX.
|
|
||||||
*/
|
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import type { WorkflowDefinition } from "@fusion/core";
|
import type { WorkflowDefinition } from "@fusion/core";
|
||||||
import { ProjectDefaultWorkflowField } from "../../WorkflowSelector";
|
import { ProjectDefaultWorkflowField } from "../../WorkflowSelector";
|
||||||
@@ -17,373 +5,206 @@ import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoS
|
|||||||
import { fetchWorkflows } from "../../../api";
|
import { fetchWorkflows } from "../../../api";
|
||||||
import type { ToastType } from "../../../hooks/useToast";
|
import type { ToastType } from "../../../hooks/useToast";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface GeneralSectionProps extends SectionBaseProps {
|
export interface GeneralSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
prefixError: string | null;
|
prefixError: string | null;
|
||||||
setPrefixError: (value: string | null) => void;
|
setPrefixError: (value: string | null) => void;
|
||||||
projectTrackingRepoOptions: TrackingRepoOption[];
|
projectTrackingRepoOptions: TrackingRepoOption[];
|
||||||
projectTrackingRepoLoading: boolean;
|
projectTrackingRepoLoading: boolean;
|
||||||
projectTrackingRepoError: string | null;
|
projectTrackingRepoError: string | null;
|
||||||
}
|
}
|
||||||
|
export function GeneralSection({ scopeBanner, form, setForm, projectId, addToast, prefixError, setPrefixError, projectTrackingRepoOptions, projectTrackingRepoLoading, projectTrackingRepoError, }: GeneralSectionProps) {
|
||||||
export function GeneralSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
const [builtinWorkflows, setBuiltinWorkflows] = useState<WorkflowDefinition[]>([]);
|
||||||
form,
|
useEffect(() => {
|
||||||
setForm,
|
let cancelled = false;
|
||||||
projectId,
|
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
||||||
addToast,
|
.then((workflows) => {
|
||||||
prefixError,
|
if (!cancelled) {
|
||||||
setPrefixError,
|
setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"));
|
||||||
projectTrackingRepoOptions,
|
}
|
||||||
projectTrackingRepoLoading,
|
})
|
||||||
projectTrackingRepoError,
|
.catch(() => {
|
||||||
}: GeneralSectionProps) {
|
if (!cancelled)
|
||||||
const [builtinWorkflows, setBuiltinWorkflows] = useState<WorkflowDefinition[]>([]);
|
setBuiltinWorkflows([]);
|
||||||
|
});
|
||||||
useEffect(() => {
|
return () => {
|
||||||
let cancelled = false;
|
cancelled = true;
|
||||||
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
};
|
||||||
.then((workflows) => {
|
}, [projectId]);
|
||||||
if (!cancelled) {
|
const enabledBuiltinWorkflowIds = useMemo(() => {
|
||||||
setBuiltinWorkflows(
|
const configured = Array.isArray(form.enabledBuiltinWorkflowIds) ? form.enabledBuiltinWorkflowIds : undefined;
|
||||||
workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"),
|
return new Set(configured ?? builtinWorkflows.map((workflow) => workflow.id));
|
||||||
);
|
}, [builtinWorkflows, form.enabledBuiltinWorkflowIds]);
|
||||||
}
|
const setBuiltinWorkflowEnabled = (workflowId: string, enabled: boolean) => {
|
||||||
})
|
setForm((f) => {
|
||||||
.catch(() => {
|
const allIds = builtinWorkflows.map((workflow) => workflow.id);
|
||||||
if (!cancelled) setBuiltinWorkflows([]);
|
const current = new Set(Array.isArray(f.enabledBuiltinWorkflowIds) ? f.enabledBuiltinWorkflowIds : allIds);
|
||||||
});
|
if (enabled) {
|
||||||
return () => {
|
current.add(workflowId);
|
||||||
cancelled = true;
|
}
|
||||||
|
else {
|
||||||
|
current.delete(workflowId);
|
||||||
|
}
|
||||||
|
const nextIds = allIds.filter((id) => current.has(id));
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
enabledBuiltinWorkflowIds: nextIds.length === allIds.length ? undefined : nextIds,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [projectId]);
|
return (<>
|
||||||
|
|
||||||
const enabledBuiltinWorkflowIds = useMemo(() => {
|
|
||||||
const configured = Array.isArray(form.enabledBuiltinWorkflowIds) ? form.enabledBuiltinWorkflowIds : undefined;
|
|
||||||
return new Set(configured ?? builtinWorkflows.map((workflow) => workflow.id));
|
|
||||||
}, [builtinWorkflows, form.enabledBuiltinWorkflowIds]);
|
|
||||||
|
|
||||||
const setBuiltinWorkflowEnabled = (workflowId: string, enabled: boolean) => {
|
|
||||||
setForm((f) => {
|
|
||||||
const allIds = builtinWorkflows.map((workflow) => workflow.id);
|
|
||||||
const current = new Set(Array.isArray(f.enabledBuiltinWorkflowIds) ? f.enabledBuiltinWorkflowIds : allIds);
|
|
||||||
if (enabled) {
|
|
||||||
current.add(workflowId);
|
|
||||||
} else {
|
|
||||||
current.delete(workflowId);
|
|
||||||
}
|
|
||||||
const nextIds = allIds.filter((id) => current.has(id));
|
|
||||||
return {
|
|
||||||
...f,
|
|
||||||
enabledBuiltinWorkflowIds: nextIds.length === allIds.length ? undefined : nextIds,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">General</h4>
|
<h4 className="settings-section-heading">{t("settings.general.general", "General")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="taskPrefix">Task Prefix</label>
|
<label htmlFor="taskPrefix">{t("settings.general.taskPrefix", "Task Prefix")}</label>
|
||||||
<input
|
<input id="taskPrefix" type="text" placeholder={t("settings.general.fN", "FN")} value={form.taskPrefix || ""} onChange={(e) => {
|
||||||
id="taskPrefix"
|
|
||||||
type="text"
|
|
||||||
placeholder="FN"
|
|
||||||
value={form.taskPrefix || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, taskPrefix: val || undefined }));
|
setForm((f) => ({ ...f, taskPrefix: val || undefined }));
|
||||||
if (val && !/^[A-Z]{1,10}$/.test(val)) {
|
if (val && !/^[A-Z]{1,10}$/.test(val)) {
|
||||||
setPrefixError("Prefix must be 1–10 uppercase letters");
|
setPrefixError(t("settings.general.prefixMustBe110UppercaseLetters", "Prefix must be 1–10 uppercase letters"));
|
||||||
} else {
|
|
||||||
setPrefixError(null);
|
|
||||||
}
|
}
|
||||||
}}
|
else {
|
||||||
/>
|
setPrefixError(null);
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
{prefixError && <small className="field-error">{prefixError}</small>}
|
{prefixError && <small className="field-error">{prefixError}</small>}
|
||||||
{!prefixError && <small>Prefix for new task IDs (e.g. KB, PROJ)</small>}
|
{!prefixError && <small>{t("settings.general.prefixForNewTaskIDsEGKB", "Prefix for new task IDs (e.g. KB, PROJ)")}</small>}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<ProjectDefaultWorkflowField projectId={projectId} addToast={addToast} />
|
<ProjectDefaultWorkflowField projectId={projectId} addToast={addToast}/>
|
||||||
<small>New tasks inherit this custom workflow's steps (overridable per task)</small>
|
<small>{t("settings.general.newTasksInheritThisCustomWorkflowsStepsOverridable", "New tasks inherit this custom workflow's steps (overridable per task)")}</small>
|
||||||
</div>
|
</div>
|
||||||
{builtinWorkflows.length > 0 && (
|
{builtinWorkflows.length > 0 && (<div className="form-group">
|
||||||
<div className="form-group">
|
<label>{t("settings.general.builtInWorkflows", "Built-in workflows")}</label>
|
||||||
<label>Built-in workflows</label>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||||
{builtinWorkflows.map((workflow) => (
|
{builtinWorkflows.map((workflow) => (<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label">
|
||||||
<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label">
|
<input id={`builtin-workflow-${workflow.id}`} type="checkbox" checked={enabledBuiltinWorkflowIds.has(workflow.id)} onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}/>
|
||||||
<input
|
|
||||||
id={`builtin-workflow-${workflow.id}`}
|
|
||||||
type="checkbox"
|
|
||||||
checked={enabledBuiltinWorkflowIds.has(workflow.id)}
|
|
||||||
onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>{workflow.name}</span>
|
<span>{workflow.name}</span>
|
||||||
</label>
|
</label>))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<small>Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.</small>
|
<small>{t("settings.general.disabledBuiltInWorkflowsAreHiddenFromWorkflow", "Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.")}</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
|
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="ephemeralAgentsEnabled" type="checkbox" checked={form.ephemeralAgentsEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))}/>{t("settings.general.useEphemeralTaskWorkerAgents", " Use ephemeral task-worker agents ")}</label>
|
||||||
id="ephemeralAgentsEnabled"
|
<small>{t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}<code>executor-FN-XXXX</code>{t("settings.general.agentsToRunEachTaskWhenDisabledOnly", " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.ephemeralAgentsEnabled !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Use ephemeral task-worker agents
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled (default), Fusion spawns short-lived <code>executor-FN-XXXX</code> agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="completionDocumentationMode">Completion Documentation Automation</label>
|
<label htmlFor="completionDocumentationMode">{t("settings.general.completionDocumentationAutomation", "Completion Documentation Automation")}</label>
|
||||||
<select
|
<select id="completionDocumentationMode" value={form.completionDocumentationMode || "off"} onChange={(e) => setForm((f) => ({
|
||||||
id="completionDocumentationMode"
|
...f,
|
||||||
value={form.completionDocumentationMode || "off"}
|
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
|
||||||
onChange={(e) =>
|
}))}>
|
||||||
setForm((f) => ({
|
<option value="off">{t("settings.general.off", "Off")}</option>
|
||||||
...f,
|
<option value="changeset">{t("settings.general.requireChangesetChangesetMd", "Require changeset (.changeset/*.md)")}</option>
|
||||||
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
|
<option value="changelog">{t("settings.general.requireChangelogUpdateExistingChangelog", "Require changelog update (existing changelog)")}</option>
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="off">Off</option>
|
|
||||||
<option value="changeset">Require changeset (.changeset/*.md)</option>
|
|
||||||
<option value="changelog">Require changelog update (existing changelog)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.general.controlsHowFutureTaskSpecsHandleReleaseNote", " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ")}<code>.changeset</code>{t("settings.general.workflowsOrChangelogModeWhenContributorsShouldUpdate", " workflows, or changelog mode when contributors should update an existing changelog file. ")}</small>
|
||||||
Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow
|
|
||||||
<code>.changeset</code> workflows, or changelog mode when contributors should update an existing changelog file.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="showQuickChatFAB" className="checkbox-label">
|
<label htmlFor="showQuickChatFAB" className="checkbox-label">
|
||||||
<input
|
<input id="showQuickChatFAB" type="checkbox" checked={form.showQuickChatFAB === true} onChange={(e) => setForm((f) => ({ ...f, showQuickChatFAB: e.target.checked }))}/>{t("settings.general.showQuickChatButton", " Show quick chat button ")}</label>
|
||||||
id="showQuickChatFAB"
|
<small>{t("settings.general.showTheFloatingChatButtonInTheDashboard", "Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.showQuickChatFAB === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, showQuickChatFAB: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Show quick chat button
|
|
||||||
</label>
|
|
||||||
<small>Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Chat history</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.chatHistory", "Chat history")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="chatAutoCleanupDays">Auto-cleanup old chats</label>
|
<label htmlFor="chatAutoCleanupDays">{t("settings.general.autoCleanupOldChats", "Auto-cleanup old chats")}</label>
|
||||||
<select
|
<select id="chatAutoCleanupDays" className="select" value={form.chatAutoCleanupDays ?? 0} onChange={(e) => setForm((f) => ({ ...f, chatAutoCleanupDays: Number(e.target.value) || 0 }))}>
|
||||||
id="chatAutoCleanupDays"
|
<option value={0}>{t("settings.general.off", "Off")}</option>
|
||||||
className="select"
|
<option value={7}>{t("settings.general.7Days", "7 days")}</option>
|
||||||
value={form.chatAutoCleanupDays ?? 0}
|
<option value={14}>{t("settings.general.14Days", "14 days")}</option>
|
||||||
onChange={(e) =>
|
<option value={30}>{t("settings.general.30Days", "30 days")}</option>
|
||||||
setForm((f) => ({ ...f, chatAutoCleanupDays: Number(e.target.value) || 0 }))
|
<option value={60}>{t("settings.general.60Days", "60 days")}</option>
|
||||||
}
|
<option value={90}>{t("settings.general.90Days", "90 days")}</option>
|
||||||
>
|
|
||||||
<option value={0}>Off</option>
|
|
||||||
<option value={7}>7 days</option>
|
|
||||||
<option value={14}>14 days</option>
|
|
||||||
<option value={30}>30 days</option>
|
|
||||||
<option value={60}>60 days</option>
|
|
||||||
<option value={90}>90 days</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>Delete chat sessions and rooms that have been idle for this many days. Default: Off.</small>
|
<small>{t("settings.general.deleteChatSessionsAndRoomsThatHaveBeen", "Delete chat sessions and rooms that have been idle for this many days. Default: Off.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mailAutoCleanupDays">Auto-prune old mail</label>
|
<label htmlFor="mailAutoCleanupDays">{t("settings.general.autoPruneOldMail", "Auto-prune old mail")}</label>
|
||||||
<select
|
<select id="mailAutoCleanupDays" className="select" value={form.mailAutoCleanupDays ?? 0} onChange={(e) => setForm((f) => ({ ...f, mailAutoCleanupDays: Number(e.target.value) || 0 }))}>
|
||||||
id="mailAutoCleanupDays"
|
<option value={0}>{t("settings.general.off", "Off")}</option>
|
||||||
className="select"
|
<option value={7}>{t("settings.general.7Days", "7 days")}</option>
|
||||||
value={form.mailAutoCleanupDays ?? 0}
|
<option value={14}>{t("settings.general.14Days", "14 days")}</option>
|
||||||
onChange={(e) =>
|
<option value={30}>{t("settings.general.30Days", "30 days")}</option>
|
||||||
setForm((f) => ({ ...f, mailAutoCleanupDays: Number(e.target.value) || 0 }))
|
<option value={60}>{t("settings.general.60Days", "60 days")}</option>
|
||||||
}
|
<option value={90}>{t("settings.general.90Days", "90 days")}</option>
|
||||||
>
|
|
||||||
<option value={0}>Off</option>
|
|
||||||
<option value={7}>7 days</option>
|
|
||||||
<option value={14}>14 days</option>
|
|
||||||
<option value={30}>30 days</option>
|
|
||||||
<option value={60}>60 days</option>
|
|
||||||
<option value={90}>90 days</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.</small>
|
<small>{t("settings.general.deleteInboxOutboxMessagesOlderThanThisMany", "Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="operationalLogRetentionDays">Operational log retention</label>
|
<label htmlFor="operationalLogRetentionDays">{t("settings.general.operationalLogRetention", "Operational log retention")}</label>
|
||||||
<select
|
<select id="operationalLogRetentionDays" className="select" value={form.operationalLogRetentionDays ?? 30} onChange={(e) => setForm((f) => ({ ...f, operationalLogRetentionDays: Number(e.target.value) || 0 }))}>
|
||||||
id="operationalLogRetentionDays"
|
<option value={0}>{t("settings.general.off", "Off")}</option>
|
||||||
className="select"
|
<option value={7}>{t("settings.general.7Days", "7 days")}</option>
|
||||||
value={form.operationalLogRetentionDays ?? 30}
|
<option value={14}>{t("settings.general.14Days", "14 days")}</option>
|
||||||
onChange={(e) =>
|
<option value={30}>{t("settings.general.30Days", "30 days")}</option>
|
||||||
setForm((f) => ({ ...f, operationalLogRetentionDays: Number(e.target.value) || 0 }))
|
<option value={60}>{t("settings.general.60Days", "60 days")}</option>
|
||||||
}
|
<option value={90}>{t("settings.general.90Days", "90 days")}</option>
|
||||||
>
|
|
||||||
<option value={0}>Off</option>
|
|
||||||
<option value={7}>7 days</option>
|
|
||||||
<option value={14}>14 days</option>
|
|
||||||
<option value={30}>30 days</option>
|
|
||||||
<option value={60}>60 days</option>
|
|
||||||
<option value={90}>90 days</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.general.loweringThisWindowMeansReliabilityMetricsChartsAnd", " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ")}</small>
|
||||||
Lowering this window means Reliability metrics/charts and the Activity feed will not show history older
|
|
||||||
than the selected range. Per-task task detail history is unaffected. Default: 30 days.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Chat Rooms</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.chatRooms", "Chat Rooms")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="chatRoomRecentVerbatimMessages">Recent verbatim room messages</label>
|
<label htmlFor="chatRoomRecentVerbatimMessages">{t("settings.general.recentVerbatimRoomMessages", "Recent verbatim room messages")}</label>
|
||||||
<input
|
<input id="chatRoomRecentVerbatimMessages" type="number" min="1" className="input" placeholder={t("settings.general.25", "25")} value={form.chatRoomRecentVerbatimMessages ?? ""} onChange={(e) => setForm((f) => ({ ...f, chatRoomRecentVerbatimMessages: Number(e.target.value) || undefined }))}/>
|
||||||
id="chatRoomRecentVerbatimMessages"
|
<small>{t("settings.general.numberOfMostRecentChatRoomMessagesKept", "Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.")}</small>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
className="input"
|
|
||||||
placeholder="25"
|
|
||||||
value={form.chatRoomRecentVerbatimMessages ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, chatRoomRecentVerbatimMessages: Number(e.target.value) || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="chatRoomCompactionFetchLimit">Room compaction fetch limit</label>
|
<label htmlFor="chatRoomCompactionFetchLimit">{t("settings.general.roomCompactionFetchLimit", "Room compaction fetch limit")}</label>
|
||||||
<input
|
<input id="chatRoomCompactionFetchLimit" type="number" min="1" className="input" placeholder={t("settings.general.200", "200")} value={form.chatRoomCompactionFetchLimit ?? ""} onChange={(e) => setForm((f) => ({ ...f, chatRoomCompactionFetchLimit: Number(e.target.value) || undefined }))}/>
|
||||||
id="chatRoomCompactionFetchLimit"
|
<small>{t("settings.general.upperBoundOnMessagesFetchedFromTheRoom", "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.")}</small>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
className="input"
|
|
||||||
placeholder="200"
|
|
||||||
value={form.chatRoomCompactionFetchLimit ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, chatRoomCompactionFetchLimit: Number(e.target.value) || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Upper bound on messages fetched from the room store for compaction consideration. Default: 200.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="chatRoomSummaryMaxChars">Room summary max characters</label>
|
<label htmlFor="chatRoomSummaryMaxChars">{t("settings.general.roomSummaryMaxCharacters", "Room summary max characters")}</label>
|
||||||
<input
|
<input id="chatRoomSummaryMaxChars" type="number" min="200" className="input" placeholder={t("settings.general.3000", "3000")} value={form.chatRoomSummaryMaxChars ?? ""} onChange={(e) => setForm((f) => ({ ...f, chatRoomSummaryMaxChars: Number(e.target.value) || undefined }))}/>
|
||||||
id="chatRoomSummaryMaxChars"
|
<small>{t("settings.general.hardCapOnTheSynthesizedEarlierRoomContext", "Hard cap on the synthesized \"Earlier room context\" summary block. Default: 3000.")}</small>
|
||||||
type="number"
|
|
||||||
min="200"
|
|
||||||
className="input"
|
|
||||||
placeholder="3000"
|
|
||||||
value={form.chatRoomSummaryMaxChars ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, chatRoomSummaryMaxChars: Number(e.target.value) || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Hard cap on the synthesized "Earlier room context" summary block. Default: 3000.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Capacity Risk Banner</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.capacityRiskBanner", "Capacity Risk Banner")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="capacityRiskBannerEnabled" className="checkbox-label">
|
<label htmlFor="capacityRiskBannerEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="capacityRiskBannerEnabled" type="checkbox" checked={form.capacityRiskBannerEnabled === true} onChange={(e) => setForm((f) => ({ ...f, capacityRiskBannerEnabled: e.target.checked }))}/>{t("settings.general.showCapacityRiskBanner", " Show capacity risk banner ")}</label>
|
||||||
id="capacityRiskBannerEnabled"
|
<small>{t("settings.general.warnOnTheBoardWhenTodoWorkExceeds", "Warn on the board when todo work exceeds the threshold and no idle agents are available.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.capacityRiskBannerEnabled === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, capacityRiskBannerEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Show capacity risk banner
|
|
||||||
</label>
|
|
||||||
<small>Warn on the board when todo work exceeds the threshold and no idle agents are available.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="capacityRiskTodoThresholdGeneral">Todo threshold</label>
|
<label htmlFor="capacityRiskTodoThresholdGeneral">{t("settings.general.todoThreshold", "Todo threshold")}</label>
|
||||||
<input
|
<input id="capacityRiskTodoThresholdGeneral" type="number" min={0} className="input" value={form.capacityRiskTodoThreshold ?? 20} onChange={(e) => setForm((f) => ({
|
||||||
id="capacityRiskTodoThresholdGeneral"
|
...f,
|
||||||
type="number"
|
capacityRiskTodoThreshold: e.target.value === ""
|
||||||
min={0}
|
? 0
|
||||||
className="input"
|
: Math.max(0, Number.parseInt(e.target.value, 10) || 0),
|
||||||
value={form.capacityRiskTodoThreshold ?? 20}
|
}))}/>
|
||||||
onChange={(e) =>
|
<small>{t("settings.general.bannerFiresWhenTodoCountIsStrictlyGreater", "Banner fires when todo count is strictly greater than this value (default 20). Applies when the banner is enabled.")}</small>
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
capacityRiskTodoThreshold:
|
|
||||||
e.target.value === ""
|
|
||||||
? 0
|
|
||||||
: Math.max(0, Number.parseInt(e.target.value, 10) || 0),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Banner fires when todo count is strictly greater than this value (default 20). Applies when the banner is enabled.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Tracking</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.gitHubTracking", "GitHub Tracking")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="githubTrackingMode">Default tracking mode for new tasks</label>
|
<label htmlFor="githubTrackingMode">{t("settings.general.defaultTrackingModeForNewTasks", "Default tracking mode for new tasks")}</label>
|
||||||
<select
|
<select id="githubTrackingMode" className="select" value={form.githubTrackingEnabledByDefault ? "new-tasks" : "off"} onChange={(e) => setForm((f) => ({
|
||||||
id="githubTrackingMode"
|
...f,
|
||||||
className="select"
|
githubTrackingEnabledByDefault: e.target.value === "new-tasks",
|
||||||
value={form.githubTrackingEnabledByDefault ? "new-tasks" : "off"}
|
}))}>
|
||||||
onChange={(e) =>
|
<option value="off">{t("settings.general.offDefault", "Off (default)")}</option>
|
||||||
setForm((f) => ({
|
<option value="new-tasks">{t("settings.general.onForNewTasks", "On for new tasks")}</option>
|
||||||
...f,
|
|
||||||
githubTrackingEnabledByDefault: e.target.value === "new-tasks",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="off">Off (default)</option>
|
|
||||||
<option value="new-tasks">On for new tasks</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.general.controlsWhetherNewlyCreatedTasksHaveGitHubIssue", " Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal. ")}</small>
|
||||||
Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal.
|
<small>{t("settings.general.trackingIssuesUseThisTaskAposSTitle", " Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models. ")}{!form.autoSummarizeTitles && !form.useAiMergeCommitSummary && !form.githubTrackingEnabledByDefault
|
||||||
</small>
|
? t("settings.general.enableSummarizationInProjectModelsToConfigureThatModel", " Enable summarization in Project Models to configure that model.")
|
||||||
<small>
|
|
||||||
Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models.
|
|
||||||
{!form.autoSummarizeTitles && !form.useAiMergeCommitSummary && !form.githubTrackingEnabledByDefault
|
|
||||||
? " Enable summarization in Project Models to configure that model."
|
|
||||||
: ""}
|
: ""}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
|
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">{t("settings.general.projectDefaultTrackingRepo", "Project default tracking repo")}</label>
|
||||||
<TrackingRepoSelect
|
<TrackingRepoSelect id="projectGithubTrackingDefaultRepoGeneral" ariaLabel="Project default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={projectTrackingRepoOptions} loading={projectTrackingRepoLoading} error={projectTrackingRepoError ?? undefined} placeholder={t("settings.general.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
|
||||||
id="projectGithubTrackingDefaultRepoGeneral"
|
<small>{t("settings.general.defaultRepoUsedWhenCreatingGitHubIssuesFor", "Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.")}</small>
|
||||||
ariaLabel="Project default tracking repo"
|
|
||||||
value={form.githubTrackingDefaultRepo ?? ""}
|
|
||||||
options={projectTrackingRepoOptions}
|
|
||||||
loading={projectTrackingRepoLoading}
|
|
||||||
error={projectTrackingRepoError ?? undefined}
|
|
||||||
placeholder="owner/repo"
|
|
||||||
onChange={(nextValue) =>
|
|
||||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="githubTrackingDedupEnabled" className="checkbox-label">
|
<label htmlFor="githubTrackingDedupEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="githubTrackingDedupEnabled" type="checkbox" checked={form.githubTrackingDedupEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, githubTrackingDedupEnabled: e.target.checked }))}/>{t("settings.general.searchTheTrackingRepoForLikelyDuplicatesBefore", " Search the tracking repo for likely duplicates before opening a new issue ")}</label>
|
||||||
id="githubTrackingDedupEnabled"
|
<small>{t("settings.general.whenEnabledFusionChecksOpenAndClosedIssues", " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.githubTrackingDedupEnabled !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, githubTrackingDedupEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Search the tracking repo for likely duplicates before opening a new issue
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GeneralSection;
|
export default GeneralSection;
|
||||||
|
|||||||
@@ -1,176 +1,71 @@
|
|||||||
/**
|
|
||||||
* Global General section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* The global default tracking repo, CLI binary panel, agent-log persistence
|
|
||||||
* toggles (tool output + thinking logs), the `fn` binary probe toggle, and the
|
|
||||||
* update-check controls. The tracking-repo option list/loading/error live in
|
|
||||||
* the shell (fetched on demand) and are relayed as props. The thinking-log
|
|
||||||
* resolution helper is imported directly from core.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { resolvePersistAgentThinkingLog } from "@fusion/core";
|
import { resolvePersistAgentThinkingLog } from "@fusion/core";
|
||||||
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
|
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
|
||||||
import { CliBinaryPanel } from "../../CliBinaryPanel";
|
import { CliBinaryPanel } from "../../CliBinaryPanel";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface GlobalGeneralSectionProps extends SectionBaseProps {
|
export interface GlobalGeneralSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
globalTrackingRepoOptions: TrackingRepoOption[];
|
globalTrackingRepoOptions: TrackingRepoOption[];
|
||||||
globalTrackingRepoLoading: boolean;
|
globalTrackingRepoLoading: boolean;
|
||||||
globalTrackingRepoError: string | null;
|
globalTrackingRepoError: string | null;
|
||||||
}
|
}
|
||||||
|
export function GlobalGeneralSection({ scopeBanner, form, setForm, globalTrackingRepoOptions, globalTrackingRepoLoading, globalTrackingRepoError, }: GlobalGeneralSectionProps) {
|
||||||
export function GlobalGeneralSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
form,
|
|
||||||
setForm,
|
|
||||||
globalTrackingRepoOptions,
|
|
||||||
globalTrackingRepoLoading,
|
|
||||||
globalTrackingRepoError,
|
|
||||||
}: GlobalGeneralSectionProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">General</h4>
|
<h4 className="settings-section-heading">{t("settings.globalGeneral.general", "General")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="globalGithubTrackingDefaultRepo">Global default tracking repo</label>
|
<label htmlFor="globalGithubTrackingDefaultRepo">{t("settings.globalGeneral.globalDefaultTrackingRepo", "Global default tracking repo")}</label>
|
||||||
<TrackingRepoSelect
|
<TrackingRepoSelect id="globalGithubTrackingDefaultRepo" ariaLabel="Global default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={globalTrackingRepoOptions} loading={globalTrackingRepoLoading} error={globalTrackingRepoError ?? undefined} placeholder={t("settings.globalGeneral.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
|
||||||
id="globalGithubTrackingDefaultRepo"
|
<small>{t("settings.globalGeneral.projectsInheritThisValueWhenTheyDoNot", "Projects inherit this value when they do not set a project default tracking repo.")}</small>
|
||||||
ariaLabel="Global default tracking repo"
|
|
||||||
value={form.githubTrackingDefaultRepo ?? ""}
|
|
||||||
options={globalTrackingRepoOptions}
|
|
||||||
loading={globalTrackingRepoLoading}
|
|
||||||
error={globalTrackingRepoError ?? undefined}
|
|
||||||
placeholder="owner/repo"
|
|
||||||
onChange={(nextValue) =>
|
|
||||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Projects inherit this value when they do not set a project default tracking repo.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<CliBinaryPanel />
|
<CliBinaryPanel />
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="persistAgentToolOutput" className="checkbox-label">
|
<label htmlFor="persistAgentToolOutput" className="checkbox-label">
|
||||||
<input
|
<input id="persistAgentToolOutput" type="checkbox" checked={form.persistAgentToolOutput !== false} onChange={(e) => setForm((f) => ({ ...f, persistAgentToolOutput: e.target.checked }))}/>{t("settings.globalGeneral.saveToolOutputInAgentLogs", " Save tool output in agent logs ")}</label>
|
||||||
id="persistAgentToolOutput"
|
<small>{t("settings.globalGeneral.whenDisabledToolRowsAreStillLoggedBut", " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.persistAgentToolOutput !== false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, persistAgentToolOutput: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Save tool output in agent logs
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When disabled, tool rows are still logged but detailed tool payloads are omitted.
|
|
||||||
Very large tool payloads may still be clipped even when this stays enabled.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<h5 className="settings-section-heading">Save AI thinking logs</h5>
|
<h5 className="settings-section-heading">{t("settings.globalGeneral.saveAIThinkingLogs", "Save AI thinking logs")}</h5>
|
||||||
<label htmlFor="persistAgentThinkingLogPermanent" className="checkbox-label">
|
<label htmlFor="persistAgentThinkingLogPermanent" className="checkbox-label">
|
||||||
<input
|
<input id="persistAgentThinkingLogPermanent" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: false })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogPermanent: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForPermanentAgents", " Save AI thinking for permanent agents ")}</label>
|
||||||
id="persistAgentThinkingLogPermanent"
|
|
||||||
type="checkbox"
|
|
||||||
checked={resolvePersistAgentThinkingLog(form, { ephemeral: false })}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, persistAgentThinkingLogPermanent: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Save AI thinking for permanent agents
|
|
||||||
</label>
|
|
||||||
<label htmlFor="persistAgentThinkingLogEphemeral" className="checkbox-label">
|
<label htmlFor="persistAgentThinkingLogEphemeral" className="checkbox-label">
|
||||||
<input
|
<input id="persistAgentThinkingLogEphemeral" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: true })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogEphemeral: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForEphemeralTaskWorkerAgents", " Save AI thinking for ephemeral / task-worker agents ")}</label>
|
||||||
id="persistAgentThinkingLogEphemeral"
|
<small>{t("settings.globalGeneral.leaveBothThinkingTogglesOffToKeepThe", " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ")}<code>thinking</code>{t("settings.globalGeneral.rowsAndDoesNotAffectAssistantTextOr", " rows and does not affect assistant text or tool rows. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={resolvePersistAgentThinkingLog(form, { ephemeral: true })}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, persistAgentThinkingLogEphemeral: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Save AI thinking for ephemeral / task-worker agents
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
Leave both thinking toggles off to keep the original default behavior.
|
|
||||||
This only controls persisted <code>thinking</code> rows and does not affect assistant text or tool rows.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="fnBinaryCheckEnabled" type="checkbox" checked={form.fnBinaryCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, fnBinaryCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForThe", " Check for the ")}<code>fn</code>{t("settings.globalGeneral.cLIBinaryOnPATH", " CLI binary on PATH ")}</label>
|
||||||
id="fnBinaryCheckEnabled"
|
<small>{t("settings.globalGeneral.whenEnabledTheDashboardProbesForAGlobally", " When enabled, the dashboard probes for a globally-installed")}{" "}
|
||||||
type="checkbox"
|
<code>fn</code> / <code>fusion</code>{t("settings.globalGeneral.cLIBySpawning", " CLI by spawning")}{" "}
|
||||||
checked={form.fnBinaryCheckEnabled !== false}
|
<code><bin> --version</code>{t("settings.globalGeneral.disableThisIfYourLocalDevProcessIs", ". Disable this if your local dev process is the source of truth and you don't want any outdated globally-installed binary executed during the probe. ")}</small>
|
||||||
onChange={(e) => setForm((f) => ({ ...f, fnBinaryCheckEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Check for the <code>fn</code> CLI binary on PATH
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled, the dashboard probes for a globally-installed{" "}
|
|
||||||
<code>fn</code> / <code>fusion</code> CLI by spawning{" "}
|
|
||||||
<code><bin> --version</code>. Disable this if your local
|
|
||||||
dev process is the source of truth and you don't want any
|
|
||||||
outdated globally-installed binary executed during the probe.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Updates</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalGeneral.updates", "Updates")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="updateCheckEnabled" className="checkbox-label">
|
<label htmlFor="updateCheckEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="updateCheckEnabled" type="checkbox" checked={form.updateCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForUpdatesAutomatically", " Check for updates automatically ")}</label>
|
||||||
id="updateCheckEnabled"
|
<small>{t("settings.globalGeneral.whenEnabledFusionChecksNpmForNewVersions", " When enabled, Fusion checks npm for new versions of")}{" "}
|
||||||
type="checkbox"
|
<code>@runfusion/fusion</code>{t("settings.globalGeneral.andShowsUpdateNoticesInTheCLIAnd", " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. ")}</small>
|
||||||
checked={form.updateCheckEnabled !== false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Check for updates automatically
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled, Fusion checks npm for new versions of{" "}
|
|
||||||
<code>@runfusion/fusion</code> and shows update notices in the CLI and dashboard.
|
|
||||||
Cadence is governed by the frequency below.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="updateCheckFrequency">Frequency</label>
|
<label htmlFor="updateCheckFrequency">{t("settings.globalGeneral.frequency", "Frequency")}</label>
|
||||||
<select
|
<select id="updateCheckFrequency" value={form.updateCheckFrequency ?? "daily"} onChange={(e) => setForm((f) => ({
|
||||||
id="updateCheckFrequency"
|
...f,
|
||||||
value={form.updateCheckFrequency ?? "daily"}
|
updateCheckFrequency: e.target.value as "manual" | "on-startup" | "daily" | "weekly",
|
||||||
onChange={(e) =>
|
}))} disabled={form.updateCheckEnabled === false}>
|
||||||
setForm((f) => ({
|
<option value="manual">{t("settings.globalGeneral.manualOnlyNeverAutoCheck", "Manual only \u2014 never auto-check")}</option>
|
||||||
...f,
|
<option value="on-startup">{t("settings.globalGeneral.onStartupOncePerServerLaunch", "On startup \u2014 once per server launch")}</option>
|
||||||
updateCheckFrequency: e.target.value as "manual" | "on-startup" | "daily" | "weekly",
|
<option value="daily">{t("settings.globalGeneral.dailyRecommended", "Daily (recommended)")}</option>
|
||||||
}))
|
<option value="weekly">{t("settings.globalGeneral.weekly", "Weekly")}</option>
|
||||||
}
|
|
||||||
disabled={form.updateCheckEnabled === false}
|
|
||||||
>
|
|
||||||
<option value="manual">Manual only — never auto-check</option>
|
|
||||||
<option value="on-startup">On startup — once per server launch</option>
|
|
||||||
<option value="daily">Daily (recommended)</option>
|
|
||||||
<option value="weekly">Weekly</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.globalGeneral.controlsHowOftenTheDashboardReFetchesThe", " Controls how often the dashboard re-fetches the npm registry. Use the version + refresh control in the header to trigger an immediate check at any time. ")}</small>
|
||||||
Controls how often the dashboard re-fetches the npm registry.
|
|
||||||
Use the version + refresh control in the header to trigger an
|
|
||||||
immediate check at any time.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoReloadOnVersionChange" className="checkbox-label">
|
<label htmlFor="autoReloadOnVersionChange" className="checkbox-label">
|
||||||
<input
|
<input id="autoReloadOnVersionChange" type="checkbox" checked={form.autoReloadOnVersionChange !== false} onChange={(e) => setForm((f) => ({ ...f, autoReloadOnVersionChange: e.target.checked }))}/>{t("settings.globalGeneral.autoReloadDashboardOnVersionChange", " Auto-reload dashboard on version change ")}</label>
|
||||||
id="autoReloadOnVersionChange"
|
<small>{t("settings.globalGeneral.whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen", " When enabled (default), the dashboard automatically reloads when it detects a new build version \u2014 either from server rebuilds or service worker updates. Disable this to stay on the current version until you manually refresh. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.autoReloadOnVersionChange !== false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, autoReloadOnVersionChange: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Auto-reload dashboard on version change
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled (default), the dashboard automatically reloads when it
|
|
||||||
detects a new build version — either from server rebuilds or service
|
|
||||||
worker updates. Disable this to stay on the current version until you
|
|
||||||
manually refresh.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GlobalGeneralSection;
|
export default GlobalGeneralSection;
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
/**
|
|
||||||
* Global Models section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Default + fallback model pickers, thinking-effort selector (only for reasoning
|
|
||||||
* models), the per-role global model lanes, startup model-sync toggles, and the
|
|
||||||
* OpenRouter advanced routing/attribution knobs. Model catalog, favorites, and
|
|
||||||
* the favorite-toggle handlers live in the shell (fetched + persisted there) and
|
|
||||||
* are relayed as props. The comma-list (de)serializers are reproduced locally as
|
|
||||||
* pure helpers — identical to the modal's.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { THINKING_LEVELS } from "@fusion/core";
|
import { THINKING_LEVELS } from "@fusion/core";
|
||||||
@@ -15,424 +5,271 @@ import type { Settings, ThinkingLevel } from "@fusion/core";
|
|||||||
import type { ModelInfo } from "../../../api";
|
import type { ModelInfo } from "../../../api";
|
||||||
import { CustomModelDropdown } from "../../CustomModelDropdown";
|
import { CustomModelDropdown } from "../../CustomModelDropdown";
|
||||||
import type { SectionBaseProps, ModelLane } from "./context";
|
import type { SectionBaseProps, ModelLane } from "./context";
|
||||||
|
|
||||||
function toCommaSeparatedInput(values?: string[]): string {
|
function toCommaSeparatedInput(values?: string[]): string {
|
||||||
return values?.join(", ") ?? "";
|
return values?.join(", ") ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromCommaSeparatedInput(value: string): string[] {
|
function fromCommaSeparatedInput(value: string): string[] {
|
||||||
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalModelsSectionProps extends SectionBaseProps {
|
export interface GlobalModelsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
availableModels: ModelInfo[];
|
availableModels: ModelInfo[];
|
||||||
modelsLoading: boolean;
|
modelsLoading: boolean;
|
||||||
/** Global model lanes (i.e. MODEL_LANES without the `default` lane). */
|
/** Global model lanes (i.e. MODEL_LANES without the `default` lane). */
|
||||||
globalModelLanes: ModelLane[];
|
globalModelLanes: ModelLane[];
|
||||||
favoriteProviders: string[];
|
favoriteProviders: string[];
|
||||||
favoriteModels: string[];
|
favoriteModels: string[];
|
||||||
onToggleFavorite: (provider: string) => void;
|
onToggleFavorite: (provider: string) => void;
|
||||||
onToggleModelFavorite: (modelId: string) => void;
|
onToggleModelFavorite: (modelId: string) => void;
|
||||||
}
|
}
|
||||||
|
export function GlobalModelsSection({ scopeBanner, form, setForm, availableModels, modelsLoading, globalModelLanes, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, }: GlobalModelsSectionProps) {
|
||||||
export function GlobalModelsSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
const selectedValue = form.defaultProvider && form.defaultModelId
|
||||||
form,
|
? `${form.defaultProvider}/${form.defaultModelId}`
|
||||||
setForm,
|
: "";
|
||||||
availableModels,
|
return (<>
|
||||||
modelsLoading,
|
|
||||||
globalModelLanes,
|
|
||||||
favoriteProviders,
|
|
||||||
favoriteModels,
|
|
||||||
onToggleFavorite,
|
|
||||||
onToggleModelFavorite,
|
|
||||||
}: GlobalModelsSectionProps) {
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
const selectedValue =
|
|
||||||
form.defaultProvider && form.defaultModelId
|
|
||||||
? `${form.defaultProvider}/${form.defaultModelId}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
|
|
||||||
{/* --- Default Model --- */}
|
{/* --- Default Model --- */}
|
||||||
<h4 className="settings-section-heading">Default Model</h4>
|
<h4 className="settings-section-heading">{t("settings.globalModels.defaultModel", "Default Model")}</h4>
|
||||||
{modelsLoading ? (
|
{modelsLoading ? (<div className="settings-empty-state">{t("settings.models.loadingModels", "Loading available models…")}</div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">
|
||||||
<div className="settings-empty-state">{t("settings.models.loadingModels", "Loading available models…")}</div>
|
|
||||||
) : availableModels.length === 0 ? (
|
|
||||||
<div className="settings-empty-state settings-muted">
|
|
||||||
{t("settings.models.noModels", "No models available. Configure authentication first.")}
|
{t("settings.models.noModels", "No models available. Configure authentication first.")}
|
||||||
</div>
|
</div>) : (<>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="defaultModel">Default Model</label>
|
<label htmlFor="defaultModel">{t("settings.globalModels.defaultModel", "Default Model")}</label>
|
||||||
<CustomModelDropdown
|
<CustomModelDropdown id="defaultModel" label="Default Model" models={availableModels} value={selectedValue} onChange={(val) => {
|
||||||
id="defaultModel"
|
|
||||||
label="Default Model"
|
|
||||||
models={availableModels}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (!val) {
|
if (!val) {
|
||||||
setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined }));
|
setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined }));
|
||||||
} else {
|
|
||||||
const slashIdx = val.indexOf("/");
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
defaultProvider: val.slice(0, slashIdx),
|
|
||||||
defaultModelId: val.slice(slashIdx + 1),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}}
|
else {
|
||||||
placeholder="Use default"
|
const slashIdx = val.indexOf("/");
|
||||||
favoriteProviders={favoriteProviders}
|
|
||||||
onToggleFavorite={onToggleFavorite}
|
|
||||||
favoriteModels={favoriteModels}
|
|
||||||
onToggleModelFavorite={onToggleModelFavorite}
|
|
||||||
/>
|
|
||||||
<small>Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="fallbackModel">Fallback Model</label>
|
|
||||||
<CustomModelDropdown
|
|
||||||
id="fallbackModel"
|
|
||||||
label="Fallback Model"
|
|
||||||
models={availableModels}
|
|
||||||
value={form.fallbackProvider && form.fallbackModelId ? `${form.fallbackProvider}/${form.fallbackModelId}` : ""}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (!val) {
|
|
||||||
setForm((f) => ({ ...f, fallbackProvider: undefined, fallbackModelId: undefined }));
|
|
||||||
} else {
|
|
||||||
const slashIdx = val.indexOf("/");
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
fallbackProvider: val.slice(0, slashIdx),
|
|
||||||
fallbackModelId: val.slice(slashIdx + 1),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="No fallback"
|
|
||||||
favoriteProviders={favoriteProviders}
|
|
||||||
onToggleFavorite={onToggleFavorite}
|
|
||||||
favoriteModels={favoriteModels}
|
|
||||||
onToggleModelFavorite={onToggleModelFavorite}
|
|
||||||
/>
|
|
||||||
<small>Used automatically if the primary default model hits a retryable provider error like rate limiting or overload.</small>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(() => {
|
|
||||||
const selectedModel = availableModels.find(
|
|
||||||
(m) => m.provider === form.defaultProvider && m.id === form.defaultModelId,
|
|
||||||
);
|
|
||||||
if (selectedModel && !selectedModel.reasoning) return null;
|
|
||||||
return (
|
|
||||||
<div className="form-group">
|
|
||||||
{/* FNXC:Settings-ThinkingLevel 2026-06-19-14:55: This global selector renders the canonical THINKING_LEVELS list so newly added `xhigh` stays available anywhere the default reasoning effort is configured. */}
|
|
||||||
<label htmlFor="defaultThinkingLevel">Thinking Effort</label>
|
|
||||||
<select
|
|
||||||
id="defaultThinkingLevel"
|
|
||||||
value={form.defaultThinkingLevel || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
setForm((f) => ({ ...f, defaultThinkingLevel: (val as ThinkingLevel) || undefined }));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">Default</option>
|
|
||||||
{THINKING_LEVELS.map((level) => (
|
|
||||||
<option key={level} value={level}>
|
|
||||||
{level.charAt(0).toUpperCase() + level.slice(1)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<small>Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.</small>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{availableModels.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Model Lanes</h4>
|
|
||||||
<p className="settings-description">
|
|
||||||
Global baseline models for each AI role. Project settings can override these per-project.
|
|
||||||
</p>
|
|
||||||
{globalModelLanes.map((lane) => {
|
|
||||||
const provider = form[lane.globalProviderKey as keyof Settings] as string | undefined;
|
|
||||||
const model = form[lane.globalModelKey as keyof Settings] as string | undefined;
|
|
||||||
const value = provider && model ? `${provider}/${model}` : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-group" key={`global-${lane.laneId}`}>
|
|
||||||
<label htmlFor={`global-${lane.laneId}-model`}>{lane.label}</label>
|
|
||||||
<CustomModelDropdown
|
|
||||||
id={`global-${lane.laneId}-model`}
|
|
||||||
label={lane.label}
|
|
||||||
models={availableModels}
|
|
||||||
value={value}
|
|
||||||
onChange={(selected) => {
|
|
||||||
if (!selected) {
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
[lane.globalProviderKey]: undefined,
|
|
||||||
[lane.globalModelKey]: undefined,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slashIdx = selected.indexOf("/");
|
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
[lane.globalProviderKey]: selected.slice(0, slashIdx),
|
defaultProvider: val.slice(0, slashIdx),
|
||||||
[lane.globalModelKey]: selected.slice(slashIdx + 1),
|
defaultModelId: val.slice(slashIdx + 1),
|
||||||
}));
|
}));
|
||||||
}}
|
}
|
||||||
placeholder="Use default"
|
}} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
||||||
favoriteProviders={favoriteProviders}
|
<small>{t("settings.globalModels.defaultAIModelUsedForTaskExecutionWhen", "Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.")}</small>
|
||||||
onToggleFavorite={onToggleFavorite}
|
</div>
|
||||||
favoriteModels={favoriteModels}
|
|
||||||
onToggleModelFavorite={onToggleModelFavorite}
|
<div className="form-group">
|
||||||
/>
|
<label htmlFor="fallbackModel">{t("settings.globalModels.fallbackModel", "Fallback Model")}</label>
|
||||||
|
<CustomModelDropdown id="fallbackModel" label="Fallback Model" models={availableModels} value={form.fallbackProvider && form.fallbackModelId ? `${form.fallbackProvider}/${form.fallbackModelId}` : ""} onChange={(val) => {
|
||||||
|
if (!val) {
|
||||||
|
setForm((f) => ({ ...f, fallbackProvider: undefined, fallbackModelId: undefined }));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const slashIdx = val.indexOf("/");
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
fallbackProvider: val.slice(0, slashIdx),
|
||||||
|
fallbackModelId: val.slice(slashIdx + 1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}} placeholder={t("settings.globalModels.noFallback", "No fallback")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
||||||
|
<small>{t("settings.globalModels.usedAutomaticallyIfThePrimaryDefaultModelHits", "Used automatically if the primary default model hits a retryable provider error like rate limiting or overload.")}</small>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
{(() => {
|
||||||
|
const selectedModel = availableModels.find((m) => m.provider === form.defaultProvider && m.id === form.defaultModelId);
|
||||||
|
if (selectedModel && !selectedModel.reasoning)
|
||||||
|
return null;
|
||||||
|
return (<div className="form-group">
|
||||||
|
{/* FNXC:Settings-ThinkingLevel 2026-06-19-14:55: This global selector renders the canonical THINKING_LEVELS list so newly added `xhigh` stays available anywhere the default reasoning effort is configured. */}
|
||||||
|
<label htmlFor="defaultThinkingLevel">{t("settings.globalModels.thinkingEffort", "Thinking Effort")}</label>
|
||||||
|
<select id="defaultThinkingLevel" value={form.defaultThinkingLevel || ""} onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setForm((f) => ({ ...f, defaultThinkingLevel: (val as ThinkingLevel) || undefined }));
|
||||||
|
}}>
|
||||||
|
<option value="">{t("settings.globalModels.default", "Default")}</option>
|
||||||
|
{THINKING_LEVELS.map((level) => (<option key={level} value={level}>
|
||||||
|
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||||
|
</option>))}
|
||||||
|
</select>
|
||||||
|
<small>{t("settings.globalModels.controlsHowMuchReasoningEffortTheAIModel", "Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.")}</small>
|
||||||
|
</div>);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{availableModels.length > 0 && (<>
|
||||||
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalModels.modelLanes", "Model Lanes")}</h4>
|
||||||
|
<p className="settings-description">{t("settings.globalModels.globalBaselineModelsForEachAIRoleProject", " Global baseline models for each AI role. Project settings can override these per-project. ")}</p>
|
||||||
|
{globalModelLanes.map((lane) => {
|
||||||
|
const provider = form[lane.globalProviderKey as keyof Settings] as string | undefined;
|
||||||
|
const model = form[lane.globalModelKey as keyof Settings] as string | undefined;
|
||||||
|
const value = provider && model ? `${provider}/${model}` : "";
|
||||||
|
return (<div className="form-group" key={`global-${lane.laneId}`}>
|
||||||
|
<label htmlFor={`global-${lane.laneId}-model`}>{lane.label}</label>
|
||||||
|
<CustomModelDropdown id={`global-${lane.laneId}-model`} label={lane.label} models={availableModels} value={value} onChange={(selected) => {
|
||||||
|
if (!selected) {
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
[lane.globalProviderKey]: undefined,
|
||||||
|
[lane.globalModelKey]: undefined,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const slashIdx = selected.indexOf("/");
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
[lane.globalProviderKey]: selected.slice(0, slashIdx),
|
||||||
|
[lane.globalModelKey]: selected.slice(slashIdx + 1),
|
||||||
|
}));
|
||||||
|
}} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
|
||||||
<small>{lane.helperText}</small>
|
<small>{lane.helperText}</small>
|
||||||
</div>
|
</div>);
|
||||||
);
|
})}
|
||||||
})}
|
</>)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* --- Startup Model Sync --- */}
|
{/* --- Startup Model Sync --- */}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Startup Model Sync</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalModels.startupModelSync", "Startup Model Sync")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterModelSync" className="checkbox-label">
|
<label htmlFor="openrouterModelSync" className="checkbox-label">
|
||||||
<input
|
<input id="openrouterModelSync" type="checkbox" checked={form.openrouterModelSync !== false} onChange={(e) => setForm((f) => ({ ...f, openrouterModelSync: e.target.checked }))}/>{t("settings.globalModels.syncOpenRouterModelListAtStartup", " Sync OpenRouter model list at startup ")}</label>
|
||||||
id="openrouterModelSync"
|
<small>{t("settings.globalModels.whenEnabledStartupFetchesTheLatestAvailableModels", " When enabled, startup fetches the latest available models from the OpenRouter API so model pickers always include the newest catalog. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.openrouterModelSync !== false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, openrouterModelSync: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Sync OpenRouter model list at startup
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled, startup fetches the latest available models from the OpenRouter API so
|
|
||||||
model pickers always include the newest catalog.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="opencodeGoModelSync" className="checkbox-label">
|
<label htmlFor="opencodeGoModelSync" className="checkbox-label">
|
||||||
<input
|
<input id="opencodeGoModelSync" type="checkbox" checked={form.opencodeGoModelSync !== false} onChange={(e) => setForm((f) => ({ ...f, opencodeGoModelSync: e.target.checked }))}/>{t("settings.globalModels.syncOpencodeGoModelListAtStartup", " Sync opencode-go model list at startup ")}</label>
|
||||||
id="opencodeGoModelSync"
|
<small>{t("settings.globalModels.whenEnabledStartupRefreshesModelsThroughTheLocal", " When enabled, startup refreshes models through the local ")}<code>opencode models opencode --refresh</code>{t("settings.globalModels.flowAndPublishesThemUnderTheOpencodeGo", " flow and publishes them under the opencode-go provider in model pickers. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.opencodeGoModelSync !== false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, opencodeGoModelSync: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Sync opencode-go model list at startup
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
When enabled, startup refreshes models through the local <code>opencode models opencode --refresh</code>
|
|
||||||
flow and publishes them under the opencode-go provider in model pickers.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary>OpenRouter advanced</summary>
|
<summary>{t("settings.globalModels.openRouterAdvanced", "OpenRouter advanced")}</summary>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterAppAttributionReferer">OpenRouter HTTP-Referer</label>
|
<label htmlFor="openrouterAppAttributionReferer">{t("settings.globalModels.openRouterHTTPReferer", "OpenRouter HTTP-Referer")}</label>
|
||||||
<input
|
<input id="openrouterAppAttributionReferer" className="input" placeholder={t("settings.globalModels.httpsRunfusionAi", "https://runfusion.ai")} value={form.openrouterAppAttribution?.referer ?? ""} onChange={(e) => setForm((f) => ({
|
||||||
id="openrouterAppAttributionReferer"
|
...f,
|
||||||
className="input"
|
openrouterAppAttribution: {
|
||||||
placeholder="https://runfusion.ai"
|
|
||||||
value={form.openrouterAppAttribution?.referer ?? ""}
|
|
||||||
onChange={(e) => setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
openrouterAppAttribution: {
|
|
||||||
...(f.openrouterAppAttribution || {}),
|
...(f.openrouterAppAttribution || {}),
|
||||||
referer: e.target.value,
|
referer: e.target.value,
|
||||||
},
|
},
|
||||||
}))}
|
}))}/>
|
||||||
/>
|
<small>{t("settings.globalModels.leaveEmptyToOmitThisHeaderDefaultHttps", "Leave empty to omit this header. Default: https://runfusion.ai.")}</small>
|
||||||
<small>Leave empty to omit this header. Default: https://runfusion.ai.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterAppAttributionTitle">OpenRouter X-Title</label>
|
<label htmlFor="openrouterAppAttributionTitle">{t("settings.globalModels.openRouterXTitle", "OpenRouter X-Title")}</label>
|
||||||
<input
|
<input id="openrouterAppAttributionTitle" className="input" placeholder={t("settings.globalModels.fusion", "Fusion")} value={form.openrouterAppAttribution?.title ?? ""} onChange={(e) => setForm((f) => ({
|
||||||
id="openrouterAppAttributionTitle"
|
...f,
|
||||||
className="input"
|
openrouterAppAttribution: {
|
||||||
placeholder="Fusion"
|
|
||||||
value={form.openrouterAppAttribution?.title ?? ""}
|
|
||||||
onChange={(e) => setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
openrouterAppAttribution: {
|
|
||||||
...(f.openrouterAppAttribution || {}),
|
...(f.openrouterAppAttribution || {}),
|
||||||
title: e.target.value,
|
title: e.target.value,
|
||||||
},
|
},
|
||||||
}))}
|
}))}/>
|
||||||
/>
|
<small>{t("settings.globalModels.leaveEmptyToOmitThisHeaderDefaultFusion", "Leave empty to omit this header. Default: Fusion.")}</small>
|
||||||
<small>Leave empty to omit this header. Default: Fusion.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterModelFiltersSupportedParameters">OpenRouter supported_parameters filter</label>
|
<label htmlFor="openrouterModelFiltersSupportedParameters">{t("settings.globalModels.openRouterSupportedParametersFilter", "OpenRouter supported_parameters filter")}</label>
|
||||||
<input
|
<input id="openrouterModelFiltersSupportedParameters" className="input" placeholder={t("settings.globalModels.toolsStructuredOutputs", "tools, structured_outputs")} value={toCommaSeparatedInput(form.openrouterModelFilters?.supported_parameters)} onChange={(e) => {
|
||||||
id="openrouterModelFiltersSupportedParameters"
|
const parsed = fromCommaSeparatedInput(e.target.value);
|
||||||
className="input"
|
setForm((f) => ({
|
||||||
placeholder="tools, structured_outputs"
|
|
||||||
value={toCommaSeparatedInput(form.openrouterModelFilters?.supported_parameters)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = fromCommaSeparatedInput(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterModelFilters: {
|
openrouterModelFilters: {
|
||||||
...(f.openrouterModelFilters || {}),
|
...(f.openrouterModelFilters || {}),
|
||||||
supported_parameters: parsed.length > 0 ? parsed : undefined,
|
supported_parameters: parsed.length > 0 ? parsed : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.globalModels.commaSeparatedValuesSentToOpenRouterModelSync", "Comma-separated values sent to OpenRouter model sync.")}</small>
|
||||||
<small>Comma-separated values sent to OpenRouter model sync.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterModelFiltersOutputModalities">OpenRouter output_modalities filter</label>
|
<label htmlFor="openrouterModelFiltersOutputModalities">{t("settings.globalModels.openRouterOutputModalitiesFilter", "OpenRouter output_modalities filter")}</label>
|
||||||
<input
|
<input id="openrouterModelFiltersOutputModalities" className="input" placeholder={t("settings.globalModels.text", "text")} value={toCommaSeparatedInput(form.openrouterModelFilters?.output_modalities)} onChange={(e) => {
|
||||||
id="openrouterModelFiltersOutputModalities"
|
const parsed = fromCommaSeparatedInput(e.target.value);
|
||||||
className="input"
|
setForm((f) => ({
|
||||||
placeholder="text"
|
|
||||||
value={toCommaSeparatedInput(form.openrouterModelFilters?.output_modalities)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = fromCommaSeparatedInput(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterModelFilters: {
|
openrouterModelFilters: {
|
||||||
...(f.openrouterModelFilters || {}),
|
...(f.openrouterModelFilters || {}),
|
||||||
output_modalities: parsed.length > 0 ? parsed : undefined,
|
output_modalities: parsed.length > 0 ? parsed : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.globalModels.commaSeparatedValuesSentToOpenRouterModelSync", "Comma-separated values sent to OpenRouter model sync.")}</small>
|
||||||
<small>Comma-separated values sent to OpenRouter model sync.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesOrder">OpenRouter routing order</label>
|
<label htmlFor="openrouterProviderPreferencesOrder">{t("settings.globalModels.openRouterRoutingOrder", "OpenRouter routing order")}</label>
|
||||||
<input
|
<input id="openrouterProviderPreferencesOrder" className="input" placeholder={t("settings.globalModels.openaiAnthropic", "openai, anthropic")} value={toCommaSeparatedInput(form.openrouterProviderPreferences?.order)} onChange={(e) => {
|
||||||
id="openrouterProviderPreferencesOrder"
|
const parsed = fromCommaSeparatedInput(e.target.value);
|
||||||
className="input"
|
setForm((f) => ({
|
||||||
placeholder="openai, anthropic"
|
|
||||||
value={toCommaSeparatedInput(form.openrouterProviderPreferences?.order)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = fromCommaSeparatedInput(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterProviderPreferences: {
|
openrouterProviderPreferences: {
|
||||||
...(f.openrouterProviderPreferences || {}),
|
...(f.openrouterProviderPreferences || {}),
|
||||||
order: parsed.length > 0 ? parsed : undefined,
|
order: parsed.length > 0 ? parsed : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesIgnore">OpenRouter routing ignore</label>
|
<label htmlFor="openrouterProviderPreferencesIgnore">{t("settings.globalModels.openRouterRoutingIgnore", "OpenRouter routing ignore")}</label>
|
||||||
<input
|
<input id="openrouterProviderPreferencesIgnore" className="input" placeholder={t("settings.globalModels.providerName", "provider-name")} value={toCommaSeparatedInput(form.openrouterProviderPreferences?.ignore)} onChange={(e) => {
|
||||||
id="openrouterProviderPreferencesIgnore"
|
const parsed = fromCommaSeparatedInput(e.target.value);
|
||||||
className="input"
|
setForm((f) => ({
|
||||||
placeholder="provider-name"
|
|
||||||
value={toCommaSeparatedInput(form.openrouterProviderPreferences?.ignore)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = fromCommaSeparatedInput(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterProviderPreferences: {
|
openrouterProviderPreferences: {
|
||||||
...(f.openrouterProviderPreferences || {}),
|
...(f.openrouterProviderPreferences || {}),
|
||||||
ignore: parsed.length > 0 ? parsed : undefined,
|
ignore: parsed.length > 0 ? parsed : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesOnly">OpenRouter routing only</label>
|
<label htmlFor="openrouterProviderPreferencesOnly">{t("settings.globalModels.openRouterRoutingOnly", "OpenRouter routing only")}</label>
|
||||||
<input
|
<input id="openrouterProviderPreferencesOnly" className="input" placeholder={t("settings.globalModels.providerName", "provider-name")} value={toCommaSeparatedInput(form.openrouterProviderPreferences?.only)} onChange={(e) => {
|
||||||
id="openrouterProviderPreferencesOnly"
|
const parsed = fromCommaSeparatedInput(e.target.value);
|
||||||
className="input"
|
setForm((f) => ({
|
||||||
placeholder="provider-name"
|
|
||||||
value={toCommaSeparatedInput(form.openrouterProviderPreferences?.only)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = fromCommaSeparatedInput(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterProviderPreferences: {
|
openrouterProviderPreferences: {
|
||||||
...(f.openrouterProviderPreferences || {}),
|
...(f.openrouterProviderPreferences || {}),
|
||||||
only: parsed.length > 0 ? parsed : undefined,
|
only: parsed.length > 0 ? parsed : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesAllowFallbacks">OpenRouter allow fallbacks</label>
|
<label htmlFor="openrouterProviderPreferencesAllowFallbacks">{t("settings.globalModels.openRouterAllowFallbacks", "OpenRouter allow fallbacks")}</label>
|
||||||
<select
|
<select id="openrouterProviderPreferencesAllowFallbacks" className="select" value={form.openrouterProviderPreferences?.allow_fallbacks === undefined ? "default" : form.openrouterProviderPreferences.allow_fallbacks ? "allow" : "deny"} onChange={(e) => {
|
||||||
id="openrouterProviderPreferencesAllowFallbacks"
|
const value = e.target.value;
|
||||||
className="select"
|
setForm((f) => ({
|
||||||
value={form.openrouterProviderPreferences?.allow_fallbacks === undefined ? "default" : form.openrouterProviderPreferences.allow_fallbacks ? "allow" : "deny"}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterProviderPreferences: {
|
openrouterProviderPreferences: {
|
||||||
...(f.openrouterProviderPreferences || {}),
|
...(f.openrouterProviderPreferences || {}),
|
||||||
allow_fallbacks: value === "default" ? undefined : value === "allow",
|
allow_fallbacks: value === "default" ? undefined : value === "allow",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}>
|
||||||
>
|
<option value="default">{t("settings.globalModels.default2", "default")}</option>
|
||||||
<option value="default">default</option>
|
<option value="allow">{t("settings.globalModels.allow", "allow")}</option>
|
||||||
<option value="allow">allow</option>
|
<option value="deny">{t("settings.globalModels.deny", "deny")}</option>
|
||||||
<option value="deny">deny</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesSort">OpenRouter routing sort</label>
|
<label htmlFor="openrouterProviderPreferencesSort">{t("settings.globalModels.openRouterRoutingSort", "OpenRouter routing sort")}</label>
|
||||||
<select
|
<select id="openrouterProviderPreferencesSort" className="select" value={form.openrouterProviderPreferences?.sort ?? "default"} onChange={(e) => {
|
||||||
id="openrouterProviderPreferencesSort"
|
const value = e.target.value;
|
||||||
className="select"
|
setForm((f) => ({
|
||||||
value={form.openrouterProviderPreferences?.sort ?? "default"}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
openrouterProviderPreferences: {
|
openrouterProviderPreferences: {
|
||||||
...(f.openrouterProviderPreferences || {}),
|
...(f.openrouterProviderPreferences || {}),
|
||||||
sort: value === "default" ? undefined : value as "price" | "throughput" | "latency",
|
sort: value === "default" ? undefined : value as "price" | "throughput" | "latency",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}}
|
}}>
|
||||||
>
|
<option value="default">{t("settings.globalModels.default2", "default")}</option>
|
||||||
<option value="default">default</option>
|
<option value="price">{t("settings.globalModels.price", "price")}</option>
|
||||||
<option value="price">price</option>
|
<option value="throughput">{t("settings.globalModels.throughput", "throughput")}</option>
|
||||||
<option value="throughput">throughput</option>
|
<option value="latency">{t("settings.globalModels.latency", "latency")}</option>
|
||||||
<option value="latency">latency</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="openrouterProviderPreferencesRequireParameters" className="checkbox-label">
|
<label htmlFor="openrouterProviderPreferencesRequireParameters" className="checkbox-label">
|
||||||
<input
|
<input id="openrouterProviderPreferencesRequireParameters" type="checkbox" checked={form.openrouterProviderPreferences?.require_parameters === true} onChange={(e) => setForm((f) => ({
|
||||||
id="openrouterProviderPreferencesRequireParameters"
|
...f,
|
||||||
type="checkbox"
|
openrouterProviderPreferences: {
|
||||||
checked={form.openrouterProviderPreferences?.require_parameters === true}
|
...(f.openrouterProviderPreferences || {}),
|
||||||
onChange={(e) => setForm((f) => ({
|
require_parameters: e.target.checked,
|
||||||
...f,
|
},
|
||||||
openrouterProviderPreferences: {
|
}))}/>{t("settings.globalModels.requireParameters", " Require parameters ")}</label>
|
||||||
...(f.openrouterProviderPreferences || {}),
|
|
||||||
require_parameters: e.target.checked,
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
Require parameters
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GlobalModelsSection;
|
export default GlobalModelsSection;
|
||||||
|
|||||||
@@ -1,354 +1,179 @@
|
|||||||
/**
|
|
||||||
* Memory section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped memory configuration: enable toggle, qmd install affordance,
|
|
||||||
* auto-summarize schedule, dream processing, the retrieval test panel, and the
|
|
||||||
* file editor with backend-writability gating. All memory fetch/state/handlers
|
|
||||||
* and the backend-status hook live in the shell (they touch the API, share state
|
|
||||||
* with the save flow, and the backend hook is enabled only while this section is
|
|
||||||
* active) and are relayed through a `memory` prop bag — mirroring the
|
|
||||||
* Authentication/Remote section conventions. The option-label truncation helpers
|
|
||||||
* are co-located. Keys, conditional gating, and editor wiring are preserved
|
|
||||||
* verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type {
|
import type { MemoryBackendCapabilities, MemoryBackendStatus, MemoryFileInfo, MemoryRetrievalTestResult, } from "../../../api";
|
||||||
MemoryBackendCapabilities,
|
|
||||||
MemoryBackendStatus,
|
|
||||||
MemoryFileInfo,
|
|
||||||
MemoryRetrievalTestResult,
|
|
||||||
} from "../../../api";
|
|
||||||
import { FileEditor } from "../../FileEditor";
|
import { FileEditor } from "../../FileEditor";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72;
|
const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72;
|
||||||
|
|
||||||
function truncateMiddle(value: string, maxChars: number): string {
|
function truncateMiddle(value: string, maxChars: number): string {
|
||||||
if (value.length <= maxChars) {
|
if (value.length <= maxChars) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
const visibleChars = Math.max(1, maxChars - 1);
|
||||||
const visibleChars = Math.max(1, maxChars - 1);
|
const startChars = Math.ceil(visibleChars / 2);
|
||||||
const startChars = Math.ceil(visibleChars / 2);
|
const endChars = Math.floor(visibleChars / 2);
|
||||||
const endChars = Math.floor(visibleChars / 2);
|
return `${value.slice(0, startChars)}…${value.slice(value.length - endChars)}`;
|
||||||
return `${value.slice(0, startChars)}…${value.slice(value.length - endChars)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMemoryFileOptionLabel(file: MemoryFileInfo): string {
|
function formatMemoryFileOptionLabel(file: MemoryFileInfo): string {
|
||||||
const fullLabel = `${file.label} — ${file.path}`;
|
const fullLabel = `${file.label} — ${file.path}`;
|
||||||
return truncateMiddle(fullLabel, MEMORY_FILE_OPTION_LABEL_MAX_CHARS);
|
return truncateMiddle(fullLabel, MEMORY_FILE_OPTION_LABEL_MAX_CHARS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemorySectionMemoryProps {
|
export interface MemorySectionMemoryProps {
|
||||||
memoryCapabilities: MemoryBackendCapabilities | null;
|
memoryCapabilities: MemoryBackendCapabilities | null;
|
||||||
memoryBackendStatus: MemoryBackendStatus | null;
|
memoryBackendStatus: MemoryBackendStatus | null;
|
||||||
memoryBackendLoading: boolean;
|
memoryBackendLoading: boolean;
|
||||||
memoryBackendError: string | null;
|
memoryBackendError: string | null;
|
||||||
memoryFiles: MemoryFileInfo[];
|
memoryFiles: MemoryFileInfo[];
|
||||||
selectedMemoryPath: string;
|
selectedMemoryPath: string;
|
||||||
setSelectedMemoryPath: (path: string) => void;
|
setSelectedMemoryPath: (path: string) => void;
|
||||||
memoryContent: string;
|
memoryContent: string;
|
||||||
setMemoryContent: (content: string) => void;
|
setMemoryContent: (content: string) => void;
|
||||||
memoryLoading: boolean;
|
memoryLoading: boolean;
|
||||||
memoryDirty: boolean;
|
memoryDirty: boolean;
|
||||||
setMemoryDirty: (dirty: boolean) => void;
|
setMemoryDirty: (dirty: boolean) => void;
|
||||||
memoryTestQuery: string;
|
memoryTestQuery: string;
|
||||||
setMemoryTestQuery: (query: string) => void;
|
setMemoryTestQuery: (query: string) => void;
|
||||||
memoryTestLoading: boolean;
|
memoryTestLoading: boolean;
|
||||||
memoryTestResult: MemoryRetrievalTestResult | null;
|
memoryTestResult: MemoryRetrievalTestResult | null;
|
||||||
qmdInstallLoading: boolean;
|
qmdInstallLoading: boolean;
|
||||||
dreamRunning: boolean;
|
dreamRunning: boolean;
|
||||||
memoryCompactLoading: boolean;
|
memoryCompactLoading: boolean;
|
||||||
onInstallQmd: () => void;
|
onInstallQmd: () => void;
|
||||||
onTestMemoryRetrieval: () => void;
|
onTestMemoryRetrieval: () => void;
|
||||||
onDreamNow: () => void;
|
onDreamNow: () => void;
|
||||||
onCompactMemory: () => void;
|
onCompactMemory: () => void;
|
||||||
onSaveMemory: () => void;
|
onSaveMemory: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemorySectionProps extends SectionBaseProps {
|
export interface MemorySectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
memory: MemorySectionMemoryProps;
|
memory: MemorySectionMemoryProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemorySection({ scopeBanner, form, setForm, memory }: MemorySectionProps) {
|
export function MemorySection({ scopeBanner, form, setForm, memory }: MemorySectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const {
|
const { memoryCapabilities: capabilities, memoryBackendStatus: backendStatus, memoryBackendLoading: backendLoading, memoryBackendError: backendError, memoryFiles, selectedMemoryPath, setSelectedMemoryPath, memoryContent, setMemoryContent, memoryLoading, memoryDirty, setMemoryDirty, memoryTestQuery, setMemoryTestQuery, memoryTestLoading, memoryTestResult, qmdInstallLoading, dreamRunning, memoryCompactLoading, onInstallQmd, onTestMemoryRetrieval, onDreamNow, onCompactMemory, onSaveMemory, } = memory;
|
||||||
memoryCapabilities: capabilities,
|
// Determine if editing is allowed
|
||||||
memoryBackendStatus: backendStatus,
|
const isMemoryEnabled = form.memoryEnabled !== false;
|
||||||
memoryBackendLoading: backendLoading,
|
const backendStatusResolved = !backendLoading && backendStatus !== null;
|
||||||
memoryBackendError: backendError,
|
const isBackendWritable = backendStatusResolved ? (capabilities?.writable ?? true) : true;
|
||||||
memoryFiles,
|
const isEditingAllowed = isMemoryEnabled && isBackendWritable;
|
||||||
selectedMemoryPath,
|
const selectedMemoryFile = memoryFiles.find((file) => file.path === selectedMemoryPath);
|
||||||
setSelectedMemoryPath,
|
const memoryLayerNames: Record<MemoryFileInfo["layer"], string> = {
|
||||||
memoryContent,
|
"long-term": "Long-term",
|
||||||
setMemoryContent,
|
daily: "Daily",
|
||||||
memoryLoading,
|
dreams: "Dreams",
|
||||||
memoryDirty,
|
};
|
||||||
setMemoryDirty,
|
return (<>
|
||||||
memoryTestQuery,
|
|
||||||
setMemoryTestQuery,
|
|
||||||
memoryTestLoading,
|
|
||||||
memoryTestResult,
|
|
||||||
qmdInstallLoading,
|
|
||||||
dreamRunning,
|
|
||||||
memoryCompactLoading,
|
|
||||||
onInstallQmd,
|
|
||||||
onTestMemoryRetrieval,
|
|
||||||
onDreamNow,
|
|
||||||
onCompactMemory,
|
|
||||||
onSaveMemory,
|
|
||||||
} = memory;
|
|
||||||
|
|
||||||
// Determine if editing is allowed
|
|
||||||
const isMemoryEnabled = form.memoryEnabled !== false;
|
|
||||||
const backendStatusResolved = !backendLoading && backendStatus !== null;
|
|
||||||
const isBackendWritable = backendStatusResolved ? (capabilities?.writable ?? true) : true;
|
|
||||||
const isEditingAllowed = isMemoryEnabled && isBackendWritable;
|
|
||||||
|
|
||||||
const selectedMemoryFile = memoryFiles.find((file) => file.path === selectedMemoryPath);
|
|
||||||
const memoryLayerNames: Record<MemoryFileInfo["layer"], string> = {
|
|
||||||
"long-term": "Long-term",
|
|
||||||
daily: "Daily",
|
|
||||||
dreams: "Dreams",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Memory</h4>
|
<h4 className="settings-section-heading">{t("settings.memory.memory", "Memory")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<small className="settings-muted">
|
<small className="settings-muted">{t("settings.memory.memoryLivesIn", " Memory lives in ")}<code>.fusion/memory/</code>{t("settings.memory.agentsSearchWithQmdFirstFallBackTo", ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ")}</small>
|
||||||
Memory lives in <code>.fusion/memory/</code>. Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryEnabled" className="checkbox-label">
|
<label htmlFor="memoryEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="memoryEnabled" type="checkbox" checked={form.memoryEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, memoryEnabled: e.target.checked }))}/>{t("settings.memory.enableMemoryTools", " Enable memory tools ")}</label>
|
||||||
id="memoryEnabled"
|
<small>{t("settings.memory.agentsGetMemorySearchMemoryGetAndMemory", "Agents get memory_search, memory_get, and memory_append tools. Search defaults to qmd with a local file fallback.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.memoryEnabled !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, memoryEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable memory tools
|
|
||||||
</label>
|
|
||||||
<small>Agents get memory_search, memory_get, and memory_append tools. Search defaults to qmd with a local file fallback.</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{backendLoading ? (
|
{backendLoading ? (<div className="form-group">
|
||||||
<div className="form-group">
|
<small className="settings-muted">{t("settings.memory.checkingMemoryWriteAccess", "Checking memory write access...")}</small>
|
||||||
<small className="settings-muted">Checking memory write access...</small>
|
</div>) : backendError ? (<div className="form-group">
|
||||||
</div>
|
<small className="field-error">{t("settings.memory.failedToLoadBackendStatus", "Failed to load backend status: ")}{backendError}</small>
|
||||||
) : backendError ? (
|
</div>) : null}
|
||||||
<div className="form-group">
|
|
||||||
<small className="field-error">Failed to load backend status: {backendError}</small>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{backendStatusResolved && backendStatus.qmdAvailable === false && (
|
{backendStatusResolved && backendStatus.qmdAvailable === false && (<div className="settings-empty-state memory-status-message">
|
||||||
<div className="settings-empty-state memory-status-message">
|
<span>{t("settings.memory.qmdIsNotInstalledSearchWillUseLocal", " qmd is not installed. Search will use local files. Install indexed retrieval: ")}<code>{backendStatus.qmdInstallCommand || "bun install -g @tobilu/qmd"}</code>
|
||||||
<span>
|
|
||||||
qmd is not installed. Search will use local files.
|
|
||||||
Install indexed retrieval: <code>{backendStatus.qmdInstallCommand || "bun install -g @tobilu/qmd"}</code>
|
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button type="button" className="btn btn-secondary btn-sm" onClick={onInstallQmd} disabled={qmdInstallLoading}>
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
onClick={onInstallQmd}
|
|
||||||
disabled={qmdInstallLoading}
|
|
||||||
>
|
|
||||||
{qmdInstallLoading ? t("settings.memory.installing", "Installing…") : t("settings.memory.installQmd", "Install qmd")}
|
{qmdInstallLoading ? t("settings.memory.installing", "Installing…") : t("settings.memory.installQmd", "Install qmd")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryAutoSummarizeEnabled" className="checkbox-label">
|
<label htmlFor="memoryAutoSummarizeEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="memoryAutoSummarizeEnabled" type="checkbox" checked={form.memoryAutoSummarizeEnabled || false} onChange={(e) => setForm((f) => ({ ...f, memoryAutoSummarizeEnabled: e.target.checked }))}/>{t("settings.memory.autoSummarizeMemory", " Auto-Summarize Memory ")}</label>
|
||||||
id="memoryAutoSummarizeEnabled"
|
<small>{t("settings.memory.automaticallyCompactMemoryWhenItExceedsTheThreshold", "Automatically compact memory when it exceeds the threshold on a schedule")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.memoryAutoSummarizeEnabled || false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, memoryAutoSummarizeEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Auto-Summarize Memory
|
|
||||||
</label>
|
|
||||||
<small>Automatically compact memory when it exceeds the threshold on a schedule</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(form.memoryAutoSummarizeEnabled || false) && (
|
{(form.memoryAutoSummarizeEnabled || false) && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryAutoSummarizeThresholdChars">Compaction Threshold (chars)</label>
|
<label htmlFor="memoryAutoSummarizeThresholdChars">{t("settings.memory.compactionThresholdChars", "Compaction Threshold (chars)")}</label>
|
||||||
<input
|
<input id="memoryAutoSummarizeThresholdChars" type="number" className="input" value={form.memoryAutoSummarizeThresholdChars ?? 50000} onChange={(e) => setForm((f) => ({
|
||||||
id="memoryAutoSummarizeThresholdChars"
|
...f,
|
||||||
type="number"
|
memoryAutoSummarizeThresholdChars: parseInt(e.target.value, 10) || 50000,
|
||||||
className="input"
|
}))} min={1000}/>
|
||||||
value={form.memoryAutoSummarizeThresholdChars ?? 50000}
|
<small>{t("settings.memory.memoryWillBeCompactedWhenItExceedsThis", "Memory will be compacted when it exceeds this character count")}</small>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
memoryAutoSummarizeThresholdChars: parseInt(e.target.value, 10) || 50000,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
min={1000}
|
|
||||||
/>
|
|
||||||
<small>Memory will be compacted when it exceeds this character count</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryAutoSummarizeSchedule">Schedule (cron)</label>
|
<label htmlFor="memoryAutoSummarizeSchedule">{t("settings.memory.scheduleCron", "Schedule (cron)")}</label>
|
||||||
<input
|
<input id="memoryAutoSummarizeSchedule" type="text" className="input" value={form.memoryAutoSummarizeSchedule ?? "0 3 * * *"} onChange={(e) => setForm((f) => ({ ...f, memoryAutoSummarizeSchedule: e.target.value }))} placeholder={t("settings.memory.03", "0 3 * * *")}/>
|
||||||
id="memoryAutoSummarizeSchedule"
|
<small>{t("settings.memory.cronExpressionForAutoSummarizeScheduleDefaultDaily", "Cron expression for auto-summarize schedule (default: daily at 3 AM)")}</small>
|
||||||
type="text"
|
|
||||||
className="input"
|
|
||||||
value={form.memoryAutoSummarizeSchedule ?? "0 3 * * *"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, memoryAutoSummarizeSchedule: e.target.value }))
|
|
||||||
}
|
|
||||||
placeholder="0 3 * * *"
|
|
||||||
/>
|
|
||||||
<small>Cron expression for auto-summarize schedule (default: daily at 3 AM)</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ borderTop: "1px solid var(--border)", margin: "var(--space-lg) 0" }} />
|
<div style={{ borderTop: "1px solid var(--border)", margin: "var(--space-lg) 0" }}/>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryDreamsEnabled" className="checkbox-label">
|
<label htmlFor="memoryDreamsEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="memoryDreamsEnabled" type="checkbox" checked={form.memoryDreamsEnabled === true} onChange={(e) => setForm((f) => ({ ...f, memoryDreamsEnabled: e.target.checked }))} disabled={!isMemoryEnabled}/>{t("settings.memory.processDreamsFromDailyMemory", " Process dreams from daily memory ")}</label>
|
||||||
id="memoryDreamsEnabled"
|
<small>{t("settings.memory.turnsDailyNotesIntoDREAMSMdAndPromotes", "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.memoryDreamsEnabled === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, memoryDreamsEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
disabled={!isMemoryEnabled}
|
|
||||||
/>
|
|
||||||
Process dreams from daily memory
|
|
||||||
</label>
|
|
||||||
<small>Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md.</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMemoryEnabled && form.memoryDreamsEnabled === true && (
|
{isMemoryEnabled && form.memoryDreamsEnabled === true && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryDreamsSchedule">Dream Schedule</label>
|
<label htmlFor="memoryDreamsSchedule">{t("settings.memory.dreamSchedule", "Dream Schedule")}</label>
|
||||||
<input
|
<input id="memoryDreamsSchedule" type="text" value={form.memoryDreamsSchedule ?? "0 4 * * *"} onChange={(e) => setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value }))}/>
|
||||||
id="memoryDreamsSchedule"
|
<small>{t("settings.memory.cronExpressionForDreamProcessing", "Cron expression for dream processing.")}</small>
|
||||||
type="text"
|
|
||||||
value={form.memoryDreamsSchedule ?? "0 4 * * *"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, memoryDreamsSchedule: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Cron expression for dream processing.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={onDreamNow} disabled={dreamRunning || form.memoryDreamsEnabled !== true}>
|
||||||
type="button"
|
{dreamRunning ? (<>
|
||||||
className="btn btn-sm"
|
<Loader2 size={14} className="animate-spin"/>{t("settings.memory.dreaming", " Dreaming\u2026 ")}</>) : (t("settings.memory.dreamNow", "Dream Now"))}
|
||||||
onClick={onDreamNow}
|
|
||||||
disabled={dreamRunning || form.memoryDreamsEnabled !== true}
|
|
||||||
>
|
|
||||||
{dreamRunning ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={14} className="animate-spin" />
|
|
||||||
Dreaming…
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("settings.memory.dreamNow", "Dream Now")
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<small>Manually trigger dream processing now.</small>
|
<small>{t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")}</small>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="memory-retrieval-test">
|
<div className="memory-retrieval-test">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryRetrievalQuery">Test Retrieval</label>
|
<label htmlFor="memoryRetrievalQuery">{t("settings.memory.testRetrieval", "Test Retrieval")}</label>
|
||||||
<input
|
<input id="memoryRetrievalQuery" type="text" value={memoryTestQuery} onChange={(e) => setMemoryTestQuery(e.target.value)} placeholder={t("settings.memory.searchMemoryWithQmd", "Search memory with qmd")}/>
|
||||||
id="memoryRetrievalQuery"
|
<small>{t("settings.memory.runsTheSameQmdBackedMemorySearchPath", "Runs the same qmd-backed memory_search path agents use.")}</small>
|
||||||
type="text"
|
|
||||||
value={memoryTestQuery}
|
|
||||||
onChange={(e) => setMemoryTestQuery(e.target.value)}
|
|
||||||
placeholder="Search memory with qmd"
|
|
||||||
/>
|
|
||||||
<small>Runs the same qmd-backed memory_search path agents use.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button
|
<button type="button" className="btn btn-secondary btn-sm" onClick={onTestMemoryRetrieval} disabled={memoryTestLoading}>
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
onClick={onTestMemoryRetrieval}
|
|
||||||
disabled={memoryTestLoading}
|
|
||||||
>
|
|
||||||
{memoryTestLoading ? t("settings.memory.testing", "Testing…") : t("settings.memory.testRetrieval", "Test Retrieval")}
|
{memoryTestLoading ? t("settings.memory.testing", "Testing…") : t("settings.memory.testRetrieval", "Test Retrieval")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{memoryTestResult && (
|
{memoryTestResult && (<div className="memory-test-result">
|
||||||
<div className="memory-test-result">
|
|
||||||
<strong>
|
<strong>
|
||||||
{memoryTestResult.results.length} result{memoryTestResult.results.length === 1 ? "" : "s"}
|
{memoryTestResult.results.length}{t("settings.memory.result", " result")}{memoryTestResult.results.length === 1 ? "" : "s"}
|
||||||
{" "}for "{memoryTestResult.query}"
|
{" "}{t("settings.memory.for", "for \"")}{memoryTestResult.query}"
|
||||||
</strong>
|
</strong>
|
||||||
<small>
|
<small>{t("settings.memory.qmd", " qmd ")}{memoryTestResult.qmdAvailable ? "available" : "missing"} · {memoryTestResult.usedFallback ? "local fallback used" : "qmd path used"}
|
||||||
qmd {memoryTestResult.qmdAvailable ? "available" : "missing"} · {memoryTestResult.usedFallback ? "local fallback used" : "qmd path used"}
|
|
||||||
</small>
|
</small>
|
||||||
{memoryTestResult.results.length > 0 ? (
|
{memoryTestResult.results.length > 0 ? (<ul>
|
||||||
<ul>
|
{memoryTestResult.results.map((result, index) => (<li key={`${result.path}-${result.lineStart}-${index}`}>
|
||||||
{memoryTestResult.results.map((result, index) => (
|
|
||||||
<li key={`${result.path}-${result.lineStart}-${index}`}>
|
|
||||||
<span>{result.path}:{result.lineStart}</span>
|
<span>{result.path}:{result.lineStart}</span>
|
||||||
<p>{result.snippet}</p>
|
<p>{result.snippet}</p>
|
||||||
</li>
|
</li>))}
|
||||||
))}
|
</ul>) : (<small>{t("settings.memory.noMatchingMemoryFound", "No matching memory found.")}</small>)}
|
||||||
</ul>
|
</div>)}
|
||||||
) : (
|
|
||||||
<small>No matching memory found.</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMemoryEnabled && (
|
{!isMemoryEnabled && (<div className="settings-empty-state memory-status-message">{t("settings.memory.memoryIsCurrentlyDisabledYouCanViewThe", " Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled. ")}</div>)}
|
||||||
<div className="settings-empty-state memory-status-message">
|
{isMemoryEnabled && backendStatusResolved && !isBackendWritable && (<div className="settings-empty-state memory-status-message">{t("settings.memory.memoryIsConfiguredWithAReadOnlyBackend", " Memory is configured with a read-only backend. You can view the file, but saving is disabled. ")}</div>)}
|
||||||
Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isMemoryEnabled && backendStatusResolved && !isBackendWritable && (
|
|
||||||
<div className="settings-empty-state memory-status-message">
|
|
||||||
Memory is configured with a read-only backend. You can view the file, but saving is disabled.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{memoryLoading ? (
|
{memoryLoading ? (<div className="settings-empty-state">{t("settings.memory.loadingMemory", "Loading memory\u2026")}</div>) : (<div className="memory-editor-section">
|
||||||
<div className="settings-empty-state">Loading memory…</div>
|
|
||||||
) : (
|
|
||||||
<div className="memory-editor-section">
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryFilePath">Memory File</label>
|
<label htmlFor="memoryFilePath">{t("settings.memory.memoryFile", "Memory File")}</label>
|
||||||
<select
|
<select id="memoryFilePath" value={selectedMemoryPath} onChange={(e) => {
|
||||||
id="memoryFilePath"
|
|
||||||
value={selectedMemoryPath}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedMemoryPath(e.target.value);
|
setSelectedMemoryPath(e.target.value);
|
||||||
setMemoryDirty(false);
|
setMemoryDirty(false);
|
||||||
}}
|
}} disabled={memoryDirty}>
|
||||||
disabled={memoryDirty}
|
{memoryFiles.map((file) => (<option key={file.path} value={file.path} title={`${file.label} — ${file.path}`}>
|
||||||
>
|
|
||||||
{memoryFiles.map((file) => (
|
|
||||||
<option key={file.path} value={file.path} title={`${file.label} — ${file.path}`}>
|
|
||||||
{formatMemoryFileOptionLabel(file)}
|
{formatMemoryFileOptionLabel(file)}
|
||||||
</option>
|
</option>))}
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>
|
||||||
{memoryDirty
|
{memoryDirty
|
||||||
@@ -356,15 +181,13 @@ export function MemorySection({ scopeBanner, form, setForm, memory }: MemorySect
|
|||||||
: "Choose any project memory file to view or edit. Dreams is selected by default."}
|
: "Choose any project memory file to view or edit. Dreams is selected by default."}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{selectedMemoryFile && (
|
{selectedMemoryFile && (<div className="memory-file-summary">
|
||||||
<div className="memory-file-summary">
|
|
||||||
<span>{memoryLayerNames[selectedMemoryFile.layer]}</span>
|
<span>{memoryLayerNames[selectedMemoryFile.layer]}</span>
|
||||||
<strong>{selectedMemoryFile.path}</strong>
|
<strong>{selectedMemoryFile.path}</strong>
|
||||||
<small>
|
<small>
|
||||||
{selectedMemoryFile.size.toLocaleString()} bytes · updated {new Date(selectedMemoryFile.updatedAt).toLocaleString()}
|
{selectedMemoryFile.size.toLocaleString()}{t("settings.memory.bytesUpdated", " bytes \u00B7 updated ")}{new Date(selectedMemoryFile.updatedAt).toLocaleString()}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
<div className="form-group memory-editor-form-group">
|
<div className="form-group memory-editor-form-group">
|
||||||
<label>{selectedMemoryFile?.label || "Memory Editor"}</label>
|
<label>{selectedMemoryFile?.label || "Memory Editor"}</label>
|
||||||
<small>
|
<small>
|
||||||
@@ -374,56 +197,33 @@ export function MemorySection({ scopeBanner, form, setForm, memory }: MemorySect
|
|||||||
{!selectedMemoryFile && "Edits the selected memory file."}
|
{!selectedMemoryFile && "Edits the selected memory file."}
|
||||||
</small>
|
</small>
|
||||||
<div className="memory-editor-frame">
|
<div className="memory-editor-frame">
|
||||||
<FileEditor
|
<FileEditor content={memoryContent} onChange={(content) => {
|
||||||
content={memoryContent}
|
setMemoryContent(content);
|
||||||
onChange={(content) => {
|
setMemoryDirty(true);
|
||||||
setMemoryContent(content);
|
}} readOnly={!isEditingAllowed} filePath={selectedMemoryPath}/>
|
||||||
setMemoryDirty(true);
|
|
||||||
}}
|
|
||||||
readOnly={!isEditingAllowed}
|
|
||||||
filePath={selectedMemoryPath}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
|
|
||||||
{!memoryLoading && (
|
{!memoryLoading && (<div className="form-group">
|
||||||
<div className="form-group">
|
<button type="button" className="btn btn-secondary btn-sm" onClick={onCompactMemory} disabled={!isEditingAllowed || memoryDirty || memoryCompactLoading}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
onClick={onCompactMemory}
|
|
||||||
disabled={!isEditingAllowed || memoryDirty || memoryCompactLoading}
|
|
||||||
>
|
|
||||||
{memoryCompactLoading ? t("settings.memory.compacting", "Compacting…") : t("settings.memory.compactSelectedFile", "Compact Selected File")}
|
{memoryCompactLoading ? t("settings.memory.compacting", "Compacting…") : t("settings.memory.compactSelectedFile", "Compact Selected File")}
|
||||||
</button>
|
</button>
|
||||||
<small>
|
<small>
|
||||||
{memoryDirty
|
{memoryDirty
|
||||||
? "Save or discard edits before compacting this file."
|
? "Save or discard edits before compacting this file."
|
||||||
: `Compacts ${selectedMemoryPath} and writes the result back to the same file.`}
|
: `Compacts ${selectedMemoryPath} and writes the result back to the same file.`}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
|
|
||||||
{memoryDirty && isEditingAllowed && (
|
{memoryDirty && isEditingAllowed && (<div className="form-group">
|
||||||
<div className="form-group">
|
<button type="button" className="btn btn-primary btn-sm" onClick={onSaveMemory}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={onSaveMemory}
|
|
||||||
>
|
|
||||||
{t("settings.memory.saveMemory", "Save Memory")}
|
{t("settings.memory.saveMemory", "Save Memory")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
{memoryDirty && !isEditingAllowed && (<div className="form-group">
|
||||||
{memoryDirty && !isEditingAllowed && (
|
<small className="field-error">{t("settings.memory.cannotSave", "Cannot save: ")}{isMemoryEnabled ? "Backend is read-only" : "Memory is disabled"}</small>
|
||||||
<div className="form-group">
|
</div>)}
|
||||||
<small className="field-error">Cannot save: {isMemoryEnabled ? "Backend is read-only" : "Memory is disabled"}</small>
|
</>);
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MemorySection;
|
export default MemorySection;
|
||||||
|
|||||||
@@ -1,740 +1,395 @@
|
|||||||
/**
|
|
||||||
* Merge section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped merge policy: auto-merge, AI-merge mode + review passes, test
|
|
||||||
* mode, merge strategy / integration branch, direct-merge routing, GitHub auth,
|
|
||||||
* commit attribution, and conflict-resolution strategy. The review/verification
|
|
||||||
* scope-enforcement knobs moved to the workflow (U4) and render as a redirect
|
|
||||||
* stub. The integration-branch custom-mode toggle is shell state (it interplays
|
|
||||||
* with the fetched branch-option list) and relayed as props. Keys, conditional
|
|
||||||
* visibility, and the legacy-mode warning banner are preserved verbatim from the
|
|
||||||
* original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Settings } from "@fusion/core";
|
import type { Settings } from "@fusion/core";
|
||||||
import { MovedSettingsStub } from "./MovedSettingsStub";
|
import { MovedSettingsStub } from "./MovedSettingsStub";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
function resolveMaxAutoMergeRetriesForMergeForm(value: unknown): number {
|
function resolveMaxAutoMergeRetriesForMergeForm(value: unknown): number {
|
||||||
const configured = Number(value);
|
const configured = Number(value);
|
||||||
return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 3;
|
return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyAutoMergeStampCandidate {
|
interface LegacyAutoMergeStampCandidate {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
column: string;
|
column: string;
|
||||||
cleared: boolean;
|
cleared: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyAutoMergeStampListResponse {
|
interface LegacyAutoMergeStampListResponse {
|
||||||
candidates: LegacyAutoMergeStampCandidate[];
|
candidates: LegacyAutoMergeStampCandidate[];
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyAutoMergeStampApplyResponse {
|
interface LegacyAutoMergeStampApplyResponse {
|
||||||
cleared: LegacyAutoMergeStampCandidate[];
|
cleared: LegacyAutoMergeStampCandidate[];
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLegacyAutoMergeStampResponse(response: Response): Promise<LegacyAutoMergeStampListResponse> {
|
async function readLegacyAutoMergeStampResponse(response: Response): Promise<LegacyAutoMergeStampListResponse> {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(await response.text() || "Failed to load legacy auto-merge stamps");
|
throw new Error(await response.text() || "Failed to load legacy auto-merge stamps");
|
||||||
}
|
}
|
||||||
return response.json() as Promise<LegacyAutoMergeStampListResponse>;
|
return response.json() as Promise<LegacyAutoMergeStampListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MergeSectionProps extends SectionBaseProps {
|
export interface MergeSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
integrationBranchOptions: string[];
|
integrationBranchOptions: string[];
|
||||||
integrationBranchCustomMode: boolean;
|
integrationBranchCustomMode: boolean;
|
||||||
setIntegrationBranchCustomMode: (value: boolean) => void;
|
setIntegrationBranchCustomMode: (value: boolean) => void;
|
||||||
onOpenWorkflowSettings?: () => void;
|
onOpenWorkflowSettings?: () => void;
|
||||||
}
|
}
|
||||||
|
export function MergeSection({ scopeBanner, form, setForm, integrationBranchOptions, integrationBranchCustomMode, setIntegrationBranchCustomMode, onOpenWorkflowSettings, }: MergeSectionProps) {
|
||||||
export function MergeSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
const [legacyStampCandidates, setLegacyStampCandidates] = useState<LegacyAutoMergeStampCandidate[]>([]);
|
||||||
form,
|
const [legacyStampLoading, setLegacyStampLoading] = useState(true);
|
||||||
setForm,
|
const [legacyStampApplying, setLegacyStampApplying] = useState(false);
|
||||||
integrationBranchOptions,
|
const [legacyStampError, setLegacyStampError] = useState<string | null>(null);
|
||||||
integrationBranchCustomMode,
|
const [legacyStampSuccess, setLegacyStampSuccess] = useState<string | null>(null);
|
||||||
setIntegrationBranchCustomMode,
|
const loadLegacyAutoMergeStamps = useCallback(async () => {
|
||||||
onOpenWorkflowSettings,
|
setLegacyStampLoading(true);
|
||||||
}: MergeSectionProps) {
|
setLegacyStampError(null);
|
||||||
const { t } = useTranslation("app");
|
try {
|
||||||
const [legacyStampCandidates, setLegacyStampCandidates] = useState<LegacyAutoMergeStampCandidate[]>([]);
|
const data = await readLegacyAutoMergeStampResponse(await fetch("/api/maintenance/legacy-automerge-stamps"));
|
||||||
const [legacyStampLoading, setLegacyStampLoading] = useState(true);
|
setLegacyStampCandidates(Array.isArray(data.candidates) ? data.candidates : []);
|
||||||
const [legacyStampApplying, setLegacyStampApplying] = useState(false);
|
}
|
||||||
const [legacyStampError, setLegacyStampError] = useState<string | null>(null);
|
catch (err) {
|
||||||
const [legacyStampSuccess, setLegacyStampSuccess] = useState<string | null>(null);
|
setLegacyStampError(err instanceof Error ? err.message : "Failed to load legacy auto-merge stamps");
|
||||||
|
}
|
||||||
const loadLegacyAutoMergeStamps = useCallback(async () => {
|
finally {
|
||||||
setLegacyStampLoading(true);
|
setLegacyStampLoading(false);
|
||||||
setLegacyStampError(null);
|
}
|
||||||
try {
|
}, []);
|
||||||
const data = await readLegacyAutoMergeStampResponse(
|
useEffect(() => {
|
||||||
await fetch("/api/maintenance/legacy-automerge-stamps"),
|
void loadLegacyAutoMergeStamps();
|
||||||
);
|
}, [loadLegacyAutoMergeStamps]);
|
||||||
setLegacyStampCandidates(Array.isArray(data.candidates) ? data.candidates : []);
|
const applyLegacyAutoMergeStampCleanup = async () => {
|
||||||
} catch (err) {
|
const confirmed = window.confirm("Apply cleanup for legacy auto-merge stamps? This clears only legacy non-override in-review stamps returned by the store and never touches genuine per-task overrides.");
|
||||||
setLegacyStampError(err instanceof Error ? err.message : "Failed to load legacy auto-merge stamps");
|
if (!confirmed)
|
||||||
} finally {
|
return;
|
||||||
setLegacyStampLoading(false);
|
setLegacyStampApplying(true);
|
||||||
}
|
setLegacyStampError(null);
|
||||||
}, []);
|
setLegacyStampSuccess(null);
|
||||||
|
try {
|
||||||
useEffect(() => {
|
const response = await fetch("/api/maintenance/legacy-automerge-stamps/apply", { method: "POST" });
|
||||||
void loadLegacyAutoMergeStamps();
|
if (!response.ok) {
|
||||||
}, [loadLegacyAutoMergeStamps]);
|
throw new Error(await response.text() || "Failed to apply legacy auto-merge stamp cleanup");
|
||||||
|
}
|
||||||
const applyLegacyAutoMergeStampCleanup = async () => {
|
const data = await response.json() as LegacyAutoMergeStampApplyResponse;
|
||||||
const confirmed = window.confirm(
|
setLegacyStampSuccess(`Cleared ${data.count} legacy auto-merge stamp${data.count === 1 ? "" : "s"}.`);
|
||||||
"Apply cleanup for legacy auto-merge stamps? This clears only legacy non-override in-review stamps returned by the store and never touches genuine per-task overrides.",
|
await loadLegacyAutoMergeStamps();
|
||||||
);
|
}
|
||||||
if (!confirmed) return;
|
catch (err) {
|
||||||
setLegacyStampApplying(true);
|
setLegacyStampError(err instanceof Error ? err.message : "Failed to apply legacy auto-merge stamp cleanup");
|
||||||
setLegacyStampError(null);
|
}
|
||||||
setLegacyStampSuccess(null);
|
finally {
|
||||||
try {
|
setLegacyStampApplying(false);
|
||||||
const response = await fetch("/api/maintenance/legacy-automerge-stamps/apply", { method: "POST" });
|
}
|
||||||
if (!response.ok) {
|
};
|
||||||
throw new Error(await response.text() || "Failed to apply legacy auto-merge stamp cleanup");
|
return (<>
|
||||||
}
|
|
||||||
const data = await response.json() as LegacyAutoMergeStampApplyResponse;
|
|
||||||
setLegacyStampSuccess(`Cleared ${data.count} legacy auto-merge stamp${data.count === 1 ? "" : "s"}.`);
|
|
||||||
await loadLegacyAutoMergeStamps();
|
|
||||||
} catch (err) {
|
|
||||||
setLegacyStampError(err instanceof Error ? err.message : "Failed to apply legacy auto-merge stamp cleanup");
|
|
||||||
} finally {
|
|
||||||
setLegacyStampApplying(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Merge</h4>
|
<h4 className="settings-section-heading">{t("settings.merge.merge", "Merge")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoMerge" className="checkbox-label">
|
<label htmlFor="autoMerge" className="checkbox-label">
|
||||||
<input
|
<input id="autoMerge" type="checkbox" checked={form.autoMerge} onChange={(e) => setForm((f) => ({ ...f, autoMerge: e.target.checked }))}/>{t("settings.merge.autoMergeCompletedTasks", " Auto-merge completed tasks ")}</label>
|
||||||
id="autoMerge"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.autoMerge}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, autoMerge: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Auto-merge completed tasks
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>When enabled, tasks that pass review are automatically merged into the main branch</small>
|
<small>{t("settings.merge.whenEnabledTasksThatPassReviewAreAutomatically", "When enabled, tasks that pass review are automatically merged into the main branch")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxAutoMergeRetries">Auto-merge conflict retries</label>
|
<label htmlFor="maxAutoMergeRetries">{t("settings.merge.autoMergeConflictRetries", "Auto-merge conflict retries")}</label>
|
||||||
{/*
|
{/*
|
||||||
FNXC:AutoMergeRetries 2026-06-17-04:20:
|
FNXC:AutoMergeRetries 2026-06-17-04:20:
|
||||||
Operators need a merge-section control for maxAutoMergeRetries so conflict-heavy projects can tune how many auto-resolution attempts occur before Fusion parks a task for human recovery. Invalid input falls back to 3 to preserve prior behavior.
|
Operators need a merge-section control for maxAutoMergeRetries so conflict-heavy projects can tune how many auto-resolution attempts occur before Fusion parks a task for human recovery. Invalid input falls back to 3 to preserve prior behavior.
|
||||||
*/}
|
*/}
|
||||||
<input
|
<input id="maxAutoMergeRetries" type="number" min={1} step={1} value={form.maxAutoMergeRetries ?? 3} onChange={(e) => setForm((f) => ({
|
||||||
id="maxAutoMergeRetries"
|
...f,
|
||||||
type="number"
|
maxAutoMergeRetries: e.target.value === "" ? undefined : resolveMaxAutoMergeRetriesForMergeForm(e.target.value),
|
||||||
min={1}
|
}))}/>
|
||||||
step={1}
|
<small>{t("settings.merge.positiveIntegerRetryCapForAutoMergeConflict", "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.")}</small>
|
||||||
value={form.maxAutoMergeRetries ?? 3}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
maxAutoMergeRetries: e.target.value === "" ? undefined : resolveMaxAutoMergeRetriesForMergeForm(e.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" data-testid="legacy-automerge-stamp-cleanup-panel">
|
<div className="form-group" data-testid="legacy-automerge-stamp-cleanup-panel">
|
||||||
<h5 className="settings-section-heading">Legacy auto-merge stamp cleanup</h5>
|
<h5 className="settings-section-heading">{t("settings.merge.legacyAutoMergeStampCleanup", "Legacy auto-merge stamp cleanup")}</h5>
|
||||||
<small>
|
<small>{t("settings.merge.findsInReviewTasksWhoseAutoMergeValue", " Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp. Dry-run is automatic; applying delegates to the store cleanup and preserves genuine per-task overrides. ")}</small>
|
||||||
Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp.
|
{legacyStampLoading ? (<small aria-live="polite">{t("settings.merge.checkingForLegacyAutoMergeStamps", "Checking for legacy auto-merge stamps\u2026")}</small>) : legacyStampCandidates.length === 0 ? (<small data-testid="legacy-automerge-stamp-empty-state">{t("settings.merge.noLegacyAutoMergeStampsToCleanUp", " No legacy auto-merge stamps to clean up. ")}</small>) : (<>
|
||||||
Dry-run is automatic; applying delegates to the store cleanup and preserves genuine
|
<small>{legacyStampCandidates.length}{t("settings.merge.legacyAutoMergeStamp", " legacy auto-merge stamp")}{legacyStampCandidates.length === 1 ? "" : "s"}{t("settings.merge.readyToCleanUp", " ready to clean up.")}</small>
|
||||||
per-task overrides.
|
|
||||||
</small>
|
|
||||||
{legacyStampLoading ? (
|
|
||||||
<small aria-live="polite">Checking for legacy auto-merge stamps…</small>
|
|
||||||
) : legacyStampCandidates.length === 0 ? (
|
|
||||||
<small data-testid="legacy-automerge-stamp-empty-state">
|
|
||||||
No legacy auto-merge stamps to clean up.
|
|
||||||
</small>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<small>{legacyStampCandidates.length} legacy auto-merge stamp{legacyStampCandidates.length === 1 ? "" : "s"} ready to clean up.</small>
|
|
||||||
<ul>
|
<ul>
|
||||||
{legacyStampCandidates.map((candidate) => (
|
{legacyStampCandidates.map((candidate) => (<li key={candidate.taskId} data-testid="legacy-automerge-stamp-candidate-row">
|
||||||
<li key={candidate.taskId} data-testid="legacy-automerge-stamp-candidate-row">
|
|
||||||
<strong>{candidate.taskId}</strong> — {candidate.column}
|
<strong>{candidate.taskId}</strong> — {candidate.column}
|
||||||
</li>
|
</li>))}
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<button type="button" className="btn" onClick={applyLegacyAutoMergeStampCleanup} disabled={legacyStampApplying} data-testid="legacy-automerge-stamp-apply-button">
|
||||||
type="button"
|
|
||||||
className="btn"
|
|
||||||
onClick={applyLegacyAutoMergeStampCleanup}
|
|
||||||
disabled={legacyStampApplying}
|
|
||||||
data-testid="legacy-automerge-stamp-apply-button"
|
|
||||||
>
|
|
||||||
{legacyStampApplying ? "Applying cleanup…" : "Apply cleanup"}
|
{legacyStampApplying ? "Applying cleanup…" : "Apply cleanup"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
{legacyStampSuccess ? <small className="settings-success" aria-live="polite">{legacyStampSuccess}</small> : null}
|
{legacyStampSuccess ? <small className="settings-success" aria-live="polite">{legacyStampSuccess}</small> : null}
|
||||||
{legacyStampError ? <small className="settings-error" role="alert">{legacyStampError}</small> : null}
|
{legacyStampError ? <small className="settings-error" role="alert">{legacyStampError}</small> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergerMode">AI merge</label>
|
<label htmlFor="mergerMode">{t("settings.merge.aIMerge", "AI merge")}</label>
|
||||||
<select
|
<select id="mergerMode" className="select" value={form.merger?.mode ?? "ai"} onChange={(e) => setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), mode: e.target.value as "ai" | "deterministic" } }))}>
|
||||||
id="mergerMode"
|
<option value="ai">{t("settings.merge.aIMergeDefaultAIMergesInAClean", "AI merge (default) \u2014 AI merges in a clean room, an AI reviewer audits with retries, then lands")}</option>
|
||||||
className="select"
|
<option value="deterministic">{t("settings.merge.deterministicLegacyRebaseConflictStrategyAuditPipeline", "Deterministic (legacy) \u2014 rebase / conflict-strategy / audit pipeline")}</option>
|
||||||
value={form.merger?.mode ?? "ai"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), mode: e.target.value as "ai" | "deterministic" } }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="ai">AI merge (default) — AI merges in a clean room, an AI reviewer audits with retries, then lands</option>
|
|
||||||
<option value="deterministic">Deterministic (legacy) — rebase / conflict-strategy / audit pipeline</option>
|
|
||||||
</select>
|
</select>
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.aIModeMergesTheTaskBranchIntoAn", " AI mode merges the task branch into an isolated clean-room checkout at the target branch's tip, has an AI reviewer audit the squash (with corrective retries \u2014 advisory concerns land with a logged warning, an unfixable correctness concern hard-fails), then fast-forwards the target branch and syncs your local checkout (AI reconciles a conflicting restore). Each task merges to its own target branch, or the default integration branch. ")}<strong>{t("settings.merge.theLegacyMergeSettingsBelowDoNotApply", "The legacy merge settings below do not apply while AI merge is on.")}</strong>
|
||||||
AI mode merges the task branch into an isolated clean-room checkout at the target
|
|
||||||
branch's tip, has an AI reviewer audit the squash (with corrective retries —
|
|
||||||
advisory concerns land with a logged warning, an unfixable correctness concern
|
|
||||||
hard-fails), then fast-forwards the target branch and syncs your local checkout
|
|
||||||
(AI reconciles a conflicting restore). Each task merges to its own target branch,
|
|
||||||
or the default integration branch. <strong>The legacy merge settings below do not
|
|
||||||
apply while AI merge is on.</strong>
|
|
||||||
</small>
|
</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{(form.merger?.mode ?? "ai") === "ai" && (
|
{(form.merger?.mode ?? "ai") === "ai" && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergerMaxReviewPasses">Max AI review passes</label>
|
<label htmlFor="mergerMaxReviewPasses">{t("settings.merge.maxAIReviewPasses", "Max AI review passes")}</label>
|
||||||
<input
|
<input id="mergerMaxReviewPasses" type="number" min={0} max={10} value={form.merger?.maxReviewPasses ?? 3} onChange={(e) => setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), maxReviewPasses: e.target.value === "" ? undefined : Number(e.target.value) } }))}/>
|
||||||
id="mergerMaxReviewPasses"
|
<small>{t("settings.merge.aICorrectiveRoundsBeforeLandingTheBestResult", "AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3. The reviewer uses your project's reviewer/validator model.")}</small>
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={10}
|
|
||||||
value={form.merger?.maxReviewPasses ?? 3}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), maxReviewPasses: e.target.value === "" ? undefined : Number(e.target.value) } }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3. The reviewer uses your project's reviewer/validator model.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergerAllowDirtyLocalCheckoutSync" className="checkbox-label">
|
<label htmlFor="mergerAllowDirtyLocalCheckoutSync" className="checkbox-label">
|
||||||
<input
|
<input id="mergerAllowDirtyLocalCheckoutSync" type="checkbox" checked={form.merger?.allowDirtyLocalCheckoutSync === true} onChange={(e) => setForm((f) => ({
|
||||||
id="mergerAllowDirtyLocalCheckoutSync"
|
...f,
|
||||||
type="checkbox"
|
merger: { ...(f.merger ?? {}), allowDirtyLocalCheckoutSync: e.target.checked },
|
||||||
checked={form.merger?.allowDirtyLocalCheckoutSync === true}
|
}))}/>{t("settings.merge.allowAIMergeToSyncADirtyChecked", " Allow AI merge to sync a dirty checked-out integration branch ")}</label>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
merger: { ...(f.merger ?? {}), allowDirtyLocalCheckoutSync: e.target.checked },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Allow AI merge to sync a dirty checked-out integration branch
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.dangerousCompatibilityEscapeHatchLeaveOffUnlessYou", " Dangerous compatibility escape hatch. Leave off unless you explicitly want the legacy stash \u2192 fast-forward \u2192 restore behavior when your checked-out integration branch has unrelated local edits. When off, AI merge blocks before advancing the branch so dirty project-root edits cannot contaminate a completed merge. ")}</small>
|
||||||
Dangerous compatibility escape hatch. Leave off unless you explicitly want the legacy
|
|
||||||
stash → fast-forward → restore behavior when your checked-out integration branch has
|
|
||||||
unrelated local edits. When off, AI merge blocks before advancing the branch so dirty
|
|
||||||
project-root edits cannot contaminate a completed merge.
|
|
||||||
</small>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="testMode" className="checkbox-label">
|
<label htmlFor="testMode" className="checkbox-label">
|
||||||
<input
|
<input id="testMode" type="checkbox" checked={form.testMode === true} onChange={(e) => setForm((f) => ({ ...f, testMode: e.target.checked }))}/>{t("settings.merge.enableTestMode", " Enable test mode ")}</label>
|
||||||
id="testMode"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.testMode === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, testMode: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable test mode
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>Forces all AI lanes to use the deterministic mock provider. No network calls, zero token cost.</small>
|
<small>{t("settings.merge.forcesAllAILanesToUseTheDeterministic", "Forces all AI lanes to use the deterministic mock provider. No network calls, zero token cost.")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<MovedSettingsStub
|
<MovedSettingsStub message={t("settings.movedStub.reviewVerification", "Review, verification auto-fix, and scope-enforcement settings now live on the workflow.")} onOpenWorkflowSettings={onOpenWorkflowSettings}/>
|
||||||
message={t(
|
|
||||||
"settings.movedStub.reviewVerification",
|
|
||||||
"Review, verification auto-fix, and scope-enforcement settings now live on the workflow.",
|
|
||||||
)}
|
|
||||||
onOpenWorkflowSettings={onOpenWorkflowSettings}
|
|
||||||
/>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeStrategy">Auto-completion mode</label>
|
<label htmlFor="mergeStrategy">{t("settings.merge.autoCompletionMode", "Auto-completion mode")}</label>
|
||||||
<select
|
<select id="mergeStrategy" value={form.mergeStrategy || "direct"} onChange={(e) => setForm((f) => ({ ...f, mergeStrategy: e.target.value as Settings["mergeStrategy"] }))}>
|
||||||
id="mergeStrategy"
|
<option value="direct">{t("settings.merge.directMergeIntoTheCurrentBranch", "Direct merge into the current branch")}</option>
|
||||||
value={form.mergeStrategy || "direct"}
|
<option value="pull-request">{t("settings.merge.createMonitorAndMergeAGitHubPullRequest", "Create, monitor, and merge a GitHub pull request")}</option>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, mergeStrategy: e.target.value as Settings["mergeStrategy"] }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="direct">Direct merge into the current branch</option>
|
|
||||||
<option value="pull-request">Create, monitor, and merge a GitHub pull request</option>
|
|
||||||
</select>
|
</select>
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.controlsWhatHappensAfterATaskReachesIn", " Controls what happens after a task reaches In Review. Direct mode merges into the current branch locally. Pull request mode keeps the task in In Review while Fusion waits for GitHub reviews and required checks before merging the PR. ")}</small>
|
||||||
Controls what happens after a task reaches In Review. Direct mode merges into the current branch locally. Pull request mode keeps the task in In Review while Fusion waits for GitHub reviews and required checks before merging the PR.
|
|
||||||
</small>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="integrationBranch">Integration branch</label>
|
<label htmlFor="integrationBranch">{t("settings.merge.integrationBranch", "Integration branch")}</label>
|
||||||
{(() => {
|
{(() => {
|
||||||
const currentValue = form.integrationBranch ?? "";
|
const currentValue = form.integrationBranch ?? "";
|
||||||
const valueIsKnown = currentValue.length > 0 && integrationBranchOptions.includes(currentValue);
|
const valueIsKnown = currentValue.length > 0 && integrationBranchOptions.includes(currentValue);
|
||||||
const isCustomMode = integrationBranchCustomMode || (currentValue.length > 0 && !valueIsKnown);
|
const isCustomMode = integrationBranchCustomMode || (currentValue.length > 0 && !valueIsKnown);
|
||||||
if (isCustomMode) {
|
if (isCustomMode) {
|
||||||
return (
|
return (<div className="form-inline-group">
|
||||||
<div className="form-inline-group">
|
<input id="integrationBranch" type="text" className="input" placeholder={t("settings.merge.branchName", "branch name")} value={currentValue} onChange={(e) => {
|
||||||
<input
|
const trimmed = e.target.value.trim();
|
||||||
id="integrationBranch"
|
setForm((f) => ({
|
||||||
type="text"
|
...f,
|
||||||
className="input"
|
integrationBranch: trimmed.length === 0 ? undefined : trimmed,
|
||||||
placeholder="branch name"
|
}));
|
||||||
value={currentValue}
|
}} data-testid="integration-branch-custom-input"/>
|
||||||
onChange={(e) => {
|
<button type="button" className="btn-link" onClick={() => {
|
||||||
const trimmed = e.target.value.trim();
|
setIntegrationBranchCustomMode(false);
|
||||||
|
setForm((f) => ({ ...f, integrationBranch: undefined }));
|
||||||
|
}} data-testid="integration-branch-use-dropdown">{t("settings.merge.useDropdown", " Use dropdown ")}</button>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
const CUSTOM = "__fusion-custom__";
|
||||||
|
const AUTO = "";
|
||||||
|
return (<select id="integrationBranch" className="select" value={currentValue} onChange={(e) => {
|
||||||
|
const next = e.target.value;
|
||||||
|
if (next === CUSTOM) {
|
||||||
|
setIntegrationBranchCustomMode(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
integrationBranch: trimmed.length === 0 ? undefined : trimmed,
|
integrationBranch: next === AUTO ? undefined : next,
|
||||||
}));
|
}));
|
||||||
}}
|
}} data-testid="integration-branch-select">
|
||||||
data-testid="integration-branch-custom-input"
|
<option value={AUTO}>{t("settings.merge.autoDetectOriginHEADMain", "(auto-detect \u2014 origin/HEAD \u2192 main)")}</option>
|
||||||
/>
|
{integrationBranchOptions.map((name) => (<option key={name} value={name}>{name}</option>))}
|
||||||
<button
|
<option value={CUSTOM}>{t("settings.merge.custom", "Custom\u2026")}</option>
|
||||||
type="button"
|
</select>);
|
||||||
className="btn-link"
|
|
||||||
onClick={() => {
|
|
||||||
setIntegrationBranchCustomMode(false);
|
|
||||||
setForm((f) => ({ ...f, integrationBranch: undefined }));
|
|
||||||
}}
|
|
||||||
data-testid="integration-branch-use-dropdown"
|
|
||||||
>
|
|
||||||
Use dropdown
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const CUSTOM = "__fusion-custom__";
|
|
||||||
const AUTO = "";
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
id="integrationBranch"
|
|
||||||
className="select"
|
|
||||||
value={currentValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
const next = e.target.value;
|
|
||||||
if (next === CUSTOM) {
|
|
||||||
setIntegrationBranchCustomMode(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
integrationBranch: next === AUTO ? undefined : next,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
data-testid="integration-branch-select"
|
|
||||||
>
|
|
||||||
<option value={AUTO}>(auto-detect — origin/HEAD → main)</option>
|
|
||||||
{integrationBranchOptions.map((name) => (
|
|
||||||
<option key={name} value={name}>{name}</option>
|
|
||||||
))}
|
|
||||||
<option value={CUSTOM}>Custom…</option>
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
})()}
|
})()}
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.theCanonicalBranchFusionMergesTasksIntoAnd", " The canonical branch Fusion merges tasks into and uses as the reference for all ahead/behind / overlap / pre-rebase computations. Leave on ")}<em>{t("settings.merge.autoDetect", "auto-detect")}</em>{t("settings.merge.toResolveViaTheStandardCascade", " to resolve via the standard cascade (")}<code>integrationBranch</code>{t("settings.merge.legacy", " \u2192 legacy ")}<code>baseBranch</code> →
|
||||||
The canonical branch Fusion merges tasks into and uses as the reference for all
|
<code>origin/HEAD</code>{t("settings.merge.symbolicRefFallback", " symbolic ref \u2192 fallback ")}<code>main</code>{t("settings.merge.pickALocalBranchFromTheDropdownCommon", "). Pick a local branch from the dropdown \u2014 common integration names like ")}<code>main</code>,
|
||||||
ahead/behind / overlap / pre-rebase computations. Leave on <em>auto-detect</em>
|
<code>master</code>, <code>trunk</code>{t("settings.merge.and", ", and ")}<code>develop</code>{t("settings.merge.areListedFirstOrChoose", " are listed first \u2014 or choose ")}<em>{t("settings.merge.custom", "Custom\u2026")}</em>{t("settings.merge.toTypeABranchThatDoesnAposT", " to type a branch that doesn't exist locally yet. Applies to both direct merges and pull-request mode; individual tasks can still override via task metadata. ")}</small>
|
||||||
to resolve via the standard cascade
|
|
||||||
(<code>integrationBranch</code> → legacy <code>baseBranch</code> →
|
|
||||||
<code>origin/HEAD</code> symbolic ref → fallback <code>main</code>). Pick a
|
|
||||||
local branch from the dropdown — common integration names like <code>main</code>,
|
|
||||||
<code>master</code>, <code>trunk</code>, and <code>develop</code> are listed
|
|
||||||
first — or choose <em>Custom…</em> to type a branch that doesn't exist
|
|
||||||
locally yet. Applies to both direct merges and pull-request mode; individual
|
|
||||||
tasks can still override via task metadata.
|
|
||||||
</small>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{form.mergeStrategy !== "pull-request" && (form.merger?.mode ?? "ai") !== "ai" && (
|
{form.mergeStrategy !== "pull-request" && (form.merger?.mode ?? "ai") !== "ai" && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="directMergeCommitStrategy">Direct merge commit routing</label>
|
<label htmlFor="directMergeCommitStrategy">{t("settings.merge.directMergeCommitRouting", "Direct merge commit routing")}</label>
|
||||||
<select
|
<select id="directMergeCommitStrategy" className="select" value={form.directMergeCommitStrategy ?? "always-squash"} onChange={(e) => setForm((f) => ({
|
||||||
id="directMergeCommitStrategy"
|
...f,
|
||||||
className="select"
|
directMergeCommitStrategy: e.target.value as "auto" | "always-squash" | "always-rebase",
|
||||||
value={form.directMergeCommitStrategy ?? "always-squash"}
|
}))}>
|
||||||
onChange={(e) =>
|
<option value="auto">{t("settings.merge.autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive", "Auto \u2014 squash single-substantive branches, preserve multi-substantive history")}</option>
|
||||||
setForm((f) => ({
|
<option value="always-squash">{t("settings.merge.alwaysSquashDirectMerges", "Always squash direct merges")}</option>
|
||||||
...f,
|
<option value="always-rebase">{t("settings.merge.alwaysPreserveDirectMergeCommitHistory", "Always preserve direct-merge commit history")}</option>
|
||||||
directMergeCommitStrategy: e.target.value as "auto" | "always-squash" | "always-rebase",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="auto">Auto — squash single-substantive branches, preserve multi-substantive history</option>
|
|
||||||
<option value="always-squash">Always squash direct merges</option>
|
|
||||||
<option value="always-rebase">Always preserve direct-merge commit history</option>
|
|
||||||
</select>
|
</select>
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.autoKeepsTodayAposSSquashBehaviorFor", " Auto keeps today's squash behavior for branches with zero or one substantive commit, but switches multi-substantive branches to a history-preserving rebase-and-merge path. Individual tasks can override this in PROMPT.md with ")}<code>**Direct Merge Commit Strategy:** auto|always-squash|always-rebase</code>.
|
||||||
Auto keeps today's squash behavior for branches with zero or one substantive commit, but switches multi-substantive branches to a history-preserving rebase-and-merge path. Individual tasks can override this in PROMPT.md with <code>**Direct Merge Commit Strategy:** auto|always-squash|always-rebase</code>.
|
|
||||||
</small>
|
</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeIntegrationWorktree">Integration worktree</label>
|
<label htmlFor="mergeIntegrationWorktree">{t("settings.merge.integrationWorktree", "Integration worktree")}</label>
|
||||||
<select
|
<select id="mergeIntegrationWorktree" className="select" value={form.mergeIntegrationWorktree ?? "reuse-task-worktree"} onChange={(e) => setForm((f) => ({
|
||||||
id="mergeIntegrationWorktree"
|
...f,
|
||||||
className="select"
|
mergeIntegrationWorktree: e.target.value as Settings["mergeIntegrationWorktree"],
|
||||||
value={form.mergeIntegrationWorktree ?? "reuse-task-worktree"}
|
}))}>
|
||||||
onChange={(e) =>
|
<option value="reuse-task-worktree">{t("settings.merge.reuseTaskWorktreeDefault", "Reuse task worktree (default)")}</option>
|
||||||
setForm((f) => ({
|
<option value="cwd-main">{t("settings.merge.useProjectRootLegacy", "Use project root (legacy)")}</option>
|
||||||
...f,
|
|
||||||
mergeIntegrationWorktree: e.target.value as Settings["mergeIntegrationWorktree"],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="reuse-task-worktree">Reuse task worktree (default)</option>
|
|
||||||
<option value="cwd-main">Use project root (legacy)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.merge.autoMergeRunsInTheTaskWorktreeBy", " Auto-merge runs in the task worktree by default. Switch to the legacy project-root path only if you need the pre-FN-5279 fallback; worktrunk-managed projects still defer to worktrunk. ")}</small>
|
||||||
Auto-merge runs in the task worktree by default. Switch to the legacy project-root path only if you need the pre-FN-5279 fallback; worktrunk-managed projects still defer to worktrunk.
|
{(form.mergeIntegrationWorktree ?? "reuse-task-worktree") !== "reuse-task-worktree" && (<div className="settings-warning-banner" role="alert" aria-live="polite" data-testid="merge-integration-worktree-warning">
|
||||||
</small>
|
<strong>{t("settings.merge.legacyIntegrationBranchMode", "Legacy integration-branch mode.")}</strong>{" "}{t("settings.merge.autoMergeWillRunRebaseConflictResolutionAnd", " Auto-merge will run rebase, conflict resolution, and squash commits inside the project root (the user's checked-out integration-branch worktree) instead of the task worktree. Fusion assumes that directory is already on the integration branch and clean; if it isn't, merges may fail or touch the user's working tree. Reuse-task-worktree is the recommended default (FN-5279). Switch back unless you have a specific reason to opt in (FN-5348). ")}</div>)}
|
||||||
{(form.mergeIntegrationWorktree ?? "reuse-task-worktree") !== "reuse-task-worktree" && (
|
|
||||||
<div
|
|
||||||
className="settings-warning-banner"
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
data-testid="merge-integration-worktree-warning"
|
|
||||||
>
|
|
||||||
<strong>Legacy integration-branch mode.</strong>{" "}
|
|
||||||
Auto-merge will run rebase, conflict resolution, and squash commits inside the
|
|
||||||
project root (the user's checked-out integration-branch worktree) instead of
|
|
||||||
the task worktree. Fusion assumes that directory is already on the integration
|
|
||||||
branch and clean; if it isn't, merges may fail or touch the user's working
|
|
||||||
tree. Reuse-task-worktree is the recommended default (FN-5279). Switch back unless
|
|
||||||
you have a specific reason to opt in (FN-5348).
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeAdvanceAutoSync">Auto-sync project checkout after merge</label>
|
<label htmlFor="mergeAdvanceAutoSync">{t("settings.merge.autoSyncProjectCheckoutAfterMerge", "Auto-sync project checkout after merge")}</label>
|
||||||
<select
|
<select id="mergeAdvanceAutoSync" className="select" value={form.mergeAdvanceAutoSync ?? "stash-and-ff"} onChange={(e) => setForm((f) => ({
|
||||||
id="mergeAdvanceAutoSync"
|
...f,
|
||||||
className="select"
|
mergeAdvanceAutoSync: e.target.value as "off" | "ff-only" | "stash-and-ff",
|
||||||
value={form.mergeAdvanceAutoSync ?? "stash-and-ff"}
|
}))} data-testid="merge-advance-auto-sync-select">
|
||||||
onChange={(e) =>
|
<option value="stash-and-ff">{t("settings.merge.stashFastForwardDefaultPreserveLocalEdits", "Stash + fast-forward (default) \u2014 preserve local edits")}</option>
|
||||||
setForm((f) => ({
|
<option value="ff-only">{t("settings.merge.fastForwardOnlySkipDirtyWorktrees", "Fast-forward only \u2014 skip dirty worktrees")}</option>
|
||||||
...f,
|
<option value="off">{t("settings.merge.offLeaveTheProjectRootStaleLegacyBehavior", "Off \u2014 leave the project root stale (legacy behavior)")}</option>
|
||||||
mergeAdvanceAutoSync: e.target.value as "off" | "ff-only" | "stash-and-ff",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
data-testid="merge-advance-auto-sync-select"
|
|
||||||
>
|
|
||||||
<option value="stash-and-ff">Stash + fast-forward (default) — preserve local edits</option>
|
|
||||||
<option value="ff-only">Fast-forward only — skip dirty worktrees</option>
|
|
||||||
<option value="off">Off — leave the project root stale (legacy behavior)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.afterFusionAdvancesTheIntegrationBranchRefThe", " After Fusion advances the integration branch ref, the merger can auto-sync other worktrees still checked out on that branch (typically your project-root checkout). ")}<code>Stash + fast-forward</code>{t("settings.merge.snapshotsRealLocalEditsAsAPatchAgainst", " snapshots real local edits as a patch against the previous tip, snaps the worktree to the new tip, then reapplies the patch \u2014 untracked files that collide with newly-tracked paths are left in a temp dir for manual recovery. ")}<code>Fast-forward only</code>{t("settings.merge.snapsCleanlyWhenTheWorktreeHasNoEdits", " snaps cleanly when the worktree has no edits and skips otherwise. ")}<code>Off</code>{t("settings.merge.isTheLegacyBehavior", " is the legacy behavior: ")}<code>git status</code>{t("settings.merge.inYourProjectRootWillShowTheNew", " in your project root will show the new commits inverted as "staged changes" until you pull manually. Only applies to direct merges. ")}</small>
|
||||||
After Fusion advances the integration branch ref, the merger can auto-sync other
|
|
||||||
worktrees still checked out on that branch (typically your project-root
|
|
||||||
checkout). <code>Stash + fast-forward</code> snapshots real local edits as a patch
|
|
||||||
against the previous tip, snaps the worktree to the new tip, then reapplies the
|
|
||||||
patch — untracked files that collide with newly-tracked paths are left in a temp
|
|
||||||
dir for manual recovery. <code>Fast-forward only</code> snaps cleanly when the
|
|
||||||
worktree has no edits and skips otherwise. <code>Off</code> is the legacy
|
|
||||||
behavior: <code>git status</code> in your project root will show the new commits
|
|
||||||
inverted as "staged changes" until you pull manually. Only applies to direct
|
|
||||||
merges.
|
|
||||||
</small>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.merge.gitHubAuthentication", "GitHub Authentication")}</h4>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Authentication</h4>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="githubAuthMode">GitHub auth mode</label>
|
<label htmlFor="githubAuthMode">{t("settings.merge.gitHubAuthMode", "GitHub auth mode")}</label>
|
||||||
<select
|
<select id="githubAuthMode" className="select" value={form.githubAuthMode ?? "gh-cli"} onChange={(e) => setForm((f) => ({ ...f, githubAuthMode: e.target.value as "gh-cli" | "token" }))}>
|
||||||
id="githubAuthMode"
|
<option value="gh-cli">{t("settings.merge.gitHubCLIGhAuth", "GitHub CLI (gh auth)")}</option>
|
||||||
className="select"
|
<option value="token">{t("settings.merge.personalAccessToken", "Personal access token")}</option>
|
||||||
value={form.githubAuthMode ?? "gh-cli"}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, githubAuthMode: e.target.value as "gh-cli" | "token" }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="gh-cli">GitHub CLI (gh auth)</option>
|
|
||||||
<option value="token">Personal access token</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{(form.githubAuthMode ?? "gh-cli") === "token" && (
|
{(form.githubAuthMode ?? "gh-cli") === "token" && (<div className="form-group">
|
||||||
<div className="form-group">
|
<label htmlFor="githubAuthToken">{t("settings.merge.gitHubPersonalAccessToken", "GitHub personal access token")}</label>
|
||||||
<label htmlFor="githubAuthToken">GitHub personal access token</label>
|
<input id="githubAuthToken" type="password" className="input" value={form.githubAuthToken ?? ""} onChange={(e) => setForm((f) => ({ ...f, githubAuthToken: e.target.value || undefined }))}/>
|
||||||
<input
|
</div>)}
|
||||||
id="githubAuthToken"
|
|
||||||
type="password"
|
|
||||||
className="input"
|
|
||||||
value={form.githubAuthToken ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, githubAuthToken: e.target.value || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="includeTaskIdInCommit" className="checkbox-label">
|
<label htmlFor="includeTaskIdInCommit" className="checkbox-label">
|
||||||
<input
|
<input id="includeTaskIdInCommit" type="checkbox" checked={form.includeTaskIdInCommit !== false} onChange={(e) => setForm((f) => ({ ...f, includeTaskIdInCommit: e.target.checked }))}/>{t("settings.merge.includeTaskIDInCommitScope", " Include task ID in commit scope ")}</label>
|
||||||
id="includeTaskIdInCommit"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.includeTaskIdInCommit !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, includeTaskIdInCommit: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Include task ID in commit scope
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(KB-001): ...</code>)</small>
|
<small>{t("settings.merge.whenDisabledMergeCommitMessagesOmitTheTask", "When disabled, merge commit messages omit the task ID from the scope (e.g. ")}<code>feat: ...</code>{t("settings.merge.insteadOf", " instead of ")}<code>feat(KB-001): ...</code>)</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="commitAuthorEnabled" className="checkbox-label">
|
<label htmlFor="commitAuthorEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="commitAuthorEnabled" type="checkbox" checked={form.commitAuthorEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, commitAuthorEnabled: e.target.checked }))}/>{t("settings.merge.addFusionAsCoAuthorOnCommits", " Add Fusion as co-author on commits ")}</label>
|
||||||
id="commitAuthorEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.commitAuthorEnabled !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, commitAuthorEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Add Fusion as co-author on commits
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.whenEnabledCommitsMadeByFusionKeepYour", " When enabled, commits made by Fusion keep your git identity as the primary author and append a ")}<code>Co-authored-by</code>{t("settings.merge.trailerCreditingFusionRecognizedByGitHubForShared", " trailer crediting Fusion (recognized by GitHub for shared attribution). ")}</small>
|
||||||
When enabled, commits made by Fusion keep your git identity as the
|
|
||||||
primary author and append a <code>Co-authored-by</code> trailer crediting
|
|
||||||
Fusion (recognized by GitHub for shared attribution).
|
|
||||||
</small>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{form.commitAuthorEnabled !== false && (
|
{form.commitAuthorEnabled !== false && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="commitAuthorName">Co-author Name</label>
|
<label htmlFor="commitAuthorName">{t("settings.merge.coAuthorName", "Co-author Name")}</label>
|
||||||
<input
|
<input id="commitAuthorName" type="text" value={form.commitAuthorName ?? ""} placeholder={t("settings.merge.fusion", "Fusion")} onChange={(e) => setForm((f) => ({
|
||||||
id="commitAuthorName"
|
...f,
|
||||||
type="text"
|
commitAuthorName: e.target.value || undefined,
|
||||||
value={form.commitAuthorName ?? ""}
|
}))}/>
|
||||||
placeholder="Fusion"
|
<small>{t("settings.merge.nameUsedInThe", "Name used in the ")}<code>Co-authored-by</code>{t("settings.merge.trailer", " trailer")}</small>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
commitAuthorName: e.target.value || undefined,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Name used in the <code>Co-authored-by</code> trailer</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="commitAuthorEmail">Co-author Email</label>
|
<label htmlFor="commitAuthorEmail">{t("settings.merge.coAuthorEmail", "Co-author Email")}</label>
|
||||||
<input
|
<input id="commitAuthorEmail" type="email" value={form.commitAuthorEmail ?? ""} placeholder={t("settings.merge.noreplyRunfusionAi", "noreply@runfusion.ai")} onChange={(e) => setForm((f) => ({
|
||||||
id="commitAuthorEmail"
|
...f,
|
||||||
type="email"
|
commitAuthorEmail: e.target.value || undefined,
|
||||||
value={form.commitAuthorEmail ?? ""}
|
}))}/>
|
||||||
placeholder="noreply@runfusion.ai"
|
<small>{t("settings.merge.emailUsedInThe", "Email used in the ")}<code>Co-authored-by</code>{t("settings.merge.trailer", " trailer")}</small>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
commitAuthorEmail: e.target.value || undefined,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Email used in the <code>Co-authored-by</code> trailer</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoResolveConflicts" className="checkbox-label">
|
<label htmlFor="autoResolveConflicts" className="checkbox-label">
|
||||||
<input
|
<input id="autoResolveConflicts" type="checkbox" checked={form.autoResolveConflicts !== false} onChange={(e) => setForm((f) => ({ ...f, autoResolveConflicts: e.target.checked }))}/>{t("settings.merge.autoResolveConflictsInLockFilesAndGenerated", " Auto-resolve conflicts in lock files and generated files ")}</label>
|
||||||
id="autoResolveConflicts"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.autoResolveConflicts !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, autoResolveConflicts: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Auto-resolve conflicts in lock files and generated files
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.), generated files (dist/*, *.gen.ts), and trivial whitespace conflicts are resolved automatically without AI intervention. Complex code conflicts still require AI review.</small>
|
<small>{t("settings.merge.whenEnabledLockFilesPackageLockJsonPnpm", "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.), generated files (dist/*, *.gen.ts), and trivial whitespace conflicts are resolved automatically without AI intervention. Complex code conflicts still require AI review.")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{(form.merger?.mode ?? "ai") !== "ai" && (
|
{(form.merger?.mode ?? "ai") !== "ai" && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="smartConflictResolution" className="checkbox-label">
|
<label htmlFor="smartConflictResolution" className="checkbox-label">
|
||||||
<input
|
<input id="smartConflictResolution" type="checkbox" checked={form.smartConflictResolution !== false} onChange={(e) => setForm((f) => ({ ...f, smartConflictResolution: e.target.checked }))}/>{t("settings.merge.smartConflictResolution", " Smart conflict resolution ")}</label>
|
||||||
id="smartConflictResolution"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.smartConflictResolution !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, smartConflictResolution: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Smart conflict resolution
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.</small>
|
<small>{t("settings.merge.whenEnabledLockFilesPackageLockJsonPnpm2", "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeConflictStrategy">Conflict Fallback Strategy</label>
|
<label htmlFor="mergeConflictStrategy">{t("settings.merge.conflictFallbackStrategy", "Conflict Fallback Strategy")}</label>
|
||||||
<select
|
<select id="mergeConflictStrategy" value={form.mergeConflictStrategy ?? "smart-prefer-main"} onChange={(e) => setForm((f) => ({ ...f, mergeConflictStrategy: e.target.value as "smart-prefer-main" | "smart-prefer-branch" | "ai-only" | "abort" }))}>
|
||||||
id="mergeConflictStrategy"
|
<option value="smart-prefer-main">{t("settings.merge.smartPreferMainOnFallbackFetchFfOrigin", "Smart, prefer main on fallback \u2014 fetch+ff origin \u2192 AI \u2192 auto-resolve \u2192 -X ours (default; protects just-merged sibling work)")}</option>
|
||||||
value={form.mergeConflictStrategy ?? "smart-prefer-main"}
|
<option value="smart-prefer-branch">{t("settings.merge.smartPreferTaskOnFallbackFetchFfOrigin", "Smart, prefer task on fallback \u2014 fetch+ff origin \u2192 AI \u2192 auto-resolve \u2192 -X theirs (legacy \"smart\" behavior; task branch wins)")}</option>
|
||||||
onChange={(e) =>
|
<option value="ai-only">{t("settings.merge.aIOnlyAIAutoResolveAIRetryNever", "AI only \u2014 AI \u2192 auto-resolve \u2192 AI retry; never silently pick a side")}</option>
|
||||||
setForm((f) => ({ ...f, mergeConflictStrategy: e.target.value as "smart-prefer-main" | "smart-prefer-branch" | "ai-only" | "abort" }))
|
<option value="abort">{t("settings.merge.abortOneAIAttemptRequireManualResolutionIf", "Abort \u2014 one AI attempt; require manual resolution if it fails")}</option>
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="smart-prefer-main">Smart, prefer main on fallback — fetch+ff origin → AI → auto-resolve → -X ours (default; protects just-merged sibling work)</option>
|
|
||||||
<option value="smart-prefer-branch">Smart, prefer task on fallback — fetch+ff origin → AI → auto-resolve → -X theirs (legacy "smart" behavior; task branch wins)</option>
|
|
||||||
<option value="ai-only">AI only — AI → auto-resolve → AI retry; never silently pick a side</option>
|
|
||||||
<option value="abort">Abort — one AI attempt; require manual resolution if it fails</option>
|
|
||||||
</select>
|
</select>
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>
|
<small>{t("settings.merge.both", " Both ")}<strong>{t("settings.merge.smart", "Smart")}</strong>{t("settings.merge.optionsStartWithABestEffort", " options start with a best-effort ")}<code>git fetch</code>{t("settings.merge.fastForwardOfLocalMainFrom", " + fast-forward of local main from ")}<code>origin</code>{t("settings.merge.soAFreshlyPushedSiblingCommitDoesntGet", " (so a freshly-pushed sibling commit doesn't get clobbered), then run an AI agent, then auto-resolve handles lock/generated/trivial files. They differ only in the ")}<em>{t("settings.merge.finalFallback", "final fallback")}</em>:
|
||||||
Both <strong>Smart</strong> options start with a best-effort <code>git fetch</code> + fast-forward of local main from <code>origin</code> (so a freshly-pushed sibling commit doesn't get clobbered), then run an AI agent, then auto-resolve handles lock/generated/trivial files. They differ only in the <em>final fallback</em>:
|
|
||||||
{" "}
|
{" "}
|
||||||
<strong>Smart, prefer main</strong> uses <code>-X ours</code> so main wins — protects just-merged sibling work and is the new default.
|
<strong>{t("settings.merge.smartPreferMain", "Smart, prefer main")}</strong>{t("settings.merge.uses", " uses ")}<code>-X ours</code>{t("settings.merge.soMainWinsProtectsJustMergedSiblingWork", " so main wins \u2014 protects just-merged sibling work and is the new default. ")}{" "}
|
||||||
{" "}
|
<strong>{t("settings.merge.smartPreferTask", "Smart, prefer task")}</strong>{t("settings.merge.uses", " uses ")}<code>-X theirs</code>{t("settings.merge.soTheTaskBranchWinsFastButCan", " so the task branch wins \u2014 fast, but can resurrect code an earlier sibling task deleted (the FN-2887 class of regression). ")}{" "}
|
||||||
<strong>Smart, prefer task</strong> uses <code>-X theirs</code> so the task branch wins — fast, but can resurrect code an earlier sibling task deleted (the FN-2887 class of regression).
|
<strong>{t("settings.merge.aIOnly", "AI only")}</strong>{t("settings.merge.retriesTheAIAgentRatherThanAutoPicking", " retries the AI agent rather than auto-picking a side. ")}{" "}
|
||||||
{" "}
|
<strong>{t("settings.merge.abort", "Abort")}</strong>{t("settings.merge.stopsAfterTheFirstAIAttemptAndWaits", " stops after the first AI attempt and waits for a human. ")}{" "}
|
||||||
<strong>AI only</strong> retries the AI agent rather than auto-picking a side.
|
<em>{t("settings.merge.legacy2", "Legacy ")}<code>"smart"</code>{t("settings.merge.and2", " and ")}<code>"prefer-main"</code>{t("settings.merge.valuesFromOlderSettingsAreMigratedAutomatically", " values from older settings are migrated automatically.")}</em>
|
||||||
{" "}
|
|
||||||
<strong>Abort</strong> stops after the first AI attempt and waits for a human.
|
|
||||||
{" "}
|
|
||||||
<em>Legacy <code>"smart"</code> and <code>"prefer-main"</code> values from older settings are migrated automatically.</em>
|
|
||||||
</small>
|
</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="mergeStrategyOverlapBehavior">Smart Prefer Main Overlap Guard</label>
|
<label htmlFor="mergeStrategyOverlapBehavior">{t("settings.merge.smartPreferMainOverlapGuard", "Smart Prefer Main Overlap Guard")}</label>
|
||||||
<select
|
<select id="mergeStrategyOverlapBehavior" value={form.mergeStrategyOverlapBehavior ?? "flip-to-prefer-branch"} onChange={(e) => setForm((f) => ({
|
||||||
id="mergeStrategyOverlapBehavior"
|
...f,
|
||||||
value={form.mergeStrategyOverlapBehavior ?? "flip-to-prefer-branch"}
|
mergeStrategyOverlapBehavior: e.target.value as "flip-to-prefer-branch" | "warn-only" | "ignore",
|
||||||
onChange={(e) =>
|
}))}>
|
||||||
setForm((f) => ({
|
<option value="flip-to-prefer-branch">{t("settings.merge.flipOverlappingFilesToPreferTheTaskBranch", "Flip overlapping files to prefer the task branch (default)")}</option>
|
||||||
...f,
|
<option value="warn-only">{t("settings.merge.warnOnlyKeepLegacyMainWinsFallback", "Warn only \u2014 keep legacy main-wins fallback")}</option>
|
||||||
mergeStrategyOverlapBehavior: e.target.value as "flip-to-prefer-branch" | "warn-only" | "ignore",
|
<option value="ignore">{t("settings.merge.ignoreOverlapDetectionPreserveLegacyBehavior", "Ignore overlap detection \u2014 preserve legacy behavior")}</option>
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="flip-to-prefer-branch">Flip overlapping files to prefer the task branch (default)</option>
|
|
||||||
<option value="warn-only">Warn only — keep legacy main-wins fallback</option>
|
|
||||||
<option value="ignore">Ignore overlap detection — preserve legacy behavior</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.merge.whenUsingSmartPreferMainAutomaticallyPreferThe", " When using smart-prefer-main, automatically prefer the branch side for files that main has recently modified to avoid silently discarding branch work. ")}</small>
|
||||||
When using smart-prefer-main, automatically prefer the branch side for files that main has recently modified to avoid silently discarding branch work.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="postMergeAuditMode">Post-merge audit mode</label>
|
<label htmlFor="postMergeAuditMode">{t("settings.merge.postMergeAuditMode", "Post-merge audit mode")}</label>
|
||||||
<select
|
<select className="select" id="postMergeAuditMode" value={form.postMergeAuditMode ?? "warn"} onChange={(e) => setForm((f) => ({
|
||||||
className="select"
|
...f,
|
||||||
id="postMergeAuditMode"
|
postMergeAuditMode: e.target.value as "block" | "warn" | "off",
|
||||||
value={form.postMergeAuditMode ?? "warn"}
|
}))}>
|
||||||
onChange={(e) =>
|
<option value="block">{t("settings.merge.blockStrict", "Block (strict)")}</option>
|
||||||
setForm((f) => ({
|
<option value="warn">{t("settings.merge.warnDefaultLogFindingsContinue", "Warn (default; log findings, continue)")}</option>
|
||||||
...f,
|
<option value="off">{t("settings.merge.offSkipAudit", "Off (skip audit)")}</option>
|
||||||
postMergeAuditMode: e.target.value as "block" | "warn" | "off",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="block">Block (strict)</option>
|
|
||||||
<option value="warn">Warn (default; log findings, continue)</option>
|
|
||||||
<option value="off">Off (skip audit)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.merge.controlsThePostMergeAuditGate", " Controls the post-merge audit gate. ")}<strong>{t("settings.merge.warn", "Warn")}</strong>{t("settings.merge.defaultLogsFindingsButAutoCompletesTheMerge", " (default) logs findings but auto-completes the merge. ")}<strong>{t("settings.merge.block", "Block")}</strong>{t("settings.merge.isTheStricterOptInModeThatRefuses", " is the stricter opt-in mode that refuses to auto-complete merges with duplicate-subject or touched-file overlap risks. ")}<strong>{t("settings.merge.off", "Off")}</strong>{t("settings.merge.skipsTheAuditEntirelySwitchingToOffIs", " skips the audit entirely. Switching to Off is recommended only if you trust your branches don't silently drop edits. ")}</small>
|
||||||
Controls the post-merge audit gate. <strong>Warn</strong> (default) logs findings but auto-completes the merge. <strong>Block</strong> is the stricter opt-in mode that refuses to auto-complete merges with duplicate-subject or touched-file overlap risks. <strong>Off</strong> skips the audit entirely. Switching to Off is recommended only if you trust your branches don't silently drop edits.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="pushAfterMerge" className="checkbox-label">
|
<label htmlFor="pushAfterMerge" className="checkbox-label">
|
||||||
<input
|
<input id="pushAfterMerge" type="checkbox" checked={form.pushAfterMerge === true} onChange={(e) => setForm((f) => ({ ...f, pushAfterMerge: e.target.checked }))}/>{t("settings.merge.pushToRemoteAfterMerge", " Push to remote after merge ")}</label>
|
||||||
id="pushAfterMerge"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.pushAfterMerge === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, pushAfterMerge: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Push to remote after merge
|
|
||||||
</label>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>When enabled, the merged result is automatically pushed to the configured git remote. This includes pulling the latest from the remote first (rebase) and resolving any conflicts with AI if needed.</small>
|
<small>{t("settings.merge.whenEnabledTheMergedResultIsAutomaticallyPushed", "When enabled, the merged result is automatically pushed to the configured git remote. This includes pulling the latest from the remote first (rebase) and resolving any conflicts with AI if needed.")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{form.pushAfterMerge && (
|
{form.pushAfterMerge && (<div className="form-group">
|
||||||
<div className="form-group">
|
<label htmlFor="pushRemote">{t("settings.merge.pushRemote", "Push Remote")}</label>
|
||||||
<label htmlFor="pushRemote">Push Remote</label>
|
<input id="pushRemote" type="text" placeholder={t("settings.merge.origin", "origin")} value={form.pushRemote || ""} onChange={(e) => setForm((f) => ({ ...f, pushRemote: e.target.value || undefined }))}/>
|
||||||
<input
|
|
||||||
id="pushRemote"
|
|
||||||
type="text"
|
|
||||||
placeholder="origin"
|
|
||||||
value={form.pushRemote || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, pushRemote: e.target.value || undefined }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<details className="settings-option-details">
|
<details className="settings-option-details">
|
||||||
<summary>More details</summary>
|
<summary>{t("settings.merge.moreDetails", "More details")}</summary>
|
||||||
<small>Git remote to push to (e.g. "origin"). Can include branch name (e.g. "origin main"). Default: "origin".</small>
|
<small>{t("settings.merge.gitRemoteToPushToEGOrigin", "Git remote to push to (e.g. \"origin\"). Can include branch name (e.g. \"origin main\"). Default: \"origin\".")}</small>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
</>);
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MergeSection;
|
export default MergeSection;
|
||||||
|
|||||||
@@ -1,88 +1,62 @@
|
|||||||
/**
|
|
||||||
* Node Routing section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped execution-node default + unavailable-node policy. The node list
|
|
||||||
* is fetched in the shell (shared with other surfaces) and passed down. Keys,
|
|
||||||
* node-status rendering, and the inline status label helper are preserved
|
|
||||||
* verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { NodeInfo } from "../../../api";
|
import type { NodeInfo } from "../../../api";
|
||||||
import { NodeHealthDot } from "../../NodeHealthDot";
|
import { NodeHealthDot } from "../../NodeHealthDot";
|
||||||
import type { SettingsFormState, SetSettingsForm } from "./context";
|
import type { SettingsFormState, SetSettingsForm } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error"): string {
|
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error", t: ReturnType<typeof useTranslation<"app">>["t"]): string {
|
||||||
if (status === "online") return "Online";
|
if (status === "online")
|
||||||
if (status === "connecting") return "Connecting";
|
return t("settings.nodeRouting.statusOnline", "Online");
|
||||||
if (status === "error") return "Error";
|
if (status === "connecting")
|
||||||
return "Offline";
|
return t("settings.nodeRouting.statusConnecting", "Connecting");
|
||||||
|
if (status === "error")
|
||||||
|
return t("settings.nodeRouting.statusError", "Error");
|
||||||
|
return t("settings.nodeRouting.statusOffline", "Offline");
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeRoutingSectionProps {
|
export interface NodeRoutingSectionProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
form: SettingsFormState;
|
form: SettingsFormState;
|
||||||
setForm: SetSettingsForm;
|
setForm: SetSettingsForm;
|
||||||
nodes: NodeInfo[];
|
nodes: NodeInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeRoutingSection({ scopeBanner, form, setForm, nodes }: NodeRoutingSectionProps) {
|
export function NodeRoutingSection({ scopeBanner, form, setForm, nodes }: NodeRoutingSectionProps) {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Node Routing</h4>
|
<h4 className="settings-section-heading">{t("settings.nodeRouting.nodeRouting", "Node Routing")}</h4>
|
||||||
<p className="settings-section-description">Configure how tasks are routed to execution nodes.</p>
|
<p className="settings-section-description">{t("settings.nodeRouting.configureHowTasksAreRoutedToExecutionNodes", "Configure how tasks are routed to execution nodes.")}</p>
|
||||||
<p className="settings-node-routing-note">These settings apply at the project level.</p>
|
<p className="settings-node-routing-note">{t("settings.nodeRouting.theseSettingsApplyAtTheProjectLevel", "These settings apply at the project level.")}</p>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="defaultNodeId">Default Execution Node</label>
|
<label htmlFor="defaultNodeId">{t("settings.nodeRouting.defaultExecutionNode", "Default Execution Node")}</label>
|
||||||
<select
|
<select id="defaultNodeId" className="select" value={typeof form.defaultNodeId === "string" ? form.defaultNodeId : ""} onChange={(e) => {
|
||||||
id="defaultNodeId"
|
|
||||||
className="select"
|
|
||||||
value={typeof form.defaultNodeId === "string" ? form.defaultNodeId : ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, defaultNodeId: val || undefined } as SettingsFormState));
|
setForm((f) => ({ ...f, defaultNodeId: val || undefined } as SettingsFormState));
|
||||||
}}
|
}}>
|
||||||
>
|
<option value="">{t("settings.nodeRouting.localExecutionNoDefaultNode", "Local execution (no default node)")}</option>
|
||||||
<option value="">Local execution (no default node)</option>
|
{nodes.map((node) => (<option key={node.id} value={node.id}>
|
||||||
{nodes.map((node) => (
|
{node.name} ({getNodeStatusLabel(node.status, t)})
|
||||||
<option key={node.id} value={node.id}>
|
</option>))}
|
||||||
{node.name} ({getNodeStatusLabel(node.status)})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
{(() => {
|
{(() => {
|
||||||
const selectedNode = nodes.find((node) => node.id === form.defaultNodeId);
|
const selectedNode = nodes.find((node) => node.id === form.defaultNodeId);
|
||||||
if (!selectedNode) return null;
|
if (!selectedNode)
|
||||||
return (
|
return null;
|
||||||
<div className="settings-node-status">
|
return (<div className="settings-node-status">
|
||||||
<span>Selected node:</span>
|
<span>{t("settings.nodeRouting.selectedNode", "Selected node:")}</span>
|
||||||
<NodeHealthDot status={selectedNode.status} showLabel />
|
<NodeHealthDot status={selectedNode.status} showLabel/>
|
||||||
</div>
|
</div>);
|
||||||
);
|
|
||||||
})()}
|
})()}
|
||||||
<small>Used when a task has no node override. Node status is shown for safer routing selection.</small>
|
<small>{t("settings.nodeRouting.usedWhenATaskHasNoNodeOverride", "Used when a task has no node override. Node status is shown for safer routing selection.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="unavailableNodePolicy">Unavailable Node Policy</label>
|
<label htmlFor="unavailableNodePolicy">{t("settings.nodeRouting.unavailableNodePolicy", "Unavailable Node Policy")}</label>
|
||||||
<select
|
<select id="unavailableNodePolicy" className="select" value={form.unavailableNodePolicy === "fallback-local" ? "fallback-local" : "block"} onChange={(e) => setForm((f) => ({
|
||||||
id="unavailableNodePolicy"
|
...f,
|
||||||
className="select"
|
unavailableNodePolicy: e.target.value as "block" | "fallback-local",
|
||||||
value={
|
} as SettingsFormState))}>
|
||||||
form.unavailableNodePolicy === "fallback-local" ? "fallback-local" : "block"
|
<option value="block">{t("settings.nodeRouting.blockExecution", "Block execution")}</option>
|
||||||
}
|
<option value="fallback-local">{t("settings.nodeRouting.fallBackToLocal", "Fall back to local")}</option>
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
unavailableNodePolicy: e.target.value as "block" | "fallback-local",
|
|
||||||
} as SettingsFormState))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="block">Block execution</option>
|
|
||||||
<option value="fallback-local">Fall back to local</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NodeRoutingSection;
|
export default NodeRoutingSection;
|
||||||
|
|||||||
@@ -1,101 +1,52 @@
|
|||||||
/**
|
|
||||||
* Node Sync section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Cross-node settings synchronization toggles. Preserves the existing
|
|
||||||
* "Workflow settings are not synced across nodes yet" informational note
|
|
||||||
* (KTD-8) verbatim, including its i18n key.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
export interface NodeSyncSectionProps extends SectionBaseProps {
|
export interface NodeSyncSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeSyncSection({ scopeBanner, form, setForm }: NodeSyncSectionProps) {
|
export function NodeSyncSection({ scopeBanner, form, setForm }: NodeSyncSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
return (
|
return (<>
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Node Sync</h4>
|
<h4 className="settings-section-heading">{t("settings.nodeSync.nodeSync", "Node Sync")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="settingsSyncEnabled" className="checkbox-label">
|
<label htmlFor="settingsSyncEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="settingsSyncEnabled" type="checkbox" checked={form.settingsSyncEnabled || false} onChange={(e) => setForm((f) => ({ ...f, settingsSyncEnabled: e.target.checked }))}/>{t("settings.nodeSync.enableAutomaticSettingsSync", " Enable automatic settings sync ")}</label>
|
||||||
id="settingsSyncEnabled"
|
<small>{t("settings.nodeSync.automaticallySynchronizeSettingsBetweenThisNodeAndConnected", "Automatically synchronize settings between this node and connected remote nodes")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.settingsSyncEnabled || false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, settingsSyncEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Enable automatic settings sync
|
|
||||||
</label>
|
|
||||||
<small>Automatically synchronize settings between this node and connected remote nodes</small>
|
|
||||||
</div>
|
</div>
|
||||||
{form.settingsSyncEnabled && (
|
{form.settingsSyncEnabled && (<>
|
||||||
<>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="settingsSyncAuth" className="checkbox-label">
|
<label htmlFor="settingsSyncAuth" className="checkbox-label">
|
||||||
<input
|
<input id="settingsSyncAuth" type="checkbox" checked={form.settingsSyncAuth || false} onChange={(e) => setForm((f) => ({ ...f, settingsSyncAuth: e.target.checked }))}/>{t("settings.nodeSync.syncModelAuthCredentials", " Sync model auth credentials ")}</label>
|
||||||
id="settingsSyncAuth"
|
<small>{t("settings.nodeSync.includeAPIKeysAndOAuthTokensInSync", "Include API keys and OAuth tokens in sync operations")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.settingsSyncAuth || false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, settingsSyncAuth: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Sync model auth credentials
|
|
||||||
</label>
|
|
||||||
<small>Include API keys and OAuth tokens in sync operations</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="settingsSyncInterval">Sync interval</label>
|
<label htmlFor="settingsSyncInterval">{t("settings.nodeSync.syncInterval", "Sync interval")}</label>
|
||||||
<select
|
<select id="settingsSyncInterval" className="select" value={form.settingsSyncInterval || 900000} onChange={(e) => setForm((f) => ({ ...f, settingsSyncInterval: parseInt(e.target.value, 10) }))}>
|
||||||
id="settingsSyncInterval"
|
<option value={300000}>{t("settings.nodeSync.every5Minutes", "Every 5 minutes")}</option>
|
||||||
className="select"
|
<option value={900000}>{t("settings.nodeSync.every15Minutes", "Every 15 minutes")}</option>
|
||||||
value={form.settingsSyncInterval || 900000}
|
<option value={1800000}>{t("settings.nodeSync.every30Minutes", "Every 30 minutes")}</option>
|
||||||
onChange={(e) =>
|
<option value={3600000}>{t("settings.nodeSync.every1Hour", "Every 1 hour")}</option>
|
||||||
setForm((f) => ({ ...f, settingsSyncInterval: parseInt(e.target.value, 10) }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value={300000}>Every 5 minutes</option>
|
|
||||||
<option value={900000}>Every 15 minutes</option>
|
|
||||||
<option value={1800000}>Every 30 minutes</option>
|
|
||||||
<option value={3600000}>Every 1 hour</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="settingsSyncConflictResolution">Conflict resolution</label>
|
<label htmlFor="settingsSyncConflictResolution">{t("settings.nodeSync.conflictResolution", "Conflict resolution")}</label>
|
||||||
<select
|
<select id="settingsSyncConflictResolution" className="select" value={form.settingsSyncConflictResolution || "last-write-wins"} onChange={(e) => setForm((f) => ({
|
||||||
id="settingsSyncConflictResolution"
|
...f,
|
||||||
className="select"
|
settingsSyncConflictResolution: e.target.value as "last-write-wins" | "always-ask" | "keep-local" | "keep-remote",
|
||||||
value={form.settingsSyncConflictResolution || "last-write-wins"}
|
}))}>
|
||||||
onChange={(e) =>
|
<option value="last-write-wins">{t("settings.nodeSync.lastWriteWins", "Last write wins")}</option>
|
||||||
setForm((f) => ({
|
<option value="always-ask">{t("settings.nodeSync.alwaysAsk", "Always ask")}</option>
|
||||||
...f,
|
<option value="keep-local">{t("settings.nodeSync.keepLocal", "Keep local")}</option>
|
||||||
settingsSyncConflictResolution: e.target.value as
|
<option value="keep-remote">{t("settings.nodeSync.keepRemote", "Keep remote")}</option>
|
||||||
| "last-write-wins"
|
|
||||||
| "always-ask"
|
|
||||||
| "keep-local"
|
|
||||||
| "keep-remote",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="last-write-wins">Last write wins</option>
|
|
||||||
<option value="always-ask">Always ask</option>
|
|
||||||
<option value="keep-local">Keep local</option>
|
|
||||||
<option value="keep-remote">Keep remote</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
{/* KTD-8: workflow settings are not yet part of the cross-node sync
|
{/* KTD-8: workflow settings are not yet part of the cross-node sync
|
||||||
channel. Non-dismissible, informational only, no action affordance. */}
|
channel. Non-dismissible, informational only, no action affordance. */}
|
||||||
<p className="settings-sync-workflow-note text-muted" role="note">
|
<p className="settings-sync-workflow-note text-muted" role="note">
|
||||||
{t(
|
{t("settings.nodeSync.workflowSettingsNotSynced", "Workflow settings are not synced across nodes yet.")}
|
||||||
"settings.nodeSync.workflowSettingsNotSynced",
|
|
||||||
"Workflow settings are not synced across nodes yet.",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NodeSyncSection;
|
export default NodeSyncSection;
|
||||||
|
|||||||
@@ -1,408 +1,246 @@
|
|||||||
/**
|
|
||||||
* Notifications section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Failure-notification policy plus the ntfy and webhook provider cards,
|
|
||||||
* including per-event toggles and the "test notification" affordances. The
|
|
||||||
* test-send handler and its loading/result state live in the shell (they touch
|
|
||||||
* the API and toast); this section relays them as props. Behavior, keys, and
|
|
||||||
* validation regexes are preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { NtfyNotificationEvent } from "@fusion/core";
|
import type { NtfyNotificationEvent } from "@fusion/core";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
/** Default event set used when a provider has no explicit `*Events` override. */
|
/** Default event set used when a provider has no explicit `*Events` override. */
|
||||||
export const DEFAULT_NTFY_EVENTS: NtfyNotificationEvent[] = [
|
export const DEFAULT_NTFY_EVENTS: NtfyNotificationEvent[] = [
|
||||||
"in-review",
|
"in-review",
|
||||||
"merged",
|
"merged",
|
||||||
"failed",
|
"failed",
|
||||||
"awaiting-approval",
|
"awaiting-approval",
|
||||||
"awaiting-user-review",
|
"awaiting-user-review",
|
||||||
"planning-awaiting-input",
|
"planning-awaiting-input",
|
||||||
"gridlock",
|
"gridlock",
|
||||||
"fallback-used",
|
"fallback-used",
|
||||||
"memory-dreams-processed",
|
"memory-dreams-processed",
|
||||||
"message:agent-to-user",
|
"message:agent-to-user",
|
||||||
"message:agent-to-agent",
|
"message:agent-to-agent",
|
||||||
"message:room",
|
"message:room",
|
||||||
"oauth-token-expired",
|
"oauth-token-expired",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NOTIFICATION_EVENT_OPTIONS: Array<{
|
export const NOTIFICATION_EVENT_OPTIONS: Array<{
|
||||||
event: NtfyNotificationEvent;
|
event: NtfyNotificationEvent;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ event: "in-review", label: "Task completed (in-review)", description: "When a task moves to In Review (ready for review)" },
|
{ event: "in-review", label: "Task completed (in-review)", description: "When a task moves to In Review (ready for review)" },
|
||||||
{ event: "merged", label: "Task merged", description: "When a task is successfully merged to main" },
|
{ event: "merged", label: "Task merged", description: "When a task is successfully merged to main" },
|
||||||
{ event: "failed", label: "Task failed", description: "When a task fails during execution (high priority)" },
|
{ event: "failed", label: "Task failed", description: "When a task fails during execution (high priority)" },
|
||||||
{ event: "awaiting-approval", label: "Plan needs approval", description: "When a task specification needs manual approval before execution" },
|
{ event: "awaiting-approval", label: "Plan needs approval", description: "When a task specification needs manual approval before execution" },
|
||||||
{ event: "awaiting-user-review", label: "User review needed", description: "When an agent hands off a task for human review (high priority)" },
|
{ event: "awaiting-user-review", label: "User review needed", description: "When an agent hands off a task for human review (high priority)" },
|
||||||
{ event: "planning-awaiting-input", label: "Planning needs input", description: "When planning mode is waiting for your response to continue" },
|
{ event: "planning-awaiting-input", label: "Planning needs input", description: "When planning mode is waiting for your response to continue" },
|
||||||
{ event: "gridlock", label: "Pipeline gridlocked", description: "When all schedulable todo tasks are blocked and work cannot advance" },
|
{ event: "gridlock", label: "Pipeline gridlocked", description: "When all schedulable todo tasks are blocked and work cannot advance" },
|
||||||
{ event: "fallback-used", label: "Fallback model used (recovered)", description: "When Fusion recovers from a retryable model failure by switching to a fallback model" },
|
{ event: "fallback-used", label: "Fallback model used (recovered)", description: "When Fusion recovers from a retryable model failure by switching to a fallback model" },
|
||||||
{ event: "task-created", label: "Agent created a task", description: "When an agent files a new task on the board" },
|
{ event: "task-created", label: "Agent created a task", description: "When an agent files a new task on the board" },
|
||||||
{ event: "memory-dreams-processed", label: "DREAMS.md entry added", description: "When manual dream processing writes a new entry to project or agent DREAMS.md" },
|
{ event: "memory-dreams-processed", label: "DREAMS.md entry added", description: "When manual dream processing writes a new entry to project or agent DREAMS.md" },
|
||||||
{ event: "message:agent-to-user", label: "Agent → user message", description: "An agent sent you a direct message" },
|
{ event: "message:agent-to-user", label: "Agent → user message", description: "An agent sent you a direct message" },
|
||||||
{ event: "message:agent-to-agent", label: "Agent → agent message", description: "Agents are talking to each other (including replies)" },
|
{ event: "message:agent-to-agent", label: "Agent → agent message", description: "Agents are talking to each other (including replies)" },
|
||||||
{ event: "message:room", label: "Agent message in room", description: "An agent posted a reply in a chat room you're watching" },
|
{ event: "message:room", label: "Agent message in room", description: "An agent posted a reply in a chat room you're watching" },
|
||||||
{ event: "oauth-token-expired", label: "OAuth token expired", description: "Notify when a provider OAuth token (Codex, Claude, etc.) expires." },
|
{ event: "oauth-token-expired", label: "OAuth token expired", description: "Notify when a provider OAuth token (Codex, Claude, etc.) expires." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type TestNotificationProvider = "ntfy" | "webhook" | "ntfy-message" | "ntfy-room";
|
export type TestNotificationProvider = "ntfy" | "webhook" | "ntfy-message" | "ntfy-room";
|
||||||
|
|
||||||
export interface NotificationsSectionProps extends SectionBaseProps {
|
export interface NotificationsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
testNotificationLoading: Record<string, boolean>;
|
testNotificationLoading: Record<string, boolean>;
|
||||||
testNotificationResult: Record<string, { status: "success" | "error"; message: string }>;
|
testNotificationResult: Record<string, {
|
||||||
onTestProviderNotification: (provider: TestNotificationProvider) => void;
|
status: "success" | "error";
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
onTestProviderNotification: (provider: TestNotificationProvider) => void;
|
||||||
}
|
}
|
||||||
|
export function NotificationsSection({ scopeBanner, form, setForm, testNotificationLoading, testNotificationResult, onTestProviderNotification, }: NotificationsSectionProps) {
|
||||||
export function NotificationsSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
form,
|
|
||||||
setForm,
|
|
||||||
testNotificationLoading,
|
|
||||||
testNotificationResult,
|
|
||||||
onTestProviderNotification,
|
|
||||||
}: NotificationsSectionProps) {
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Notifications</h4>
|
<h4 className="settings-section-heading">{t("settings.notifications.notifications", "Notifications")}</h4>
|
||||||
|
|
||||||
<div className="notification-provider-card">
|
<div className="notification-provider-card">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="failureNotificationMode">Failure notification mode</label>
|
<label htmlFor="failureNotificationMode">{t("settings.notifications.failureNotificationMode", "Failure notification mode")}</label>
|
||||||
<select
|
<select id="failureNotificationMode" value={form.failureNotificationMode ?? "sticky-only"} onChange={(e) => {
|
||||||
id="failureNotificationMode"
|
const value = e.target.value as "sticky-only" | "all" | "terminal-only";
|
||||||
value={form.failureNotificationMode ?? "sticky-only"}
|
setForm((f) => ({ ...f, failureNotificationMode: value }));
|
||||||
onChange={(e) => {
|
}}>
|
||||||
const value = e.target.value as "sticky-only" | "all" | "terminal-only";
|
<option value="sticky-only">{t("settings.notifications.stickyFailuresOnlyDefault", "Sticky failures only (default)")}</option>
|
||||||
setForm((f) => ({ ...f, failureNotificationMode: value }));
|
<option value="terminal-only">{t("settings.notifications.terminalFailuresOnlySuppressAutoRetried", "Terminal failures only (suppress auto-retried)")}</option>
|
||||||
}}
|
<option value="all">{t("settings.notifications.allFailuresLegacy", "All failures (legacy)")}</option>
|
||||||
>
|
|
||||||
<option value="sticky-only">Sticky failures only (default)</option>
|
|
||||||
<option value="terminal-only">Terminal failures only (suppress auto-retried)</option>
|
|
||||||
<option value="all">All failures (legacy)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>Sticky-only suppresses recovered failures; terminal-only waits for paused/in-review failed tasks; all restores legacy alerts.</small>
|
<small>{t("settings.notifications.stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits", "Sticky-only suppresses recovered failures; terminal-only waits for paused/in-review failed tasks; all restores legacy alerts.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="failureNotificationDelayMs">Failure notification delay (ms)</label>
|
<label htmlFor="failureNotificationDelayMs">{t("settings.notifications.failureNotificationDelayMs", "Failure notification delay (ms)")}</label>
|
||||||
<input
|
<input id="failureNotificationDelayMs" type="number" min={0} step={1000} disabled={(form.failureNotificationMode ?? "sticky-only") === "all"} value={form.failureNotificationDelayMs ?? 30000} onChange={(e) => {
|
||||||
id="failureNotificationDelayMs"
|
const parsed = Number(e.target.value);
|
||||||
type="number"
|
setForm((f) => ({
|
||||||
min={0}
|
|
||||||
step={1000}
|
|
||||||
disabled={(form.failureNotificationMode ?? "sticky-only") === "all"}
|
|
||||||
value={form.failureNotificationDelayMs ?? 30000}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = Number(e.target.value);
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
...f,
|
||||||
failureNotificationDelayMs: Number.isFinite(parsed) && parsed >= 0 ? parsed : 0,
|
failureNotificationDelayMs: Number.isFinite(parsed) && parsed >= 0 ? parsed : 0,
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.notifications.howLongAFailureMustPersistBeforeA", " How long a failure must persist before a push notification is sent. 0 = notify immediately. ")}</small>
|
||||||
<small>
|
|
||||||
How long a failure must persist before a push notification is sent. 0 = notify immediately.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="notification-provider-card">
|
<div className="notification-provider-card">
|
||||||
<div className="notification-provider-header">
|
<div className="notification-provider-header">
|
||||||
<strong>ntfy</strong>
|
<strong>{t("settings.notifications.ntfy", "ntfy")}</strong>
|
||||||
<label htmlFor="ntfyEnabled" className="checkbox-label">
|
<label htmlFor="ntfyEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="ntfyEnabled" type="checkbox" checked={form.ntfyEnabled || false} onChange={(e) => setForm((f) => ({ ...f, ntfyEnabled: e.target.checked }))}/>{t("settings.notifications.enable", " Enable ")}</label>
|
||||||
id="ntfyEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.ntfyEnabled || false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, ntfyEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Enable
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{form.ntfyEnabled && (
|
{form.ntfyEnabled && (<div className="notification-provider-body">
|
||||||
<div className="notification-provider-body">
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="ntfyTopic">ntfy Topic</label>
|
<label htmlFor="ntfyTopic">{t("settings.notifications.ntfyTopic", "ntfy Topic")}</label>
|
||||||
<input
|
<input id="ntfyTopic" type="text" placeholder={t("settings.notifications.myTopicName", "my-topic-name")} value={form.ntfyTopic || ""} onChange={(e) => {
|
||||||
id="ntfyTopic"
|
const val = e.target.value;
|
||||||
type="text"
|
setForm((f) => ({ ...f, ntfyTopic: val || undefined }));
|
||||||
placeholder="my-topic-name"
|
}}/>
|
||||||
value={form.ntfyTopic || ""}
|
<small>{t("settings.notifications.yourNtfyShTopicName164Alphanumeric", " Your ntfy.sh topic name (1\u201364 alphanumeric/hyphen/underscore characters).")}{" "}
|
||||||
onChange={(e) => {
|
<a href="https://ntfy.sh" target="_blank" rel="noopener noreferrer" className="settings-inline-link">{t("settings.notifications.learnMoreAboutNtfySh", " Learn more about ntfy.sh ")}</a>
|
||||||
const val = e.target.value;
|
|
||||||
setForm((f) => ({ ...f, ntfyTopic: val || undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<small>
|
|
||||||
Your ntfy.sh topic name (1–64 alphanumeric/hyphen/underscore characters).{" "}
|
|
||||||
<a
|
|
||||||
href="https://ntfy.sh"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="settings-inline-link"
|
|
||||||
>
|
|
||||||
Learn more about ntfy.sh
|
|
||||||
</a>
|
|
||||||
</small>
|
</small>
|
||||||
{form.ntfyTopic && !/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic) && (
|
{form.ntfyTopic && !/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic) && (<small className="field-error">{t("settings.notifications.topicMustBe164AlphanumericHyphenOr", " Topic must be 1\u201364 alphanumeric, hyphen, or underscore characters ")}</small>)}
|
||||||
<small className="field-error">
|
|
||||||
Topic must be 1–64 alphanumeric, hyphen, or underscore characters
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
<details className="ntfy-advanced-disclosure">
|
<details className="ntfy-advanced-disclosure">
|
||||||
<summary>Advanced</summary>
|
<summary>{t("settings.notifications.advanced", "Advanced")}</summary>
|
||||||
<div className="ntfy-advanced-content">
|
<div className="ntfy-advanced-content">
|
||||||
<label htmlFor="ntfyBaseUrl">Custom ntfy server URL (optional)</label>
|
<label htmlFor="ntfyBaseUrl">{t("settings.notifications.customNtfyServerURLOptional", "Custom ntfy server URL (optional)")}</label>
|
||||||
<input
|
<input id="ntfyBaseUrl" type="url" placeholder={t("settings.notifications.httpsNtfySh", "https://ntfy.sh")} value={form.ntfyBaseUrl || ""} onChange={(e) => {
|
||||||
id="ntfyBaseUrl"
|
const value = e.target.value;
|
||||||
type="url"
|
setForm((f) => ({ ...f, ntfyBaseUrl: value || undefined }));
|
||||||
placeholder="https://ntfy.sh"
|
}}/>
|
||||||
value={form.ntfyBaseUrl || ""}
|
<small>{t("settings.notifications.leaveBlankToKeepTheDefaultServerHttps", " Leave blank to keep the default server: https://ntfy.sh. Custom servers must use http:// or https://. ")}</small>
|
||||||
onChange={(e) => {
|
<label htmlFor="ntfyAccessToken">{t("settings.notifications.accessTokenOptional", "Access token (optional)")}</label>
|
||||||
const value = e.target.value;
|
<input id="ntfyAccessToken" type="password" autoComplete="off" placeholder={t("settings.notifications.tk", "tk_...")} value={form.ntfyAccessToken || ""} onChange={(e) => {
|
||||||
setForm((f) => ({ ...f, ntfyBaseUrl: value || undefined }));
|
const value = e.target.value;
|
||||||
}}
|
setForm((f) => ({ ...f, ntfyAccessToken: value || undefined }));
|
||||||
/>
|
}}/>
|
||||||
<small>
|
<small>{t("settings.notifications.leaveBlankToPublishWithoutAuthenticationWhenSet", " Leave blank to publish without authentication. When set, Fusion sends an Authorization Bearer header with ntfy requests. ")}</small>
|
||||||
Leave blank to keep the default server: https://ntfy.sh. Custom servers must use http:// or https://.
|
|
||||||
</small>
|
|
||||||
<label htmlFor="ntfyAccessToken">Access token (optional)</label>
|
|
||||||
<input
|
|
||||||
id="ntfyAccessToken"
|
|
||||||
type="password"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="tk_..."
|
|
||||||
value={form.ntfyAccessToken || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setForm((f) => ({ ...f, ntfyAccessToken: value || undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<small>
|
|
||||||
Leave blank to publish without authentication. When set, Fusion sends an Authorization Bearer header with ntfy requests.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Notify on events</label>
|
<label>{t("settings.notifications.notifyOnEvents", "Notify on events")}</label>
|
||||||
<div className="ntfy-events-list">
|
<div className="ntfy-events-list">
|
||||||
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
||||||
const checked = form.ntfyEvents?.includes(event) ?? true;
|
const checked = form.ntfyEvents?.includes(event) ?? true;
|
||||||
return (
|
return (<div key={`ntfy-${event}`}>
|
||||||
<div key={`ntfy-${event}`}>
|
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
<input
|
<input type="checkbox" checked={checked} onChange={(e) => {
|
||||||
type="checkbox"
|
const current = form.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
checked={checked}
|
const newEvents = e.target.checked
|
||||||
onChange={(e) => {
|
? (current.includes(event) ? current : [...current, event])
|
||||||
const current = form.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS];
|
: current.filter((ev): ev is NtfyNotificationEvent => ev !== event);
|
||||||
const newEvents = e.target.checked
|
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||||
? (current.includes(event) ? current : [...current, event])
|
}}/>
|
||||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== event);
|
|
||||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<small>{description}</small>
|
<small>{description}</small>
|
||||||
</div>
|
</div>);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="ntfyDashboardHost">Dashboard Hostname</label>
|
<label htmlFor="ntfyDashboardHost">{t("settings.notifications.dashboardHostname", "Dashboard Hostname")}</label>
|
||||||
<input
|
<input id="ntfyDashboardHost" type="text" placeholder={t("settings.notifications.httpLocalhost3000", "http://localhost:3000")} value={form.ntfyDashboardHost || ""} onChange={(e) => {
|
||||||
id="ntfyDashboardHost"
|
const val = e.target.value;
|
||||||
type="text"
|
setForm((f) => ({ ...f, ntfyDashboardHost: val || undefined }));
|
||||||
placeholder="http://localhost:3000"
|
}}/>
|
||||||
value={form.ntfyDashboardHost || ""}
|
<small>{t("settings.notifications.baseURLForDeepLinksInNotificationsWhen", " Base URL for deep links in notifications. When set, clicking a notification opens the dashboard directly to the task. ")}</small>
|
||||||
onChange={(e) => {
|
{form.ntfyDashboardHost && !/^https?:\/\/.+/.test(form.ntfyDashboardHost) && (<small className="field-error">{t("settings.notifications.mustBeAValidURLStartingWithHttp", " Must be a valid URL starting with http:// or https:// ")}</small>)}
|
||||||
const val = e.target.value;
|
|
||||||
setForm((f) => ({ ...f, ntfyDashboardHost: val || undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<small>
|
|
||||||
Base URL for deep links in notifications. When set, clicking a notification
|
|
||||||
opens the dashboard directly to the task.
|
|
||||||
</small>
|
|
||||||
{form.ntfyDashboardHost && !/^https?:\/\/.+/.test(form.ntfyDashboardHost) && (
|
|
||||||
<small className="field-error">
|
|
||||||
Must be a valid URL starting with http:// or https://
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="notification-provider-actions">
|
<div className="notification-provider-actions">
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={() => onTestProviderNotification("ntfy")} disabled={testNotificationLoading["ntfy"] ||
|
||||||
type="button"
|
testNotificationLoading["ntfy-message"] ||
|
||||||
className="btn btn-sm"
|
testNotificationLoading["ntfy-room"] ||
|
||||||
onClick={() => onTestProviderNotification("ntfy")}
|
!form.ntfyEnabled ||
|
||||||
disabled={
|
!form.ntfyTopic ||
|
||||||
testNotificationLoading["ntfy"] ||
|
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)}>
|
||||||
testNotificationLoading["ntfy-message"] ||
|
|
||||||
testNotificationLoading["ntfy-room"] ||
|
|
||||||
!form.ntfyEnabled ||
|
|
||||||
!form.ntfyTopic ||
|
|
||||||
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{testNotificationLoading["ntfy"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testNotification", "Test notification")}
|
{testNotificationLoading["ntfy"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testNotification", "Test notification")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={() => onTestProviderNotification("ntfy-message")} disabled={testNotificationLoading["ntfy"] ||
|
||||||
type="button"
|
testNotificationLoading["ntfy-message"] ||
|
||||||
className="btn btn-sm"
|
testNotificationLoading["ntfy-room"] ||
|
||||||
onClick={() => onTestProviderNotification("ntfy-message")}
|
!form.ntfyEnabled ||
|
||||||
disabled={
|
!form.ntfyTopic ||
|
||||||
testNotificationLoading["ntfy"] ||
|
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)}>
|
||||||
testNotificationLoading["ntfy-message"] ||
|
|
||||||
testNotificationLoading["ntfy-room"] ||
|
|
||||||
!form.ntfyEnabled ||
|
|
||||||
!form.ntfyTopic ||
|
|
||||||
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{testNotificationLoading["ntfy-message"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testMessageInbox", "Test message inbox")}
|
{testNotificationLoading["ntfy-message"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testMessageInbox", "Test message inbox")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={() => onTestProviderNotification("ntfy-room")} disabled={testNotificationLoading["ntfy"] ||
|
||||||
type="button"
|
testNotificationLoading["ntfy-message"] ||
|
||||||
className="btn btn-sm"
|
testNotificationLoading["ntfy-room"] ||
|
||||||
onClick={() => onTestProviderNotification("ntfy-room")}
|
!form.ntfyEnabled ||
|
||||||
disabled={
|
!form.ntfyTopic ||
|
||||||
testNotificationLoading["ntfy"] ||
|
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)}>
|
||||||
testNotificationLoading["ntfy-message"] ||
|
|
||||||
testNotificationLoading["ntfy-room"] ||
|
|
||||||
!form.ntfyEnabled ||
|
|
||||||
!form.ntfyTopic ||
|
|
||||||
!/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{testNotificationLoading["ntfy-room"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testRoomReply", "Test room reply")}
|
{testNotificationLoading["ntfy-room"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testRoomReply", "Test room reply")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(testNotificationResult["ntfy"] || testNotificationResult["ntfy-message"] || testNotificationResult["ntfy-room"]) && (
|
{(testNotificationResult["ntfy"] || testNotificationResult["ntfy-message"] || testNotificationResult["ntfy-room"]) && (<div className="notification-test-feedback" aria-live="polite">
|
||||||
<div className="notification-test-feedback" aria-live="polite">
|
{testNotificationResult["ntfy"] && (<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy"].status}`}>{t("settings.notifications.general", " General: ")}{testNotificationResult["ntfy"].message}
|
||||||
{testNotificationResult["ntfy"] && (
|
</small>)}
|
||||||
<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy"].status}`}>
|
{testNotificationResult["ntfy-message"] && (<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy-message"].status}`}>{t("settings.notifications.messageInbox", " Message inbox: ")}{testNotificationResult["ntfy-message"].message}
|
||||||
General: {testNotificationResult["ntfy"].message}
|
</small>)}
|
||||||
</small>
|
{testNotificationResult["ntfy-room"] && (<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy-room"].status}`}>{t("settings.notifications.roomReply", " Room reply: ")}{testNotificationResult["ntfy-room"].message}
|
||||||
)}
|
</small>)}
|
||||||
{testNotificationResult["ntfy-message"] && (
|
</div>)}
|
||||||
<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy-message"].status}`}>
|
</div>)}
|
||||||
Message inbox: {testNotificationResult["ntfy-message"].message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{testNotificationResult["ntfy-room"] && (
|
|
||||||
<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["ntfy-room"].status}`}>
|
|
||||||
Room reply: {testNotificationResult["ntfy-room"].message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="notification-provider-card">
|
<div className="notification-provider-card">
|
||||||
<div className="notification-provider-header">
|
<div className="notification-provider-header">
|
||||||
<strong>Webhook</strong>
|
<strong>{t("settings.notifications.webhook", "Webhook")}</strong>
|
||||||
<label htmlFor="webhookEnabled" className="checkbox-label">
|
<label htmlFor="webhookEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="webhookEnabled" type="checkbox" checked={form.webhookEnabled || false} onChange={(e) => setForm((f) => ({ ...f, webhookEnabled: e.target.checked }))}/>{t("settings.notifications.webhookNotifications", " Webhook notifications ")}</label>
|
||||||
id="webhookEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.webhookEnabled || false}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, webhookEnabled: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
Webhook notifications
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{form.webhookEnabled && (
|
{form.webhookEnabled && (<div className="notification-provider-body">
|
||||||
<div className="notification-provider-body">
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="webhookUrl">Webhook URL</label>
|
<label htmlFor="webhookUrl">{t("settings.notifications.webhookURL", "Webhook URL")}</label>
|
||||||
<input
|
<input id="webhookUrl" type="text" placeholder={t("settings.notifications.httpsHooksExampleCom", "https://hooks.example.com/...")} value={form.webhookUrl || ""} onChange={(e) => {
|
||||||
id="webhookUrl"
|
const val = e.target.value;
|
||||||
type="text"
|
setForm((f) => ({ ...f, webhookUrl: val || undefined }));
|
||||||
placeholder="https://hooks.example.com/..."
|
}}/>
|
||||||
value={form.webhookUrl || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
setForm((f) => ({ ...f, webhookUrl: val || undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="webhookFormat">Format</label>
|
<label htmlFor="webhookFormat">{t("settings.notifications.format", "Format")}</label>
|
||||||
<select
|
<select id="webhookFormat" value={form.webhookFormat || "generic"} onChange={(e) => {
|
||||||
id="webhookFormat"
|
const val = e.target.value as "slack" | "discord" | "generic";
|
||||||
value={form.webhookFormat || "generic"}
|
setForm((f) => ({ ...f, webhookFormat: val }));
|
||||||
onChange={(e) => {
|
}}>
|
||||||
const val = e.target.value as "slack" | "discord" | "generic";
|
<option value="slack">{t("settings.notifications.slack", "Slack")}</option>
|
||||||
setForm((f) => ({ ...f, webhookFormat: val }));
|
<option value="discord">{t("settings.notifications.discord", "Discord")}</option>
|
||||||
}}
|
<option value="generic">{t("settings.notifications.generic", "Generic")}</option>
|
||||||
>
|
|
||||||
<option value="slack">Slack</option>
|
|
||||||
<option value="discord">Discord</option>
|
|
||||||
<option value="generic">Generic</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Notify on events</label>
|
<label>{t("settings.notifications.notifyOnEvents", "Notify on events")}</label>
|
||||||
<div className="ntfy-events-list">
|
<div className="ntfy-events-list">
|
||||||
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
||||||
const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
const checked = currentEvents.includes(event);
|
const checked = currentEvents.includes(event);
|
||||||
return (
|
return (<div key={`webhook-${event}`}>
|
||||||
<div key={`webhook-${event}`}>
|
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
<input
|
<input type="checkbox" checked={checked} onChange={(e) => {
|
||||||
type="checkbox"
|
const current = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
checked={checked}
|
const newEvents = e.target.checked
|
||||||
onChange={(e) => {
|
? (current.includes(event) ? current : [...current, event])
|
||||||
const current = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
: current.filter((ev) => ev !== event);
|
||||||
const newEvents = e.target.checked
|
setForm((f) => ({ ...f, webhookEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||||
? (current.includes(event) ? current : [...current, event])
|
}}/>
|
||||||
: current.filter((ev) => ev !== event);
|
|
||||||
setForm((f) => ({ ...f, webhookEvents: newEvents.length > 0 ? newEvents : undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<small>{description}</small>
|
<small>{description}</small>
|
||||||
</div>
|
</div>);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="notification-provider-actions">
|
<div className="notification-provider-actions">
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={() => onTestProviderNotification("webhook")} disabled={testNotificationLoading["webhook"] || !form.webhookUrl}>
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => onTestProviderNotification("webhook")}
|
|
||||||
disabled={testNotificationLoading["webhook"] || !form.webhookUrl}
|
|
||||||
>
|
|
||||||
{testNotificationLoading["webhook"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testNotification", "Test notification")}
|
{testNotificationLoading["webhook"] ? t("settings.notifications.sending", "Sending…") : t("settings.notifications.testNotification", "Test notification")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{testNotificationResult["webhook"] && (
|
{testNotificationResult["webhook"] && (<div className="notification-test-feedback" aria-live="polite">
|
||||||
<div className="notification-test-feedback" aria-live="polite">
|
|
||||||
<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["webhook"].status}`}>
|
<small className={`notification-test-feedback-item notification-test-feedback-item--${testNotificationResult["webhook"].status}`}>
|
||||||
{testNotificationResult["webhook"].message}
|
{testNotificationResult["webhook"].message}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
</div>)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationsSection;
|
export default NotificationsSection;
|
||||||
|
|||||||
@@ -1,98 +1,39 @@
|
|||||||
/**
|
|
||||||
* Plugins section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped plugin manager with the Fusion-plugins / Pi-extensions subsection
|
|
||||||
* tab pair. The active-subsection state lives in the shell (its initial value is
|
|
||||||
* derived from the modal's entry section) and is relayed as props. The lazy
|
|
||||||
* managers and the plugin slot are co-located here. Markup, ARIA wiring, and the
|
|
||||||
* lazy-load Suspense boundaries are preserved verbatim from the original inline
|
|
||||||
* JSX.
|
|
||||||
*/
|
|
||||||
import { lazy, Suspense, type ReactNode } from "react";
|
import { lazy, Suspense, type ReactNode } from "react";
|
||||||
import { PluginSlot } from "../../PluginSlot";
|
import { PluginSlot } from "../../PluginSlot";
|
||||||
import type { ToastType } from "../../../hooks/useToast";
|
import type { ToastType } from "../../../hooks/useToast";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
const PluginManager = lazy(() => import("../../PluginManager").then((m) => ({ default: m.PluginManager })));
|
const PluginManager = lazy(() => import("../../PluginManager").then((m) => ({ default: m.PluginManager })));
|
||||||
const PiExtensionsManager = lazy(() => import("../../PiExtensionsManager").then((m) => ({ default: m.PiExtensionsManager })));
|
const PiExtensionsManager = lazy(() => import("../../PiExtensionsManager").then((m) => ({ default: m.PiExtensionsManager })));
|
||||||
|
|
||||||
export type PluginsSubsectionId = "fusion-plugins" | "pi-extensions";
|
export type PluginsSubsectionId = "fusion-plugins" | "pi-extensions";
|
||||||
|
|
||||||
export interface PluginsSectionProps {
|
export interface PluginsSectionProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
activePluginsSubsection: PluginsSubsectionId;
|
activePluginsSubsection: PluginsSubsectionId;
|
||||||
setActivePluginsSubsection: (id: PluginsSubsectionId) => void;
|
setActivePluginsSubsection: (id: PluginsSubsectionId) => void;
|
||||||
}
|
}
|
||||||
|
export function PluginsSection({ scopeBanner, projectId, addToast, activePluginsSubsection, setActivePluginsSubsection, }: PluginsSectionProps) {
|
||||||
export function PluginsSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
projectId,
|
|
||||||
addToast,
|
|
||||||
activePluginsSubsection,
|
|
||||||
setActivePluginsSubsection,
|
|
||||||
}: PluginsSectionProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Plugins</h4>
|
<h4 className="settings-section-heading">{t("settings.plugins.plugins", "Plugins")}</h4>
|
||||||
<div className="settings-plugins-subsection-toggle" role="tablist" aria-label="Plugin manager type">
|
<div className="settings-plugins-subsection-toggle" role="tablist" aria-label={t("settings.plugins.pluginManagerType", "Plugin manager type")}>
|
||||||
<button
|
<button type="button" id="plugins-tab-fusion-plugins" role="tab" aria-controls="plugins-panel-fusion-plugins" aria-selected={activePluginsSubsection === "fusion-plugins"} tabIndex={activePluginsSubsection === "fusion-plugins" ? 0 : -1} className={`settings-plugins-subsection-btn${activePluginsSubsection === "fusion-plugins" ? " active" : ""}`} onClick={() => setActivePluginsSubsection("fusion-plugins")}>{t("settings.plugins.fusionPlugins", " Fusion Plugins ")}</button>
|
||||||
type="button"
|
<button type="button" id="plugins-tab-pi-extensions" role="tab" aria-controls="plugins-panel-pi-extensions" aria-selected={activePluginsSubsection === "pi-extensions"} tabIndex={activePluginsSubsection === "pi-extensions" ? 0 : -1} className={`settings-plugins-subsection-btn${activePluginsSubsection === "pi-extensions" ? " active" : ""}`} onClick={() => setActivePluginsSubsection("pi-extensions")}>{t("settings.plugins.piExtensions", " Pi Extensions ")}</button>
|
||||||
id="plugins-tab-fusion-plugins"
|
|
||||||
role="tab"
|
|
||||||
aria-controls="plugins-panel-fusion-plugins"
|
|
||||||
aria-selected={activePluginsSubsection === "fusion-plugins"}
|
|
||||||
tabIndex={activePluginsSubsection === "fusion-plugins" ? 0 : -1}
|
|
||||||
className={`settings-plugins-subsection-btn${activePluginsSubsection === "fusion-plugins" ? " active" : ""}`}
|
|
||||||
onClick={() => setActivePluginsSubsection("fusion-plugins")}
|
|
||||||
>
|
|
||||||
Fusion Plugins
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="plugins-tab-pi-extensions"
|
|
||||||
role="tab"
|
|
||||||
aria-controls="plugins-panel-pi-extensions"
|
|
||||||
aria-selected={activePluginsSubsection === "pi-extensions"}
|
|
||||||
tabIndex={activePluginsSubsection === "pi-extensions" ? 0 : -1}
|
|
||||||
className={`settings-plugins-subsection-btn${activePluginsSubsection === "pi-extensions" ? " active" : ""}`}
|
|
||||||
onClick={() => setActivePluginsSubsection("pi-extensions")}
|
|
||||||
>
|
|
||||||
Pi Extensions
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div id="plugins-panel-fusion-plugins" role="tabpanel" aria-labelledby="plugins-tab-fusion-plugins" className="settings-plugins-subsection-panel" hidden={activePluginsSubsection !== "fusion-plugins"}>
|
||||||
id="plugins-panel-fusion-plugins"
|
{activePluginsSubsection === "fusion-plugins" && (<>
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="plugins-tab-fusion-plugins"
|
|
||||||
className="settings-plugins-subsection-panel"
|
|
||||||
hidden={activePluginsSubsection !== "fusion-plugins"}
|
|
||||||
>
|
|
||||||
{activePluginsSubsection === "fusion-plugins" && (
|
|
||||||
<>
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<PluginManager addToast={addToast} projectId={projectId} />
|
<PluginManager addToast={addToast} projectId={projectId}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<PluginSlot slotId="settings-section" projectId={projectId} />
|
<PluginSlot slotId="settings-section" projectId={projectId}/>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div id="plugins-panel-pi-extensions" role="tabpanel" aria-labelledby="plugins-tab-pi-extensions" className="settings-plugins-subsection-panel" hidden={activePluginsSubsection !== "pi-extensions"}>
|
||||||
id="plugins-panel-pi-extensions"
|
{activePluginsSubsection === "pi-extensions" && (<Suspense fallback={null}>
|
||||||
role="tabpanel"
|
<PiExtensionsManager addToast={addToast} projectId={projectId}/>
|
||||||
aria-labelledby="plugins-tab-pi-extensions"
|
</Suspense>)}
|
||||||
className="settings-plugins-subsection-panel"
|
|
||||||
hidden={activePluginsSubsection !== "pi-extensions"}
|
|
||||||
>
|
|
||||||
{activePluginsSubsection === "pi-extensions" && (
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<PiExtensionsManager addToast={addToast} projectId={projectId} />
|
|
||||||
</Suspense>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PluginsSection;
|
export default PluginsSection;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,424 +1,332 @@
|
|||||||
/**
|
|
||||||
* Remote Access section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Tunnel status, provider selection (Tailscale / Cloudflare), cloudflared
|
|
||||||
* install affordance, start/stop/use-existing flows, and the auth-link / QR
|
|
||||||
* tooling. This section is heavily stateful and side-effecting; rather than
|
|
||||||
* lift all of that into the section, the shell continues to own the remote
|
|
||||||
* state machine (status polling, busy-action guard, install handler, token
|
|
||||||
* previews) and the `runRemoteAction` wrapper. The section receives them via a
|
|
||||||
* single `remote` prop bag plus the modal form. API calls are imported directly
|
|
||||||
* here (pure module functions) so they don't bloat the prop surface. Behavior
|
|
||||||
* and i18n keys are preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Globe, CheckCircle, AlertTriangle } from "lucide-react";
|
import { Globe, CheckCircle, AlertTriangle } from "lucide-react";
|
||||||
import {
|
import { updateRemoteSettings, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteUrl, fetchRemoteQr, type RemoteSettings, type RemoteStatus, } from "../../../api";
|
||||||
updateRemoteSettings,
|
|
||||||
startRemoteTunnel,
|
|
||||||
stopRemoteTunnel,
|
|
||||||
killExternalTunnel,
|
|
||||||
regenerateRemotePersistentToken,
|
|
||||||
generateShortLivedRemoteToken,
|
|
||||||
fetchRemoteUrl,
|
|
||||||
fetchRemoteQr,
|
|
||||||
type RemoteSettings,
|
|
||||||
type RemoteStatus,
|
|
||||||
} from "../../../api";
|
|
||||||
import type { ToastType } from "../../../hooks/useToast";
|
import type { ToastType } from "../../../hooks/useToast";
|
||||||
import type { SectionBaseProps, SettingsFormState } from "./context";
|
import type { SectionBaseProps, SettingsFormState } from "./context";
|
||||||
|
|
||||||
export interface RemoteSectionData {
|
export interface RemoteSectionData {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
remoteStatus: RemoteStatus | null;
|
remoteStatus: RemoteStatus | null;
|
||||||
externalTunnel: { provider: string; url: string | null } | null;
|
externalTunnel: {
|
||||||
tunnelShareLink: { url: string; qrSvg: string | null } | null;
|
provider: string;
|
||||||
remoteBusyAction: string | null;
|
url: string | null;
|
||||||
cloudflaredInstalling: boolean;
|
} | null;
|
||||||
cloudflaredInstallError: string | null;
|
tunnelShareLink: {
|
||||||
cloudflaredManualInstallCommand: () => string;
|
url: string;
|
||||||
cloudflaredMacFallbackCommand: () => string | null;
|
qrSvg: string | null;
|
||||||
handleInstallCloudflared: () => Promise<void>;
|
} | null;
|
||||||
runRemoteAction: (label: string, action: () => Promise<void>) => Promise<void>;
|
remoteBusyAction: string | null;
|
||||||
remoteShortLivedToken: { token: string; expiresAt: string; ttlMs: number } | null;
|
cloudflaredInstalling: boolean;
|
||||||
setRemoteShortLivedToken: (value: { token: string; expiresAt: string; ttlMs: number } | null) => void;
|
cloudflaredInstallError: string | null;
|
||||||
remoteAuthLinkTokenType: "persistent" | "short-lived";
|
cloudflaredManualInstallCommand: () => string;
|
||||||
setRemoteAuthLinkTokenType: (value: "persistent" | "short-lived") => void;
|
cloudflaredMacFallbackCommand: () => string | null;
|
||||||
remoteUrlPreview: { url: string; expiresAt: string | null; tokenType: "persistent" | "short-lived" } | null;
|
handleInstallCloudflared: () => Promise<void>;
|
||||||
setRemoteUrlPreview: (
|
runRemoteAction: (label: string, action: () => Promise<void>) => Promise<void>;
|
||||||
value: { url: string; expiresAt: string | null; tokenType: "persistent" | "short-lived" } | null,
|
remoteShortLivedToken: {
|
||||||
) => void;
|
token: string;
|
||||||
remoteQrSvg: string | null;
|
expiresAt: string;
|
||||||
setRemoteQrSvg: (value: string | null) => void;
|
ttlMs: number;
|
||||||
|
} | null;
|
||||||
|
setRemoteShortLivedToken: (value: {
|
||||||
|
token: string;
|
||||||
|
expiresAt: string;
|
||||||
|
ttlMs: number;
|
||||||
|
} | null) => void;
|
||||||
|
remoteAuthLinkTokenType: "persistent" | "short-lived";
|
||||||
|
setRemoteAuthLinkTokenType: (value: "persistent" | "short-lived") => void;
|
||||||
|
remoteUrlPreview: {
|
||||||
|
url: string;
|
||||||
|
expiresAt: string | null;
|
||||||
|
tokenType: "persistent" | "short-lived";
|
||||||
|
} | null;
|
||||||
|
setRemoteUrlPreview: (value: {
|
||||||
|
url: string;
|
||||||
|
expiresAt: string | null;
|
||||||
|
tokenType: "persistent" | "short-lived";
|
||||||
|
} | null) => void;
|
||||||
|
remoteQrSvg: string | null;
|
||||||
|
setRemoteQrSvg: (value: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteSectionProps extends SectionBaseProps {
|
export interface RemoteSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
remote: RemoteSectionData;
|
remote: RemoteSectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RemoteSection({ scopeBanner, form, setForm, remote }: RemoteSectionProps) {
|
export function RemoteSection({ scopeBanner, form, setForm, remote }: RemoteSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const {
|
const { projectId, addToast, remoteStatus, externalTunnel, tunnelShareLink, remoteBusyAction, cloudflaredInstalling, cloudflaredInstallError, cloudflaredManualInstallCommand, cloudflaredMacFallbackCommand, handleInstallCloudflared, runRemoteAction, remoteShortLivedToken, setRemoteShortLivedToken, remoteAuthLinkTokenType, setRemoteAuthLinkTokenType, remoteUrlPreview, setRemoteUrlPreview, remoteQrSvg, setRemoteQrSvg, } = remote;
|
||||||
projectId,
|
const remoteForm = form as Record<string, unknown>;
|
||||||
addToast,
|
const activeProvider = (remoteForm.remoteActiveProvider as "tailscale" | "cloudflare" | null) ?? null;
|
||||||
remoteStatus,
|
const tunnelState = (remoteStatus?.state as RemoteStatus["state"] | "error" | undefined) ?? "stopped";
|
||||||
externalTunnel,
|
const statusColor = tunnelState === "running"
|
||||||
tunnelShareLink,
|
? "running"
|
||||||
remoteBusyAction,
|
: tunnelState === "starting"
|
||||||
cloudflaredInstalling,
|
? "starting"
|
||||||
cloudflaredInstallError,
|
: tunnelState === "failed" || tunnelState === "error"
|
||||||
cloudflaredManualInstallCommand,
|
? "error"
|
||||||
cloudflaredMacFallbackCommand,
|
: "stopped";
|
||||||
handleInstallCloudflared,
|
const buildSavePayload = (provider: "tailscale" | "cloudflare"): Partial<RemoteSettings> => {
|
||||||
runRemoteAction,
|
const formState = form as Record<string, unknown>;
|
||||||
remoteShortLivedToken,
|
return {
|
||||||
setRemoteShortLivedToken,
|
remoteActiveProvider: provider,
|
||||||
remoteAuthLinkTokenType,
|
remoteTailscaleEnabled: provider === "tailscale",
|
||||||
setRemoteAuthLinkTokenType,
|
remoteTailscaleHostname: String(formState.remoteTailscaleHostname ?? ""),
|
||||||
remoteUrlPreview,
|
remoteTailscaleTargetPort: Number(formState.remoteTailscaleTargetPort ?? 4040),
|
||||||
setRemoteUrlPreview,
|
remoteTailscaleAcceptRoutes: Boolean(formState.remoteTailscaleAcceptRoutes),
|
||||||
remoteQrSvg,
|
remoteCloudflareEnabled: provider === "cloudflare",
|
||||||
setRemoteQrSvg,
|
remoteCloudflareQuickTunnel: Boolean(formState.remoteCloudflareQuickTunnel ?? true),
|
||||||
} = remote;
|
remoteCloudflareTunnelName: String(formState.remoteCloudflareTunnelName ?? ""),
|
||||||
|
remoteCloudflareTunnelToken: (formState.remoteCloudflareTunnelToken as string | null) || null,
|
||||||
const remoteForm = form as Record<string, unknown>;
|
remoteCloudflareIngressUrl: String(formState.remoteCloudflareIngressUrl ?? ""),
|
||||||
const activeProvider = (remoteForm.remoteActiveProvider as "tailscale" | "cloudflare" | null) ?? null;
|
remoteShortLivedEnabled: Boolean(formState.remoteShortLivedEnabled),
|
||||||
const tunnelState = (remoteStatus?.state as RemoteStatus["state"] | "error" | undefined) ?? "stopped";
|
remoteShortLivedTtlMs: Number(formState.remoteShortLivedTtlMs ?? 900000),
|
||||||
const statusColor =
|
remoteRememberLastRunning: Boolean(formState.remoteRememberLastRunning),
|
||||||
tunnelState === "running"
|
};
|
||||||
? "running"
|
|
||||||
: tunnelState === "starting"
|
|
||||||
? "starting"
|
|
||||||
: tunnelState === "failed" || tunnelState === "error"
|
|
||||||
? "error"
|
|
||||||
: "stopped";
|
|
||||||
|
|
||||||
const buildSavePayload = (provider: "tailscale" | "cloudflare"): Partial<RemoteSettings> => {
|
|
||||||
const formState = form as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
remoteActiveProvider: provider,
|
|
||||||
remoteTailscaleEnabled: provider === "tailscale",
|
|
||||||
remoteTailscaleHostname: String(formState.remoteTailscaleHostname ?? ""),
|
|
||||||
remoteTailscaleTargetPort: Number(formState.remoteTailscaleTargetPort ?? 4040),
|
|
||||||
remoteTailscaleAcceptRoutes: Boolean(formState.remoteTailscaleAcceptRoutes),
|
|
||||||
remoteCloudflareEnabled: provider === "cloudflare",
|
|
||||||
remoteCloudflareQuickTunnel: Boolean(formState.remoteCloudflareQuickTunnel ?? true),
|
|
||||||
remoteCloudflareTunnelName: String(formState.remoteCloudflareTunnelName ?? ""),
|
|
||||||
remoteCloudflareTunnelToken: (formState.remoteCloudflareTunnelToken as string | null) || null,
|
|
||||||
remoteCloudflareIngressUrl: String(formState.remoteCloudflareIngressUrl ?? ""),
|
|
||||||
remoteShortLivedEnabled: Boolean(formState.remoteShortLivedEnabled),
|
|
||||||
remoteShortLivedTtlMs: Number(formState.remoteShortLivedTtlMs ?? 900000),
|
|
||||||
remoteRememberLastRunning: Boolean(formState.remoteRememberLastRunning),
|
|
||||||
};
|
};
|
||||||
};
|
return (<>
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Remote Access</h4>
|
<h4 className="settings-section-heading">{t("settings.remote.remoteAccess", "Remote Access")}</h4>
|
||||||
<div className={`remote-status-bar remote-status-bar--${statusColor}`}>
|
<div className={`remote-status-bar remote-status-bar--${statusColor}`}>
|
||||||
<span className={`remote-status-dot remote-status-dot--${statusColor}`} />
|
<span className={`remote-status-dot remote-status-dot--${statusColor}`}/>
|
||||||
<strong>{tunnelState}</strong>
|
<strong>{tunnelState}</strong>
|
||||||
{remoteStatus?.provider && <span> · {remoteStatus.provider}</span>}
|
{remoteStatus?.provider && <span> · {remoteStatus.provider}</span>}
|
||||||
{remoteStatus?.url && <code className="remote-status-url">{remoteStatus.url}</code>}
|
{remoteStatus?.url && <code className="remote-status-url">{remoteStatus.url}</code>}
|
||||||
{remoteStatus?.lastError && <span className="field-error">{remoteStatus.lastError}</span>}
|
{remoteStatus?.lastError && <span className="field-error">{remoteStatus.lastError}</span>}
|
||||||
</div>
|
</div>
|
||||||
{tunnelState === "stopped" && externalTunnel && (
|
{tunnelState === "stopped" && externalTunnel && (<div className="remote-external-tunnel-panel" role="status">
|
||||||
<div className="remote-external-tunnel-panel" role="status">
|
|
||||||
<div className="remote-external-tunnel-header">
|
<div className="remote-external-tunnel-header">
|
||||||
<Globe aria-hidden="true" />
|
<Globe aria-hidden="true"/>
|
||||||
<strong>External {externalTunnel.provider} tunnel detected</strong>
|
<strong>{t("settings.remote.external", "External ")}{externalTunnel.provider}{t("settings.remote.tunnelDetected", " tunnel detected")}</strong>
|
||||||
</div>
|
</div>
|
||||||
{externalTunnel.url && <code className="settings-url-output">{externalTunnel.url}</code>}
|
{externalTunnel.url && <code className="settings-url-output">{externalTunnel.url}</code>}
|
||||||
{tunnelShareLink?.qrSvg && (
|
{tunnelShareLink?.qrSvg && (<div className="remote-external-tunnel-qr">
|
||||||
<div className="remote-external-tunnel-qr">
|
<small>{t("settings.remote.scanToOpen", "Scan to open:")}</small>
|
||||||
<small>Scan to open:</small>
|
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(tunnelShareLink.qrSvg)}`} alt={t("settings.remote.externalTunnelQRCode", "External tunnel QR code")} className="settings-qr-preview-image"/>
|
||||||
<img
|
</div>)}
|
||||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(tunnelShareLink.qrSvg)}`}
|
</div>)}
|
||||||
alt="External tunnel QR code"
|
|
||||||
className="settings-qr-preview-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (() => {
|
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (() => {
|
||||||
let accessCode: string | null = null;
|
let accessCode: string | null = null;
|
||||||
let tailnetUrl: string | null = remoteStatus?.url ?? null;
|
let tailnetUrl: string | null = remoteStatus?.url ?? null;
|
||||||
if (tunnelShareLink?.url) {
|
if (tunnelShareLink?.url) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(tunnelShareLink.url);
|
const parsed = new URL(tunnelShareLink.url);
|
||||||
accessCode = parsed.searchParams.get("rt");
|
accessCode = parsed.searchParams.get("rt");
|
||||||
if (!tailnetUrl) tailnetUrl = `${parsed.origin}/`;
|
if (!tailnetUrl)
|
||||||
} catch {
|
tailnetUrl = `${parsed.origin}/`;
|
||||||
// fall through
|
}
|
||||||
}
|
catch {
|
||||||
}
|
// fall through
|
||||||
return (
|
}
|
||||||
<div className="remote-share-block">
|
}
|
||||||
{tailnetUrl && (
|
return (<div className="remote-share-block">
|
||||||
<div className="remote-share-row">
|
{tailnetUrl && (<div className="remote-share-row">
|
||||||
<small>Tailnet URL:</small>
|
<small>{t("settings.remote.tailnetURL", "Tailnet URL:")}</small>
|
||||||
<code className="settings-url-output">{tailnetUrl}</code>
|
<code className="settings-url-output">{tailnetUrl}</code>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
{accessCode && (<div className="remote-share-row">
|
||||||
{accessCode && (
|
<small>{t("settings.remote.remoteAccessCode", "Remote access code:")}</small>
|
||||||
<div className="remote-share-row">
|
|
||||||
<small>Remote access code:</small>
|
|
||||||
<code className="settings-url-output">{accessCode}</code>
|
<code className="settings-url-output">{accessCode}</code>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
{tunnelShareLink?.qrSvg && (<div className="remote-share-row">
|
||||||
{tunnelShareLink?.qrSvg && (
|
<small>{t("settings.remote.scanToConnect", "Scan to connect:")}</small>
|
||||||
<div className="remote-share-row">
|
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(tunnelShareLink.qrSvg)}`} alt={t("settings.remote.remoteAccessQRCode", "Remote access QR code")} className="settings-qr-preview-image"/>
|
||||||
<small>Scan to connect:</small>
|
</div>)}
|
||||||
<img
|
</div>);
|
||||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(tunnelShareLink.qrSvg)}`}
|
})()}
|
||||||
alt="Remote access QR code"
|
|
||||||
className="settings-qr-preview-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="remote-provider-selector" role="radiogroup" aria-label="Remote provider">
|
<div className="remote-provider-selector" role="radiogroup" aria-label={t("settings.remote.remoteProvider", "Remote provider")}>
|
||||||
<label className="remote-provider-option">
|
<label className="remote-provider-option">
|
||||||
<input type="radio" name="remoteProvider" value="tailscale" checked={activeProvider === "tailscale"} onChange={() => setForm((f) => ({ ...f, remoteActiveProvider: "tailscale" } as SettingsFormState))} />
|
<input type="radio" name="remoteProvider" value="tailscale" checked={activeProvider === "tailscale"} onChange={() => setForm((f) => ({ ...f, remoteActiveProvider: "tailscale" } as SettingsFormState))}/>
|
||||||
<span>
|
<span>
|
||||||
<span className="remote-provider-option-content">
|
<span className="remote-provider-option-content">
|
||||||
<span data-testid="remote-provider-icon-tailscale" aria-hidden="true"><Globe size={16} /></span>
|
<span data-testid="remote-provider-icon-tailscale" aria-hidden="true"><Globe size={16}/></span>
|
||||||
<span>Tailscale</span>
|
<span>{t("settings.remote.tailscale", "Tailscale")}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="remote-provider-option">
|
<label className="remote-provider-option">
|
||||||
<input type="radio" name="remoteProvider" value="cloudflare" checked={activeProvider === "cloudflare"} onChange={() => setForm((f) => ({ ...f, remoteActiveProvider: "cloudflare" } as SettingsFormState))} />
|
<input type="radio" name="remoteProvider" value="cloudflare" checked={activeProvider === "cloudflare"} onChange={() => setForm((f) => ({ ...f, remoteActiveProvider: "cloudflare" } as SettingsFormState))}/>
|
||||||
<span>
|
<span>
|
||||||
<span className="remote-provider-option-content">
|
<span className="remote-provider-option-content">
|
||||||
<span data-testid="remote-provider-icon-cloudflare" aria-hidden="true" className="remote-provider-option-icon">
|
<span data-testid="remote-provider-icon-cloudflare" aria-hidden="true" className="remote-provider-option-icon">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-testid="remote-cloudflare-option-icon">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-testid="remote-cloudflare-option-icon">
|
||||||
<path d="M7 16.5h10.8a2.9 2.9 0 0 0 .3-5.8 4.9 4.9 0 0 0-9.3-1.6A3.6 3.6 0 0 0 7 16.5m-1.9 0h3.2a2.5 2.5 0 0 0 .2-5 3.4 3.4 0 0 0-3.4 3.4c0 .6 0 1 .2 1.6" fill="var(--provider-cloudflare)" />
|
<path d="M7 16.5h10.8a2.9 2.9 0 0 0 .3-5.8 4.9 4.9 0 0 0-9.3-1.6A3.6 3.6 0 0 0 7 16.5m-1.9 0h3.2a2.5 2.5 0 0 0 .2-5 3.4 3.4 0 0 0-3.4 3.4c0 .6 0 1 .2 1.6" fill="var(--provider-cloudflare)"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span>Cloudflare</span>
|
<span>{t("settings.remote.cloudflare", "Cloudflare")}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{!activeProvider && <small>Select a provider above to configure remote access.</small>}
|
{!activeProvider && <small>{t("settings.remote.selectAProviderAboveToConfigureRemoteAccess", "Select a provider above to configure remote access.")}</small>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === true && (
|
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === true && (<div className="remote-cli-detection remote-cli-detection--available" role="status">
|
||||||
<div className="remote-cli-detection remote-cli-detection--available" role="status">
|
<CheckCircle aria-hidden="true"/>
|
||||||
<CheckCircle aria-hidden="true" />
|
<span>{t("settings.remote.cloudflaredIsInstalled", "cloudflared is installed")}</span>
|
||||||
<span>cloudflared is installed</span>
|
</div>)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === false && (
|
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === false && (<div className="remote-cli-detection remote-cli-detection--missing" role="status">
|
||||||
<div className="remote-cli-detection remote-cli-detection--missing" role="status">
|
<AlertTriangle aria-hidden="true"/>
|
||||||
<AlertTriangle aria-hidden="true" />
|
|
||||||
<div className="remote-cli-detection-content">
|
<div className="remote-cli-detection-content">
|
||||||
<span>cloudflared is not installed</span>
|
<span>{t("settings.remote.cloudflaredIsNotInstalled", "cloudflared is not installed")}</span>
|
||||||
<button
|
<button type="button" className="btn btn-sm" disabled={cloudflaredInstalling || remoteBusyAction !== null} onClick={() => void handleInstallCloudflared()}>
|
||||||
type="button"
|
{cloudflaredInstalling ? t("settings.remote.installing", "Installing…") : t("settings.remote.installCloudflared", "Install cloudflared")}
|
||||||
className="btn btn-sm"
|
|
||||||
disabled={cloudflaredInstalling || remoteBusyAction !== null}
|
|
||||||
onClick={() => void handleInstallCloudflared()}
|
|
||||||
>
|
|
||||||
{cloudflaredInstalling ? "Installing…" : "Install cloudflared"}
|
|
||||||
</button>
|
</button>
|
||||||
{cloudflaredInstallError && <small className="remote-cli-install-error">{cloudflaredInstallError}</small>}
|
{cloudflaredInstallError && <small className="remote-cli-install-error">{cloudflaredInstallError}</small>}
|
||||||
<small className="remote-cli-manual">Manual install: <code>{cloudflaredManualInstallCommand()}</code></small>
|
<small className="remote-cli-manual">{t("settings.remote.manualInstall", "Manual install: ")}<code>{cloudflaredManualInstallCommand()}</code></small>
|
||||||
{cloudflaredMacFallbackCommand()
|
{cloudflaredMacFallbackCommand()
|
||||||
? <small className="remote-cli-manual">If Homebrew is unavailable: <code>{cloudflaredMacFallbackCommand()}</code></small>
|
? <small className="remote-cli-manual">{t("settings.remote.ifHomebrewIsUnavailable", "If Homebrew is unavailable: ")}<code>{cloudflaredMacFallbackCommand()}</code></small>
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
|
|
||||||
{activeProvider && (
|
{activeProvider && (<div className="form-group remote-provider-settings">
|
||||||
<div className="form-group remote-provider-settings">
|
{activeProvider === "tailscale" ? (<>
|
||||||
{activeProvider === "tailscale" ? (
|
<small>{t("settings.remote.tailscaleFunnelWillExposeThisDashboardOnYour", "Tailscale Funnel will expose this dashboard on your tailnet's public ")}{`https://<machine>.<tailnet>.ts.net/`}{t("settings.remote.uRLNoHostnameOrPortConfigurationNeeded", " URL \u2014 no hostname or port configuration needed.")}</small>
|
||||||
<>
|
|
||||||
<small>Tailscale Funnel will expose this dashboard on your tailnet's public {`https://<machine>.<tailnet>.ts.net/`} URL — no hostname or port configuration needed.</small>
|
|
||||||
<label htmlFor="remoteTailscaleAcceptRoutes" className="checkbox-label">
|
<label htmlFor="remoteTailscaleAcceptRoutes" className="checkbox-label">
|
||||||
<input id="remoteTailscaleAcceptRoutes" type="checkbox" checked={Boolean(remoteForm.remoteTailscaleAcceptRoutes)} onChange={(e) => setForm((f) => ({ ...f, remoteTailscaleAcceptRoutes: e.target.checked } as SettingsFormState))} />
|
<input id="remoteTailscaleAcceptRoutes" type="checkbox" checked={Boolean(remoteForm.remoteTailscaleAcceptRoutes)} onChange={(e) => setForm((f) => ({ ...f, remoteTailscaleAcceptRoutes: e.target.checked } as SettingsFormState))}/>{t("settings.remote.acceptRoutes", " Accept routes ")}</label>
|
||||||
Accept routes
|
</>) : (<>
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<small>
|
<small>
|
||||||
{(remoteForm.remoteCloudflareQuickTunnel ?? true)
|
{(remoteForm.remoteCloudflareQuickTunnel ?? true)
|
||||||
? "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed."
|
? t("settings.remote.usingQuickTunnel", "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed.")
|
||||||
: "Named Tunnel mode enabled — configure tunnel name, token, and ingress URL below."}
|
: t("settings.remote.namedTunnelModeEnabled", "Named Tunnel mode enabled — configure tunnel name, token, and ingress URL below.")}
|
||||||
</small>
|
</small>
|
||||||
<details
|
<details className="remote-cf-advanced-details" open={!(remoteForm.remoteCloudflareQuickTunnel ?? true)} onToggle={(event) => {
|
||||||
className="remote-cf-advanced-details"
|
const detailsOpen = event.currentTarget.open;
|
||||||
open={!(remoteForm.remoteCloudflareQuickTunnel ?? true)}
|
setForm((f) => {
|
||||||
onToggle={(event) => {
|
const currentQuickTunnel = Boolean((f as Record<string, unknown>).remoteCloudflareQuickTunnel ?? true);
|
||||||
const detailsOpen = event.currentTarget.open;
|
const nextQuickTunnel = !detailsOpen;
|
||||||
setForm((f) => {
|
if (currentQuickTunnel === nextQuickTunnel) {
|
||||||
const currentQuickTunnel = Boolean((f as Record<string, unknown>).remoteCloudflareQuickTunnel ?? true);
|
return f;
|
||||||
const nextQuickTunnel = !detailsOpen;
|
}
|
||||||
if (currentQuickTunnel === nextQuickTunnel) {
|
return { ...f, remoteCloudflareQuickTunnel: nextQuickTunnel } as SettingsFormState;
|
||||||
return f;
|
});
|
||||||
}
|
}}>
|
||||||
return { ...f, remoteCloudflareQuickTunnel: nextQuickTunnel } as SettingsFormState;
|
<summary>{t("settings.remote.advancedNamedTunnel", "Advanced (Named Tunnel)")}</summary>
|
||||||
});
|
{!(remoteForm.remoteCloudflareQuickTunnel ?? true) ? (<div className="remote-cf-advanced-fields">
|
||||||
}}
|
<label htmlFor="remoteCloudflareTunnelName">{t("settings.remote.tunnelName", "Tunnel name")}</label>
|
||||||
>
|
<input id="remoteCloudflareTunnelName" type="text" placeholder={t("settings.remote.tunnelName", "Tunnel name")} value={String(remoteForm.remoteCloudflareTunnelName ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareTunnelName: e.target.value } as SettingsFormState))}/>
|
||||||
<summary>Advanced (Named Tunnel)</summary>
|
<label htmlFor="remoteCloudflareTunnelToken">{t("settings.remote.tunnelToken", "Tunnel token")}</label>
|
||||||
{!(remoteForm.remoteCloudflareQuickTunnel ?? true) ? (
|
<input id="remoteCloudflareTunnelToken" type="password" placeholder={t("settings.remote.tunnelToken", "Tunnel token")} value={String(remoteForm.remoteCloudflareTunnelToken ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareTunnelToken: e.target.value } as SettingsFormState))}/>
|
||||||
<div className="remote-cf-advanced-fields">
|
<label htmlFor="remoteCloudflareIngressUrl">{t("settings.remote.ingressURL", "Ingress URL")}</label>
|
||||||
<label htmlFor="remoteCloudflareTunnelName">Tunnel name</label>
|
<input id="remoteCloudflareIngressUrl" type="text" placeholder={t("settings.remote.httpsYourDomainExample", "https://your-domain.example")} value={String(remoteForm.remoteCloudflareIngressUrl ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareIngressUrl: e.target.value } as SettingsFormState))}/>
|
||||||
<input id="remoteCloudflareTunnelName" type="text" placeholder="Tunnel name" value={String(remoteForm.remoteCloudflareTunnelName ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareTunnelName: e.target.value } as SettingsFormState))} />
|
</div>) : null}
|
||||||
<label htmlFor="remoteCloudflareTunnelToken">Tunnel token</label>
|
|
||||||
<input id="remoteCloudflareTunnelToken" type="password" placeholder="Tunnel token" value={String(remoteForm.remoteCloudflareTunnelToken ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareTunnelToken: e.target.value } as SettingsFormState))} />
|
|
||||||
<label htmlFor="remoteCloudflareIngressUrl">Ingress URL</label>
|
|
||||||
<input id="remoteCloudflareIngressUrl" type="text" placeholder="https://your-domain.example" value={String(remoteForm.remoteCloudflareIngressUrl ?? "")} onChange={(e) => setForm((f) => ({ ...f, remoteCloudflareIngressUrl: e.target.value } as SettingsFormState))} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</details>
|
</details>
|
||||||
</>
|
</>)}
|
||||||
)}
|
</div>)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group remote-tunnel-actions">
|
<div className="form-group remote-tunnel-actions">
|
||||||
{tunnelState === "running" || tunnelState === "starting" ? (
|
{tunnelState === "running" || tunnelState === "starting" ? (<button type="button" className="btn btn-danger" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("stop", async () => {
|
||||||
<button type="button" className="btn btn-danger" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("stop", async () => {
|
await stopRemoteTunnel(projectId);
|
||||||
await stopRemoteTunnel(projectId);
|
addToast(t("settings.remote.tunnelStopped", "Remote tunnel stopped"), "success");
|
||||||
addToast(t("settings.remote.tunnelStopped", "Remote tunnel stopped"), "success");
|
})}>
|
||||||
})}>
|
|
||||||
{remoteBusyAction === "stop" ? t("settings.remote.stopping", "Stopping…") : t("settings.remote.stopTunnel", "Stop Tunnel")}
|
{remoteBusyAction === "stop" ? t("settings.remote.stopping", "Stopping…") : t("settings.remote.stopTunnel", "Stop Tunnel")}
|
||||||
</button>
|
</button>) : (<>
|
||||||
) : (
|
{externalTunnel ? (<div className="remote-external-tunnel-actions">
|
||||||
<>
|
|
||||||
{externalTunnel ? (
|
|
||||||
<div className="remote-external-tunnel-actions">
|
|
||||||
<button type="button" className="btn" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("start fresh", async () => {
|
<button type="button" className="btn" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("start fresh", async () => {
|
||||||
if (!activeProvider) return;
|
if (!activeProvider)
|
||||||
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
return;
|
||||||
await killExternalTunnel(projectId);
|
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
||||||
await startRemoteTunnel(projectId);
|
await killExternalTunnel(projectId);
|
||||||
addToast(t("settings.remote.tunnelRestarted", "Remote tunnel restarted"), "success");
|
await startRemoteTunnel(projectId);
|
||||||
|
addToast(t("settings.remote.tunnelRestarted", "Remote tunnel restarted"), "success");
|
||||||
})}>
|
})}>
|
||||||
{remoteBusyAction === "start fresh" ? t("settings.remote.restarting", "Restarting…") : t("settings.remote.startFresh", "Start Fresh")}
|
{remoteBusyAction === "start fresh" ? t("settings.remote.restarting", "Restarting…") : t("settings.remote.startFresh", "Start Fresh")}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-primary" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("use existing", async () => {
|
<button type="button" className="btn btn-primary" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("use existing", async () => {
|
||||||
if (!activeProvider) return;
|
if (!activeProvider)
|
||||||
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
return;
|
||||||
await startRemoteTunnel(projectId);
|
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
||||||
addToast(t("settings.remote.tunnelStarted", "Remote tunnel started"), "success");
|
await startRemoteTunnel(projectId);
|
||||||
|
addToast(t("settings.remote.tunnelStarted", "Remote tunnel started"), "success");
|
||||||
})}>
|
})}>
|
||||||
{remoteBusyAction === "use existing" ? t("settings.remote.starting", "Starting…") : t("settings.remote.useExisting", "Use Existing")}
|
{remoteBusyAction === "use existing" ? t("settings.remote.starting", "Starting…") : t("settings.remote.useExisting", "Use Existing")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>) : (<button type="button" className="btn btn-primary" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("start", async () => {
|
||||||
) : (
|
if (!activeProvider)
|
||||||
<button type="button" className="btn btn-primary" disabled={!activeProvider || remoteBusyAction !== null} onClick={() => void runRemoteAction("start", async () => {
|
return;
|
||||||
if (!activeProvider) return;
|
// Server overrides remoteTailscaleTargetPort with
|
||||||
// Server overrides remoteTailscaleTargetPort with
|
// req.socket.localPort when starting the tunnel; the value sent
|
||||||
// req.socket.localPort when starting the tunnel; the value sent
|
// here is only a fallback if that override doesn't fire.
|
||||||
// here is only a fallback if that override doesn't fire.
|
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
||||||
await updateRemoteSettings(buildSavePayload(activeProvider), projectId);
|
await startRemoteTunnel(projectId);
|
||||||
await startRemoteTunnel(projectId);
|
addToast(t("settings.remote.tunnelStarted", "Remote tunnel started"), "success");
|
||||||
addToast(t("settings.remote.tunnelStarted", "Remote tunnel started"), "success");
|
})}>
|
||||||
})}>
|
|
||||||
{remoteBusyAction === "start" ? t("settings.remote.starting", "Starting…") : t("settings.remote.startTunnel", "Start Tunnel")}
|
{remoteBusyAction === "start" ? t("settings.remote.starting", "Starting…") : t("settings.remote.startTunnel", "Start Tunnel")}
|
||||||
</button>
|
</button>)}
|
||||||
)}
|
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === false ? (<small className="field-error">{t("settings.remote.cloudflaredMustBeInstalledToStartTheTunnel", "cloudflared must be installed to start the tunnel")}</small>) : null}
|
||||||
{activeProvider === "cloudflare" && remoteStatus?.cloudflaredAvailable === false ? (
|
</>)}
|
||||||
<small className="field-error">cloudflared must be installed to start the tunnel</small>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details className="remote-advanced-details">
|
<details className="remote-advanced-details">
|
||||||
<summary>Advanced Settings</summary>
|
<summary>{t("settings.remote.advancedSettings", "Advanced Settings")}</summary>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="remoteShortLivedEnabled" className="checkbox-label">
|
<label htmlFor="remoteShortLivedEnabled" className="checkbox-label">
|
||||||
<input id="remoteShortLivedEnabled" type="checkbox" checked={Boolean(remoteForm.remoteShortLivedEnabled)} onChange={(e) => setForm((f) => ({ ...f, remoteShortLivedEnabled: e.target.checked } as SettingsFormState))} />
|
<input id="remoteShortLivedEnabled" type="checkbox" checked={Boolean(remoteForm.remoteShortLivedEnabled)} onChange={(e) => setForm((f) => ({ ...f, remoteShortLivedEnabled: e.target.checked } as SettingsFormState))}/>{t("settings.remote.enableShortLivedTokens", " Enable short-lived tokens ")}</label>
|
||||||
Enable short-lived tokens
|
<label htmlFor="remoteShortLivedTtlMs">{t("settings.remote.shortLivedTTLMs", "Short-lived TTL (ms)")}</label>
|
||||||
</label>
|
<input id="remoteShortLivedTtlMs" type="number" min={60000} max={86400000} value={Number(remoteForm.remoteShortLivedTtlMs ?? 900000)} onChange={(e) => setForm((f) => ({ ...f, remoteShortLivedTtlMs: Number(e.target.value || 900000) } as SettingsFormState))}/>
|
||||||
<label htmlFor="remoteShortLivedTtlMs">Short-lived TTL (ms)</label>
|
{remoteShortLivedToken && <small>{t("settings.remote.lastShortLivedTokenExpiresAt", "Last short-lived token expires at ")}{new Date(remoteShortLivedToken.expiresAt).toLocaleString()} ({remoteShortLivedToken.ttlMs}{t("settings.remote.ms", "ms)")}</small>}
|
||||||
<input id="remoteShortLivedTtlMs" type="number" min={60000} max={86400000} value={Number(remoteForm.remoteShortLivedTtlMs ?? 900000)} onChange={(e) => setForm((f) => ({ ...f, remoteShortLivedTtlMs: Number(e.target.value || 900000) } as SettingsFormState))} />
|
|
||||||
{remoteShortLivedToken && <small>Last short-lived token expires at {new Date(remoteShortLivedToken.expiresAt).toLocaleString()} ({remoteShortLivedToken.ttlMs}ms)</small>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="remoteRememberLastRunning" className="checkbox-label">
|
<label htmlFor="remoteRememberLastRunning" className="checkbox-label">
|
||||||
<input id="remoteRememberLastRunning" type="checkbox" checked={Boolean(remoteForm.remoteRememberLastRunning)} onChange={(e) => setForm((f) => ({ ...f, remoteRememberLastRunning: e.target.checked } as SettingsFormState))} />
|
<input id="remoteRememberLastRunning" type="checkbox" checked={Boolean(remoteForm.remoteRememberLastRunning)} onChange={(e) => setForm((f) => ({ ...f, remoteRememberLastRunning: e.target.checked } as SettingsFormState))}/>{t("settings.remote.rememberLastRunningState", " Remember last running state ")}</label>
|
||||||
Remember last running state
|
<small>{t("settings.remote.automaticallyRestoreTunnelOnStartupIfItWas", "Automatically restore tunnel on startup if it was running when last stopped.")}</small>
|
||||||
</label>
|
|
||||||
<small>Automatically restore tunnel on startup if it was running when last stopped.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Auth Links</label>
|
<label>{t("settings.remote.authLinks", "Auth Links")}</label>
|
||||||
<div className="settings-button-row">
|
<div className="settings-button-row">
|
||||||
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("regenerate persistent token", async () => {
|
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("regenerate persistent token", async () => {
|
||||||
await regenerateRemotePersistentToken(projectId);
|
await regenerateRemotePersistentToken(projectId);
|
||||||
addToast(t("settings.remote.persistentTokenRegenerated", "Persistent token regenerated"), "success");
|
addToast(t("settings.remote.persistentTokenRegenerated", "Persistent token regenerated"), "success");
|
||||||
})}>Regenerate persistent token</button>
|
})}>{t("settings.remote.regeneratePersistentToken", "Regenerate persistent token")}</button>
|
||||||
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("generate short-lived token", async () => {
|
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("generate short-lived token", async () => {
|
||||||
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
||||||
const generated = await generateShortLivedRemoteToken(ttlMs, projectId);
|
const generated = await generateShortLivedRemoteToken(ttlMs, projectId);
|
||||||
setRemoteShortLivedToken(generated);
|
setRemoteShortLivedToken(generated);
|
||||||
addToast(t("settings.remote.shortLivedTokenGenerated", "Short-lived token generated"), "success");
|
addToast(t("settings.remote.shortLivedTokenGenerated", "Short-lived token generated"), "success");
|
||||||
})}>Generate short-lived token</button>
|
})}>{t("settings.remote.generateShortLivedToken", "Generate short-lived token")}</button>
|
||||||
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("fetch remote url", async () => {
|
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("fetch remote url", async () => {
|
||||||
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
||||||
const nextUrl = await fetchRemoteUrl({ projectId, tokenType: remoteAuthLinkTokenType, ttlMs: remoteAuthLinkTokenType === "short-lived" ? ttlMs : undefined });
|
const nextUrl = await fetchRemoteUrl({ projectId, tokenType: remoteAuthLinkTokenType, ttlMs: remoteAuthLinkTokenType === "short-lived" ? ttlMs : undefined });
|
||||||
setRemoteUrlPreview(nextUrl);
|
setRemoteUrlPreview(nextUrl);
|
||||||
setRemoteQrSvg(null);
|
setRemoteQrSvg(null);
|
||||||
})}>Show URL</button>
|
})}>{t("settings.remote.showURL", "Show URL")}</button>
|
||||||
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("generate QR", async () => {
|
<button type="button" className="btn btn-sm" disabled={remoteBusyAction !== null} onClick={() => void runRemoteAction("generate QR", async () => {
|
||||||
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
const ttlMs = Number(remoteForm.remoteShortLivedTtlMs ?? 900000);
|
||||||
const qr = await fetchRemoteQr("image/svg", { projectId, tokenType: remoteAuthLinkTokenType, ttlMs: remoteAuthLinkTokenType === "short-lived" ? ttlMs : undefined });
|
const qr = await fetchRemoteQr("image/svg", { projectId, tokenType: remoteAuthLinkTokenType, ttlMs: remoteAuthLinkTokenType === "short-lived" ? ttlMs : undefined });
|
||||||
setRemoteUrlPreview({ url: qr.url, expiresAt: qr.expiresAt, tokenType: qr.tokenType });
|
setRemoteUrlPreview({ url: qr.url, expiresAt: qr.expiresAt, tokenType: qr.tokenType });
|
||||||
setRemoteQrSvg(qr.data ?? null);
|
setRemoteQrSvg(qr.data ?? null);
|
||||||
})}>Generate QR</button>
|
})}>{t("settings.remote.generateQR", "Generate QR")}</button>
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="remoteAuthLinkTokenType">Auth link token type</label>
|
<label htmlFor="remoteAuthLinkTokenType">{t("settings.remote.authLinkTokenType", "Auth link token type")}</label>
|
||||||
<select id="remoteAuthLinkTokenType" value={remoteAuthLinkTokenType} onChange={(e) => setRemoteAuthLinkTokenType(e.target.value as "persistent" | "short-lived")}>
|
<select id="remoteAuthLinkTokenType" value={remoteAuthLinkTokenType} onChange={(e) => setRemoteAuthLinkTokenType(e.target.value as "persistent" | "short-lived")}>
|
||||||
<option value="persistent">Persistent token</option>
|
<option value="persistent">{t("settings.remote.persistentToken", "Persistent token")}</option>
|
||||||
<option value="short-lived">Short-lived token</option>
|
<option value="short-lived">{t("settings.remote.shortLivedToken", "Short-lived token")}</option>
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.remote.uRLAndQRGenerationUseTheSelectedToken", " URL and QR generation use the selected token type. ")}{remoteAuthLinkTokenType === "short-lived" ? ` TTL: ${Number(remoteForm.remoteShortLivedTtlMs ?? 900000)}ms.` : ""}
|
||||||
URL and QR generation use the selected token type.
|
|
||||||
{remoteAuthLinkTokenType === "short-lived" ? ` TTL: ${Number(remoteForm.remoteShortLivedTtlMs ?? 900000)}ms.` : ""}
|
|
||||||
</small>
|
</small>
|
||||||
{remoteUrlPreview?.url && (
|
{remoteUrlPreview?.url && (<>
|
||||||
<>
|
<small>{t("settings.remote.authenticatedURL", "Authenticated URL:")}<code className="settings-url-output">{remoteUrlPreview.url}</code></small>
|
||||||
<small>Authenticated URL:<code className="settings-url-output">{remoteUrlPreview.url}</code></small>
|
<small>{t("settings.remote.tokenType", " Token type: ")}<strong>{remoteUrlPreview.tokenType}</strong>
|
||||||
<small>
|
{remoteUrlPreview.expiresAt
|
||||||
Token type: <strong>{remoteUrlPreview.tokenType}</strong>
|
? t("settings.remote.expiresAt", " · Expires at {{expiresAt}}", { expiresAt: new Date(remoteUrlPreview.expiresAt).toLocaleString() })
|
||||||
{remoteUrlPreview.expiresAt ? ` · Expires at ${new Date(remoteUrlPreview.expiresAt).toLocaleString()}` : " · No expiry"}
|
: t("settings.remote.noExpiry", " · No expiry")}
|
||||||
</small>
|
</small>
|
||||||
</>
|
</>)}
|
||||||
)}
|
{remoteQrSvg && (<div className="settings-qr-preview" aria-live="polite">
|
||||||
{remoteQrSvg && (
|
<p className="settings-qr-preview-label">{t("settings.remote.scanThisQRCodeOnYourPhone", "Scan this QR code on your phone")}</p>
|
||||||
<div className="settings-qr-preview" aria-live="polite">
|
|
||||||
<p className="settings-qr-preview-label">Scan this QR code on your phone</p>
|
|
||||||
<div className="settings-qr-preview-image-wrap">
|
<div className="settings-qr-preview-image-wrap">
|
||||||
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(remoteQrSvg)}`} alt="Remote access QR code" className="settings-qr-preview-image" />
|
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(remoteQrSvg)}`} alt={t("settings.remote.remoteAccessQRCode", "Remote access QR code")} className="settings-qr-preview-image"/>
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary>QR SVG markup</summary>
|
<summary>{t("settings.remote.qRSVGMarkup", "QR SVG markup")}</summary>
|
||||||
<pre className="settings-raw-output">{remoteQrSvg}</pre>
|
<pre className="settings-raw-output">{remoteQrSvg}</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RemoteSection;
|
export default RemoteSection;
|
||||||
|
|||||||
@@ -1,131 +1,71 @@
|
|||||||
/**
|
|
||||||
* Research Defaults (global) section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Global research web-search provider selection (built-in vs external), the
|
|
||||||
* external-provider advanced disclosure, default run limits, and enabled-source
|
|
||||||
* toggles. Credential-presence checks read the shell's fetched `authProviders`;
|
|
||||||
* the "open Authentication" affordances navigate via the shell's
|
|
||||||
* `onNavigateToSection` so cross-section deep-links keep working.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { Settings } from "@fusion/core";
|
import type { Settings } from "@fusion/core";
|
||||||
import type { AuthProvider } from "../../../api";
|
import type { AuthProvider } from "../../../api";
|
||||||
import type { SectionId } from "../../SettingsModal";
|
import type { SectionId } from "../../SettingsModal";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface ResearchGlobalSectionProps extends SectionBaseProps {
|
export interface ResearchGlobalSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
authProviders: AuthProvider[];
|
authProviders: AuthProvider[];
|
||||||
onNavigateToSection: (section: SectionId) => void;
|
onNavigateToSection: (section: SectionId) => void;
|
||||||
}
|
}
|
||||||
|
export function ResearchGlobalSection({ scopeBanner, form, setForm, authProviders, onNavigateToSection, }: ResearchGlobalSectionProps) {
|
||||||
export function ResearchGlobalSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
const resolvedProvider = form.researchGlobalWebSearchProvider ??
|
||||||
form,
|
form.researchGlobalDefaults?.searchProvider ??
|
||||||
setForm,
|
"builtin";
|
||||||
authProviders,
|
const externalProvider = resolvedProvider === "searxng" ||
|
||||||
onNavigateToSection,
|
resolvedProvider === "brave" ||
|
||||||
}: ResearchGlobalSectionProps) {
|
resolvedProvider === "google" ||
|
||||||
const resolvedProvider =
|
resolvedProvider === "tavily";
|
||||||
form.researchGlobalWebSearchProvider ??
|
const selectedCredentialProvider = resolvedProvider === "brave" || resolvedProvider === "tavily" ? resolvedProvider : null;
|
||||||
form.researchGlobalDefaults?.searchProvider ??
|
const hasMissingResearchCredential = selectedCredentialProvider
|
||||||
"builtin";
|
? authProviders.some((provider) => provider.id === selectedCredentialProvider && !provider.authenticated)
|
||||||
const externalProvider =
|
: false;
|
||||||
resolvedProvider === "searxng" ||
|
const setSearchProvider = (provider: Settings["researchGlobalWebSearchProvider"]) => {
|
||||||
resolvedProvider === "brave" ||
|
setForm((current) => ({
|
||||||
resolvedProvider === "google" ||
|
...current,
|
||||||
resolvedProvider === "tavily";
|
researchGlobalWebSearchProvider: provider,
|
||||||
const selectedCredentialProvider =
|
researchGlobalDefaults: {
|
||||||
resolvedProvider === "brave" || resolvedProvider === "tavily" ? resolvedProvider : null;
|
...(current.researchGlobalDefaults ?? {}),
|
||||||
const hasMissingResearchCredential = selectedCredentialProvider
|
searchProvider: provider,
|
||||||
? authProviders.some(
|
},
|
||||||
(provider) => provider.id === selectedCredentialProvider && !provider.authenticated,
|
}));
|
||||||
)
|
};
|
||||||
: false;
|
return (<>
|
||||||
|
|
||||||
const setSearchProvider = (provider: Settings["researchGlobalWebSearchProvider"]) => {
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalWebSearchProvider: provider,
|
|
||||||
researchGlobalDefaults: {
|
|
||||||
...(current.researchGlobalDefaults ?? {}),
|
|
||||||
searchProvider: provider,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Research Defaults</h4>
|
<h4 className="settings-section-heading">{t("settings.researchGlobal.researchDefaults", "Research Defaults")}</h4>
|
||||||
<div className="form-group settings-research-provider-group">
|
<div className="form-group settings-research-provider-group">
|
||||||
<label htmlFor="research-global-provider-builtin" className="checkbox-label">
|
<label htmlFor="research-global-provider-builtin" className="checkbox-label">
|
||||||
<input
|
<input id="research-global-provider-builtin" type="radio" name="research-global-search-provider" checked={!externalProvider} onChange={() => setSearchProvider("builtin")}/>{t("settings.researchGlobal.builtInUsesAgentWebTools", " Built-in (uses agent web tools) ")}</label>
|
||||||
id="research-global-provider-builtin"
|
<small>{t("settings.researchGlobal.searchesAndFetchesUseTheAgentsNativeWebSearch", " Searches and fetches use the agent's native WebSearch/WebFetch tools. No API key required. ")}</small>
|
||||||
type="radio"
|
|
||||||
name="research-global-search-provider"
|
|
||||||
checked={!externalProvider}
|
|
||||||
onChange={() => setSearchProvider("builtin")}
|
|
||||||
/>
|
|
||||||
Built-in (uses agent web tools)
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
Searches and fetches use the agent's native WebSearch/WebFetch tools. No API key required.
|
|
||||||
</small>
|
|
||||||
<details className="settings-option-details settings-research-provider-advanced-details">
|
<details className="settings-option-details settings-research-provider-advanced-details">
|
||||||
<summary>Advanced — external search providers</summary>
|
<summary>{t("settings.researchGlobal.advancedExternalSearchProviders", "Advanced \u2014 external search providers")}</summary>
|
||||||
<div className="settings-research-provider-advanced-body">
|
<div className="settings-research-provider-advanced-body">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="research-global-search-provider-advanced">Search Provider</label>
|
<label htmlFor="research-global-search-provider-advanced">{t("settings.researchGlobal.searchProvider", "Search Provider")}</label>
|
||||||
<select
|
<select id="research-global-search-provider-advanced" className="input" value={externalProvider ? resolvedProvider : "searxng"} onChange={(event) => setSearchProvider(event.target.value as Settings["researchGlobalWebSearchProvider"])}>
|
||||||
id="research-global-search-provider-advanced"
|
<option value="searxng">{t("settings.researchGlobal.searXNG", "SearXNG")}</option>
|
||||||
className="input"
|
<option value="brave">{t("settings.researchGlobal.brave", "Brave")}</option>
|
||||||
value={externalProvider ? resolvedProvider : "searxng"}
|
<option value="google">{t("settings.researchGlobal.googleCustomSearch", "Google Custom Search")}</option>
|
||||||
onChange={(event) =>
|
<option value="tavily">{t("settings.researchGlobal.tavily", "Tavily")}</option>
|
||||||
setSearchProvider(event.target.value as Settings["researchGlobalWebSearchProvider"])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="searxng">SearXNG</option>
|
|
||||||
<option value="brave">Brave</option>
|
|
||||||
<option value="google">Google Custom Search</option>
|
|
||||||
<option value="tavily">Tavily</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="research-global-searxng-url">SearXNG URL</label>
|
<label htmlFor="research-global-searxng-url">{t("settings.researchGlobal.searXNGURL", "SearXNG URL")}</label>
|
||||||
<input
|
<input id="research-global-searxng-url" className="input" value={form.researchGlobalSearxngUrl ?? ""} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-searxng-url"
|
...current,
|
||||||
className="input"
|
researchGlobalSearxngUrl: event.target.value || undefined,
|
||||||
value={form.researchGlobalSearxngUrl ?? ""}
|
}))} placeholder={t("settings.researchGlobal.httpsSearxExampleCom", "https://searx.example.com")}/>
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalSearxngUrl: event.target.value || undefined,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder="https://searx.example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="research-global-google-cx">Google Search CX</label>
|
<label htmlFor="research-global-google-cx">{t("settings.researchGlobal.googleSearchCX", "Google Search CX")}</label>
|
||||||
<input
|
<input id="research-global-google-cx" className="input" value={form.researchGlobalGoogleSearchCx ?? ""} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-google-cx"
|
...current,
|
||||||
className="input"
|
researchGlobalGoogleSearchCx: event.target.value || undefined,
|
||||||
value={form.researchGlobalGoogleSearchCx ?? ""}
|
}))} placeholder={t("settings.researchGlobal.customSearchEngineId", "custom-search-engine-id")}/>
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalGoogleSearchCx: event.target.value || undefined,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder="custom-search-engine-id"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-empty-state settings-research-empty-state" role="note">
|
<div className="settings-empty-state settings-research-empty-state" role="note">{t("settings.researchGlobal.configureBraveTavilyAndGoogleAPIKeysIn", " Configure Brave, Tavily, and Google API keys in Authentication. ")}<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>{t("settings.researchGlobal.openAuthenticationSettings", " Open Authentication Settings ")}</button>
|
||||||
Configure Brave, Tavily, and Google API keys in Authentication.
|
|
||||||
<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>
|
|
||||||
Open Authentication Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -133,138 +73,66 @@ export function ResearchGlobalSection({
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="settings-research-limits-grid">
|
<div className="settings-research-limits-grid">
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-global-max-concurrent">Default Max Concurrent Runs</label>
|
<label htmlFor="research-global-max-concurrent">{t("settings.researchGlobal.defaultMaxConcurrentRuns", "Default Max Concurrent Runs")}</label>
|
||||||
<input
|
<input id="research-global-max-concurrent" className="input" type="number" min={1} value={form.researchGlobalMaxConcurrentRuns ?? 3} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-max-concurrent"
|
...current,
|
||||||
className="input"
|
researchGlobalMaxConcurrentRuns: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
type="number"
|
}))}/>
|
||||||
min={1}
|
|
||||||
value={form.researchGlobalMaxConcurrentRuns ?? 3}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalMaxConcurrentRuns: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-global-max-sources">Default Max Sources Per Run</label>
|
<label htmlFor="research-global-max-sources">{t("settings.researchGlobal.defaultMaxSourcesPerRun", "Default Max Sources Per Run")}</label>
|
||||||
<input
|
<input id="research-global-max-sources" className="input" type="number" min={1} value={form.researchGlobalMaxSourcesPerRun ?? 20} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-max-sources"
|
...current,
|
||||||
className="input"
|
researchGlobalMaxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
type="number"
|
researchGlobalDefaults: {
|
||||||
min={1}
|
...(current.researchGlobalDefaults ?? {}),
|
||||||
value={form.researchGlobalMaxSourcesPerRun ?? 20}
|
maxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
onChange={(event) =>
|
},
|
||||||
setForm((current) => ({
|
}))}/>
|
||||||
...current,
|
|
||||||
researchGlobalMaxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
researchGlobalDefaults: {
|
|
||||||
...(current.researchGlobalDefaults ?? {}),
|
|
||||||
maxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-global-default-timeout">Default Max Duration (ms)</label>
|
<label htmlFor="research-global-default-timeout">{t("settings.researchGlobal.defaultMaxDurationMs", "Default Max Duration (ms)")}</label>
|
||||||
<input
|
<input id="research-global-default-timeout" className="input" type="number" min={1000} value={form.researchGlobalDefaultTimeout ?? 300000} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-default-timeout"
|
...current,
|
||||||
className="input"
|
researchGlobalDefaultTimeout: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
type="number"
|
}))}/>
|
||||||
min={1000}
|
|
||||||
value={form.researchGlobalDefaultTimeout ?? 300000}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalDefaultTimeout: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-global-fetch-timeout">Request Timeout (ms)</label>
|
<label htmlFor="research-global-fetch-timeout">{t("settings.researchGlobal.requestTimeoutMs", "Request Timeout (ms)")}</label>
|
||||||
<input
|
<input id="research-global-fetch-timeout" className="input" type="number" min={1000} value={form.researchGlobalFetchTimeoutMs ?? 30000} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-fetch-timeout"
|
...current,
|
||||||
className="input"
|
researchGlobalFetchTimeoutMs: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
type="number"
|
}))}/>
|
||||||
min={1000}
|
|
||||||
value={form.researchGlobalFetchTimeoutMs ?? 30000}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalFetchTimeoutMs: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-global-max-synthesis-rounds">Max Synthesis Rounds</label>
|
<label htmlFor="research-global-max-synthesis-rounds">{t("settings.researchGlobal.maxSynthesisRounds", "Max Synthesis Rounds")}</label>
|
||||||
<input
|
<input id="research-global-max-synthesis-rounds" className="input" type="number" min={1} value={form.researchGlobalMaxSynthesisRounds ?? 2} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-max-synthesis-rounds"
|
...current,
|
||||||
className="input"
|
researchGlobalMaxSynthesisRounds: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
type="number"
|
}))}/>
|
||||||
min={1}
|
|
||||||
value={form.researchGlobalMaxSynthesisRounds ?? 2}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalMaxSynthesisRounds: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Enabled Sources</label>
|
<label>{t("settings.researchGlobal.enabledSources", "Enabled Sources")}</label>
|
||||||
<label htmlFor="research-global-source-webSearch" className="checkbox-label settings-research-source-locked">
|
<label htmlFor="research-global-source-webSearch" className="checkbox-label settings-research-source-locked">
|
||||||
<input id="research-global-source-webSearch" type="checkbox" checked disabled readOnly />
|
<input id="research-global-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchGlobal.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchGlobal.alwaysOn", "Always on")}</span>
|
||||||
Web Search <span className="settings-muted">Always on</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div className="settings-research-source-grid">
|
<div className="settings-research-source-grid">
|
||||||
<label htmlFor="research-global-source-github" className="checkbox-label">
|
<label htmlFor="research-global-source-github" className="checkbox-label">
|
||||||
<input
|
<input id="research-global-source-github" type="checkbox" checked={form.researchGlobalGitHubEnabled ?? false} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-source-github"
|
...current,
|
||||||
type="checkbox"
|
researchGlobalGitHubEnabled: event.target.checked,
|
||||||
checked={form.researchGlobalGitHubEnabled ?? false}
|
}))}/>{t("settings.researchGlobal.gitHub", " GitHub ")}</label>
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalGitHubEnabled: event.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
GitHub
|
|
||||||
</label>
|
|
||||||
<label htmlFor="research-global-source-local-docs" className="checkbox-label">
|
<label htmlFor="research-global-source-local-docs" className="checkbox-label">
|
||||||
<input
|
<input id="research-global-source-local-docs" type="checkbox" checked={form.researchGlobalLocalDocsEnabled ?? true} onChange={(event) => setForm((current) => ({
|
||||||
id="research-global-source-local-docs"
|
...current,
|
||||||
type="checkbox"
|
researchGlobalLocalDocsEnabled: event.target.checked,
|
||||||
checked={form.researchGlobalLocalDocsEnabled ?? true}
|
}))}/>{t("settings.researchGlobal.localDocs", " Local Docs ")}</label>
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchGlobalLocalDocsEnabled: event.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Local Docs
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasMissingResearchCredential && (
|
{hasMissingResearchCredential && (<div className="settings-empty-state" role="alert">{t("settings.researchGlobal.missingCredentialsForTheSelectedResearchProvider", " Missing credentials for the selected research provider. ")}<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>{t("settings.researchGlobal.openAuthentication", " Open Authentication ")}</button>
|
||||||
<div className="settings-empty-state" role="alert">
|
</div>)}
|
||||||
Missing credentials for the selected research provider.
|
</>);
|
||||||
<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>
|
|
||||||
Open Authentication
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResearchGlobalSection;
|
export default ResearchGlobalSection;
|
||||||
|
|||||||
@@ -1,183 +1,111 @@
|
|||||||
/**
|
|
||||||
* Project Research Settings section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Per-project research enable toggle, enabled-source grid (web search always
|
|
||||||
* on), and run-limit fields. The limit-validation error is computed in the shell
|
|
||||||
* (shared with the save gate) and passed down. Keys, nested researchSettings
|
|
||||||
* shape, and conditional rendering preserved verbatim from the original inline
|
|
||||||
* JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface ResearchProjectSectionProps extends SectionBaseProps {
|
export interface ResearchProjectSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
researchLimitError: string | null;
|
researchLimitError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResearchProjectSection({ scopeBanner, form, setForm, researchLimitError }: ResearchProjectSectionProps) {
|
export function ResearchProjectSection({ scopeBanner, form, setForm, researchLimitError }: ResearchProjectSectionProps) {
|
||||||
const limits = form.researchSettings?.limits;
|
const { t } = useTranslation("app");
|
||||||
const sources = form.researchSettings?.enabledSources;
|
const limits = form.researchSettings?.limits;
|
||||||
return (
|
const sources = form.researchSettings?.enabledSources;
|
||||||
<>
|
return (<>
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Project Research Settings</h4>
|
<h4 className="settings-section-heading">{t("settings.researchProject.projectResearchSettings", "Project Research Settings")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="research-project-enabled" className="checkbox-label">
|
<label htmlFor="research-project-enabled" className="checkbox-label">
|
||||||
<input
|
<input id="research-project-enabled" type="checkbox" checked={form.researchSettings?.enabled ?? true} onChange={(event) => setForm((current) => ({
|
||||||
id="research-project-enabled"
|
...current,
|
||||||
type="checkbox"
|
researchSettings: {
|
||||||
checked={form.researchSettings?.enabled ?? true}
|
...(current.researchSettings ?? {}),
|
||||||
onChange={(event) =>
|
enabled: event.target.checked,
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
}))}/>{t("settings.researchProject.enableResearchInThisProject", " Enable research in this project ")}</label>
|
||||||
researchSettings: {
|
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
enabled: event.target.checked,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable research in this project
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Enabled Sources</label>
|
<label>{t("settings.researchProject.enabledSources", "Enabled Sources")}</label>
|
||||||
<label
|
<label htmlFor="research-project-source-webSearch" className="checkbox-label settings-research-source-locked">
|
||||||
htmlFor="research-project-source-webSearch"
|
<input id="research-project-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchProject.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchProject.alwaysOn", "Always on")}</span>
|
||||||
className="checkbox-label settings-research-source-locked"
|
|
||||||
>
|
|
||||||
<input id="research-project-source-webSearch" type="checkbox" checked disabled readOnly />
|
|
||||||
Web Search <span className="settings-muted">Always on</span>
|
|
||||||
</label>
|
</label>
|
||||||
<small className="settings-muted">
|
<small className="settings-muted">{t("settings.researchProject.webSearchIsAlwaysEnabledConfigureTheSearch", " Web search is always enabled. Configure the search provider under Research Defaults. ")}</small>
|
||||||
Web search is always enabled. Configure the search provider under Research Defaults.
|
|
||||||
</small>
|
|
||||||
<div className="settings-research-source-grid">
|
<div className="settings-research-source-grid">
|
||||||
{[
|
{[
|
||||||
["pageFetch", "Page Fetch"],
|
["pageFetch", t("settings.researchProject.pageFetch", "Page Fetch")],
|
||||||
["github", "GitHub"],
|
["github", t("settings.researchProject.github", "GitHub")],
|
||||||
["localDocs", "Local Docs"],
|
["localDocs", t("settings.researchProject.localDocs", "Local Docs")],
|
||||||
["llmSynthesis", "LLM Synthesis"],
|
["llmSynthesis", t("settings.researchProject.llmSynthesis", "LLM Synthesis")],
|
||||||
].map(([key, label]) => (
|
].map(([key, label]) => (<label key={key} htmlFor={`research-project-source-${key}`} className="checkbox-label">
|
||||||
<label key={key} htmlFor={`research-project-source-${key}`} className="checkbox-label">
|
<input id={`research-project-source-${key}`} type="checkbox" checked={sources?.[key as keyof NonNullable<typeof sources>] ?? false} onChange={(event) => setForm((current) => ({
|
||||||
<input
|
...current,
|
||||||
id={`research-project-source-${key}`}
|
researchSettings: {
|
||||||
type="checkbox"
|
...(current.researchSettings ?? {}),
|
||||||
checked={sources?.[key as keyof NonNullable<typeof sources>] ?? false}
|
enabledSources: {
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
researchSettings: {
|
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
enabledSources: {
|
|
||||||
...(current.researchSettings?.enabledSources ?? {}),
|
...(current.researchSettings?.enabledSources ?? {}),
|
||||||
[key]: event.target.checked,
|
[key]: event.target.checked,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
},
|
||||||
}
|
}))}/>
|
||||||
/>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="settings-research-limits-grid">
|
<div className="settings-research-limits-grid">
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-project-max-concurrent">Max Concurrent Runs</label>
|
<label htmlFor="research-project-max-concurrent">{t("settings.researchProject.maxConcurrentRuns", "Max Concurrent Runs")}</label>
|
||||||
<input
|
<input id="research-project-max-concurrent" className="input" type="number" min={1} value={limits?.maxConcurrentRuns ?? 3} onChange={(event) => setForm((current) => ({
|
||||||
id="research-project-max-concurrent"
|
...current,
|
||||||
className="input"
|
researchSettings: {
|
||||||
type="number"
|
...(current.researchSettings ?? {}),
|
||||||
min={1}
|
limits: {
|
||||||
value={limits?.maxConcurrentRuns ?? 3}
|
...(current.researchSettings?.limits ?? {}),
|
||||||
onChange={(event) =>
|
maxConcurrentRuns: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
},
|
||||||
researchSettings: {
|
}))}/>
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
limits: {
|
|
||||||
...(current.researchSettings?.limits ?? {}),
|
|
||||||
maxConcurrentRuns: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-project-max-sources">Max Sources Per Run</label>
|
<label htmlFor="research-project-max-sources">{t("settings.researchProject.maxSourcesPerRun", "Max Sources Per Run")}</label>
|
||||||
<input
|
<input id="research-project-max-sources" className="input" type="number" min={1} value={limits?.maxSourcesPerRun ?? 20} onChange={(event) => setForm((current) => ({
|
||||||
id="research-project-max-sources"
|
...current,
|
||||||
className="input"
|
researchSettings: {
|
||||||
type="number"
|
...(current.researchSettings ?? {}),
|
||||||
min={1}
|
limits: {
|
||||||
value={limits?.maxSourcesPerRun ?? 20}
|
...(current.researchSettings?.limits ?? {}),
|
||||||
onChange={(event) =>
|
maxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
},
|
||||||
researchSettings: {
|
}))}/>
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
limits: {
|
|
||||||
...(current.researchSettings?.limits ?? {}),
|
|
||||||
maxSourcesPerRun: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-project-max-duration">Max Duration (ms)</label>
|
<label htmlFor="research-project-max-duration">{t("settings.researchProject.maxDurationMs", "Max Duration (ms)")}</label>
|
||||||
<input
|
<input id="research-project-max-duration" className="input" type="number" min={1000} value={limits?.maxDurationMs ?? 300000} onChange={(event) => setForm((current) => ({
|
||||||
id="research-project-max-duration"
|
...current,
|
||||||
className="input"
|
researchSettings: {
|
||||||
type="number"
|
...(current.researchSettings ?? {}),
|
||||||
min={1000}
|
limits: {
|
||||||
value={limits?.maxDurationMs ?? 300000}
|
...(current.researchSettings?.limits ?? {}),
|
||||||
onChange={(event) =>
|
maxDurationMs: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
},
|
||||||
researchSettings: {
|
}))}/>
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
limits: {
|
|
||||||
...(current.researchSettings?.limits ?? {}),
|
|
||||||
maxDurationMs: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-research-limit-field">
|
<div className="settings-research-limit-field">
|
||||||
<label htmlFor="research-project-request-timeout">Request Timeout (ms)</label>
|
<label htmlFor="research-project-request-timeout">{t("settings.researchProject.requestTimeoutMs", "Request Timeout (ms)")}</label>
|
||||||
<input
|
<input id="research-project-request-timeout" className="input" type="number" min={1000} value={limits?.requestTimeoutMs ?? 30000} onChange={(event) => setForm((current) => ({
|
||||||
id="research-project-request-timeout"
|
...current,
|
||||||
className="input"
|
researchSettings: {
|
||||||
type="number"
|
...(current.researchSettings ?? {}),
|
||||||
min={1000}
|
limits: {
|
||||||
value={limits?.requestTimeoutMs ?? 30000}
|
...(current.researchSettings?.limits ?? {}),
|
||||||
onChange={(event) =>
|
requestTimeoutMs: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
},
|
||||||
researchSettings: {
|
}))}/>
|
||||||
...(current.researchSettings ?? {}),
|
|
||||||
limits: {
|
|
||||||
...(current.researchSettings?.limits ?? {}),
|
|
||||||
requestTimeoutMs: event.target.value === "" ? undefined : Number(event.target.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{researchLimitError && <small className="field-error settings-research-limits-error">{researchLimitError}</small>}
|
{researchLimitError && <small className="field-error settings-research-limits-error">{researchLimitError}</small>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResearchProjectSection;
|
export default ResearchProjectSection;
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
/**
|
|
||||||
* Runtimes group sections (U9 / KTD-10) — thin wrappers around each plugin
|
|
||||||
* runtime's self-contained card. These sections carry no modal form state; they
|
|
||||||
* just title and mount the runtime card relocated from SettingsModal's switch.
|
|
||||||
*/
|
|
||||||
import { HermesRuntimeCard } from "../../HermesRuntimeCard";
|
import { HermesRuntimeCard } from "../../HermesRuntimeCard";
|
||||||
import { OpenClawRuntimeCard } from "../../OpenClawRuntimeCard";
|
import { OpenClawRuntimeCard } from "../../OpenClawRuntimeCard";
|
||||||
import { PaperclipRuntimeCard } from "../../PaperclipRuntimeCard";
|
import { PaperclipRuntimeCard } from "../../PaperclipRuntimeCard";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
/*
|
||||||
|
* FNXC:i18n-Localization 2026-06-20-00:00:
|
||||||
|
* Each exported runtime settings section is mounted independently, so each component owns its own translation hook scope.
|
||||||
|
*/
|
||||||
export function HermesRuntimeSection() {
|
export function HermesRuntimeSection() {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">Hermes Runtime</h4>
|
<h4 className="settings-section-heading">{t("settings.runtimesRuntimes.hermesRuntime", "Hermes Runtime")}</h4>
|
||||||
<HermesRuntimeCard />
|
<HermesRuntimeCard />
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OpenClawRuntimeSection() {
|
export function OpenClawRuntimeSection() {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">OpenClaw Runtime</h4>
|
<h4 className="settings-section-heading">{t("settings.runtimesRuntimes.openClawRuntime", "OpenClaw Runtime")}</h4>
|
||||||
<OpenClawRuntimeCard />
|
<OpenClawRuntimeCard />
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaperclipRuntimeSection() {
|
export function PaperclipRuntimeSection() {
|
||||||
return (
|
const { t } = useTranslation("app");
|
||||||
<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">Paperclip Runtime</h4>
|
<h4 className="settings-section-heading">{t("settings.runtimesRuntimes.paperclipRuntime", "Paperclip Runtime")}</h4>
|
||||||
<PaperclipRuntimeCard />
|
<PaperclipRuntimeCard />
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,152 +1,81 @@
|
|||||||
/**
|
|
||||||
* Scheduled Evals section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Per-project scheduled evaluation run configuration (enable, interval,
|
|
||||||
* evaluator provider/model, follow-up policy, retention). Section visibility is
|
|
||||||
* gated by the shell (evalsViewEnabled). All keys and conditional disabling are
|
|
||||||
* preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export interface ScheduledEvalsSectionProps extends SectionBaseProps {
|
export interface ScheduledEvalsSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScheduledEvalsSection({ scopeBanner, form, setForm }: ScheduledEvalsSectionProps) {
|
export function ScheduledEvalsSection({ scopeBanner, form, setForm }: ScheduledEvalsSectionProps) {
|
||||||
const evalSettings = form.evalSettings ?? {};
|
const { t } = useTranslation("app");
|
||||||
const isScheduledEvalEnabled = evalSettings.enabled ?? false;
|
const evalSettings = form.evalSettings ?? {};
|
||||||
|
const isScheduledEvalEnabled = evalSettings.enabled ?? false;
|
||||||
return (
|
return (<>
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Scheduled Evals</h4>
|
<h4 className="settings-section-heading">{t("settings.scheduledEvals.scheduledEvals", "Scheduled Evals")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-enabled" className="checkbox-label">
|
<label htmlFor="scheduled-evals-enabled" className="checkbox-label">
|
||||||
<input
|
<input id="scheduled-evals-enabled" type="checkbox" checked={isScheduledEvalEnabled} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-enabled"
|
...current,
|
||||||
type="checkbox"
|
evalSettings: {
|
||||||
checked={isScheduledEvalEnabled}
|
...(current.evalSettings ?? {}),
|
||||||
onChange={(event) =>
|
enabled: event.target.checked,
|
||||||
setForm((current) => ({
|
},
|
||||||
...current,
|
}))}/>{t("settings.scheduledEvals.enableScheduledEvalRunsForThisProject", " Enable scheduled eval runs for this project ")}</label>
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
|
||||||
enabled: event.target.checked,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable scheduled eval runs for this project
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-interval">Interval (ms)</label>
|
<label htmlFor="scheduled-evals-interval">{t("settings.scheduledEvals.intervalMs", "Interval (ms)")}</label>
|
||||||
<input
|
<input id="scheduled-evals-interval" className="input" type="number" min={60000} max={604800000} step={1000} disabled={!isScheduledEvalEnabled} value={evalSettings.intervalMs ?? 86400000} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-interval"
|
...current,
|
||||||
className="input"
|
evalSettings: {
|
||||||
type="number"
|
|
||||||
min={60000}
|
|
||||||
max={604800000}
|
|
||||||
step={1000}
|
|
||||||
disabled={!isScheduledEvalEnabled}
|
|
||||||
value={evalSettings.intervalMs ?? 86_400_000}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
...(current.evalSettings ?? {}),
|
||||||
intervalMs: event.target.value === "" ? undefined : Number(event.target.value),
|
intervalMs: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
},
|
},
|
||||||
}))
|
}))}/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-provider">Evaluator Provider</label>
|
<label htmlFor="scheduled-evals-provider">{t("settings.scheduledEvals.evaluatorProvider", "Evaluator Provider")}</label>
|
||||||
<input
|
<input id="scheduled-evals-provider" className="input" value={evalSettings.evaluatorProvider ?? ""} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-provider"
|
...current,
|
||||||
className="input"
|
evalSettings: {
|
||||||
value={evalSettings.evaluatorProvider ?? ""}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
...(current.evalSettings ?? {}),
|
||||||
evaluatorProvider: event.target.value.trim() === "" ? undefined : event.target.value,
|
evaluatorProvider: event.target.value.trim() === "" ? undefined : event.target.value,
|
||||||
},
|
},
|
||||||
}))
|
}))} placeholder={t("settings.scheduledEvals.openai", "openai")}/>
|
||||||
}
|
|
||||||
placeholder="openai"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-model">Evaluator Model</label>
|
<label htmlFor="scheduled-evals-model">{t("settings.scheduledEvals.evaluatorModel", "Evaluator Model")}</label>
|
||||||
<input
|
<input id="scheduled-evals-model" className="input" value={evalSettings.evaluatorModelId ?? ""} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-model"
|
...current,
|
||||||
className="input"
|
evalSettings: {
|
||||||
value={evalSettings.evaluatorModelId ?? ""}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
...(current.evalSettings ?? {}),
|
||||||
evaluatorModelId: event.target.value.trim() === "" ? undefined : event.target.value,
|
evaluatorModelId: event.target.value.trim() === "" ? undefined : event.target.value,
|
||||||
},
|
},
|
||||||
}))
|
}))} placeholder={t("settings.scheduledEvals.gpt5", "gpt-5")}/>
|
||||||
}
|
<small className="form-text text-muted">{t("settings.scheduledEvals.leaveProviderAndModelBlankToInheritThe", " Leave provider and model blank to inherit the project validator lane model settings. ")}</small>
|
||||||
placeholder="gpt-5"
|
|
||||||
/>
|
|
||||||
<small className="form-text text-muted">
|
|
||||||
Leave provider and model blank to inherit the project validator lane model settings.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-follow-up-policy">Follow-up Policy</label>
|
<label htmlFor="scheduled-evals-follow-up-policy">{t("settings.scheduledEvals.followUpPolicy", "Follow-up Policy")}</label>
|
||||||
<select
|
<select id="scheduled-evals-follow-up-policy" className="select" disabled={!isScheduledEvalEnabled} value={evalSettings.followUpPolicy ?? "suggest-only"} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-follow-up-policy"
|
...current,
|
||||||
className="select"
|
evalSettings: {
|
||||||
disabled={!isScheduledEvalEnabled}
|
|
||||||
value={evalSettings.followUpPolicy ?? "suggest-only"}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
...(current.evalSettings ?? {}),
|
||||||
followUpPolicy: event.target.value as "disabled" | "suggest-only" | "auto-create",
|
followUpPolicy: event.target.value as "disabled" | "suggest-only" | "auto-create",
|
||||||
},
|
},
|
||||||
}))
|
}))}>
|
||||||
}
|
<option value="disabled">{t("settings.scheduledEvals.disabled", "Disabled")}</option>
|
||||||
>
|
<option value="suggest-only">{t("settings.scheduledEvals.suggestOnly", "Suggest only")}</option>
|
||||||
<option value="disabled">Disabled</option>
|
<option value="auto-create">{t("settings.scheduledEvals.autoCreateTasks", "Auto-create tasks")}</option>
|
||||||
<option value="suggest-only">Suggest only</option>
|
|
||||||
<option value="auto-create">Auto-create tasks</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="scheduled-evals-retention-days">Retention (days)</label>
|
<label htmlFor="scheduled-evals-retention-days">{t("settings.scheduledEvals.retentionDays", "Retention (days)")}</label>
|
||||||
<input
|
<input id="scheduled-evals-retention-days" className="input" type="number" min={1} max={365} step={1} disabled={!isScheduledEvalEnabled} value={evalSettings.retentionDays ?? 30} onChange={(event) => setForm((current) => ({
|
||||||
id="scheduled-evals-retention-days"
|
...current,
|
||||||
className="input"
|
evalSettings: {
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={365}
|
|
||||||
step={1}
|
|
||||||
disabled={!isScheduledEvalEnabled}
|
|
||||||
value={evalSettings.retentionDays ?? 30}
|
|
||||||
onChange={(event) =>
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
evalSettings: {
|
|
||||||
...(current.evalSettings ?? {}),
|
...(current.evalSettings ?? {}),
|
||||||
retentionDays: event.target.value === "" ? undefined : Number(event.target.value),
|
retentionDays: event.target.value === "" ? undefined : Number(event.target.value),
|
||||||
},
|
},
|
||||||
}))
|
}))}/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScheduledEvalsSection;
|
export default ScheduledEvalsSection;
|
||||||
|
|||||||
@@ -1,369 +1,182 @@
|
|||||||
/**
|
|
||||||
* Scheduling section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped scheduling/capacity knobs: global + per-project concurrency,
|
|
||||||
* poll interval, heartbeat discipline, stuck/stale detection, plan staleness,
|
|
||||||
* auto-archive, overlap serialization with the ignored-paths editor, plus the
|
|
||||||
* step-execution redirect stub (settings moved to the workflow, U4). The global
|
|
||||||
* concurrency value is shell state (it persists via a separate API and is
|
|
||||||
* dirty-tracked) and is relayed through props; the overlap-path editor handlers
|
|
||||||
* also live in the shell (they share the file-browser hook). The day/archive
|
|
||||||
* constants are co-located. Keys, unit conversions, and conditional disabling
|
|
||||||
* are preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MovedSettingsStub } from "./MovedSettingsStub";
|
import { MovedSettingsStub } from "./MovedSettingsStub";
|
||||||
import type { SettingsFormState, SetSettingsForm } from "./context";
|
import type { SettingsFormState, SetSettingsForm } from "./context";
|
||||||
|
|
||||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
||||||
|
|
||||||
export interface SchedulingSectionProps {
|
export interface SchedulingSectionProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
form: SettingsFormState;
|
form: SettingsFormState;
|
||||||
setForm: SetSettingsForm;
|
setForm: SetSettingsForm;
|
||||||
globalMaxConcurrent: number | undefined;
|
globalMaxConcurrent: number | undefined;
|
||||||
onGlobalMaxConcurrentChange: (value: number | undefined) => void;
|
onGlobalMaxConcurrentChange: (value: number | undefined) => void;
|
||||||
onOverlapIgnorePathChange: (index: number, value: string) => void;
|
onOverlapIgnorePathChange: (index: number, value: string) => void;
|
||||||
onOpenOverlapPathPicker: (index: number) => void;
|
onOpenOverlapPathPicker: (index: number) => void;
|
||||||
onRemoveOverlapIgnorePath: (index: number) => void;
|
onRemoveOverlapIgnorePath: (index: number) => void;
|
||||||
onAddOverlapIgnorePath: () => void;
|
onAddOverlapIgnorePath: () => void;
|
||||||
onOpenWorkflowSettings?: () => void;
|
onOpenWorkflowSettings?: () => void;
|
||||||
}
|
}
|
||||||
|
export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurrent, onGlobalMaxConcurrentChange, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) {
|
||||||
export function SchedulingSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
form,
|
|
||||||
setForm,
|
|
||||||
globalMaxConcurrent,
|
|
||||||
onGlobalMaxConcurrentChange,
|
|
||||||
onOverlapIgnorePathChange,
|
|
||||||
onOpenOverlapPathPicker,
|
|
||||||
onRemoveOverlapIgnorePath,
|
|
||||||
onAddOverlapIgnorePath,
|
|
||||||
onOpenWorkflowSettings,
|
|
||||||
}: SchedulingSectionProps) {
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Scheduling</h4>
|
<h4 className="settings-section-heading">{t("settings.scheduling.scheduling", "Scheduling")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="globalMaxConcurrent">Global Max Concurrent</label>
|
<label htmlFor="globalMaxConcurrent">{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}</label>
|
||||||
<input
|
<input id="globalMaxConcurrent" type="number" min={0} max={10000} value={globalMaxConcurrent ?? ""} onChange={(e) => {
|
||||||
id="globalMaxConcurrent"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={10000}
|
|
||||||
value={globalMaxConcurrent ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
onGlobalMaxConcurrentChange(val === "" ? undefined : Number(val));
|
onGlobalMaxConcurrentChange(val === "" ? undefined : Number(val));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small className="form-text text-muted">{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}</small>
|
||||||
<small className="form-text text-muted">Maximum concurrent agents across all projects</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxConcurrent">Max Concurrent Tasks</label>
|
<label htmlFor="maxConcurrent">{t("settings.scheduling.maxConcurrentTasks", "Max Concurrent Tasks")}</label>
|
||||||
<input
|
<input id="maxConcurrent" type="number" min={1} max={10} value={form.maxConcurrent ?? ""} onChange={(e) => {
|
||||||
id="maxConcurrent"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
value={form.maxConcurrent ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, maxConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState));
|
setForm((f) => ({ ...f, maxConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState));
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxTriageConcurrent">Max Triage Concurrent</label>
|
<label htmlFor="maxTriageConcurrent">{t("settings.scheduling.maxTriageConcurrent", "Max Triage Concurrent")}</label>
|
||||||
<input
|
<input id="maxTriageConcurrent" type="number" min={1} max={10} value={form.maxTriageConcurrent ?? ""} onChange={(e) => {
|
||||||
id="maxTriageConcurrent"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
value={form.maxTriageConcurrent ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, maxTriageConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState));
|
setForm((f) => ({ ...f, maxTriageConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.scheduling.maximumConcurrentPlanningAgents", "Maximum concurrent planning agents")}</small>
|
||||||
<small>Maximum concurrent planning agents</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="pollIntervalMs">Poll Interval (ms)</label>
|
<label htmlFor="pollIntervalMs">{t("settings.scheduling.pollIntervalMs", "Poll Interval (ms)")}</label>
|
||||||
<input
|
<input id="pollIntervalMs" type="number" min={5000} step={1000} value={form.pollIntervalMs ?? ""} onChange={(e) => {
|
||||||
id="pollIntervalMs"
|
|
||||||
type="number"
|
|
||||||
min={5000}
|
|
||||||
step={1000}
|
|
||||||
value={form.pollIntervalMs ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, pollIntervalMs: val === "" ? undefined : Number(val) } as SettingsFormState));
|
setForm((f) => ({ ...f, pollIntervalMs: val === "" ? undefined : Number(val) } as SettingsFormState));
|
||||||
}}
|
}}/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="heartbeatScopeDiscipline">Heartbeat Scope Discipline</label>
|
<label htmlFor="heartbeatScopeDiscipline">{t("settings.scheduling.heartbeatScopeDiscipline", "Heartbeat Scope Discipline")}</label>
|
||||||
<select
|
<select id="heartbeatScopeDiscipline" className="select" value={form.heartbeatScopeDiscipline ?? "strict"} onChange={(e) => {
|
||||||
id="heartbeatScopeDiscipline"
|
|
||||||
className="select"
|
|
||||||
value={form.heartbeatScopeDiscipline ?? "strict"}
|
|
||||||
onChange={(e) => {
|
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
heartbeatScopeDiscipline: e.target.value as "strict" | "lite" | "off",
|
heartbeatScopeDiscipline: e.target.value as "strict" | "lite" | "off",
|
||||||
}));
|
}));
|
||||||
}}
|
}}>
|
||||||
>
|
<option value="strict">{t("settings.scheduling.strictDefault", "Strict (default)")}</option>
|
||||||
<option value="strict">Strict (default)</option>
|
<option value="lite">{t("settings.scheduling.lite", "Lite")}</option>
|
||||||
<option value="lite">Lite</option>
|
<option value="off">{t("settings.scheduling.off", "Off")}</option>
|
||||||
<option value="off">Off</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>Strict — coordination-focused; higher per-tick tokens. Lite — pre-2026-05-11 behavior. Off — minimal procedure.</small>
|
<small>{t("settings.scheduling.strictCoordinationFocusedHigherPerTickTokensLite", "Strict \u2014 coordination-focused; higher per-tick tokens. Lite \u2014 pre-2026-05-11 behavior. Off \u2014 minimal procedure.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="engineerBacklogAutoClaim" className="checkbox-label">
|
<label htmlFor="engineerBacklogAutoClaim" className="checkbox-label">
|
||||||
<input
|
<input id="engineerBacklogAutoClaim" type="checkbox" checked={form.engineerBacklogAutoClaim === true} onChange={(e) => setForm((f) => ({ ...f, engineerBacklogAutoClaim: e.target.checked }))}/>{t("settings.scheduling.letEngineerAgentsAutoClaimBacklogTasks", " Let engineer agents auto-claim backlog tasks ")}</label>
|
||||||
id="engineerBacklogAutoClaim"
|
<small>{t("settings.scheduling.backlogNoTaskAutoClaimIsExecutorOnly", "Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.engineerBacklogAutoClaim === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, engineerBacklogAutoClaim: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Let engineer agents auto-claim backlog tasks
|
|
||||||
</label>
|
|
||||||
<small>Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="taskStuckTimeoutMs">Stuck Task Timeout (minutes)</label>
|
<label htmlFor="taskStuckTimeoutMs">{t("settings.scheduling.stuckTaskTimeoutMinutes", "Stuck Task Timeout (minutes)")}</label>
|
||||||
<input
|
<input id="taskStuckTimeoutMs" type="number" min={1} step={1} value={form.taskStuckTimeoutMs ? Math.round(form.taskStuckTimeoutMs / 60000) : ""} onChange={(e) => {
|
||||||
id="taskStuckTimeoutMs"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={form.taskStuckTimeoutMs ? Math.round(form.taskStuckTimeoutMs / 60000) : ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
setForm((f) => ({ ...f, taskStuckTimeoutMs: val && num > 0 ? num * 60000 : undefined }));
|
setForm((f) => ({ ...f, taskStuckTimeoutMs: val && num > 0 ? num * 60000 : undefined }));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.scheduling.timeoutInMinutesForDetectingStuckTasksWhen", "Timeout in minutes for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.")}</small>
|
||||||
<small>Timeout in minutes for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="staleHighFanoutBlockerAgeThresholdMs">Stale High Fan-out Escalation (hours)</label>
|
<label htmlFor="staleHighFanoutBlockerAgeThresholdMs">{t("settings.scheduling.staleHighFanOutEscalationHours", "Stale High Fan-out Escalation (hours)")}</label>
|
||||||
<input
|
<input id="staleHighFanoutBlockerAgeThresholdMs" type="number" min={1} step={1} value={form.staleHighFanoutBlockerAgeThresholdMs ? Math.round(form.staleHighFanoutBlockerAgeThresholdMs / 3600000) : ""} onChange={(e) => {
|
||||||
id="staleHighFanoutBlockerAgeThresholdMs"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={form.staleHighFanoutBlockerAgeThresholdMs ? Math.round(form.staleHighFanoutBlockerAgeThresholdMs / 3600000) : ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
staleHighFanoutBlockerAgeThresholdMs: val && num > 0 ? num * 3600000 : undefined,
|
staleHighFanoutBlockerAgeThresholdMs: val && num > 0 ? num * 3600000 : undefined,
|
||||||
}));
|
}));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.scheduling.escalateHighFanOutBlockersOnlyAfterThey", "Escalate high fan-out blockers only after they remain in in-progress or in-review for this many hours (age source: columnMovedAt, fallback updatedAt). Default: 2 hours.")}</small>
|
||||||
<small>Escalate high fan-out blockers only after they remain in in-progress or in-review for this many hours (age source: columnMovedAt, fallback updatedAt). Default: 2 hours.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="preserveProgressOnStuckRequeue" className="checkbox-label">
|
<label htmlFor="preserveProgressOnStuckRequeue" className="checkbox-label">
|
||||||
<input
|
<input id="preserveProgressOnStuckRequeue" type="checkbox" checked={form.preserveProgressOnStuckRequeue !== false} onChange={(e) => setForm((f) => ({ ...f, preserveProgressOnStuckRequeue: e.target.checked }))}/>{t("settings.scheduling.preserveStepProgressOnStuckTaskRequeue", " Preserve step progress on stuck-task requeue ")}</label>
|
||||||
id="preserveProgressOnStuckRequeue"
|
<small>{t("settings.scheduling.whenTheStuckDetectorKillsAndReQueues", "When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.preserveProgressOnStuckRequeue !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, preserveProgressOnStuckRequeue: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Preserve step progress on stuck-task requeue
|
|
||||||
</label>
|
|
||||||
<small>When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="specStalenessEnabled" className="checkbox-label">
|
<label htmlFor="specStalenessEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="specStalenessEnabled" type="checkbox" checked={form.specStalenessEnabled || false} onChange={(e) => setForm((f) => ({ ...f, specStalenessEnabled: e.target.checked }))}/>{t("settings.scheduling.enablePlanStalenessEnforcement", " Enable plan staleness enforcement ")}</label>
|
||||||
id="specStalenessEnabled"
|
<small>{t("settings.scheduling.whenEnabledTasksWithStalePlansPROMPTMd", "When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.specStalenessEnabled || false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, specStalenessEnabled: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable plan staleness enforcement
|
|
||||||
</label>
|
|
||||||
<small>When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="specStalenessMaxAgeMs">Stale Spec Threshold (hours)</label>
|
<label htmlFor="specStalenessMaxAgeMs">{t("settings.scheduling.staleSpecThresholdHours", "Stale Spec Threshold (hours)")}</label>
|
||||||
<input
|
<input id="specStalenessMaxAgeMs" type="number" min={0} step={1} value={form.specStalenessMaxAgeMs !== undefined ? Math.round(form.specStalenessMaxAgeMs / 3600000) : ""} onChange={(e) => {
|
||||||
id="specStalenessMaxAgeMs"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
value={form.specStalenessMaxAgeMs !== undefined ? Math.round(form.specStalenessMaxAgeMs / 3600000) : ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
setForm((f) => ({ ...f, specStalenessMaxAgeMs: val !== "" ? num * 3600000 : undefined }));
|
setForm((f) => ({ ...f, specStalenessMaxAgeMs: val !== "" ? num * 3600000 : undefined }));
|
||||||
}}
|
}} disabled={!form.specStalenessEnabled}/>
|
||||||
disabled={!form.specStalenessEnabled}
|
<small>{t("settings.scheduling.maximumAgeInHoursBeforeAPlanIs", "Maximum age in hours before a plan is considered stale. Default: 6 hours.")}</small>
|
||||||
/>
|
|
||||||
<small>Maximum age in hours before a plan is considered stale. Default: 6 hours.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label">
|
<label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="autoArchiveDoneTasksEnabled" type="checkbox" checked={form.autoArchiveDoneTasksEnabled ?? true} onChange={(e) => setForm((f) => ({
|
||||||
id="autoArchiveDoneTasksEnabled"
|
...f,
|
||||||
type="checkbox"
|
autoArchiveDoneTasksEnabled: e.target.checked,
|
||||||
checked={form.autoArchiveDoneTasksEnabled ?? true}
|
}))}/>{t("settings.scheduling.enableAutomaticTaskArchiving", " Enable automatic task archiving ")}</label>
|
||||||
onChange={(e) =>
|
<small>{t("settings.scheduling.completedTasksOlderThanTheThresholdAreMoved", "Completed tasks older than the threshold are moved out of the active task database.")}</small>
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
autoArchiveDoneTasksEnabled: e.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable automatic task archiving
|
|
||||||
</label>
|
|
||||||
<small>Completed tasks older than the threshold are moved out of the active task database.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="autoArchiveDoneAfterMs">Archive Completed Tasks After (days)</label>
|
<label htmlFor="autoArchiveDoneAfterMs">{t("settings.scheduling.archiveCompletedTasksAfterDays", "Archive Completed Tasks After (days)")}</label>
|
||||||
<input
|
<input id="autoArchiveDoneAfterMs" type="number" min={1} step={1} value={form.autoArchiveDoneAfterMs !== undefined ? Math.round(form.autoArchiveDoneAfterMs / MS_PER_DAY) : AUTO_ARCHIVE_DEFAULT_AFTER_DAYS} onChange={(e) => {
|
||||||
id="autoArchiveDoneAfterMs"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={form.autoArchiveDoneAfterMs !== undefined ? Math.round(form.autoArchiveDoneAfterMs / MS_PER_DAY) : AUTO_ARCHIVE_DEFAULT_AFTER_DAYS}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
autoArchiveDoneAfterMs: val === "" ? undefined : num * MS_PER_DAY,
|
autoArchiveDoneAfterMs: val === "" ? undefined : num * MS_PER_DAY,
|
||||||
}));
|
}));
|
||||||
}}
|
}} disabled={form.autoArchiveDoneTasksEnabled === false}/>
|
||||||
disabled={form.autoArchiveDoneTasksEnabled === false}
|
<small>{t("settings.scheduling.numberOfDaysATaskCanStayIn", "Number of days a task can stay in Done before it is archived. Default: 2 days (48 hours).")}</small>
|
||||||
/>
|
|
||||||
<small>Number of days a task can stay in Done before it is archived. Default: 2 days (48 hours).</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="archiveAgentLogMode">Archive Agent Log</label>
|
<label htmlFor="archiveAgentLogMode">{t("settings.scheduling.archiveAgentLog", "Archive Agent Log")}</label>
|
||||||
<select
|
<select id="archiveAgentLogMode" value={form.archiveAgentLogMode ?? "compact"} onChange={(e) => setForm((f) => ({
|
||||||
id="archiveAgentLogMode"
|
...f,
|
||||||
value={form.archiveAgentLogMode ?? "compact"}
|
archiveAgentLogMode: e.target.value as "none" | "compact" | "full",
|
||||||
onChange={(e) =>
|
}))} disabled={form.autoArchiveDoneTasksEnabled === false}>
|
||||||
setForm((f) => ({
|
<option value="compact">{t("settings.scheduling.compactSummaryAndRecentEntries", "Compact summary and recent entries")}</option>
|
||||||
...f,
|
<option value="none">{t("settings.scheduling.doNotArchiveAgentLogs", "Do not archive agent logs")}</option>
|
||||||
archiveAgentLogMode: e.target.value as "none" | "compact" | "full",
|
<option value="full">{t("settings.scheduling.fullAgentLog", "Full agent log")}</option>
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={form.autoArchiveDoneTasksEnabled === false}
|
|
||||||
>
|
|
||||||
<option value="compact">Compact summary and recent entries</option>
|
|
||||||
<option value="none">Do not archive agent logs</option>
|
|
||||||
<option value="full">Full agent log</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>Compact mode keeps archive size low while preserving recent agent activity for context.</small>
|
<small>{t("settings.scheduling.compactModeKeepsArchiveSizeLowWhilePreserving", "Compact mode keeps archive size low while preserving recent agent activity for context.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxStuckKills">Max Stuck Retries</label>
|
<label htmlFor="maxStuckKills">{t("settings.scheduling.maxStuckRetries", "Max Stuck Retries")}</label>
|
||||||
<input
|
<input id="maxStuckKills" type="number" min={1} step={1} value={form.maxStuckKills ?? ""} onChange={(e) => {
|
||||||
id="maxStuckKills"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={form.maxStuckKills ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
setForm((f) => ({ ...f, maxStuckKills: val && num > 0 ? num : undefined }));
|
setForm((f) => ({ ...f, maxStuckKills: val && num > 0 ? num : undefined }));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.scheduling.maximumStuckDetectorRetriesBeforeATaskIs", "Maximum stuck-detector retries before a task is marked failed. Default: 6.")}</small>
|
||||||
<small>Maximum stuck-detector retries before a task is marked failed. Default: 6.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="groupOverlappingFiles" className="checkbox-label">
|
<label htmlFor="groupOverlappingFiles" className="checkbox-label">
|
||||||
<input
|
<input id="groupOverlappingFiles" type="checkbox" checked={form.groupOverlappingFiles} onChange={(e) => setForm((f) => ({ ...f, groupOverlappingFiles: e.target.checked }))}/>{t("settings.scheduling.serializeTasksWithOverlappingFiles", " Serialize tasks with overlapping files ")}</label>
|
||||||
id="groupOverlappingFiles"
|
<small>{t("settings.scheduling.whenEnabledTasksThatModifyTheSameFiles", "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.groupOverlappingFiles}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, groupOverlappingFiles: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Serialize tasks with overlapping files
|
|
||||||
</label>
|
|
||||||
<small>When enabled, tasks that modify the same files are queued serially to avoid merge conflicts</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group settings-overlap-ignore-group">
|
<div className="form-group settings-overlap-ignore-group">
|
||||||
<label>Ignored overlap paths</label>
|
<label>{t("settings.scheduling.ignoredOverlapPaths", "Ignored overlap paths")}</label>
|
||||||
<small>
|
<small>{t("settings.scheduling.optionalFileOrDirectoryPathsToIgnoreWhen", " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ")}<code>docs/</code>{t("settings.scheduling.or", " or ")}<code>generated/*</code>{t("settings.scheduling.closeParenPeriod", ").")}
|
||||||
Optional file or directory paths to ignore when overlap serialization is enabled.
|
|
||||||
Paths are project-relative (for example <code>docs/</code> or <code>generated/*</code>).
|
|
||||||
</small>
|
</small>
|
||||||
<div className="settings-overlap-ignore-list">
|
<div className="settings-overlap-ignore-list">
|
||||||
{(form.overlapIgnorePaths && form.overlapIgnorePaths.length > 0 ? form.overlapIgnorePaths : [""]).map((path, index) => (
|
{(form.overlapIgnorePaths && form.overlapIgnorePaths.length > 0 ? form.overlapIgnorePaths : [""]).map((path, index) => (<div key={`overlap-ignore-${index}`} className="settings-overlap-ignore-row">
|
||||||
<div key={`overlap-ignore-${index}`} className="settings-overlap-ignore-row">
|
|
||||||
<div className="settings-overlap-ignore-path-controls">
|
<div className="settings-overlap-ignore-path-controls">
|
||||||
<input
|
<input type="text" value={path} placeholder={t("settings.scheduling.docs", "docs/")} onChange={(e) => onOverlapIgnorePathChange(index, e.target.value)}/>
|
||||||
type="text"
|
<button type="button" className="btn btn-sm" onClick={() => onOpenOverlapPathPicker(index)} aria-label={`Browse path for ignored overlap entry ${index + 1}`}>{t("settings.scheduling.browse", " Browse ")}</button>
|
||||||
value={path}
|
|
||||||
placeholder="docs/"
|
|
||||||
onChange={(e) => onOverlapIgnorePathChange(index, e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => onOpenOverlapPathPicker(index)}
|
|
||||||
aria-label={`Browse path for ignored overlap entry ${index + 1}`}
|
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={() => onRemoveOverlapIgnorePath(index)} disabled={(form.overlapIgnorePaths ?? []).length === 0 && index === 0}>{t("settings.scheduling.remove", " Remove ")}</button>
|
||||||
type="button"
|
</div>))}
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => onRemoveOverlapIgnorePath(index)}
|
|
||||||
disabled={(form.overlapIgnorePaths ?? []).length === 0 && index === 0}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" className="btn btn-sm" onClick={onAddOverlapIgnorePath}>{t("settings.scheduling.addIgnoredPath", " Add ignored path ")}</button>
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={onAddOverlapIgnorePath}
|
|
||||||
>
|
|
||||||
Add ignored path
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-section-divider" />
|
<div className="settings-section-divider"/>
|
||||||
|
|
||||||
<h5 className="settings-section-heading">Step Execution</h5>
|
<h5 className="settings-section-heading">{t("settings.scheduling.stepExecution", "Step Execution")}</h5>
|
||||||
<MovedSettingsStub
|
<MovedSettingsStub message={t("settings.movedStub.stepExecution", "Step execution settings (run steps in new sessions, max parallel steps) now live on the workflow.")} onOpenWorkflowSettings={onOpenWorkflowSettings}/>
|
||||||
message={t(
|
</>);
|
||||||
"settings.movedStub.stepExecution",
|
|
||||||
"Step execution settings (run steps in new sessions, max parallel steps) now live on the workflow.",
|
|
||||||
)}
|
|
||||||
onOpenWorkflowSettings={onOpenWorkflowSettings}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SchedulingSection;
|
export default SchedulingSection;
|
||||||
|
|||||||
@@ -1,115 +1,50 @@
|
|||||||
/**
|
|
||||||
* Worktrees section (U9 / KTD-10).
|
|
||||||
*
|
|
||||||
* Project-scoped worktree limits/naming/dir, pre-merge rebase options, and the
|
|
||||||
* Worktrunk integration block (install affordance + binary path + failure mode).
|
|
||||||
* The worktrunk install status hook result is owned by the shell (its
|
|
||||||
* `installed` flag also gates the save flow) and relayed as props alongside the
|
|
||||||
* fetched git-remotes list, the worktrees-dir picker, and the approvals opener.
|
|
||||||
* Keys, conditional disabling, and the install-state affordance markup are
|
|
||||||
* preserved verbatim from the original inline JSX.
|
|
||||||
*/
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { GitRemoteDetailed } from "../../../api";
|
import type { GitRemoteDetailed } from "../../../api";
|
||||||
import type { useWorktrunkInstallStatus } from "../../../hooks/useWorktrunkInstallStatus";
|
import type { useWorktrunkInstallStatus } from "../../../hooks/useWorktrunkInstallStatus";
|
||||||
import type { SectionBaseProps, SettingsFormState } from "./context";
|
import type { SectionBaseProps, SettingsFormState } from "./context";
|
||||||
|
|
||||||
export interface WorktreesSectionProps extends SectionBaseProps {
|
export interface WorktreesSectionProps extends SectionBaseProps {
|
||||||
scopeBanner: ReactNode;
|
scopeBanner: ReactNode;
|
||||||
gitRemotes: GitRemoteDetailed[];
|
gitRemotes: GitRemoteDetailed[];
|
||||||
worktrunkInstall: ReturnType<typeof useWorktrunkInstallStatus>;
|
worktrunkInstall: ReturnType<typeof useWorktrunkInstallStatus>;
|
||||||
worktrunkInstallVerified: boolean;
|
worktrunkInstallVerified: boolean;
|
||||||
onOpenWorktreesDirPicker: () => void;
|
onOpenWorktreesDirPicker: () => void;
|
||||||
onOpenApprovals?: (approvalId?: string) => void;
|
onOpenApprovals?: (approvalId?: string) => void;
|
||||||
}
|
}
|
||||||
|
export function WorktreesSection({ scopeBanner, form, setForm, gitRemotes, worktrunkInstall, worktrunkInstallVerified, onOpenWorktreesDirPicker, onOpenApprovals, }: WorktreesSectionProps) {
|
||||||
export function WorktreesSection({
|
const { t } = useTranslation("app");
|
||||||
scopeBanner,
|
return (<>
|
||||||
form,
|
|
||||||
setForm,
|
|
||||||
gitRemotes,
|
|
||||||
worktrunkInstall,
|
|
||||||
worktrunkInstallVerified,
|
|
||||||
onOpenWorktreesDirPicker,
|
|
||||||
onOpenApprovals,
|
|
||||||
}: WorktreesSectionProps) {
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{scopeBanner}
|
{scopeBanner}
|
||||||
<h4 className="settings-section-heading">Worktrees</h4>
|
<h4 className="settings-section-heading">{t("settings.worktrees.worktrees", "Worktrees")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxWorktrees">Max Worktrees</label>
|
<label htmlFor="maxWorktrees">{t("settings.worktrees.maxWorktrees", "Max Worktrees")}</label>
|
||||||
<input
|
<input id="maxWorktrees" type="number" min={1} max={20} value={form.maxWorktrees ?? ""} onChange={(e) => {
|
||||||
id="maxWorktrees"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={20}
|
|
||||||
value={form.maxWorktrees ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, maxWorktrees: val === "" ? undefined : Number(val) } as SettingsFormState));
|
setForm((f) => ({ ...f, maxWorktrees: val === "" ? undefined : Number(val) } as SettingsFormState));
|
||||||
}}
|
}}/>
|
||||||
/>
|
<small>{t("settings.worktrees.limitsTotalGitWorktreesIncludingInReviewTasks", "Limits total git worktrees including in-review tasks")}</small>
|
||||||
<small>Limits total git worktrees including in-review tasks</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreeInitCommand">Worktree Init Command</label>
|
<label htmlFor="worktreeInitCommand">{t("settings.worktrees.worktreeInitCommand", "Worktree Init Command")}</label>
|
||||||
<input
|
<input id="worktreeInitCommand" type="text" placeholder={t("settings.worktrees.pnpmInstallFrozenLockfile", "pnpm install --frozen-lockfile")} value={form.worktreeInitCommand || ""} onChange={(e) => setForm((f) => ({ ...f, worktreeInitCommand: e.target.value }))}/>
|
||||||
id="worktreeInitCommand"
|
<small>{t("settings.worktrees.shellCommandToRunInEachNewWorktree", "Shell command to run in each new worktree after creation")}</small>
|
||||||
type="text"
|
|
||||||
placeholder="pnpm install --frozen-lockfile"
|
|
||||||
value={form.worktreeInitCommand || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, worktreeInitCommand: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<small>Shell command to run in each new worktree after creation</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="recycleWorktrees" className="checkbox-label">
|
<label htmlFor="recycleWorktrees" className="checkbox-label">
|
||||||
<input
|
<input id="recycleWorktrees" type="checkbox" checked={form.recycleWorktrees} onChange={(e) => setForm((f) => ({ ...f, recycleWorktrees: e.target.checked }))}/>{t("settings.worktrees.recycleWorktrees", " Recycle worktrees ")}</label>
|
||||||
id="recycleWorktrees"
|
<small>{t("settings.worktrees.offByDefaultOptInWhenEnabledCompleted", "Off by default (opt-in). When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.recycleWorktrees}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, recycleWorktrees: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Recycle worktrees
|
|
||||||
</label>
|
|
||||||
<small>Off by default (opt-in). When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="executorAllowSiblingBranchRename" className="checkbox-label">
|
<label htmlFor="executorAllowSiblingBranchRename" className="checkbox-label">
|
||||||
<input
|
<input id="executorAllowSiblingBranchRename" type="checkbox" checked={form.executorAllowSiblingBranchRename === true} onChange={(e) => setForm((f) => ({ ...f, executorAllowSiblingBranchRename: e.target.checked }))}/>{t("settings.worktrees.allowSilentSiblingBranchRenameDuringExecutorConflicts", " Allow silent sibling branch rename during executor conflicts ")}</label>
|
||||||
id="executorAllowSiblingBranchRename"
|
<small>{t("settings.worktrees.discouragedThisRestoresTheLegacyBehaviorWhereA", " Discouraged. This restores the legacy behavior where a live ")}<code>fusion/<task-id></code>{t("settings.worktrees.branchCollisionSilentlyForksWorkOntoSiblingBranches", " branch collision silently forks work onto sibling branches like ")}<code>-2</code>{t("settings.worktrees.andCanHidePriorCommitsFromTheDefault", " and can hide prior commits from the default recovery flow. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.executorAllowSiblingBranchRename === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, executorAllowSiblingBranchRename: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Allow silent sibling branch rename during executor conflicts
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
Discouraged. This restores the legacy behavior where a live <code>fusion/<task-id></code> branch collision silently forks work onto sibling branches like <code>-2</code> and can hide prior commits from the default recovery flow.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreeNaming">Worktree Naming Style</label>
|
<label htmlFor="worktreeNaming">{t("settings.worktrees.worktreeNamingStyle", "Worktree Naming Style")}</label>
|
||||||
<select
|
<select id="worktreeNaming" value={form.worktreeNaming || "random"} onChange={(e) => setForm((f) => ({ ...f, worktreeNaming: e.target.value as "random" | "task-id" | "task-title" }))} disabled={form.recycleWorktrees}>
|
||||||
id="worktreeNaming"
|
<option value="random">{t("settings.worktrees.randomNamesEGSwiftFalcon", "Random names (e.g., swift-falcon)")}</option>
|
||||||
value={form.worktreeNaming || "random"}
|
<option value="task-id">{t("settings.worktrees.taskIDEGFN042", "Task ID (e.g., FN-042)")}</option>
|
||||||
onChange={(e) =>
|
<option value="task-title">{t("settings.worktrees.taskTitleEGFixLoginBug", "Task title (e.g., fix-login-bug)")}</option>
|
||||||
setForm((f) => ({ ...f, worktreeNaming: e.target.value as "random" | "task-id" | "task-title" }))
|
|
||||||
}
|
|
||||||
disabled={form.recycleWorktrees}
|
|
||||||
>
|
|
||||||
<option value="random">Random names (e.g., swift-falcon)</option>
|
|
||||||
<option value="task-id">Task ID (e.g., FN-042)</option>
|
|
||||||
<option value="task-title">Task title (e.g., fix-login-bug)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>
|
||||||
{form.recycleWorktrees
|
{form.recycleWorktrees
|
||||||
@@ -118,206 +53,103 @@ export function WorktreesSection({
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreesDir">Worktrees Directory</label>
|
<label htmlFor="worktreesDir">{t("settings.worktrees.worktreesDirectory", "Worktrees Directory")}</label>
|
||||||
<div className="settings-overlap-ignore-path-controls">
|
<div className="settings-overlap-ignore-path-controls">
|
||||||
<input
|
<input id="worktreesDir" type="text" placeholder={t("settings.worktrees.defaultsToWorktreesLeaveEmptyUnlessOverriding", "Defaults to .worktrees \u2014 leave empty unless overriding")} value={form.worktreesDir || ""} disabled={form.worktrunk?.enabled === true} onChange={(e) => setForm((f) => ({ ...f, worktreesDir: e.target.value }))}/>
|
||||||
id="worktreesDir"
|
<button type="button" className="btn btn-sm" onClick={onOpenWorktreesDirPicker} aria-label={t("settings.worktrees.browseWorktreesDirectory", "Browse worktrees directory")} disabled={form.worktrunk?.enabled === true}>{t("settings.worktrees.browse", " Browse ")}</button>
|
||||||
type="text"
|
|
||||||
placeholder="Defaults to .worktrees — leave empty unless overriding"
|
|
||||||
value={form.worktreesDir || ""}
|
|
||||||
disabled={form.worktrunk?.enabled === true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, worktreesDir: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={onOpenWorktreesDirPicker}
|
|
||||||
aria-label="Browse worktrees directory"
|
|
||||||
disabled={form.worktrunk?.enabled === true}
|
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
{form.worktrunk?.enabled === true
|
{form.worktrunk?.enabled === true
|
||||||
? "Disabled because Worktrunk integration is enabled — worktrunk manages the worktree directory layout. Disable worktrunk integration to use a custom directory."
|
? "Disabled because Worktrunk integration is enabled — worktrunk manages the worktree directory layout. Disable worktrunk integration to use a custom directory."
|
||||||
: <>
|
: <>{t("settings.worktrees.optionalSupports", " Optional. Supports ")}<code>~</code>{t("settings.worktrees.and", " and ")}<code>{"{repo}"}</code>{t("settings.worktrees.defaultsTo", ". Defaults to ")}<code><projectRoot>/.worktrees</code>{t("settings.worktrees.whenUnsetOnlyAffectsNewlyCreatedWorktrees", " when unset. Only affects newly-created worktrees. ")}</>}
|
||||||
Optional. Supports <code>~</code> and <code>{"{repo}"}</code>. Defaults to <code><projectRoot>/.worktrees</code> when unset. Only affects newly-created worktrees.
|
|
||||||
</>}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreeRebaseBeforeMerge" className="checkbox-label">
|
<label htmlFor="worktreeRebaseBeforeMerge" className="checkbox-label">
|
||||||
<input
|
<input id="worktreeRebaseBeforeMerge" type="checkbox" checked={form.worktreeRebaseBeforeMerge !== false} onChange={(e) => setForm((f) => ({ ...f, worktreeRebaseBeforeMerge: e.target.checked }))}/>{t("settings.worktrees.rebaseFromRemoteBeforeMerge", " Rebase from remote before merge ")}</label>
|
||||||
id="worktreeRebaseBeforeMerge"
|
<small>{t("settings.worktrees.whenEnabledTheMergerFetchesFromTheConfigured", "When enabled, the merger fetches from the configured remote and rebases the task branch onto the latest default-branch tip before merging \u2014 catching concurrent pushes from other collaborators or fusion workers. Any conflicts the rebase surfaces flow into the existing smart/AI resolve pipeline.")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.worktreeRebaseBeforeMerge !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, worktreeRebaseBeforeMerge: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Rebase from remote before merge
|
|
||||||
</label>
|
|
||||||
<small>When enabled, the merger fetches from the configured remote and rebases the task branch onto the latest default-branch tip before merging — catching concurrent pushes from other collaborators or fusion workers. Any conflicts the rebase surfaces flow into the existing smart/AI resolve pipeline.</small>
|
|
||||||
</div>
|
</div>
|
||||||
{form.worktreeRebaseBeforeMerge !== false && (
|
{form.worktreeRebaseBeforeMerge !== false && (<div className="form-group">
|
||||||
<div className="form-group">
|
<label htmlFor="worktreeRebaseRemote">{t("settings.worktrees.rebaseRemote", "Rebase Remote")}</label>
|
||||||
<label htmlFor="worktreeRebaseRemote">Rebase Remote</label>
|
<select id="worktreeRebaseRemote" value={form.worktreeRebaseRemote ?? ""} onChange={(e) => setForm((f) => ({ ...f, worktreeRebaseRemote: e.target.value || undefined }))}>
|
||||||
<select
|
<option value="">{t("settings.worktrees.useGitDefault", "Use git default")}</option>
|
||||||
id="worktreeRebaseRemote"
|
{gitRemotes.map((remote) => (<option key={remote.name} value={remote.name}>
|
||||||
value={form.worktreeRebaseRemote ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, worktreeRebaseRemote: e.target.value || undefined }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="">Use git default</option>
|
|
||||||
{gitRemotes.map((remote) => (
|
|
||||||
<option key={remote.name} value={remote.name}>
|
|
||||||
{remote.name} ({remote.fetchUrl})
|
{remote.name} ({remote.fetchUrl})
|
||||||
</option>
|
</option>))}
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>{t("settings.worktrees.whichRemoteToFetchForThePreMerge", " Which remote to fetch for the pre-merge rebase. \"Use git default\" falls back to the remote configured for the default branch (typically ")}<code>origin</code>{t("settings.worktrees.closeParenPeriod", ").")}
|
||||||
Which remote to fetch for the pre-merge rebase. "Use git default" falls back to the remote configured for the default branch (typically <code>origin</code>).
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>)}
|
||||||
)}
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreeRebaseLocalBase" className="checkbox-label">
|
<label htmlFor="worktreeRebaseLocalBase" className="checkbox-label">
|
||||||
<input
|
<input id="worktreeRebaseLocalBase" type="checkbox" checked={form.worktreeRebaseLocalBase !== false} onChange={(e) => setForm((f) => ({ ...f, worktreeRebaseLocalBase: e.target.checked }))}/>{t("settings.worktrees.alsoRebaseOntoLocalDefaultBranchHEAD", " Also rebase onto local default-branch HEAD ")}</label>
|
||||||
id="worktreeRebaseLocalBase"
|
<small>{t("settings.worktrees.inAdditionToTheRemoteRebaseAboveAlso", " In addition to the remote rebase above, also rebase the task branch onto the local default-branch HEAD (rootDir). This catches sibling tasks that merged locally but haven't been pushed yet \u2014 without it, two concurrent tasks where one deletes code can have the other silently re-introduce it via the fallback strategy. Enabled by default; only disable if it causes issues with your workflow. ")}</small>
|
||||||
type="checkbox"
|
|
||||||
checked={form.worktreeRebaseLocalBase !== false}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({ ...f, worktreeRebaseLocalBase: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Also rebase onto local default-branch HEAD
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
In addition to the remote rebase above, also rebase the task branch onto the local default-branch HEAD (rootDir). This catches sibling tasks that merged locally but haven't been pushed yet — without it, two concurrent tasks where one deletes code can have the other silently re-introduce it via the fallback strategy. Enabled by default; only disable if it causes issues with your workflow.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">Worktrunk integration</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.worktrees.worktrunkIntegration", "Worktrunk integration")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktrunkEnabled" className="checkbox-label">
|
<label htmlFor="worktrunkEnabled" className="checkbox-label">
|
||||||
<input
|
<input id="worktrunkEnabled" type="checkbox" checked={form.worktrunk?.enabled === true} disabled={!worktrunkInstallVerified && form.worktrunk?.enabled !== true} onChange={(e) => setForm((f) => ({
|
||||||
id="worktrunkEnabled"
|
...f,
|
||||||
type="checkbox"
|
worktrunk: {
|
||||||
checked={form.worktrunk?.enabled === true}
|
enabled: e.target.checked,
|
||||||
disabled={!worktrunkInstallVerified && form.worktrunk?.enabled !== true}
|
binaryPath: f.worktrunk?.binaryPath ?? "",
|
||||||
onChange={(e) =>
|
onFailure: f.worktrunk?.onFailure ?? "fail",
|
||||||
setForm((f) => ({
|
},
|
||||||
...f,
|
}))}/>{t("settings.worktrees.enableWorktrunkIntegration", " Enable worktrunk integration ")}</label>
|
||||||
worktrunk: {
|
<small>{t("settings.worktrees.disabledByDefaultOptInWhenEnabledFusion", " Disabled by default (opt-in). When enabled, Fusion shells out to ")}<code>worktrunk</code>{t("settings.worktrees.forWorktreeCreateSyncPruneAndRemoveOperations", " for worktree create, sync, prune, and remove operations and follows worktrunk's directory layout. ")}</small>
|
||||||
enabled: e.target.checked,
|
{!worktrunkInstallVerified && form.worktrunk?.enabled !== true && (<small className="settings-muted">{t("settings.worktrees.installTheWorktrunkBinaryBelowToEnableThis", "Install the worktrunk binary below to enable this integration.")}</small>)}
|
||||||
binaryPath: f.worktrunk?.binaryPath ?? "",
|
|
||||||
onFailure: f.worktrunk?.onFailure ?? "fail",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Enable worktrunk integration
|
|
||||||
</label>
|
|
||||||
<small>
|
|
||||||
Disabled by default (opt-in). When enabled, Fusion shells out to <code>worktrunk</code> for worktree create, sync, prune, and remove operations and follows worktrunk's directory layout.
|
|
||||||
</small>
|
|
||||||
{!worktrunkInstallVerified && form.worktrunk?.enabled !== true && (
|
|
||||||
<small className="settings-muted">Install the worktrunk binary below to enable this integration.</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" data-testid="worktrunk-install-affordance">
|
<div className="form-group" data-testid="worktrunk-install-affordance">
|
||||||
{worktrunkInstall.status === "installed" && (
|
{worktrunkInstall.status === "installed" && (<small className="settings-muted">{t("settings.worktrees.worktrunk", " worktrunk ")}{worktrunkInstall.version ?? ""}{t("settings.worktrees.installedAt", " installed at ")}{worktrunkInstall.installPath ?? "~/.fusion/bin/worktrunk"}
|
||||||
<small className="settings-muted">
|
</small>)}
|
||||||
worktrunk {worktrunkInstall.version ?? ""} installed at {worktrunkInstall.installPath ?? "~/.fusion/bin/worktrunk"}
|
{(worktrunkInstall.status === "missing" || worktrunkInstall.status === "installing") && (<>
|
||||||
</small>
|
<button type="button" className="btn btn-primary" onClick={() => void worktrunkInstall.requestInstall()} disabled={worktrunkInstall.requesting || worktrunkInstall.status === "installing"}>
|
||||||
)}
|
|
||||||
{(worktrunkInstall.status === "missing" || worktrunkInstall.status === "installing") && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => void worktrunkInstall.requestInstall()}
|
|
||||||
disabled={worktrunkInstall.requesting || worktrunkInstall.status === "installing"}
|
|
||||||
>
|
|
||||||
{t("settings.worktrees.installWorktrunk", "Install worktrunk binary")}
|
{t("settings.worktrees.installWorktrunk", "Install worktrunk binary")}
|
||||||
</button>
|
</button>
|
||||||
<small className="settings-muted">Enable worktrunk and request approval to install the pinned release.</small>
|
<small className="settings-muted">{t("settings.worktrees.enableWorktrunkAndRequestApprovalToInstallThe", "Enable worktrunk and request approval to install the pinned release.")}</small>
|
||||||
</>
|
</>)}
|
||||||
)}
|
{worktrunkInstall.status === "pending-approval" && (<>
|
||||||
{worktrunkInstall.status === "pending-approval" && (
|
|
||||||
<>
|
|
||||||
<small className="settings-muted">{t("settings.worktrees.awaitingApproval", "Awaiting approval — open Approvals to continue.")}</small>
|
<small className="settings-muted">{t("settings.worktrees.awaitingApproval", "Awaiting approval — open Approvals to continue.")}</small>
|
||||||
<button
|
<button type="button" className="btn btn-secondary" onClick={() => onOpenApprovals?.(worktrunkInstall.pendingApprovalId)}>
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => onOpenApprovals?.(worktrunkInstall.pendingApprovalId)}
|
|
||||||
>
|
|
||||||
{t("settings.worktrees.openApprovals", "Open Approvals")}
|
{t("settings.worktrees.openApprovals", "Open Approvals")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>)}
|
||||||
)}
|
{(worktrunkInstall.status === "denied" || worktrunkInstall.status === "failed") && (<>
|
||||||
{(worktrunkInstall.status === "denied" || worktrunkInstall.status === "failed") && (
|
|
||||||
<>
|
|
||||||
<small style={{ color: "var(--color-error)" }}>{worktrunkInstall.error ?? "Worktrunk install failed."}</small>
|
<small style={{ color: "var(--color-error)" }}>{worktrunkInstall.error ?? "Worktrunk install failed."}</small>
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => void worktrunkInstall.requestInstall()}>
|
<button type="button" className="btn btn-secondary" onClick={() => void worktrunkInstall.requestInstall()}>
|
||||||
{t("settings.worktrees.tryAgain", "Try again")}
|
{t("settings.worktrees.tryAgain", "Try again")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktrunkBinaryPath">Worktrunk binary path</label>
|
<label htmlFor="worktrunkBinaryPath">{t("settings.worktrees.worktrunkBinaryPath", "Worktrunk binary path")}</label>
|
||||||
<input
|
<input id="worktrunkBinaryPath" type="text" className="input" placeholder={t("settings.worktrees.autoDetectFusionBinWorktrunkOrPATH", "auto-detect (~/.fusion/bin/worktrunk or $PATH)")} value={form.worktrunk?.binaryPath ?? ""} disabled={form.worktrunk?.enabled !== true} onChange={(e) => setForm((f) => ({
|
||||||
id="worktrunkBinaryPath"
|
...f,
|
||||||
type="text"
|
worktrunk: {
|
||||||
className="input"
|
|
||||||
placeholder="auto-detect (~/.fusion/bin/worktrunk or $PATH)"
|
|
||||||
value={form.worktrunk?.binaryPath ?? ""}
|
|
||||||
disabled={form.worktrunk?.enabled !== true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
worktrunk: {
|
|
||||||
enabled: f.worktrunk?.enabled === true,
|
enabled: f.worktrunk?.enabled === true,
|
||||||
binaryPath: e.target.value,
|
binaryPath: e.target.value,
|
||||||
onFailure: f.worktrunk?.onFailure ?? "fail",
|
onFailure: f.worktrunk?.onFailure ?? "fail",
|
||||||
},
|
},
|
||||||
}))
|
}))}/>
|
||||||
}
|
<small>{t("settings.worktrees.optionalLeaveBlankToAutoResolveFusionWill", "Optional. Leave blank to auto-resolve; Fusion will offer to install on first use.")}</small>
|
||||||
/>
|
|
||||||
<small>Optional. Leave blank to auto-resolve; Fusion will offer to install on first use.</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktrunkOnFailure">Worktrunk failure behavior</label>
|
<label htmlFor="worktrunkOnFailure">{t("settings.worktrees.worktrunkFailureBehavior", "Worktrunk failure behavior")}</label>
|
||||||
<select
|
<select id="worktrunkOnFailure" className="select" value={form.worktrunk?.onFailure ?? "fail"} disabled={form.worktrunk?.enabled !== true} onChange={(e) => setForm((f) => ({
|
||||||
id="worktrunkOnFailure"
|
...f,
|
||||||
className="select"
|
worktrunk: {
|
||||||
value={form.worktrunk?.onFailure ?? "fail"}
|
|
||||||
disabled={form.worktrunk?.enabled !== true}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((f) => ({
|
|
||||||
...f,
|
|
||||||
worktrunk: {
|
|
||||||
enabled: f.worktrunk?.enabled === true,
|
enabled: f.worktrunk?.enabled === true,
|
||||||
binaryPath: f.worktrunk?.binaryPath ?? "",
|
binaryPath: f.worktrunk?.binaryPath ?? "",
|
||||||
onFailure: e.target.value as "fail" | "fallback-native",
|
onFailure: e.target.value as "fail" | "fallback-native",
|
||||||
},
|
},
|
||||||
}))
|
}))}>
|
||||||
}
|
<option value="fail">{t("settings.worktrees.failAndPauseTheTaskDefault", "Fail and pause the task (default)")}</option>
|
||||||
>
|
<option value="fallback-native">{t("settings.worktrees.fallBackToFusionsNativeWorktreeBackend", "Fall back to Fusion's native worktree backend")}</option>
|
||||||
<option value="fail">Fail and pause the task (default)</option>
|
|
||||||
<option value="fallback-native">Fall back to Fusion's native worktree backend</option>
|
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>
|
||||||
<code>fail</code> stops on worktrunk errors for explicit operator recovery; <code>fallback-native</code> keeps progress moving by switching to Fusion's built-in worktree backend.
|
<code>fail</code>{t("settings.worktrees.stopsOnWorktrunkErrorsForExplicitOperatorRecovery", " stops on worktrunk errors for explicit operator recovery; ")}<code>fallback-native</code>{t("settings.worktrees.keepsProgressMovingBySwitchingToFusionApos", " keeps progress moving by switching to Fusion's built-in worktree backend. ")}</small>
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WorktreesSection;
|
export default WorktreesSection;
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "Day of month (1–28)",
|
"resetDayMonthly": "Day of month (1–28)",
|
||||||
"resetDayWeekly": "Day of week (0=Sun)",
|
"resetDayWeekly": "Day of week (0=Sun)",
|
||||||
"resetting": "Resetting...",
|
"resetting": "Resetting...",
|
||||||
|
"resizeSidebar": "Resize agents sidebar",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
"resultCreated_one": "{{count}} created",
|
"resultCreated_one": "{{count}} created",
|
||||||
"resultCreated_other": "{{count}} created",
|
"resultCreated_other": "{{count}} created",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "Create room"
|
"title": "Create room"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "Initializing dashboard...",
|
"initializingDashboard": "Initializing dashboard...",
|
||||||
"loadingMessage": "Loading Fusion dashboard",
|
"loadingMessage": "Loading Fusion dashboard",
|
||||||
"loadingProgress": "Dashboard loading progress",
|
"loadingProgress": "Dashboard loading progress",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save"
|
"save": "Save"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "Agent Permissions",
|
||||||
|
"agentProvisioningApprovals": "Agent Provisioning Approvals",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). ",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": "Per-agent settings override project defaults. Each category controls a separate approval gate."
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "Hide AI session notification banners",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"languageAuto": "Auto",
|
"languageAuto": "Auto",
|
||||||
"languageAutoHint": "Follow the browser language",
|
"languageAutoHint": "Follow the browser language",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": " Suppress the “needs your input” banner that appears when AI sessions are awaiting input or have failed. ",
|
||||||
"title": "Appearance"
|
"title": "Appearance"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "Authentication",
|
"title": "Authentication",
|
||||||
"waitingForLogin": "Waiting for login…"
|
"waitingForLogin": "Waiting for login…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "Enter API key",
|
||||||
|
"key": "Key: "
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "0 2 * * *",
|
||||||
|
"03": "0 3 * * *",
|
||||||
|
"agentsOnlyFusionAgentMemory": "Agents only (.fusion/agent-memory)",
|
||||||
|
"allProjectAgents": "All (project + agents)",
|
||||||
|
"and": "...and ",
|
||||||
"backupCreated": "Backup created successfully",
|
"backupCreated": "Backup created successfully",
|
||||||
|
"backupDirectory": "Backup Directory",
|
||||||
"backupNow": "Backup Now",
|
"backupNow": "Backup Now",
|
||||||
"creating": "Creating…"
|
"backups": "backups",
|
||||||
|
"backupS": " backup(s)",
|
||||||
|
"backupScheduleCron": "Backup Schedule (Cron)",
|
||||||
|
"creating": "Creating…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": " Cron expression for backup timing. Default: 0 2 * * * (daily at 2 AM). Examples: 0 * * * * (hourly), 0 0 * * 0 (weekly), */15 * * * * (every 15 min) ",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).",
|
||||||
|
"currentBackups": "Current Backups",
|
||||||
|
"databaseBackups": "Database Backups",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "Directory for backup files, relative to project root",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "Directory for memory backups, relative to project root.",
|
||||||
|
"enableAutomaticDatabaseBackups": " Enable automatic database backups ",
|
||||||
|
"enableAutomaticMemoryBackups": " Enable automatic memory backups ",
|
||||||
|
"fusionBackups": ".fusion/backups",
|
||||||
|
"fusionBackupsMemory": ".fusion/backups/memory",
|
||||||
|
"invalidCronExpressionFormat": "Invalid cron expression format",
|
||||||
|
"loadingBackupInfo": "Loading backup info…",
|
||||||
|
"memoryBackupDirectory": "Memory Backup Directory",
|
||||||
|
"memoryBackups": "Memory Backups",
|
||||||
|
"memoryBackupScheduleCron": "Memory Backup Schedule (Cron)",
|
||||||
|
"memoryBackupScope": "Memory Backup Scope",
|
||||||
|
"memoryRetentionCount": "Memory Retention Count",
|
||||||
|
"more": " more",
|
||||||
|
"mustBeBetween1And100": "Must be between 1 and 100",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "Number of backup files to keep (oldest are deleted first). Range: 1-100.",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "Number of memory backups to keep (oldest are deleted first). Range: 1-100.",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "Path cannot contain parent directory traversal (..)",
|
||||||
|
"projectOnlyFusionMemory": "Project only (.fusion/memory)",
|
||||||
|
"retentionCount": "Retention Count",
|
||||||
|
"totalSize": "total size",
|
||||||
|
"view": "View ",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "When enabled, project and agent memory files are backed up automatically on a schedule.",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": "When enabled, the database is backed up automatically on a schedule"
|
||||||
},
|
},
|
||||||
"clearToDefault": "Reset to default",
|
"clearToDefault": "Reset to default",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": "Failed to save CLI agent settings"
|
"saveFailed": "Failed to save CLI agent settings"
|
||||||
},
|
},
|
||||||
"closeModal": "Close conflict modal",
|
"closeModal": "Close conflict modal",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "Build Command",
|
||||||
|
"commands": "Commands",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "Command used to build the project — injected into generated task specs",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "Command used to run tests — injected into generated task specs",
|
||||||
|
"eGPnpmBuild": "e.g. pnpm build",
|
||||||
|
"eGPnpmTest": "e.g. pnpm test",
|
||||||
|
"testCommand": "Test Command"
|
||||||
|
},
|
||||||
"conflictModalTitle": "Resolve Settings Conflicts",
|
"conflictModalTitle": "Resolve Settings Conflicts",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "Experimental Features",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. ",
|
||||||
|
"featureFlags": "Feature Flags"
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"version": "Version {{version}}"
|
"version": "Version {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "25",
|
||||||
|
"200": "200",
|
||||||
|
"3000": "3000",
|
||||||
|
"14Days": "14 days",
|
||||||
|
"30Days": "30 days",
|
||||||
|
"60Days": "60 days",
|
||||||
|
"7Days": "7 days",
|
||||||
|
"90Days": "90 days",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ",
|
||||||
|
"autoCleanupOldChats": "Auto-cleanup old chats",
|
||||||
|
"autoPruneOldMail": "Auto-prune old mail",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "Banner fires when todo count is strictly greater than this value (default 20). Applies when the banner is enabled.",
|
||||||
|
"builtInWorkflows": "Built-in workflows",
|
||||||
|
"capacityRiskBanner": "Capacity Risk Banner",
|
||||||
|
"chatHistory": "Chat history",
|
||||||
|
"chatRooms": "Chat Rooms",
|
||||||
|
"completionDocumentationAutomation": "Completion Documentation Automation",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": " Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal. ",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.",
|
||||||
|
"defaultTrackingModeForNewTasks": "Default tracking mode for new tasks",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "Delete chat sessions and rooms that have been idle for this many days. Default: Off.",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": " Enable summarization in Project Models to configure that model.",
|
||||||
|
"fN": "FN",
|
||||||
|
"general": "General",
|
||||||
|
"gitHubTracking": "GitHub Tracking",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "Hard cap on the synthesized \"Earlier room context\" summary block. Default: 3000.",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "New tasks inherit this custom workflow's steps (overridable per task)",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.",
|
||||||
|
"off": "Off",
|
||||||
|
"offDefault": "Off (default)",
|
||||||
|
"onForNewTasks": "On for new tasks",
|
||||||
|
"operationalLogRetention": "Operational log retention",
|
||||||
|
"ownerRepo": "owner/repo",
|
||||||
|
"prefixForNewTaskIDsEGKB": "Prefix for new task IDs (e.g. KB, PROJ)",
|
||||||
|
"prefixMustBe110UppercaseLetters": "Prefix must be 1–10 uppercase letters",
|
||||||
|
"projectDefaultTrackingRepo": "Project default tracking repo",
|
||||||
|
"recentVerbatimRoomMessages": "Recent verbatim room messages",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "Require changelog update (existing changelog)",
|
||||||
|
"requireChangesetChangesetMd": "Require changeset (.changeset/*.md)",
|
||||||
|
"roomCompactionFetchLimit": "Room compaction fetch limit",
|
||||||
|
"roomSummaryMaxCharacters": "Room summary max characters",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": " Search the tracking repo for likely duplicates before opening a new issue ",
|
||||||
"settingsSaved": "Settings saved",
|
"settingsSaved": "Settings saved",
|
||||||
|
"showCapacityRiskBanner": " Show capacity risk banner ",
|
||||||
|
"showQuickChatButton": " Show quick chat button ",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.",
|
||||||
|
"taskPrefix": "Task Prefix",
|
||||||
|
"todoThreshold": "Todo threshold",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": " Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models. ",
|
||||||
"updateAvailablePrefix": "v{{version}} available",
|
"updateAvailablePrefix": "v{{version}} available",
|
||||||
"updateCheckFailed": "Failed to check for updates",
|
"updateCheckFailed": "Failed to check for updates",
|
||||||
"updateFailed": "Update failed",
|
"updateFailed": "Update failed",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "Updated to v{{version}} — restart Fusion to apply",
|
"updateSuccess": "Updated to v{{version}} — restart Fusion to apply",
|
||||||
"updateSuccessToast": "Update installed. Restart Fusion to apply it.",
|
"updateSuccessToast": "Update installed. Restart Fusion to apply it.",
|
||||||
"updating": "Updating…",
|
"updating": "Updating…",
|
||||||
"upToDate": "You're up to date ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.",
|
||||||
|
"upToDate": "You're up to date ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": " Use ephemeral task-worker agents ",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "Warn on the board when todo work exceeds the threshold and no idle agents are available.",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": " When enabled (default), Fusion spawns short-lived ",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. ",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": " workflows, or changelog mode when contributors should update an existing changelog file. "
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. ",
|
||||||
|
"autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ",
|
||||||
|
"checkForThe": " Check for the ",
|
||||||
|
"checkForUpdatesAutomatically": " Check for updates automatically ",
|
||||||
|
"cLIBinaryOnPATH": " CLI binary on PATH ",
|
||||||
|
"cLIBySpawning": " CLI by spawning",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": " Controls how often the dashboard re-fetches the npm registry. Use the version + refresh control in the header to trigger an immediate check at any time. ",
|
||||||
|
"dailyRecommended": "Daily (recommended)",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": ". Disable this if your local dev process is the source of truth and you don't want any outdated globally-installed binary executed during the probe. ",
|
||||||
|
"frequency": "Frequency",
|
||||||
|
"general": "General",
|
||||||
|
"globalDefaultTrackingRepo": "Global default tracking repo",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
|
||||||
|
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
|
||||||
|
"onStartupOncePerServerLaunch": "On startup — once per server launch",
|
||||||
|
"ownerRepo": "owner/repo",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo.",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": " rows and does not affect assistant text or tool rows. ",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": " Save AI thinking for ephemeral / task-worker agents ",
|
||||||
|
"saveAIThinkingForPermanentAgents": " Save AI thinking for permanent agents ",
|
||||||
|
"saveAIThinkingLogs": "Save AI thinking logs",
|
||||||
|
"saveToolOutputInAgentLogs": " Save tool output in agent logs ",
|
||||||
|
"updates": "Updates",
|
||||||
|
"weekly": "Weekly",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. ",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": " When enabled (default), the dashboard automatically reloads when it detects a new build version — either from server rebuilds or service worker updates. Disable this to stay on the current version until you manually refresh. ",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": " When enabled, Fusion checks npm for new versions of",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": " When enabled, the dashboard probes for a globally-installed"
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "allow",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "Comma-separated values sent to OpenRouter model sync.",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.",
|
||||||
|
"default": "Default",
|
||||||
|
"default2": "default",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.",
|
||||||
|
"defaultModel": "Default Model",
|
||||||
|
"deny": "deny",
|
||||||
|
"fallbackModel": "Fallback Model",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": " flow and publishes them under the opencode-go provider in model pickers. ",
|
||||||
|
"fusion": "Fusion",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": " Global baseline models for each AI role. Project settings can override these per-project. ",
|
||||||
|
"httpsRunfusionAi": "https://runfusion.ai",
|
||||||
|
"latency": "latency",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "Leave empty to omit this header. Default: Fusion.",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "Leave empty to omit this header. Default: https://runfusion.ai.",
|
||||||
|
"modelLanes": "Model Lanes",
|
||||||
|
"noFallback": "No fallback",
|
||||||
|
"openaiAnthropic": "openai, anthropic",
|
||||||
|
"openRouterAdvanced": "OpenRouter advanced",
|
||||||
|
"openRouterAllowFallbacks": "OpenRouter allow fallbacks",
|
||||||
|
"openRouterHTTPReferer": "OpenRouter HTTP-Referer",
|
||||||
|
"openRouterOutputModalitiesFilter": "OpenRouter output_modalities filter",
|
||||||
|
"openRouterRoutingIgnore": "OpenRouter routing ignore",
|
||||||
|
"openRouterRoutingOnly": "OpenRouter routing only",
|
||||||
|
"openRouterRoutingOrder": "OpenRouter routing order",
|
||||||
|
"openRouterRoutingSort": "OpenRouter routing sort",
|
||||||
|
"openRouterSupportedParametersFilter": "OpenRouter supported_parameters filter",
|
||||||
|
"openRouterXTitle": "OpenRouter X-Title",
|
||||||
|
"price": "price",
|
||||||
|
"providerName": "provider-name",
|
||||||
|
"requireParameters": " Require parameters ",
|
||||||
|
"startupModelSync": "Startup Model Sync",
|
||||||
|
"syncOpencodeGoModelListAtStartup": " Sync opencode-go model list at startup ",
|
||||||
|
"syncOpenRouterModelListAtStartup": " Sync OpenRouter model list at startup ",
|
||||||
|
"text": "text",
|
||||||
|
"thinkingEffort": "Thinking Effort",
|
||||||
|
"throughput": "throughput",
|
||||||
|
"toolsStructuredOutputs": "tools, structured_outputs",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "Used automatically if the primary default model hits a retryable provider error like rate limiting or overload.",
|
||||||
|
"useDefault": "Use default",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": " When enabled, startup fetches the latest available models from the OpenRouter API so model pickers always include the newest catalog. ",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "Keep Remote",
|
"keepRemote": "Keep Remote",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "0 3 * * *",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "Agents get memory_search, memory_get, and memory_append tools. Search defaults to qmd with a local file fallback.",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "Automatically compact memory when it exceeds the threshold on a schedule",
|
||||||
|
"autoSummarizeMemory": " Auto-Summarize Memory ",
|
||||||
|
"bytesUpdated": " bytes · updated ",
|
||||||
|
"cannotSave": "Cannot save: ",
|
||||||
|
"checkingMemoryWriteAccess": "Checking memory write access...",
|
||||||
"compacting": "Compacting…",
|
"compacting": "Compacting…",
|
||||||
|
"compactionThresholdChars": "Compaction Threshold (chars)",
|
||||||
"compactSelectedFile": "Compact Selected File",
|
"compactSelectedFile": "Compact Selected File",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "Cron expression for auto-summarize schedule (default: daily at 3 AM)",
|
||||||
|
"cronExpressionForDreamProcessing": "Cron expression for dream processing.",
|
||||||
"dreamCompleted": "Dream processing completed",
|
"dreamCompleted": "Dream processing completed",
|
||||||
|
"dreaming": " Dreaming… ",
|
||||||
"dreamNow": "Dream Now",
|
"dreamNow": "Dream Now",
|
||||||
|
"dreamSchedule": "Dream Schedule",
|
||||||
|
"enableMemoryTools": " Enable memory tools ",
|
||||||
|
"failedToLoadBackendStatus": "Failed to load backend status: ",
|
||||||
|
"for": "for \"",
|
||||||
"installing": "Installing…",
|
"installing": "Installing…",
|
||||||
"installQmd": "Install qmd",
|
"installQmd": "Install qmd",
|
||||||
|
"loadingMemory": "Loading memory…",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "Manually trigger dream processing now.",
|
||||||
|
"memory": "Memory",
|
||||||
"memoryCompacted": "Memory file compacted",
|
"memoryCompacted": "Memory file compacted",
|
||||||
|
"memoryFile": "Memory File",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": " Memory is configured with a read-only backend. You can view the file, but saving is disabled. ",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": " Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled. ",
|
||||||
|
"memoryLivesIn": " Memory lives in ",
|
||||||
"memorySaved": "Memory saved",
|
"memorySaved": "Memory saved",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "Memory will be compacted when it exceeds this character count",
|
||||||
|
"noMatchingMemoryFound": "No matching memory found.",
|
||||||
|
"processDreamsFromDailyMemory": " Process dreams from daily memory ",
|
||||||
|
"qmd": " qmd ",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": " qmd is not installed. Search will use local files. Install indexed retrieval: ",
|
||||||
|
"result": " result",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "Runs the same qmd-backed memory_search path agents use.",
|
||||||
"saveMemory": "Save Memory",
|
"saveMemory": "Save Memory",
|
||||||
|
"scheduleCron": "Schedule (cron)",
|
||||||
|
"searchMemoryWithQmd": "Search memory with qmd",
|
||||||
"testing": "Testing…",
|
"testing": "Testing…",
|
||||||
"testRetrieval": "Test Retrieval"
|
"testRetrieval": "Test Retrieval",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md."
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "Abort",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "Abort — one AI attempt; require manual resolution if it fails",
|
||||||
|
"addFusionAsCoAuthorOnCommits": " Add Fusion as co-author on commits ",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": " After Fusion advances the integration branch ref, the merger can auto-sync other worktrees still checked out on that branch (typically your project-root checkout). ",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3. The reviewer uses your project's reviewer/validator model.",
|
||||||
|
"aIMerge": "AI merge",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "AI merge (default) — AI merges in a clean room, an AI reviewer audits with retries, then lands",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": " AI mode merges the task branch into an isolated clean-room checkout at the target branch's tip, has an AI reviewer audit the squash (with corrective retries — advisory concerns land with a logged warning, an unfixable correctness concern hard-fails), then fast-forwards the target branch and syncs your local checkout (AI reconciles a conflicting restore). Each task merges to its own target branch, or the default integration branch. ",
|
||||||
|
"aIOnly": "AI only",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "AI only — AI → auto-resolve → AI retry; never silently pick a side",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": " Allow AI merge to sync a dirty checked-out integration branch ",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "Always preserve direct-merge commit history",
|
||||||
|
"alwaysSquashDirectMerges": "Always squash direct merges",
|
||||||
|
"and": ", and ",
|
||||||
|
"and2": " and ",
|
||||||
|
"areListedFirstOrChoose": " are listed first — or choose ",
|
||||||
|
"autoCompletionMode": "Auto-completion mode",
|
||||||
|
"autoDetect": "auto-detect",
|
||||||
|
"autoDetectOriginHEADMain": "(auto-detect — origin/HEAD → main)",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": " Auto keeps today's squash behavior for branches with zero or one substantive commit, but switches multi-substantive branches to a history-preserving rebase-and-merge path. Individual tasks can override this in PROMPT.md with ",
|
||||||
|
"autoMergeCompletedTasks": " Auto-merge completed tasks ",
|
||||||
|
"autoMergeConflictRetries": "Auto-merge conflict retries",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": " Auto-merge runs in the task worktree by default. Switch to the legacy project-root path only if you need the pre-FN-5279 fallback; worktrunk-managed projects still defer to worktrunk. ",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": " Auto-merge will run rebase, conflict resolution, and squash commits inside the project root (the user's checked-out integration-branch worktree) instead of the task worktree. Fusion assumes that directory is already on the integration branch and clean; if it isn't, merges may fail or touch the user's working tree. Reuse-task-worktree is the recommended default (FN-5279). Switch back unless you have a specific reason to opt in (FN-5348). ",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": " Auto-resolve conflicts in lock files and generated files ",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "Auto — squash single-substantive branches, preserve multi-substantive history",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "Auto-sync project checkout after merge",
|
||||||
|
"block": "Block",
|
||||||
|
"blockStrict": "Block (strict)",
|
||||||
|
"both": " Both ",
|
||||||
|
"branchName": "branch name",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "Checking for legacy auto-merge stamps…",
|
||||||
|
"coAuthorEmail": "Co-author Email",
|
||||||
|
"coAuthorName": "Co-author Name",
|
||||||
|
"conflictFallbackStrategy": "Conflict Fallback Strategy",
|
||||||
|
"controlsThePostMergeAuditGate": " Controls the post-merge audit gate. ",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": " Controls what happens after a task reaches In Review. Direct mode merges into the current branch locally. Pull request mode keeps the task in In Review while Fusion waits for GitHub reviews and required checks before merging the PR. ",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "Create, monitor, and merge a GitHub pull request",
|
||||||
|
"custom": "Custom…",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": " Dangerous compatibility escape hatch. Leave off unless you explicitly want the legacy stash → fast-forward → restore behavior when your checked-out integration branch has unrelated local edits. When off, AI merge blocks before advancing the branch so dirty project-root edits cannot contaminate a completed merge. ",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": " (default) logs findings but auto-completes the merge. ",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "Deterministic (legacy) — rebase / conflict-strategy / audit pipeline",
|
||||||
|
"directMergeCommitRouting": "Direct merge commit routing",
|
||||||
|
"directMergeIntoTheCurrentBranch": "Direct merge into the current branch",
|
||||||
|
"emailUsedInThe": "Email used in the ",
|
||||||
|
"enableTestMode": " Enable test mode ",
|
||||||
|
"fastForwardOfLocalMainFrom": " + fast-forward of local main from ",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "Fast-forward only — skip dirty worktrees",
|
||||||
|
"finalFallback": "final fallback",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": " Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp. Dry-run is automatic; applying delegates to the store cleanup and preserves genuine per-task overrides. ",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "Flip overlapping files to prefer the task branch (default)",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "Forces all AI lanes to use the deterministic mock provider. No network calls, zero token cost.",
|
||||||
|
"fusion": "Fusion",
|
||||||
|
"gitHubAuthentication": "GitHub Authentication",
|
||||||
|
"gitHubAuthMode": "GitHub auth mode",
|
||||||
|
"gitHubCLIGhAuth": "GitHub CLI (gh auth)",
|
||||||
|
"gitHubPersonalAccessToken": "GitHub personal access token",
|
||||||
|
"gitRemoteToPushToEGOrigin": "Git remote to push to (e.g. \"origin\"). Can include branch name (e.g. \"origin main\"). Default: \"origin\".",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "Ignore overlap detection — preserve legacy behavior",
|
||||||
|
"includeTaskIDInCommitScope": " Include task ID in commit scope ",
|
||||||
|
"insteadOf": " instead of ",
|
||||||
|
"integrationBranch": "Integration branch",
|
||||||
|
"integrationWorktree": "Integration worktree",
|
||||||
|
"inYourProjectRootWillShowTheNew": " in your project root will show the new commits inverted as "staged changes" until you pull manually. Only applies to direct merges. ",
|
||||||
|
"isTheLegacyBehavior": " is the legacy behavior: ",
|
||||||
|
"isTheStricterOptInModeThatRefuses": " is the stricter opt-in mode that refuses to auto-complete merges with duplicate-subject or touched-file overlap risks. ",
|
||||||
|
"legacy": " → legacy ",
|
||||||
|
"legacy2": "Legacy ",
|
||||||
|
"legacyAutoMergeStamp": " legacy auto-merge stamp",
|
||||||
|
"legacyAutoMergeStampCleanup": "Legacy auto-merge stamp cleanup",
|
||||||
|
"legacyIntegrationBranchMode": "Legacy integration-branch mode.",
|
||||||
|
"maxAIReviewPasses": "Max AI review passes",
|
||||||
|
"merge": "Merge",
|
||||||
|
"moreDetails": "More details",
|
||||||
|
"nameUsedInThe": "Name used in the ",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": " No legacy auto-merge stamps to clean up. ",
|
||||||
|
"noreplyRunfusionAi": "noreply@runfusion.ai",
|
||||||
|
"off": "Off",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "Off — leave the project root stale (legacy behavior)",
|
||||||
|
"offSkipAudit": "Off (skip audit)",
|
||||||
|
"optionsStartWithABestEffort": " options start with a best-effort ",
|
||||||
|
"origin": "origin",
|
||||||
|
"personalAccessToken": "Personal access token",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "). Pick a local branch from the dropdown — common integration names like ",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.",
|
||||||
|
"postMergeAuditMode": "Post-merge audit mode",
|
||||||
|
"pushRemote": "Push Remote",
|
||||||
|
"pushToRemoteAfterMerge": " Push to remote after merge ",
|
||||||
|
"readyToCleanUp": " ready to clean up.",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": " retries the AI agent rather than auto-picking a side. ",
|
||||||
|
"reuseTaskWorktreeDefault": "Reuse task worktree (default)",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": " skips the audit entirely. Switching to Off is recommended only if you trust your branches don't silently drop edits. ",
|
||||||
|
"smart": "Smart",
|
||||||
|
"smartConflictResolution": " Smart conflict resolution ",
|
||||||
|
"smartPreferMain": "Smart, prefer main",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "Smart, prefer main on fallback — fetch+ff origin → AI → auto-resolve → -X ours (default; protects just-merged sibling work)",
|
||||||
|
"smartPreferMainOverlapGuard": "Smart Prefer Main Overlap Guard",
|
||||||
|
"smartPreferTask": "Smart, prefer task",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "Smart, prefer task on fallback — fetch+ff origin → AI → auto-resolve → -X theirs (legacy \"smart\" behavior; task branch wins)",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": " snaps cleanly when the worktree has no edits and skips otherwise. ",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": " snapshots real local edits as a patch against the previous tip, snaps the worktree to the new tip, then reapplies the patch — untracked files that collide with newly-tracked paths are left in a temp dir for manual recovery. ",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": " (so a freshly-pushed sibling commit doesn't get clobbered), then run an AI agent, then auto-resolve handles lock/generated/trivial files. They differ only in the ",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": " so main wins — protects just-merged sibling work and is the new default. ",
|
||||||
|
"soTheTaskBranchWinsFastButCan": " so the task branch wins — fast, but can resurrect code an earlier sibling task deleted (the FN-2887 class of regression). ",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "Stash + fast-forward (default) — preserve local edits",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": " stops after the first AI attempt and waits for a human. ",
|
||||||
|
"symbolicRefFallback": " symbolic ref → fallback ",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": " The canonical branch Fusion merges tasks into and uses as the reference for all ahead/behind / overlap / pre-rebase computations. Leave on ",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "The legacy merge settings below do not apply while AI merge is on.",
|
||||||
|
"toResolveViaTheStandardCascade": " to resolve via the standard cascade (",
|
||||||
|
"toTypeABranchThatDoesnAposT": " to type a branch that doesn't exist locally yet. Applies to both direct merges and pull-request mode; individual tasks can still override via task metadata. ",
|
||||||
|
"trailer": " trailer",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": " trailer crediting Fusion (recognized by GitHub for shared attribution). ",
|
||||||
|
"useDropdown": " Use dropdown ",
|
||||||
|
"useProjectRootLegacy": "Use project root (legacy)",
|
||||||
|
"uses": " uses ",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": " values from older settings are migrated automatically.",
|
||||||
|
"warn": "Warn",
|
||||||
|
"warnDefaultLogFindingsContinue": "Warn (default; log findings, continue)",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "Warn only — keep legacy main-wins fallback",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "When disabled, merge commit messages omit the task ID from the scope (e.g. ",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": " When enabled, commits made by Fusion keep your git identity as the primary author and append a ",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.), generated files (dist/*, *.gen.ts), and trivial whitespace conflicts are resolved automatically without AI intervention. Complex code conflicts still require AI review.",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "When enabled, tasks that pass review are automatically merged into the main branch",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "When enabled, the merged result is automatically pushed to the configured git remote. This includes pulling the latest from the remote first (rebase) and resolving any conflicts with AI if needed.",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": " When using smart-prefer-main, automatically prefer the branch side for files that main has recently modified to avoid silently discarding branch work. "
|
||||||
},
|
},
|
||||||
"mergeManually": "Merge Manually",
|
"mergeManually": "Merge Manually",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "Specific to this project"
|
"project": "Specific to this project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "Block execution",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "Configure how tasks are routed to execution nodes.",
|
||||||
|
"defaultExecutionNode": "Default Execution Node",
|
||||||
|
"fallBackToLocal": "Fall back to local",
|
||||||
|
"localExecutionNoDefaultNode": "Local execution (no default node)",
|
||||||
|
"nodeRouting": "Node Routing",
|
||||||
|
"selectedNode": "Selected node:",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "These settings apply at the project level.",
|
||||||
|
"unavailableNodePolicy": "Unavailable Node Policy",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": "Used when a task has no node override. Node status is shown for safer routing selection."
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "Always ask",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "Automatically synchronize settings between this node and connected remote nodes",
|
||||||
|
"conflictResolution": "Conflict resolution",
|
||||||
|
"enableAutomaticSettingsSync": " Enable automatic settings sync ",
|
||||||
|
"every15Minutes": "Every 15 minutes",
|
||||||
|
"every1Hour": "Every 1 hour",
|
||||||
|
"every30Minutes": "Every 30 minutes",
|
||||||
|
"every5Minutes": "Every 5 minutes",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "Include API keys and OAuth tokens in sync operations",
|
||||||
|
"keepLocal": "Keep local",
|
||||||
|
"keepRemote": "Keep remote",
|
||||||
|
"lastWriteWins": "Last write wins",
|
||||||
|
"nodeSync": "Node Sync",
|
||||||
|
"syncInterval": "Sync interval",
|
||||||
|
"syncModelAuthCredentials": " Sync model auth credentials ",
|
||||||
"workflowSettingsNotSynced": "Workflow settings are not synced across nodes yet."
|
"workflowSettingsNotSynced": "Workflow settings are not synced across nodes yet."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "Access token (optional)",
|
||||||
|
"advanced": "Advanced",
|
||||||
|
"allFailuresLegacy": "All failures (legacy)",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": " Base URL for deep links in notifications. When set, clicking a notification opens the dashboard directly to the task. ",
|
||||||
|
"customNtfyServerURLOptional": "Custom ntfy server URL (optional)",
|
||||||
|
"dashboardHostname": "Dashboard Hostname",
|
||||||
|
"discord": "Discord",
|
||||||
|
"enable": " Enable ",
|
||||||
|
"failureNotificationDelayMs": "Failure notification delay (ms)",
|
||||||
|
"failureNotificationMode": "Failure notification mode",
|
||||||
|
"format": "Format",
|
||||||
|
"general": " General: ",
|
||||||
|
"generic": "Generic",
|
||||||
|
"howLongAFailureMustPersistBeforeA": " How long a failure must persist before a push notification is sent. 0 = notify immediately. ",
|
||||||
|
"httpLocalhost3000": "http://localhost:3000",
|
||||||
|
"httpsHooksExampleCom": "https://hooks.example.com/...",
|
||||||
|
"httpsNtfySh": "https://ntfy.sh",
|
||||||
|
"learnMoreAboutNtfySh": " Learn more about ntfy.sh ",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": " Leave blank to keep the default server: https://ntfy.sh. Custom servers must use http:// or https://. ",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": " Leave blank to publish without authentication. When set, Fusion sends an Authorization Bearer header with ntfy requests. ",
|
||||||
|
"messageInbox": " Message inbox: ",
|
||||||
|
"mustBeAValidURLStartingWithHttp": " Must be a valid URL starting with http:// or https:// ",
|
||||||
|
"myTopicName": "my-topic-name",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"notifyOnEvents": "Notify on events",
|
||||||
|
"ntfy": "ntfy",
|
||||||
|
"ntfyTopic": "ntfy Topic",
|
||||||
|
"roomReply": " Room reply: ",
|
||||||
"sending": "Sending…",
|
"sending": "Sending…",
|
||||||
|
"slack": "Slack",
|
||||||
|
"stickyFailuresOnlyDefault": "Sticky failures only (default)",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "Sticky-only suppresses recovered failures; terminal-only waits for paused/in-review failed tasks; all restores legacy alerts.",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "Terminal failures only (suppress auto-retried)",
|
||||||
"testMessageInbox": "Test message inbox",
|
"testMessageInbox": "Test message inbox",
|
||||||
"testNotification": "Test notification",
|
"testNotification": "Test notification",
|
||||||
"testRoomReply": "Test room reply"
|
"testRoomReply": "Test room reply",
|
||||||
|
"tk": "tk_...",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": " Topic must be 1–64 alphanumeric, hyphen, or underscore characters ",
|
||||||
|
"webhook": "Webhook",
|
||||||
|
"webhookNotifications": " Webhook notifications ",
|
||||||
|
"webhookURL": "Webhook URL",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": " Your ntfy.sh topic name (1–64 alphanumeric/hyphen/underscore characters)."
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": " Fusion Plugins ",
|
||||||
|
"piExtensions": " Pi Extensions ",
|
||||||
|
"pluginManagerType": "Plugin manager type",
|
||||||
|
"plugins": "Plugins"
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": " Add Preset ",
|
||||||
|
"advancedWorkflowPolicy": " Advanced workflow policy ",
|
||||||
|
"aIMergeCommitSummaries": " AI merge commit summaries ",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": " AI Title and Git Commit Message Summarization ",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count.",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": " Auto-select preset based on task size ",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize long descriptions as titles ",
|
||||||
|
"configuredPresets": "Configured presets",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ",
|
||||||
|
"defaultWorkflowModelLaneActions": "Default workflow model lane actions",
|
||||||
|
"defaultWorkflowModelLanes": "Default workflow model lanes",
|
||||||
|
"delete": " Delete ",
|
||||||
|
"edit": " Edit ",
|
||||||
|
"executorModel": "Executor model",
|
||||||
|
"fallsBackTo": " Falls back to: ",
|
||||||
|
"loadingAvailableModels": "Loading available models…",
|
||||||
|
"loadingWorkflowModelLanes": "Loading workflow model lanes…",
|
||||||
|
"modelLanes": "Model Lanes",
|
||||||
|
"modelPresets": "Model Presets",
|
||||||
|
"name": "Name",
|
||||||
|
"noCap": "No cap",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": " No models available. Configure authentication before selecting workflow model lanes. ",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": " No models available. Configure authentication first. ",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "No models available. Configure authentication first.",
|
||||||
|
"noPreset": "No preset",
|
||||||
|
"noPresetsConfiguredYet": "No presets configured yet.",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "Open a project to edit workflow model lanes.",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ",
|
||||||
|
"presetEditor": "Preset editor",
|
||||||
|
"reset": " Reset ",
|
||||||
|
"resetToDefaultNoCap": "Reset to default (no cap)",
|
||||||
|
"resetToInheritFromGlobal": "Reset to inherit from global",
|
||||||
|
"resetToInheritFromWorkflow": "Reset to inherit from workflow",
|
||||||
|
"reviewerModel": "Reviewer model",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": " These project overrides apply to the active default workflow. ",
|
||||||
|
"tokenCap": "Token Cap",
|
||||||
|
"useDefault": "Use default",
|
||||||
|
"useWorkflowDefault": "Use workflow default",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. ",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. "
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": " Accept routes ",
|
||||||
|
"advancedNamedTunnel": "Advanced (Named Tunnel)",
|
||||||
|
"advancedSettings": "Advanced Settings",
|
||||||
|
"authenticatedURL": "Authenticated URL:",
|
||||||
|
"authLinks": "Auth Links",
|
||||||
|
"authLinkTokenType": "Auth link token type",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped.",
|
||||||
|
"cloudflare": "Cloudflare",
|
||||||
"cloudflaredInstalled": "cloudflared installed successfully",
|
"cloudflaredInstalled": "cloudflared installed successfully",
|
||||||
|
"cloudflaredIsInstalled": "cloudflared is installed",
|
||||||
|
"cloudflaredIsNotInstalled": "cloudflared is not installed",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "cloudflared must be installed to start the tunnel",
|
||||||
|
"enableShortLivedTokens": " Enable short-lived tokens ",
|
||||||
|
"expiresAt": " · Expires at {{expiresAt}}",
|
||||||
|
"external": "External ",
|
||||||
|
"externalTunnelQRCode": "External tunnel QR code",
|
||||||
|
"generateQR": "Generate QR",
|
||||||
|
"generateShortLivedToken": "Generate short-lived token",
|
||||||
|
"httpsYourDomainExample": "https://your-domain.example",
|
||||||
|
"ifHomebrewIsUnavailable": "If Homebrew is unavailable: ",
|
||||||
|
"ingressURL": "Ingress URL",
|
||||||
|
"installCloudflared": "Install cloudflared",
|
||||||
|
"installing": "Installing…",
|
||||||
|
"lastShortLivedTokenExpiresAt": "Last short-lived token expires at ",
|
||||||
|
"manualInstall": "Manual install: ",
|
||||||
|
"ms": "ms)",
|
||||||
|
"namedTunnelModeEnabled": "Named Tunnel mode enabled — configure tunnel name, token, and ingress URL below.",
|
||||||
|
"noExpiry": " · No expiry",
|
||||||
|
"persistentToken": "Persistent token",
|
||||||
"persistentTokenRegenerated": "Persistent token regenerated",
|
"persistentTokenRegenerated": "Persistent token regenerated",
|
||||||
|
"qRSVGMarkup": "QR SVG markup",
|
||||||
|
"regeneratePersistentToken": "Regenerate persistent token",
|
||||||
|
"rememberLastRunningState": " Remember last running state ",
|
||||||
|
"remoteAccess": "Remote Access",
|
||||||
|
"remoteAccessCode": "Remote access code:",
|
||||||
|
"remoteAccessQRCode": "Remote access QR code",
|
||||||
|
"remoteProvider": "Remote provider",
|
||||||
"restarting": "Restarting…",
|
"restarting": "Restarting…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "Scan this QR code on your phone",
|
||||||
|
"scanToConnect": "Scan to connect:",
|
||||||
|
"scanToOpen": "Scan to open:",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "Select a provider above to configure remote access.",
|
||||||
|
"shortLivedToken": "Short-lived token",
|
||||||
"shortLivedTokenGenerated": "Short-lived token generated",
|
"shortLivedTokenGenerated": "Short-lived token generated",
|
||||||
|
"shortLivedTTLMs": "Short-lived TTL (ms)",
|
||||||
|
"showURL": "Show URL",
|
||||||
"startFresh": "Start Fresh",
|
"startFresh": "Start Fresh",
|
||||||
"starting": "Starting…",
|
"starting": "Starting…",
|
||||||
"startTunnel": "Start Tunnel",
|
"startTunnel": "Start Tunnel",
|
||||||
"stopping": "Stopping…",
|
"stopping": "Stopping…",
|
||||||
"stopTunnel": "Stop Tunnel",
|
"stopTunnel": "Stop Tunnel",
|
||||||
|
"tailnetURL": "Tailnet URL:",
|
||||||
|
"tailscale": "Tailscale",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "Tailscale Funnel will expose this dashboard on your tailnet's public ",
|
||||||
|
"tokenType": " Token type: ",
|
||||||
|
"tunnelDetected": " tunnel detected",
|
||||||
|
"tunnelName": "Tunnel name",
|
||||||
"tunnelRestarted": "Remote tunnel restarted",
|
"tunnelRestarted": "Remote tunnel restarted",
|
||||||
"tunnelStarted": "Remote tunnel started",
|
"tunnelStarted": "Remote tunnel started",
|
||||||
"tunnelStopped": "Remote tunnel stopped",
|
"tunnelStopped": "Remote tunnel stopped",
|
||||||
"useExisting": "Use Existing"
|
"tunnelToken": "Tunnel token",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
|
||||||
|
"useExisting": "Use Existing",
|
||||||
|
"usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed."
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "Advanced — external search providers",
|
||||||
|
"alwaysOn": "Always on",
|
||||||
|
"brave": "Brave",
|
||||||
|
"builtInUsesAgentWebTools": " Built-in (uses agent web tools) ",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": " Configure Brave, Tavily, and Google API keys in Authentication. ",
|
||||||
|
"customSearchEngineId": "custom-search-engine-id",
|
||||||
|
"defaultMaxConcurrentRuns": "Default Max Concurrent Runs",
|
||||||
|
"defaultMaxDurationMs": "Default Max Duration (ms)",
|
||||||
|
"defaultMaxSourcesPerRun": "Default Max Sources Per Run",
|
||||||
|
"enabledSources": "Enabled Sources",
|
||||||
|
"gitHub": " GitHub ",
|
||||||
|
"googleCustomSearch": "Google Custom Search",
|
||||||
|
"googleSearchCX": "Google Search CX",
|
||||||
|
"httpsSearxExampleCom": "https://searx.example.com",
|
||||||
|
"localDocs": " Local Docs ",
|
||||||
|
"maxSynthesisRounds": "Max Synthesis Rounds",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": " Missing credentials for the selected research provider. ",
|
||||||
|
"openAuthentication": " Open Authentication ",
|
||||||
|
"openAuthenticationSettings": " Open Authentication Settings ",
|
||||||
|
"requestTimeoutMs": "Request Timeout (ms)",
|
||||||
|
"researchDefaults": "Research Defaults",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": " Searches and fetches use the agent's native WebSearch/WebFetch tools. No API key required. ",
|
||||||
|
"searchProvider": "Search Provider",
|
||||||
|
"searXNG": "SearXNG",
|
||||||
|
"searXNGURL": "SearXNG URL",
|
||||||
|
"tavily": "Tavily",
|
||||||
|
"webSearch": " Web Search "
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "Always on",
|
||||||
|
"enabledSources": "Enabled Sources",
|
||||||
|
"enableResearchInThisProject": " Enable research in this project ",
|
||||||
|
"github": "GitHub",
|
||||||
|
"llmSynthesis": "LLM Synthesis",
|
||||||
|
"localDocs": "Local Docs",
|
||||||
|
"maxConcurrentRuns": "Max Concurrent Runs",
|
||||||
|
"maxDurationMs": "Max Duration (ms)",
|
||||||
|
"maxSourcesPerRun": "Max Sources Per Run",
|
||||||
|
"pageFetch": "Page Fetch",
|
||||||
|
"projectResearchSettings": "Project Research Settings",
|
||||||
|
"requestTimeoutMs": "Request Timeout (ms)",
|
||||||
|
"webSearch": " Web Search ",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": " Web search is always enabled. Configure the search provider under Research Defaults. "
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "Resolve All: Keep Local",
|
"resolveAllLocal": "Resolve All: Keep Local",
|
||||||
"resolveAllRemote": "Resolve All: Keep Remote",
|
"resolveAllRemote": "Resolve All: Keep Remote",
|
||||||
"resolvedSuccess": "Settings conflicts resolved successfully",
|
"resolvedSuccess": "Settings conflicts resolved successfully",
|
||||||
"resolveFailed": "Failed to resolve conflicts",
|
"resolveFailed": "Failed to resolve conflicts",
|
||||||
"resolving": "Resolving...",
|
"resolving": "Resolving...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "Hermes Runtime",
|
||||||
|
"openClawRuntime": "OpenClaw Runtime",
|
||||||
|
"paperclipRuntime": "Paperclip Runtime"
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "Auto-create tasks",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"enableScheduledEvalRunsForThisProject": " Enable scheduled eval runs for this project ",
|
||||||
|
"evaluatorModel": "Evaluator Model",
|
||||||
|
"evaluatorProvider": "Evaluator Provider",
|
||||||
|
"followUpPolicy": "Follow-up Policy",
|
||||||
|
"gpt5": "gpt-5",
|
||||||
|
"intervalMs": "Interval (ms)",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": " Leave provider and model blank to inherit the project validator lane model settings. ",
|
||||||
|
"openai": "openai",
|
||||||
|
"retentionDays": "Retention (days)",
|
||||||
|
"scheduledEvals": "Scheduled Evals",
|
||||||
|
"suggestOnly": "Suggest only"
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": " Add ignored path ",
|
||||||
|
"archiveAgentLog": "Archive Agent Log",
|
||||||
|
"archiveCompletedTasksAfterDays": "Archive Completed Tasks After (days)",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.",
|
||||||
|
"browse": " Browse ",
|
||||||
|
"closeParenPeriod": ").",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "Compact mode keeps archive size low while preserving recent agent activity for context.",
|
||||||
|
"compactSummaryAndRecentEntries": "Compact summary and recent entries",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "Completed tasks older than the threshold are moved out of the active task database.",
|
||||||
|
"docs": "docs/",
|
||||||
|
"doNotArchiveAgentLogs": "Do not archive agent logs",
|
||||||
|
"enableAutomaticTaskArchiving": " Enable automatic task archiving ",
|
||||||
|
"enablePlanStalenessEnforcement": " Enable plan staleness enforcement ",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "Escalate high fan-out blockers only after they remain in in-progress or in-review for this many hours (age source: columnMovedAt, fallback updatedAt). Default: 2 hours.",
|
||||||
|
"fullAgentLog": "Full agent log",
|
||||||
|
"globalMaxConcurrent": "Global Max Concurrent",
|
||||||
|
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
||||||
|
"ignoredOverlapPaths": "Ignored overlap paths",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
||||||
|
"lite": "Lite",
|
||||||
|
"maxConcurrentTasks": "Max Concurrent Tasks",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "Maximum age in hours before a plan is considered stale. Default: 6 hours.",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "Maximum concurrent agents across all projects",
|
||||||
|
"maximumConcurrentPlanningAgents": "Maximum concurrent planning agents",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "Maximum stuck-detector retries before a task is marked failed. Default: 6.",
|
||||||
|
"maxStuckRetries": "Max Stuck Retries",
|
||||||
|
"maxTriageConcurrent": "Max Triage Concurrent",
|
||||||
|
"numberOfDaysATaskCanStayIn": "Number of days a task can stay in Done before it is archived. Default: 2 days (48 hours).",
|
||||||
|
"off": "Off",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ",
|
||||||
|
"or": " or ",
|
||||||
|
"pollIntervalMs": "Poll Interval (ms)",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": " Preserve step progress on stuck-task requeue ",
|
||||||
|
"remove": " Remove ",
|
||||||
|
"scheduling": "Scheduling",
|
||||||
"selectCurrentDir": "Select current directory",
|
"selectCurrentDir": "Select current directory",
|
||||||
"selectIgnoredOverlapPath": "Select ignored overlap path"
|
"selectIgnoredOverlapPath": "Select ignored overlap path",
|
||||||
|
"serializeTasksWithOverlappingFiles": " Serialize tasks with overlapping files ",
|
||||||
|
"staleHighFanOutEscalationHours": "Stale High Fan-out Escalation (hours)",
|
||||||
|
"staleSpecThresholdHours": "Stale Spec Threshold (hours)",
|
||||||
|
"stepExecution": "Step Execution",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "Strict — coordination-focused; higher per-tick tokens. Lite — pre-2026-05-11 behavior. Off — minimal procedure.",
|
||||||
|
"strictDefault": "Strict (default)",
|
||||||
|
"stuckTaskTimeoutMinutes": "Stuck Task Timeout (minutes)",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "Timeout in minutes for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": "When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled."
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "These settings are shared across all your Fusion projects.",
|
"globalBanner": "These settings are shared across all your Fusion projects.",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Allow silent sibling branch rename during executor conflicts ",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": " Also rebase onto local default-branch HEAD ",
|
||||||
|
"and": " and ",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": " and can hide prior commits from the default recovery flow. ",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "auto-detect (~/.fusion/bin/worktrunk or $PATH)",
|
||||||
"awaitingApproval": "Awaiting approval — open Approvals to continue.",
|
"awaitingApproval": "Awaiting approval — open Approvals to continue.",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": " branch collision silently forks work onto sibling branches like ",
|
||||||
|
"browse": " Browse ",
|
||||||
|
"browseWorktreesDirectory": "Browse worktrees directory",
|
||||||
|
"closeParenPeriod": ").",
|
||||||
|
"defaultsTo": ". Defaults to ",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "Defaults to .worktrees — leave empty unless overriding",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": " Disabled by default (opt-in). When enabled, Fusion shells out to ",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": " Discouraged. This restores the legacy behavior where a live ",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "Enable worktrunk and request approval to install the pinned release.",
|
||||||
|
"enableWorktrunkIntegration": " Enable worktrunk integration ",
|
||||||
|
"failAndPauseTheTaskDefault": "Fail and pause the task (default)",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "Fall back to Fusion's native worktree backend",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": " for worktree create, sync, prune, and remove operations and follows worktrunk's directory layout. ",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": " In addition to the remote rebase above, also rebase the task branch onto the local default-branch HEAD (rootDir). This catches sibling tasks that merged locally but haven't been pushed yet — without it, two concurrent tasks where one deletes code can have the other silently re-introduce it via the fallback strategy. Enabled by default; only disable if it causes issues with your workflow. ",
|
||||||
|
"installedAt": " installed at ",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "Install the worktrunk binary below to enable this integration.",
|
||||||
"installWorktrunk": "Install worktrunk binary",
|
"installWorktrunk": "Install worktrunk binary",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": " keeps progress moving by switching to Fusion's built-in worktree backend. ",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "Limits total git worktrees including in-review tasks",
|
||||||
|
"maxWorktrees": "Max Worktrees",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "Off by default (opt-in). When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup",
|
||||||
"openApprovals": "Open Approvals",
|
"openApprovals": "Open Approvals",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "Optional. Leave blank to auto-resolve; Fusion will offer to install on first use.",
|
||||||
|
"optionalSupports": " Optional. Supports ",
|
||||||
|
"pnpmInstallFrozenLockfile": "pnpm install --frozen-lockfile",
|
||||||
|
"randomNamesEGSwiftFalcon": "Random names (e.g., swift-falcon)",
|
||||||
|
"rebaseFromRemoteBeforeMerge": " Rebase from remote before merge ",
|
||||||
|
"rebaseRemote": "Rebase Remote",
|
||||||
|
"recycleWorktrees": " Recycle worktrees ",
|
||||||
"selectWorktreesDir": "Select worktrees directory",
|
"selectWorktreesDir": "Select worktrees directory",
|
||||||
"tryAgain": "Try again"
|
"shellCommandToRunInEachNewWorktree": "Shell command to run in each new worktree after creation",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": " stops on worktrunk errors for explicit operator recovery; ",
|
||||||
|
"taskIDEGFN042": "Task ID (e.g., FN-042)",
|
||||||
|
"taskTitleEGFixLoginBug": "Task title (e.g., fix-login-bug)",
|
||||||
|
"tryAgain": "Try again",
|
||||||
|
"useGitDefault": "Use git default",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "When enabled, the merger fetches from the configured remote and rebases the task branch onto the latest default-branch tip before merging — catching concurrent pushes from other collaborators or fusion workers. Any conflicts the rebase surfaces flow into the existing smart/AI resolve pipeline.",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": " when unset. Only affects newly-created worktrees. ",
|
||||||
|
"whichRemoteToFetchForThePreMerge": " Which remote to fetch for the pre-merge rebase. \"Use git default\" falls back to the remote configured for the default branch (typically ",
|
||||||
|
"worktreeInitCommand": "Worktree Init Command",
|
||||||
|
"worktreeNamingStyle": "Worktree Naming Style",
|
||||||
|
"worktrees": "Worktrees",
|
||||||
|
"worktreesDirectory": "Worktrees Directory",
|
||||||
|
"worktrunk": " worktrunk ",
|
||||||
|
"worktrunkBinaryPath": "Worktrunk binary path",
|
||||||
|
"worktrunkFailureBehavior": "Worktrunk failure behavior",
|
||||||
|
"worktrunkIntegration": "Worktrunk integration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": "Not configured"
|
"notConfigured": "Not configured"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "Connecting",
|
||||||
|
"statusError": "Error",
|
||||||
|
"statusOffline": "Offline",
|
||||||
|
"statusOnline": "Online"
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "{{providerName}} keys should follow this format: {{hint}} (e.g. {{example}})",
|
"apiKeyFormatError": "{{providerName}} keys should follow this format: {{hint}} (e.g. {{example}})",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "Día del mes (1–28)",
|
"resetDayMonthly": "Día del mes (1–28)",
|
||||||
"resetDayWeekly": "Día de la semana (0=Dom)",
|
"resetDayWeekly": "Día de la semana (0=Dom)",
|
||||||
"resetting": "Restableciendo...",
|
"resetting": "Restableciendo...",
|
||||||
|
"resizeSidebar": "",
|
||||||
"result": "Resultado",
|
"result": "Resultado",
|
||||||
"resultCreated_one": "",
|
"resultCreated_one": "",
|
||||||
"resultCreated_other": "",
|
"resultCreated_other": "",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "Crear sala"
|
"title": "Crear sala"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "Inicializando panel...",
|
"initializingDashboard": "Inicializando panel...",
|
||||||
"loadingMessage": "Cargando panel de Fusion",
|
"loadingMessage": "Cargando panel de Fusion",
|
||||||
"loadingProgress": "Progreso de carga del panel",
|
"loadingProgress": "Progreso de carga del panel",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"save": "Guardar"
|
"save": "Guardar"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "",
|
||||||
|
"agentProvisioningApprovals": "",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": "",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": ""
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"languageAuto": "Automático",
|
"languageAuto": "Automático",
|
||||||
"languageAutoHint": "Seguir el idioma del navegador",
|
"languageAutoHint": "Seguir el idioma del navegador",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": "",
|
||||||
"title": "Apariencia"
|
"title": "Apariencia"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "Autenticación",
|
"title": "Autenticación",
|
||||||
"waitingForLogin": "Esperando inicio de sesión…"
|
"waitingForLogin": "Esperando inicio de sesión…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "",
|
||||||
|
"key": ""
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "",
|
||||||
|
"03": "",
|
||||||
|
"agentsOnlyFusionAgentMemory": "",
|
||||||
|
"allProjectAgents": "",
|
||||||
|
"and": "",
|
||||||
"backupCreated": "Copia de seguridad creada exitosamente",
|
"backupCreated": "Copia de seguridad creada exitosamente",
|
||||||
|
"backupDirectory": "",
|
||||||
"backupNow": "Hacer copia ahora",
|
"backupNow": "Hacer copia ahora",
|
||||||
"creating": "Creando…"
|
"backups": "",
|
||||||
|
"backupS": "",
|
||||||
|
"backupScheduleCron": "",
|
||||||
|
"creating": "Creando…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": "",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "",
|
||||||
|
"currentBackups": "",
|
||||||
|
"databaseBackups": "",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "",
|
||||||
|
"enableAutomaticDatabaseBackups": "",
|
||||||
|
"enableAutomaticMemoryBackups": "",
|
||||||
|
"fusionBackups": "",
|
||||||
|
"fusionBackupsMemory": "",
|
||||||
|
"invalidCronExpressionFormat": "",
|
||||||
|
"loadingBackupInfo": "",
|
||||||
|
"memoryBackupDirectory": "",
|
||||||
|
"memoryBackups": "",
|
||||||
|
"memoryBackupScheduleCron": "",
|
||||||
|
"memoryBackupScope": "",
|
||||||
|
"memoryRetentionCount": "",
|
||||||
|
"more": "",
|
||||||
|
"mustBeBetween1And100": "",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "",
|
||||||
|
"projectOnlyFusionMemory": "",
|
||||||
|
"retentionCount": "",
|
||||||
|
"totalSize": "",
|
||||||
|
"view": "",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
|
||||||
},
|
},
|
||||||
"clearToDefault": "",
|
"clearToDefault": "",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": ""
|
"saveFailed": ""
|
||||||
},
|
},
|
||||||
"closeModal": "Cerrar modal de conflicto",
|
"closeModal": "Cerrar modal de conflicto",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "",
|
||||||
|
"commands": "",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "",
|
||||||
|
"eGPnpmBuild": "",
|
||||||
|
"eGPnpmTest": "",
|
||||||
|
"testCommand": ""
|
||||||
|
},
|
||||||
"conflictModalTitle": "Resolver conflictos de configuración",
|
"conflictModalTitle": "Resolver conflictos de configuración",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": "",
|
||||||
|
"featureFlags": ""
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "Ayuda",
|
"help": "Ayuda",
|
||||||
"version": "Versión {{version}}"
|
"version": "Versión {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "",
|
||||||
|
"200": "",
|
||||||
|
"3000": "",
|
||||||
|
"14Days": "",
|
||||||
|
"30Days": "",
|
||||||
|
"60Days": "",
|
||||||
|
"7Days": "",
|
||||||
|
"90Days": "",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": "",
|
||||||
|
"autoCleanupOldChats": "",
|
||||||
|
"autoPruneOldMail": "",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "",
|
||||||
|
"builtInWorkflows": "",
|
||||||
|
"capacityRiskBanner": "",
|
||||||
|
"chatHistory": "",
|
||||||
|
"chatRooms": "",
|
||||||
|
"completionDocumentationAutomation": "",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": "",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": "",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "",
|
||||||
|
"defaultTrackingModeForNewTasks": "",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": "",
|
||||||
|
"fN": "",
|
||||||
|
"general": "",
|
||||||
|
"gitHubTracking": "",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "",
|
||||||
"learnMore": "Más información",
|
"learnMore": "Más información",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": "",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "",
|
||||||
|
"off": "",
|
||||||
|
"offDefault": "",
|
||||||
|
"onForNewTasks": "",
|
||||||
|
"operationalLogRetention": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"prefixForNewTaskIDsEGKB": "",
|
||||||
|
"prefixMustBe110UppercaseLetters": "",
|
||||||
|
"projectDefaultTrackingRepo": "",
|
||||||
|
"recentVerbatimRoomMessages": "",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "",
|
||||||
|
"requireChangesetChangesetMd": "",
|
||||||
|
"roomCompactionFetchLimit": "",
|
||||||
|
"roomSummaryMaxCharacters": "",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": "",
|
||||||
"settingsSaved": "Configuración guardada",
|
"settingsSaved": "Configuración guardada",
|
||||||
|
"showCapacityRiskBanner": "",
|
||||||
|
"showQuickChatButton": "",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "",
|
||||||
|
"taskPrefix": "",
|
||||||
|
"todoThreshold": "",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": "",
|
||||||
"updateAvailablePrefix": "v{{version}} disponible",
|
"updateAvailablePrefix": "v{{version}} disponible",
|
||||||
"updateCheckFailed": "",
|
"updateCheckFailed": "",
|
||||||
"updateFailed": "",
|
"updateFailed": "",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "",
|
"updateSuccess": "",
|
||||||
"updateSuccessToast": "",
|
"updateSuccessToast": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upToDate": "Estás actualizado ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "",
|
||||||
|
"upToDate": "Estás actualizado ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": "",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": "",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": "",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": ""
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||||
|
"autoReloadDashboardOnVersionChange": "",
|
||||||
|
"checkForThe": "",
|
||||||
|
"checkForUpdatesAutomatically": "",
|
||||||
|
"cLIBinaryOnPATH": "",
|
||||||
|
"cLIBySpawning": "",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": "",
|
||||||
|
"dailyRecommended": "",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": "",
|
||||||
|
"frequency": "",
|
||||||
|
"general": "",
|
||||||
|
"globalDefaultTrackingRepo": "",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||||
|
"manualOnlyNeverAutoCheck": "",
|
||||||
|
"onStartupOncePerServerLaunch": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": "",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": "",
|
||||||
|
"saveAIThinkingForPermanentAgents": "",
|
||||||
|
"saveAIThinkingLogs": "",
|
||||||
|
"saveToolOutputInAgentLogs": "",
|
||||||
|
"updates": "",
|
||||||
|
"weekly": "",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": "",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": "",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": "",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": ""
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "",
|
||||||
|
"default": "",
|
||||||
|
"default2": "",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "",
|
||||||
|
"defaultModel": "",
|
||||||
|
"deny": "",
|
||||||
|
"fallbackModel": "",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": "",
|
||||||
|
"fusion": "",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": "",
|
||||||
|
"httpsRunfusionAi": "",
|
||||||
|
"latency": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"noFallback": "",
|
||||||
|
"openaiAnthropic": "",
|
||||||
|
"openRouterAdvanced": "",
|
||||||
|
"openRouterAllowFallbacks": "",
|
||||||
|
"openRouterHTTPReferer": "",
|
||||||
|
"openRouterOutputModalitiesFilter": "",
|
||||||
|
"openRouterRoutingIgnore": "",
|
||||||
|
"openRouterRoutingOnly": "",
|
||||||
|
"openRouterRoutingOrder": "",
|
||||||
|
"openRouterRoutingSort": "",
|
||||||
|
"openRouterSupportedParametersFilter": "",
|
||||||
|
"openRouterXTitle": "",
|
||||||
|
"price": "",
|
||||||
|
"providerName": "",
|
||||||
|
"requireParameters": "",
|
||||||
|
"startupModelSync": "",
|
||||||
|
"syncOpencodeGoModelListAtStartup": "",
|
||||||
|
"syncOpenRouterModelListAtStartup": "",
|
||||||
|
"text": "",
|
||||||
|
"thinkingEffort": "",
|
||||||
|
"throughput": "",
|
||||||
|
"toolsStructuredOutputs": "",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": "",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "Mantener remoto",
|
"keepRemote": "Mantener remoto",
|
||||||
"loading": "Cargando…",
|
"loading": "Cargando…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": "",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "",
|
||||||
|
"autoSummarizeMemory": "",
|
||||||
|
"bytesUpdated": "",
|
||||||
|
"cannotSave": "",
|
||||||
|
"checkingMemoryWriteAccess": "",
|
||||||
"compacting": "Compactando…",
|
"compacting": "Compactando…",
|
||||||
|
"compactionThresholdChars": "",
|
||||||
"compactSelectedFile": "Compactar archivo seleccionado",
|
"compactSelectedFile": "Compactar archivo seleccionado",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "",
|
||||||
|
"cronExpressionForDreamProcessing": "",
|
||||||
"dreamCompleted": "Procesamiento de sueños completado",
|
"dreamCompleted": "Procesamiento de sueños completado",
|
||||||
|
"dreaming": "",
|
||||||
"dreamNow": "Soñar ahora",
|
"dreamNow": "Soñar ahora",
|
||||||
|
"dreamSchedule": "",
|
||||||
|
"enableMemoryTools": "",
|
||||||
|
"failedToLoadBackendStatus": "",
|
||||||
|
"for": "",
|
||||||
"installing": "Instalando…",
|
"installing": "Instalando…",
|
||||||
"installQmd": "Instalar qmd",
|
"installQmd": "Instalar qmd",
|
||||||
|
"loadingMemory": "",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "",
|
||||||
|
"memory": "",
|
||||||
"memoryCompacted": "Archivo de memoria compactado",
|
"memoryCompacted": "Archivo de memoria compactado",
|
||||||
|
"memoryFile": "",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": "",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": "",
|
||||||
|
"memoryLivesIn": "",
|
||||||
"memorySaved": "Memoria guardada",
|
"memorySaved": "Memoria guardada",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "",
|
||||||
|
"noMatchingMemoryFound": "",
|
||||||
|
"processDreamsFromDailyMemory": "",
|
||||||
|
"qmd": "",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": "",
|
||||||
|
"result": "",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "",
|
||||||
"saveMemory": "Guardar memoria",
|
"saveMemory": "Guardar memoria",
|
||||||
|
"scheduleCron": "",
|
||||||
|
"searchMemoryWithQmd": "",
|
||||||
"testing": "Probando…",
|
"testing": "Probando…",
|
||||||
"testRetrieval": "Probar recuperación"
|
"testRetrieval": "Probar recuperación",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "",
|
||||||
|
"addFusionAsCoAuthorOnCommits": "",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": "",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "",
|
||||||
|
"aIMerge": "",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": "",
|
||||||
|
"aIOnly": "",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": "",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "",
|
||||||
|
"alwaysSquashDirectMerges": "",
|
||||||
|
"and": "",
|
||||||
|
"and2": "",
|
||||||
|
"areListedFirstOrChoose": "",
|
||||||
|
"autoCompletionMode": "",
|
||||||
|
"autoDetect": "",
|
||||||
|
"autoDetectOriginHEADMain": "",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": "",
|
||||||
|
"autoMergeCompletedTasks": "",
|
||||||
|
"autoMergeConflictRetries": "",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": "",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": "",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": "",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "",
|
||||||
|
"block": "",
|
||||||
|
"blockStrict": "",
|
||||||
|
"both": "",
|
||||||
|
"branchName": "",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "",
|
||||||
|
"coAuthorEmail": "",
|
||||||
|
"coAuthorName": "",
|
||||||
|
"conflictFallbackStrategy": "",
|
||||||
|
"controlsThePostMergeAuditGate": "",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": "",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "",
|
||||||
|
"custom": "",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": "",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": "",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "",
|
||||||
|
"directMergeCommitRouting": "",
|
||||||
|
"directMergeIntoTheCurrentBranch": "",
|
||||||
|
"emailUsedInThe": "",
|
||||||
|
"enableTestMode": "",
|
||||||
|
"fastForwardOfLocalMainFrom": "",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "",
|
||||||
|
"finalFallback": "",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": "",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "",
|
||||||
|
"fusion": "",
|
||||||
|
"gitHubAuthentication": "",
|
||||||
|
"gitHubAuthMode": "",
|
||||||
|
"gitHubCLIGhAuth": "",
|
||||||
|
"gitHubPersonalAccessToken": "",
|
||||||
|
"gitRemoteToPushToEGOrigin": "",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "",
|
||||||
|
"includeTaskIDInCommitScope": "",
|
||||||
|
"insteadOf": "",
|
||||||
|
"integrationBranch": "",
|
||||||
|
"integrationWorktree": "",
|
||||||
|
"inYourProjectRootWillShowTheNew": "",
|
||||||
|
"isTheLegacyBehavior": "",
|
||||||
|
"isTheStricterOptInModeThatRefuses": "",
|
||||||
|
"legacy": "",
|
||||||
|
"legacy2": "",
|
||||||
|
"legacyAutoMergeStamp": "",
|
||||||
|
"legacyAutoMergeStampCleanup": "",
|
||||||
|
"legacyIntegrationBranchMode": "",
|
||||||
|
"maxAIReviewPasses": "",
|
||||||
|
"merge": "",
|
||||||
|
"moreDetails": "",
|
||||||
|
"nameUsedInThe": "",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": "",
|
||||||
|
"noreplyRunfusionAi": "",
|
||||||
|
"off": "",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "",
|
||||||
|
"offSkipAudit": "",
|
||||||
|
"optionsStartWithABestEffort": "",
|
||||||
|
"origin": "",
|
||||||
|
"personalAccessToken": "",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "",
|
||||||
|
"postMergeAuditMode": "",
|
||||||
|
"pushRemote": "",
|
||||||
|
"pushToRemoteAfterMerge": "",
|
||||||
|
"readyToCleanUp": "",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": "",
|
||||||
|
"reuseTaskWorktreeDefault": "",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": "",
|
||||||
|
"smart": "",
|
||||||
|
"smartConflictResolution": "",
|
||||||
|
"smartPreferMain": "",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "",
|
||||||
|
"smartPreferMainOverlapGuard": "",
|
||||||
|
"smartPreferTask": "",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": "",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": "",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": "",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": "",
|
||||||
|
"soTheTaskBranchWinsFastButCan": "",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": "",
|
||||||
|
"symbolicRefFallback": "",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": "",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "",
|
||||||
|
"toResolveViaTheStandardCascade": "",
|
||||||
|
"toTypeABranchThatDoesnAposT": "",
|
||||||
|
"trailer": "",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": "",
|
||||||
|
"useDropdown": "",
|
||||||
|
"useProjectRootLegacy": "",
|
||||||
|
"uses": "",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": "",
|
||||||
|
"warn": "",
|
||||||
|
"warnDefaultLogFindingsContinue": "",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": ""
|
||||||
},
|
},
|
||||||
"mergeManually": "Fusionar manualmente",
|
"mergeManually": "Fusionar manualmente",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "Específico de este proyecto"
|
"project": "Específico de este proyecto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "",
|
||||||
|
"defaultExecutionNode": "",
|
||||||
|
"fallBackToLocal": "",
|
||||||
|
"localExecutionNoDefaultNode": "",
|
||||||
|
"nodeRouting": "",
|
||||||
|
"selectedNode": "",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "",
|
||||||
|
"unavailableNodePolicy": "",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": ""
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "",
|
||||||
|
"conflictResolution": "",
|
||||||
|
"enableAutomaticSettingsSync": "",
|
||||||
|
"every15Minutes": "",
|
||||||
|
"every1Hour": "",
|
||||||
|
"every30Minutes": "",
|
||||||
|
"every5Minutes": "",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "",
|
||||||
|
"keepLocal": "",
|
||||||
|
"keepRemote": "",
|
||||||
|
"lastWriteWins": "",
|
||||||
|
"nodeSync": "",
|
||||||
|
"syncInterval": "",
|
||||||
|
"syncModelAuthCredentials": "",
|
||||||
"workflowSettingsNotSynced": ""
|
"workflowSettingsNotSynced": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "",
|
||||||
|
"advanced": "",
|
||||||
|
"allFailuresLegacy": "",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": "",
|
||||||
|
"customNtfyServerURLOptional": "",
|
||||||
|
"dashboardHostname": "",
|
||||||
|
"discord": "",
|
||||||
|
"enable": "",
|
||||||
|
"failureNotificationDelayMs": "",
|
||||||
|
"failureNotificationMode": "",
|
||||||
|
"format": "",
|
||||||
|
"general": "",
|
||||||
|
"generic": "",
|
||||||
|
"howLongAFailureMustPersistBeforeA": "",
|
||||||
|
"httpLocalhost3000": "",
|
||||||
|
"httpsHooksExampleCom": "",
|
||||||
|
"httpsNtfySh": "",
|
||||||
|
"learnMoreAboutNtfySh": "",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": "",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": "",
|
||||||
|
"messageInbox": "",
|
||||||
|
"mustBeAValidURLStartingWithHttp": "",
|
||||||
|
"myTopicName": "",
|
||||||
|
"notifications": "",
|
||||||
|
"notifyOnEvents": "",
|
||||||
|
"ntfy": "",
|
||||||
|
"ntfyTopic": "",
|
||||||
|
"roomReply": "",
|
||||||
"sending": "Enviando…",
|
"sending": "Enviando…",
|
||||||
|
"slack": "",
|
||||||
|
"stickyFailuresOnlyDefault": "",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "",
|
||||||
"testMessageInbox": "Probar bandeja de mensajes",
|
"testMessageInbox": "Probar bandeja de mensajes",
|
||||||
"testNotification": "Notificación de prueba",
|
"testNotification": "Notificación de prueba",
|
||||||
"testRoomReply": "Probar respuesta en sala"
|
"testRoomReply": "Probar respuesta en sala",
|
||||||
|
"tk": "",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": "",
|
||||||
|
"webhook": "",
|
||||||
|
"webhookNotifications": "",
|
||||||
|
"webhookURL": "",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": ""
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": "",
|
||||||
|
"piExtensions": "",
|
||||||
|
"pluginManagerType": "",
|
||||||
|
"plugins": ""
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": "",
|
||||||
|
"advancedWorkflowPolicy": "",
|
||||||
|
"aIMergeCommitSummaries": "",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": "",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": "",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": "",
|
||||||
|
"configuredPresets": "",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": "",
|
||||||
|
"defaultWorkflowModelLaneActions": "",
|
||||||
|
"defaultWorkflowModelLanes": "",
|
||||||
|
"delete": "",
|
||||||
|
"edit": "",
|
||||||
|
"executorModel": "",
|
||||||
|
"fallsBackTo": "",
|
||||||
|
"loadingAvailableModels": "",
|
||||||
|
"loadingWorkflowModelLanes": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"modelPresets": "",
|
||||||
|
"name": "",
|
||||||
|
"noCap": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "",
|
||||||
|
"noPreset": "",
|
||||||
|
"noPresetsConfiguredYet": "",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": "",
|
||||||
|
"presetEditor": "",
|
||||||
|
"reset": "",
|
||||||
|
"resetToDefaultNoCap": "",
|
||||||
|
"resetToInheritFromGlobal": "",
|
||||||
|
"resetToInheritFromWorkflow": "",
|
||||||
|
"reviewerModel": "",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": "",
|
||||||
|
"tokenCap": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"useWorkflowDefault": "",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": "",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": ""
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": "",
|
||||||
|
"advancedNamedTunnel": "",
|
||||||
|
"advancedSettings": "",
|
||||||
|
"authenticatedURL": "",
|
||||||
|
"authLinks": "",
|
||||||
|
"authLinkTokenType": "",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "",
|
||||||
|
"cloudflare": "",
|
||||||
"cloudflaredInstalled": "cloudflared instalado exitosamente",
|
"cloudflaredInstalled": "cloudflared instalado exitosamente",
|
||||||
|
"cloudflaredIsInstalled": "",
|
||||||
|
"cloudflaredIsNotInstalled": "",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "",
|
||||||
|
"enableShortLivedTokens": "",
|
||||||
|
"expiresAt": "",
|
||||||
|
"external": "",
|
||||||
|
"externalTunnelQRCode": "",
|
||||||
|
"generateQR": "",
|
||||||
|
"generateShortLivedToken": "",
|
||||||
|
"httpsYourDomainExample": "",
|
||||||
|
"ifHomebrewIsUnavailable": "",
|
||||||
|
"ingressURL": "",
|
||||||
|
"installCloudflared": "",
|
||||||
|
"installing": "",
|
||||||
|
"lastShortLivedTokenExpiresAt": "",
|
||||||
|
"manualInstall": "",
|
||||||
|
"ms": "",
|
||||||
|
"namedTunnelModeEnabled": "",
|
||||||
|
"noExpiry": "",
|
||||||
|
"persistentToken": "",
|
||||||
"persistentTokenRegenerated": "Token persistente regenerado",
|
"persistentTokenRegenerated": "Token persistente regenerado",
|
||||||
|
"qRSVGMarkup": "",
|
||||||
|
"regeneratePersistentToken": "",
|
||||||
|
"rememberLastRunningState": "",
|
||||||
|
"remoteAccess": "",
|
||||||
|
"remoteAccessCode": "",
|
||||||
|
"remoteAccessQRCode": "",
|
||||||
|
"remoteProvider": "",
|
||||||
"restarting": "Reiniciando…",
|
"restarting": "Reiniciando…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "",
|
||||||
|
"scanToConnect": "",
|
||||||
|
"scanToOpen": "",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "",
|
||||||
|
"shortLivedToken": "",
|
||||||
"shortLivedTokenGenerated": "Token de corta duración generado",
|
"shortLivedTokenGenerated": "Token de corta duración generado",
|
||||||
|
"shortLivedTTLMs": "",
|
||||||
|
"showURL": "",
|
||||||
"startFresh": "Iniciar nuevo",
|
"startFresh": "Iniciar nuevo",
|
||||||
"starting": "Iniciando…",
|
"starting": "Iniciando…",
|
||||||
"startTunnel": "Iniciar túnel",
|
"startTunnel": "Iniciar túnel",
|
||||||
"stopping": "Deteniendo…",
|
"stopping": "Deteniendo…",
|
||||||
"stopTunnel": "Detener túnel",
|
"stopTunnel": "Detener túnel",
|
||||||
|
"tailnetURL": "",
|
||||||
|
"tailscale": "",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "",
|
||||||
|
"tokenType": "",
|
||||||
|
"tunnelDetected": "",
|
||||||
|
"tunnelName": "",
|
||||||
"tunnelRestarted": "Túnel remoto reiniciado",
|
"tunnelRestarted": "Túnel remoto reiniciado",
|
||||||
"tunnelStarted": "Túnel remoto iniciado",
|
"tunnelStarted": "Túnel remoto iniciado",
|
||||||
"tunnelStopped": "Túnel remoto detenido",
|
"tunnelStopped": "Túnel remoto detenido",
|
||||||
"useExisting": "Usar existente"
|
"tunnelToken": "",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": "",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": "",
|
||||||
|
"useExisting": "Usar existente",
|
||||||
|
"usingQuickTunnel": ""
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "",
|
||||||
|
"alwaysOn": "",
|
||||||
|
"brave": "",
|
||||||
|
"builtInUsesAgentWebTools": "",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": "",
|
||||||
|
"customSearchEngineId": "",
|
||||||
|
"defaultMaxConcurrentRuns": "",
|
||||||
|
"defaultMaxDurationMs": "",
|
||||||
|
"defaultMaxSourcesPerRun": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"gitHub": "",
|
||||||
|
"googleCustomSearch": "",
|
||||||
|
"googleSearchCX": "",
|
||||||
|
"httpsSearxExampleCom": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxSynthesisRounds": "",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": "",
|
||||||
|
"openAuthentication": "",
|
||||||
|
"openAuthenticationSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"researchDefaults": "",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": "",
|
||||||
|
"searchProvider": "",
|
||||||
|
"searXNG": "",
|
||||||
|
"searXNGURL": "",
|
||||||
|
"tavily": "",
|
||||||
|
"webSearch": ""
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"enableResearchInThisProject": "",
|
||||||
|
"github": "",
|
||||||
|
"llmSynthesis": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxConcurrentRuns": "",
|
||||||
|
"maxDurationMs": "",
|
||||||
|
"maxSourcesPerRun": "",
|
||||||
|
"pageFetch": "",
|
||||||
|
"projectResearchSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"webSearch": "",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": ""
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "Resolver todo: Mantener local",
|
"resolveAllLocal": "Resolver todo: Mantener local",
|
||||||
"resolveAllRemote": "Resolver todo: Mantener remoto",
|
"resolveAllRemote": "Resolver todo: Mantener remoto",
|
||||||
"resolvedSuccess": "Conflictos de configuración resueltos exitosamente",
|
"resolvedSuccess": "Conflictos de configuración resueltos exitosamente",
|
||||||
"resolveFailed": "No se pudieron resolver los conflictos",
|
"resolveFailed": "No se pudieron resolver los conflictos",
|
||||||
"resolving": "Resolviendo...",
|
"resolving": "Resolviendo...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "",
|
||||||
|
"openClawRuntime": "",
|
||||||
|
"paperclipRuntime": ""
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "",
|
||||||
|
"disabled": "",
|
||||||
|
"enableScheduledEvalRunsForThisProject": "",
|
||||||
|
"evaluatorModel": "",
|
||||||
|
"evaluatorProvider": "",
|
||||||
|
"followUpPolicy": "",
|
||||||
|
"gpt5": "",
|
||||||
|
"intervalMs": "",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": "",
|
||||||
|
"openai": "",
|
||||||
|
"retentionDays": "",
|
||||||
|
"scheduledEvals": "",
|
||||||
|
"suggestOnly": ""
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": "",
|
||||||
|
"archiveAgentLog": "",
|
||||||
|
"archiveCompletedTasksAfterDays": "",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "",
|
||||||
|
"browse": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "",
|
||||||
|
"compactSummaryAndRecentEntries": "",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "",
|
||||||
|
"docs": "",
|
||||||
|
"doNotArchiveAgentLogs": "",
|
||||||
|
"enableAutomaticTaskArchiving": "",
|
||||||
|
"enablePlanStalenessEnforcement": "",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "",
|
||||||
|
"fullAgentLog": "",
|
||||||
|
"globalMaxConcurrent": "",
|
||||||
|
"heartbeatScopeDiscipline": "",
|
||||||
|
"ignoredOverlapPaths": "",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": "",
|
||||||
|
"lite": "",
|
||||||
|
"maxConcurrentTasks": "",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "",
|
||||||
|
"maximumConcurrentPlanningAgents": "",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "",
|
||||||
|
"maxStuckRetries": "",
|
||||||
|
"maxTriageConcurrent": "",
|
||||||
|
"numberOfDaysATaskCanStayIn": "",
|
||||||
|
"off": "",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": "",
|
||||||
|
"or": "",
|
||||||
|
"pollIntervalMs": "",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": "",
|
||||||
|
"remove": "",
|
||||||
|
"scheduling": "",
|
||||||
"selectCurrentDir": "Seleccionar directorio actual",
|
"selectCurrentDir": "Seleccionar directorio actual",
|
||||||
"selectIgnoredOverlapPath": "Seleccionar ruta de solapamiento ignorada"
|
"selectIgnoredOverlapPath": "Seleccionar ruta de solapamiento ignorada",
|
||||||
|
"serializeTasksWithOverlappingFiles": "",
|
||||||
|
"staleHighFanOutEscalationHours": "",
|
||||||
|
"staleSpecThresholdHours": "",
|
||||||
|
"stepExecution": "",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "",
|
||||||
|
"strictDefault": "",
|
||||||
|
"stuckTaskTimeoutMinutes": "",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
|
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Configuración",
|
"title": "Configuración",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": "",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": "",
|
||||||
|
"and": "",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": "",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "",
|
||||||
"awaitingApproval": "Esperando aprobación — abre Aprobaciones para continuar.",
|
"awaitingApproval": "Esperando aprobación — abre Aprobaciones para continuar.",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": "",
|
||||||
|
"browse": "",
|
||||||
|
"browseWorktreesDirectory": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"defaultsTo": "",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": "",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": "",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "",
|
||||||
|
"enableWorktrunkIntegration": "",
|
||||||
|
"failAndPauseTheTaskDefault": "",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": "",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": "",
|
||||||
|
"installedAt": "",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "",
|
||||||
"installWorktrunk": "Instalar binario de worktrunk",
|
"installWorktrunk": "Instalar binario de worktrunk",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": "",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "",
|
||||||
|
"maxWorktrees": "",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "",
|
||||||
"openApprovals": "Abrir aprobaciones",
|
"openApprovals": "Abrir aprobaciones",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "",
|
||||||
|
"optionalSupports": "",
|
||||||
|
"pnpmInstallFrozenLockfile": "",
|
||||||
|
"randomNamesEGSwiftFalcon": "",
|
||||||
|
"rebaseFromRemoteBeforeMerge": "",
|
||||||
|
"rebaseRemote": "",
|
||||||
|
"recycleWorktrees": "",
|
||||||
"selectWorktreesDir": "Seleccionar directorio de worktrees",
|
"selectWorktreesDir": "Seleccionar directorio de worktrees",
|
||||||
"tryAgain": "Intentar de nuevo"
|
"shellCommandToRunInEachNewWorktree": "",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": "",
|
||||||
|
"taskIDEGFN042": "",
|
||||||
|
"taskTitleEGFixLoginBug": "",
|
||||||
|
"tryAgain": "Intentar de nuevo",
|
||||||
|
"useGitDefault": "",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": "",
|
||||||
|
"whichRemoteToFetchForThePreMerge": "",
|
||||||
|
"worktreeInitCommand": "",
|
||||||
|
"worktreeNamingStyle": "",
|
||||||
|
"worktrees": "",
|
||||||
|
"worktreesDirectory": "",
|
||||||
|
"worktrunk": "",
|
||||||
|
"worktrunkBinaryPath": "",
|
||||||
|
"worktrunkFailureBehavior": "",
|
||||||
|
"worktrunkIntegration": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": ""
|
"notConfigured": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "",
|
||||||
|
"statusError": "",
|
||||||
|
"statusOffline": "",
|
||||||
|
"statusOnline": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "",
|
"apiKeyFormatError": "",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "Jour du mois (1–28)",
|
"resetDayMonthly": "Jour du mois (1–28)",
|
||||||
"resetDayWeekly": "Jour de la semaine (0=Dim)",
|
"resetDayWeekly": "Jour de la semaine (0=Dim)",
|
||||||
"resetting": "Réinitialisation...",
|
"resetting": "Réinitialisation...",
|
||||||
|
"resizeSidebar": "",
|
||||||
"result": "Résultat",
|
"result": "Résultat",
|
||||||
"resultCreated_one": "",
|
"resultCreated_one": "",
|
||||||
"resultCreated_other": "",
|
"resultCreated_other": "",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "Créer une salle"
|
"title": "Créer une salle"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "Initialisation du tableau de bord...",
|
"initializingDashboard": "Initialisation du tableau de bord...",
|
||||||
"loadingMessage": "Chargement du tableau de bord Fusion",
|
"loadingMessage": "Chargement du tableau de bord Fusion",
|
||||||
"loadingProgress": "Progression du chargement du tableau de bord",
|
"loadingProgress": "Progression du chargement du tableau de bord",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"save": "Enregistrer"
|
"save": "Enregistrer"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "",
|
||||||
|
"agentProvisioningApprovals": "",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": "",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": ""
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"languageAuto": "Auto",
|
"languageAuto": "Auto",
|
||||||
"languageAutoHint": "Suivre la langue du navigateur",
|
"languageAutoHint": "Suivre la langue du navigateur",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": "",
|
||||||
"title": "Apparence"
|
"title": "Apparence"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "Authentification",
|
"title": "Authentification",
|
||||||
"waitingForLogin": "En attente de connexion…"
|
"waitingForLogin": "En attente de connexion…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "",
|
||||||
|
"key": ""
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "",
|
||||||
|
"03": "",
|
||||||
|
"agentsOnlyFusionAgentMemory": "",
|
||||||
|
"allProjectAgents": "",
|
||||||
|
"and": "",
|
||||||
"backupCreated": "Sauvegarde créée avec succès",
|
"backupCreated": "Sauvegarde créée avec succès",
|
||||||
|
"backupDirectory": "",
|
||||||
"backupNow": "Sauvegarder maintenant",
|
"backupNow": "Sauvegarder maintenant",
|
||||||
"creating": "Création…"
|
"backups": "",
|
||||||
|
"backupS": "",
|
||||||
|
"backupScheduleCron": "",
|
||||||
|
"creating": "Création…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": "",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "",
|
||||||
|
"currentBackups": "",
|
||||||
|
"databaseBackups": "",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "",
|
||||||
|
"enableAutomaticDatabaseBackups": "",
|
||||||
|
"enableAutomaticMemoryBackups": "",
|
||||||
|
"fusionBackups": "",
|
||||||
|
"fusionBackupsMemory": "",
|
||||||
|
"invalidCronExpressionFormat": "",
|
||||||
|
"loadingBackupInfo": "",
|
||||||
|
"memoryBackupDirectory": "",
|
||||||
|
"memoryBackups": "",
|
||||||
|
"memoryBackupScheduleCron": "",
|
||||||
|
"memoryBackupScope": "",
|
||||||
|
"memoryRetentionCount": "",
|
||||||
|
"more": "",
|
||||||
|
"mustBeBetween1And100": "",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "",
|
||||||
|
"projectOnlyFusionMemory": "",
|
||||||
|
"retentionCount": "",
|
||||||
|
"totalSize": "",
|
||||||
|
"view": "",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
|
||||||
},
|
},
|
||||||
"clearToDefault": "",
|
"clearToDefault": "",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": ""
|
"saveFailed": ""
|
||||||
},
|
},
|
||||||
"closeModal": "Fermer la boîte de dialogue de conflit",
|
"closeModal": "Fermer la boîte de dialogue de conflit",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "",
|
||||||
|
"commands": "",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "",
|
||||||
|
"eGPnpmBuild": "",
|
||||||
|
"eGPnpmTest": "",
|
||||||
|
"testCommand": ""
|
||||||
|
},
|
||||||
"conflictModalTitle": "Résoudre les conflits de paramètres",
|
"conflictModalTitle": "Résoudre les conflits de paramètres",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": "",
|
||||||
|
"featureFlags": ""
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
"version": "Version {{version}}"
|
"version": "Version {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "",
|
||||||
|
"200": "",
|
||||||
|
"3000": "",
|
||||||
|
"14Days": "",
|
||||||
|
"30Days": "",
|
||||||
|
"60Days": "",
|
||||||
|
"7Days": "",
|
||||||
|
"90Days": "",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": "",
|
||||||
|
"autoCleanupOldChats": "",
|
||||||
|
"autoPruneOldMail": "",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "",
|
||||||
|
"builtInWorkflows": "",
|
||||||
|
"capacityRiskBanner": "",
|
||||||
|
"chatHistory": "",
|
||||||
|
"chatRooms": "",
|
||||||
|
"completionDocumentationAutomation": "",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": "",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": "",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "",
|
||||||
|
"defaultTrackingModeForNewTasks": "",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": "",
|
||||||
|
"fN": "",
|
||||||
|
"general": "",
|
||||||
|
"gitHubTracking": "",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "",
|
||||||
"learnMore": "En savoir plus",
|
"learnMore": "En savoir plus",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": "",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "",
|
||||||
|
"off": "",
|
||||||
|
"offDefault": "",
|
||||||
|
"onForNewTasks": "",
|
||||||
|
"operationalLogRetention": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"prefixForNewTaskIDsEGKB": "",
|
||||||
|
"prefixMustBe110UppercaseLetters": "",
|
||||||
|
"projectDefaultTrackingRepo": "",
|
||||||
|
"recentVerbatimRoomMessages": "",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "",
|
||||||
|
"requireChangesetChangesetMd": "",
|
||||||
|
"roomCompactionFetchLimit": "",
|
||||||
|
"roomSummaryMaxCharacters": "",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": "",
|
||||||
"settingsSaved": "Paramètres enregistrés",
|
"settingsSaved": "Paramètres enregistrés",
|
||||||
|
"showCapacityRiskBanner": "",
|
||||||
|
"showQuickChatButton": "",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "",
|
||||||
|
"taskPrefix": "",
|
||||||
|
"todoThreshold": "",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": "",
|
||||||
"updateAvailablePrefix": "v{{version}} disponible",
|
"updateAvailablePrefix": "v{{version}} disponible",
|
||||||
"updateCheckFailed": "",
|
"updateCheckFailed": "",
|
||||||
"updateFailed": "",
|
"updateFailed": "",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "",
|
"updateSuccess": "",
|
||||||
"updateSuccessToast": "",
|
"updateSuccessToast": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upToDate": "Vous êtes à jour ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "",
|
||||||
|
"upToDate": "Vous êtes à jour ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": "",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": "",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": "",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": ""
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||||
|
"autoReloadDashboardOnVersionChange": "",
|
||||||
|
"checkForThe": "",
|
||||||
|
"checkForUpdatesAutomatically": "",
|
||||||
|
"cLIBinaryOnPATH": "",
|
||||||
|
"cLIBySpawning": "",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": "",
|
||||||
|
"dailyRecommended": "",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": "",
|
||||||
|
"frequency": "",
|
||||||
|
"general": "",
|
||||||
|
"globalDefaultTrackingRepo": "",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||||
|
"manualOnlyNeverAutoCheck": "",
|
||||||
|
"onStartupOncePerServerLaunch": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": "",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": "",
|
||||||
|
"saveAIThinkingForPermanentAgents": "",
|
||||||
|
"saveAIThinkingLogs": "",
|
||||||
|
"saveToolOutputInAgentLogs": "",
|
||||||
|
"updates": "",
|
||||||
|
"weekly": "",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": "",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": "",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": "",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": ""
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "",
|
||||||
|
"default": "",
|
||||||
|
"default2": "",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "",
|
||||||
|
"defaultModel": "",
|
||||||
|
"deny": "",
|
||||||
|
"fallbackModel": "",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": "",
|
||||||
|
"fusion": "",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": "",
|
||||||
|
"httpsRunfusionAi": "",
|
||||||
|
"latency": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"noFallback": "",
|
||||||
|
"openaiAnthropic": "",
|
||||||
|
"openRouterAdvanced": "",
|
||||||
|
"openRouterAllowFallbacks": "",
|
||||||
|
"openRouterHTTPReferer": "",
|
||||||
|
"openRouterOutputModalitiesFilter": "",
|
||||||
|
"openRouterRoutingIgnore": "",
|
||||||
|
"openRouterRoutingOnly": "",
|
||||||
|
"openRouterRoutingOrder": "",
|
||||||
|
"openRouterRoutingSort": "",
|
||||||
|
"openRouterSupportedParametersFilter": "",
|
||||||
|
"openRouterXTitle": "",
|
||||||
|
"price": "",
|
||||||
|
"providerName": "",
|
||||||
|
"requireParameters": "",
|
||||||
|
"startupModelSync": "",
|
||||||
|
"syncOpencodeGoModelListAtStartup": "",
|
||||||
|
"syncOpenRouterModelListAtStartup": "",
|
||||||
|
"text": "",
|
||||||
|
"thinkingEffort": "",
|
||||||
|
"throughput": "",
|
||||||
|
"toolsStructuredOutputs": "",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": "",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "Garder la version distante",
|
"keepRemote": "Garder la version distante",
|
||||||
"loading": "Chargement…",
|
"loading": "Chargement…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": "",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "",
|
||||||
|
"autoSummarizeMemory": "",
|
||||||
|
"bytesUpdated": "",
|
||||||
|
"cannotSave": "",
|
||||||
|
"checkingMemoryWriteAccess": "",
|
||||||
"compacting": "Compactage…",
|
"compacting": "Compactage…",
|
||||||
|
"compactionThresholdChars": "",
|
||||||
"compactSelectedFile": "Compacter le fichier sélectionné",
|
"compactSelectedFile": "Compacter le fichier sélectionné",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "",
|
||||||
|
"cronExpressionForDreamProcessing": "",
|
||||||
"dreamCompleted": "Traitement des rêves terminé",
|
"dreamCompleted": "Traitement des rêves terminé",
|
||||||
|
"dreaming": "",
|
||||||
"dreamNow": "Rêver maintenant",
|
"dreamNow": "Rêver maintenant",
|
||||||
|
"dreamSchedule": "",
|
||||||
|
"enableMemoryTools": "",
|
||||||
|
"failedToLoadBackendStatus": "",
|
||||||
|
"for": "",
|
||||||
"installing": "Installation…",
|
"installing": "Installation…",
|
||||||
"installQmd": "Installer qmd",
|
"installQmd": "Installer qmd",
|
||||||
|
"loadingMemory": "",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "",
|
||||||
|
"memory": "",
|
||||||
"memoryCompacted": "Fichier mémoire compacté",
|
"memoryCompacted": "Fichier mémoire compacté",
|
||||||
|
"memoryFile": "",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": "",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": "",
|
||||||
|
"memoryLivesIn": "",
|
||||||
"memorySaved": "Mémoire enregistrée",
|
"memorySaved": "Mémoire enregistrée",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "",
|
||||||
|
"noMatchingMemoryFound": "",
|
||||||
|
"processDreamsFromDailyMemory": "",
|
||||||
|
"qmd": "",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": "",
|
||||||
|
"result": "",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "",
|
||||||
"saveMemory": "Enregistrer la mémoire",
|
"saveMemory": "Enregistrer la mémoire",
|
||||||
|
"scheduleCron": "",
|
||||||
|
"searchMemoryWithQmd": "",
|
||||||
"testing": "Test en cours…",
|
"testing": "Test en cours…",
|
||||||
"testRetrieval": "Tester la récupération"
|
"testRetrieval": "Tester la récupération",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "",
|
||||||
|
"addFusionAsCoAuthorOnCommits": "",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": "",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "",
|
||||||
|
"aIMerge": "",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": "",
|
||||||
|
"aIOnly": "",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": "",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "",
|
||||||
|
"alwaysSquashDirectMerges": "",
|
||||||
|
"and": "",
|
||||||
|
"and2": "",
|
||||||
|
"areListedFirstOrChoose": "",
|
||||||
|
"autoCompletionMode": "",
|
||||||
|
"autoDetect": "",
|
||||||
|
"autoDetectOriginHEADMain": "",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": "",
|
||||||
|
"autoMergeCompletedTasks": "",
|
||||||
|
"autoMergeConflictRetries": "",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": "",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": "",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": "",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "",
|
||||||
|
"block": "",
|
||||||
|
"blockStrict": "",
|
||||||
|
"both": "",
|
||||||
|
"branchName": "",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "",
|
||||||
|
"coAuthorEmail": "",
|
||||||
|
"coAuthorName": "",
|
||||||
|
"conflictFallbackStrategy": "",
|
||||||
|
"controlsThePostMergeAuditGate": "",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": "",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "",
|
||||||
|
"custom": "",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": "",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": "",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "",
|
||||||
|
"directMergeCommitRouting": "",
|
||||||
|
"directMergeIntoTheCurrentBranch": "",
|
||||||
|
"emailUsedInThe": "",
|
||||||
|
"enableTestMode": "",
|
||||||
|
"fastForwardOfLocalMainFrom": "",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "",
|
||||||
|
"finalFallback": "",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": "",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "",
|
||||||
|
"fusion": "",
|
||||||
|
"gitHubAuthentication": "",
|
||||||
|
"gitHubAuthMode": "",
|
||||||
|
"gitHubCLIGhAuth": "",
|
||||||
|
"gitHubPersonalAccessToken": "",
|
||||||
|
"gitRemoteToPushToEGOrigin": "",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "",
|
||||||
|
"includeTaskIDInCommitScope": "",
|
||||||
|
"insteadOf": "",
|
||||||
|
"integrationBranch": "",
|
||||||
|
"integrationWorktree": "",
|
||||||
|
"inYourProjectRootWillShowTheNew": "",
|
||||||
|
"isTheLegacyBehavior": "",
|
||||||
|
"isTheStricterOptInModeThatRefuses": "",
|
||||||
|
"legacy": "",
|
||||||
|
"legacy2": "",
|
||||||
|
"legacyAutoMergeStamp": "",
|
||||||
|
"legacyAutoMergeStampCleanup": "",
|
||||||
|
"legacyIntegrationBranchMode": "",
|
||||||
|
"maxAIReviewPasses": "",
|
||||||
|
"merge": "",
|
||||||
|
"moreDetails": "",
|
||||||
|
"nameUsedInThe": "",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": "",
|
||||||
|
"noreplyRunfusionAi": "",
|
||||||
|
"off": "",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "",
|
||||||
|
"offSkipAudit": "",
|
||||||
|
"optionsStartWithABestEffort": "",
|
||||||
|
"origin": "",
|
||||||
|
"personalAccessToken": "",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "",
|
||||||
|
"postMergeAuditMode": "",
|
||||||
|
"pushRemote": "",
|
||||||
|
"pushToRemoteAfterMerge": "",
|
||||||
|
"readyToCleanUp": "",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": "",
|
||||||
|
"reuseTaskWorktreeDefault": "",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": "",
|
||||||
|
"smart": "",
|
||||||
|
"smartConflictResolution": "",
|
||||||
|
"smartPreferMain": "",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "",
|
||||||
|
"smartPreferMainOverlapGuard": "",
|
||||||
|
"smartPreferTask": "",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": "",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": "",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": "",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": "",
|
||||||
|
"soTheTaskBranchWinsFastButCan": "",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": "",
|
||||||
|
"symbolicRefFallback": "",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": "",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "",
|
||||||
|
"toResolveViaTheStandardCascade": "",
|
||||||
|
"toTypeABranchThatDoesnAposT": "",
|
||||||
|
"trailer": "",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": "",
|
||||||
|
"useDropdown": "",
|
||||||
|
"useProjectRootLegacy": "",
|
||||||
|
"uses": "",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": "",
|
||||||
|
"warn": "",
|
||||||
|
"warnDefaultLogFindingsContinue": "",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": ""
|
||||||
},
|
},
|
||||||
"mergeManually": "Fusionner manuellement",
|
"mergeManually": "Fusionner manuellement",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "Spécifique à ce projet"
|
"project": "Spécifique à ce projet"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "",
|
||||||
|
"defaultExecutionNode": "",
|
||||||
|
"fallBackToLocal": "",
|
||||||
|
"localExecutionNoDefaultNode": "",
|
||||||
|
"nodeRouting": "",
|
||||||
|
"selectedNode": "",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "",
|
||||||
|
"unavailableNodePolicy": "",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": ""
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "",
|
||||||
|
"conflictResolution": "",
|
||||||
|
"enableAutomaticSettingsSync": "",
|
||||||
|
"every15Minutes": "",
|
||||||
|
"every1Hour": "",
|
||||||
|
"every30Minutes": "",
|
||||||
|
"every5Minutes": "",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "",
|
||||||
|
"keepLocal": "",
|
||||||
|
"keepRemote": "",
|
||||||
|
"lastWriteWins": "",
|
||||||
|
"nodeSync": "",
|
||||||
|
"syncInterval": "",
|
||||||
|
"syncModelAuthCredentials": "",
|
||||||
"workflowSettingsNotSynced": ""
|
"workflowSettingsNotSynced": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "",
|
||||||
|
"advanced": "",
|
||||||
|
"allFailuresLegacy": "",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": "",
|
||||||
|
"customNtfyServerURLOptional": "",
|
||||||
|
"dashboardHostname": "",
|
||||||
|
"discord": "",
|
||||||
|
"enable": "",
|
||||||
|
"failureNotificationDelayMs": "",
|
||||||
|
"failureNotificationMode": "",
|
||||||
|
"format": "",
|
||||||
|
"general": "",
|
||||||
|
"generic": "",
|
||||||
|
"howLongAFailureMustPersistBeforeA": "",
|
||||||
|
"httpLocalhost3000": "",
|
||||||
|
"httpsHooksExampleCom": "",
|
||||||
|
"httpsNtfySh": "",
|
||||||
|
"learnMoreAboutNtfySh": "",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": "",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": "",
|
||||||
|
"messageInbox": "",
|
||||||
|
"mustBeAValidURLStartingWithHttp": "",
|
||||||
|
"myTopicName": "",
|
||||||
|
"notifications": "",
|
||||||
|
"notifyOnEvents": "",
|
||||||
|
"ntfy": "",
|
||||||
|
"ntfyTopic": "",
|
||||||
|
"roomReply": "",
|
||||||
"sending": "Envoi…",
|
"sending": "Envoi…",
|
||||||
|
"slack": "",
|
||||||
|
"stickyFailuresOnlyDefault": "",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "",
|
||||||
"testMessageInbox": "Tester la boîte de réception des messages",
|
"testMessageInbox": "Tester la boîte de réception des messages",
|
||||||
"testNotification": "Tester la notification",
|
"testNotification": "Tester la notification",
|
||||||
"testRoomReply": "Tester la réponse en salle"
|
"testRoomReply": "Tester la réponse en salle",
|
||||||
|
"tk": "",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": "",
|
||||||
|
"webhook": "",
|
||||||
|
"webhookNotifications": "",
|
||||||
|
"webhookURL": "",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": ""
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": "",
|
||||||
|
"piExtensions": "",
|
||||||
|
"pluginManagerType": "",
|
||||||
|
"plugins": ""
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": "",
|
||||||
|
"advancedWorkflowPolicy": "",
|
||||||
|
"aIMergeCommitSummaries": "",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": "",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": "",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": "",
|
||||||
|
"configuredPresets": "",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": "",
|
||||||
|
"defaultWorkflowModelLaneActions": "",
|
||||||
|
"defaultWorkflowModelLanes": "",
|
||||||
|
"delete": "",
|
||||||
|
"edit": "",
|
||||||
|
"executorModel": "",
|
||||||
|
"fallsBackTo": "",
|
||||||
|
"loadingAvailableModels": "",
|
||||||
|
"loadingWorkflowModelLanes": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"modelPresets": "",
|
||||||
|
"name": "",
|
||||||
|
"noCap": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "",
|
||||||
|
"noPreset": "",
|
||||||
|
"noPresetsConfiguredYet": "",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": "",
|
||||||
|
"presetEditor": "",
|
||||||
|
"reset": "",
|
||||||
|
"resetToDefaultNoCap": "",
|
||||||
|
"resetToInheritFromGlobal": "",
|
||||||
|
"resetToInheritFromWorkflow": "",
|
||||||
|
"reviewerModel": "",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": "",
|
||||||
|
"tokenCap": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"useWorkflowDefault": "",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": "",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": ""
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": "",
|
||||||
|
"advancedNamedTunnel": "",
|
||||||
|
"advancedSettings": "",
|
||||||
|
"authenticatedURL": "",
|
||||||
|
"authLinks": "",
|
||||||
|
"authLinkTokenType": "",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "",
|
||||||
|
"cloudflare": "",
|
||||||
"cloudflaredInstalled": "cloudflared installé avec succès",
|
"cloudflaredInstalled": "cloudflared installé avec succès",
|
||||||
|
"cloudflaredIsInstalled": "",
|
||||||
|
"cloudflaredIsNotInstalled": "",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "",
|
||||||
|
"enableShortLivedTokens": "",
|
||||||
|
"expiresAt": "",
|
||||||
|
"external": "",
|
||||||
|
"externalTunnelQRCode": "",
|
||||||
|
"generateQR": "",
|
||||||
|
"generateShortLivedToken": "",
|
||||||
|
"httpsYourDomainExample": "",
|
||||||
|
"ifHomebrewIsUnavailable": "",
|
||||||
|
"ingressURL": "",
|
||||||
|
"installCloudflared": "",
|
||||||
|
"installing": "",
|
||||||
|
"lastShortLivedTokenExpiresAt": "",
|
||||||
|
"manualInstall": "",
|
||||||
|
"ms": "",
|
||||||
|
"namedTunnelModeEnabled": "",
|
||||||
|
"noExpiry": "",
|
||||||
|
"persistentToken": "",
|
||||||
"persistentTokenRegenerated": "Jeton persistant régénéré",
|
"persistentTokenRegenerated": "Jeton persistant régénéré",
|
||||||
|
"qRSVGMarkup": "",
|
||||||
|
"regeneratePersistentToken": "",
|
||||||
|
"rememberLastRunningState": "",
|
||||||
|
"remoteAccess": "",
|
||||||
|
"remoteAccessCode": "",
|
||||||
|
"remoteAccessQRCode": "",
|
||||||
|
"remoteProvider": "",
|
||||||
"restarting": "Redémarrage…",
|
"restarting": "Redémarrage…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "",
|
||||||
|
"scanToConnect": "",
|
||||||
|
"scanToOpen": "",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "",
|
||||||
|
"shortLivedToken": "",
|
||||||
"shortLivedTokenGenerated": "Jeton de courte durée généré",
|
"shortLivedTokenGenerated": "Jeton de courte durée généré",
|
||||||
|
"shortLivedTTLMs": "",
|
||||||
|
"showURL": "",
|
||||||
"startFresh": "Redémarrer",
|
"startFresh": "Redémarrer",
|
||||||
"starting": "Démarrage…",
|
"starting": "Démarrage…",
|
||||||
"startTunnel": "Démarrer le tunnel",
|
"startTunnel": "Démarrer le tunnel",
|
||||||
"stopping": "Arrêt…",
|
"stopping": "Arrêt…",
|
||||||
"stopTunnel": "Arrêter le tunnel",
|
"stopTunnel": "Arrêter le tunnel",
|
||||||
|
"tailnetURL": "",
|
||||||
|
"tailscale": "",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "",
|
||||||
|
"tokenType": "",
|
||||||
|
"tunnelDetected": "",
|
||||||
|
"tunnelName": "",
|
||||||
"tunnelRestarted": "Tunnel distant redémarré",
|
"tunnelRestarted": "Tunnel distant redémarré",
|
||||||
"tunnelStarted": "Tunnel distant démarré",
|
"tunnelStarted": "Tunnel distant démarré",
|
||||||
"tunnelStopped": "Tunnel distant arrêté",
|
"tunnelStopped": "Tunnel distant arrêté",
|
||||||
"useExisting": "Utiliser l'existant"
|
"tunnelToken": "",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": "",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": "",
|
||||||
|
"useExisting": "Utiliser l'existant",
|
||||||
|
"usingQuickTunnel": ""
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "",
|
||||||
|
"alwaysOn": "",
|
||||||
|
"brave": "",
|
||||||
|
"builtInUsesAgentWebTools": "",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": "",
|
||||||
|
"customSearchEngineId": "",
|
||||||
|
"defaultMaxConcurrentRuns": "",
|
||||||
|
"defaultMaxDurationMs": "",
|
||||||
|
"defaultMaxSourcesPerRun": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"gitHub": "",
|
||||||
|
"googleCustomSearch": "",
|
||||||
|
"googleSearchCX": "",
|
||||||
|
"httpsSearxExampleCom": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxSynthesisRounds": "",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": "",
|
||||||
|
"openAuthentication": "",
|
||||||
|
"openAuthenticationSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"researchDefaults": "",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": "",
|
||||||
|
"searchProvider": "",
|
||||||
|
"searXNG": "",
|
||||||
|
"searXNGURL": "",
|
||||||
|
"tavily": "",
|
||||||
|
"webSearch": ""
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"enableResearchInThisProject": "",
|
||||||
|
"github": "",
|
||||||
|
"llmSynthesis": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxConcurrentRuns": "",
|
||||||
|
"maxDurationMs": "",
|
||||||
|
"maxSourcesPerRun": "",
|
||||||
|
"pageFetch": "",
|
||||||
|
"projectResearchSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"webSearch": "",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": ""
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "Résoudre tous : Garder la version locale",
|
"resolveAllLocal": "Résoudre tous : Garder la version locale",
|
||||||
"resolveAllRemote": "Résoudre tous : Garder la version distante",
|
"resolveAllRemote": "Résoudre tous : Garder la version distante",
|
||||||
"resolvedSuccess": "Conflits de paramètres résolus avec succès",
|
"resolvedSuccess": "Conflits de paramètres résolus avec succès",
|
||||||
"resolveFailed": "Impossible de résoudre les conflits",
|
"resolveFailed": "Impossible de résoudre les conflits",
|
||||||
"resolving": "Résolution...",
|
"resolving": "Résolution...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "",
|
||||||
|
"openClawRuntime": "",
|
||||||
|
"paperclipRuntime": ""
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "",
|
||||||
|
"disabled": "",
|
||||||
|
"enableScheduledEvalRunsForThisProject": "",
|
||||||
|
"evaluatorModel": "",
|
||||||
|
"evaluatorProvider": "",
|
||||||
|
"followUpPolicy": "",
|
||||||
|
"gpt5": "",
|
||||||
|
"intervalMs": "",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": "",
|
||||||
|
"openai": "",
|
||||||
|
"retentionDays": "",
|
||||||
|
"scheduledEvals": "",
|
||||||
|
"suggestOnly": ""
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": "",
|
||||||
|
"archiveAgentLog": "",
|
||||||
|
"archiveCompletedTasksAfterDays": "",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "",
|
||||||
|
"browse": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "",
|
||||||
|
"compactSummaryAndRecentEntries": "",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "",
|
||||||
|
"docs": "",
|
||||||
|
"doNotArchiveAgentLogs": "",
|
||||||
|
"enableAutomaticTaskArchiving": "",
|
||||||
|
"enablePlanStalenessEnforcement": "",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "",
|
||||||
|
"fullAgentLog": "",
|
||||||
|
"globalMaxConcurrent": "",
|
||||||
|
"heartbeatScopeDiscipline": "",
|
||||||
|
"ignoredOverlapPaths": "",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": "",
|
||||||
|
"lite": "",
|
||||||
|
"maxConcurrentTasks": "",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "",
|
||||||
|
"maximumConcurrentPlanningAgents": "",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "",
|
||||||
|
"maxStuckRetries": "",
|
||||||
|
"maxTriageConcurrent": "",
|
||||||
|
"numberOfDaysATaskCanStayIn": "",
|
||||||
|
"off": "",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": "",
|
||||||
|
"or": "",
|
||||||
|
"pollIntervalMs": "",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": "",
|
||||||
|
"remove": "",
|
||||||
|
"scheduling": "",
|
||||||
"selectCurrentDir": "Sélectionner le répertoire actuel",
|
"selectCurrentDir": "Sélectionner le répertoire actuel",
|
||||||
"selectIgnoredOverlapPath": "Sélectionner le chemin de chevauchement ignoré"
|
"selectIgnoredOverlapPath": "Sélectionner le chemin de chevauchement ignoré",
|
||||||
|
"serializeTasksWithOverlappingFiles": "",
|
||||||
|
"staleHighFanOutEscalationHours": "",
|
||||||
|
"staleSpecThresholdHours": "",
|
||||||
|
"stepExecution": "",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "",
|
||||||
|
"strictDefault": "",
|
||||||
|
"stuckTaskTimeoutMinutes": "",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "Ces paramètres sont partagés entre tous vos projets Fusion.",
|
"globalBanner": "Ces paramètres sont partagés entre tous vos projets Fusion.",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": "",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": "",
|
||||||
|
"and": "",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": "",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "",
|
||||||
"awaitingApproval": "En attente d'approbation — ouvrez les approbations pour continuer.",
|
"awaitingApproval": "En attente d'approbation — ouvrez les approbations pour continuer.",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": "",
|
||||||
|
"browse": "",
|
||||||
|
"browseWorktreesDirectory": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"defaultsTo": "",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": "",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": "",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "",
|
||||||
|
"enableWorktrunkIntegration": "",
|
||||||
|
"failAndPauseTheTaskDefault": "",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": "",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": "",
|
||||||
|
"installedAt": "",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "",
|
||||||
"installWorktrunk": "Installer le binaire worktrunk",
|
"installWorktrunk": "Installer le binaire worktrunk",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": "",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "",
|
||||||
|
"maxWorktrees": "",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "",
|
||||||
"openApprovals": "Ouvrir les approbations",
|
"openApprovals": "Ouvrir les approbations",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "",
|
||||||
|
"optionalSupports": "",
|
||||||
|
"pnpmInstallFrozenLockfile": "",
|
||||||
|
"randomNamesEGSwiftFalcon": "",
|
||||||
|
"rebaseFromRemoteBeforeMerge": "",
|
||||||
|
"rebaseRemote": "",
|
||||||
|
"recycleWorktrees": "",
|
||||||
"selectWorktreesDir": "Sélectionner le répertoire des worktrees",
|
"selectWorktreesDir": "Sélectionner le répertoire des worktrees",
|
||||||
"tryAgain": "Réessayer"
|
"shellCommandToRunInEachNewWorktree": "",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": "",
|
||||||
|
"taskIDEGFN042": "",
|
||||||
|
"taskTitleEGFixLoginBug": "",
|
||||||
|
"tryAgain": "Réessayer",
|
||||||
|
"useGitDefault": "",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": "",
|
||||||
|
"whichRemoteToFetchForThePreMerge": "",
|
||||||
|
"worktreeInitCommand": "",
|
||||||
|
"worktreeNamingStyle": "",
|
||||||
|
"worktrees": "",
|
||||||
|
"worktreesDirectory": "",
|
||||||
|
"worktrunk": "",
|
||||||
|
"worktrunkBinaryPath": "",
|
||||||
|
"worktrunkFailureBehavior": "",
|
||||||
|
"worktrunkIntegration": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": ""
|
"notConfigured": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "",
|
||||||
|
"statusError": "",
|
||||||
|
"statusOffline": "",
|
||||||
|
"statusOnline": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "",
|
"apiKeyFormatError": "",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "날짜 (1–28)",
|
"resetDayMonthly": "날짜 (1–28)",
|
||||||
"resetDayWeekly": "요일 (0=일요일)",
|
"resetDayWeekly": "요일 (0=일요일)",
|
||||||
"resetting": "초기화 중...",
|
"resetting": "초기화 중...",
|
||||||
|
"resizeSidebar": "",
|
||||||
"result": "결과",
|
"result": "결과",
|
||||||
"resultCreated_one": "",
|
"resultCreated_one": "",
|
||||||
"resultCreated_other": "",
|
"resultCreated_other": "",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "방 만들기"
|
"title": "방 만들기"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "대시보드 초기화 중...",
|
"initializingDashboard": "대시보드 초기화 중...",
|
||||||
"loadingMessage": "Fusion 대시보드 로드 중",
|
"loadingMessage": "Fusion 대시보드 로드 중",
|
||||||
"loadingProgress": "대시보드 로딩 진행 상황",
|
"loadingProgress": "대시보드 로딩 진행 상황",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"save": "저장"
|
"save": "저장"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "",
|
||||||
|
"agentProvisioningApprovals": "",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": "",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": ""
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "",
|
||||||
"language": "언어",
|
"language": "언어",
|
||||||
"languageAuto": "자동",
|
"languageAuto": "자동",
|
||||||
"languageAutoHint": "브라우저 언어를 따릅니다",
|
"languageAutoHint": "브라우저 언어를 따릅니다",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": "",
|
||||||
"title": "모양"
|
"title": "모양"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "인증",
|
"title": "인증",
|
||||||
"waitingForLogin": "로그인을 기다리는 중…"
|
"waitingForLogin": "로그인을 기다리는 중…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "",
|
||||||
|
"key": ""
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "",
|
||||||
|
"03": "",
|
||||||
|
"agentsOnlyFusionAgentMemory": "",
|
||||||
|
"allProjectAgents": "",
|
||||||
|
"and": "",
|
||||||
"backupCreated": "백업이 성공적으로 생성되었습니다",
|
"backupCreated": "백업이 성공적으로 생성되었습니다",
|
||||||
|
"backupDirectory": "",
|
||||||
"backupNow": "지금 백업",
|
"backupNow": "지금 백업",
|
||||||
"creating": "생성 중…"
|
"backups": "",
|
||||||
|
"backupS": "",
|
||||||
|
"backupScheduleCron": "",
|
||||||
|
"creating": "생성 중…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": "",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "",
|
||||||
|
"currentBackups": "",
|
||||||
|
"databaseBackups": "",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "",
|
||||||
|
"enableAutomaticDatabaseBackups": "",
|
||||||
|
"enableAutomaticMemoryBackups": "",
|
||||||
|
"fusionBackups": "",
|
||||||
|
"fusionBackupsMemory": "",
|
||||||
|
"invalidCronExpressionFormat": "",
|
||||||
|
"loadingBackupInfo": "",
|
||||||
|
"memoryBackupDirectory": "",
|
||||||
|
"memoryBackups": "",
|
||||||
|
"memoryBackupScheduleCron": "",
|
||||||
|
"memoryBackupScope": "",
|
||||||
|
"memoryRetentionCount": "",
|
||||||
|
"more": "",
|
||||||
|
"mustBeBetween1And100": "",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "",
|
||||||
|
"projectOnlyFusionMemory": "",
|
||||||
|
"retentionCount": "",
|
||||||
|
"totalSize": "",
|
||||||
|
"view": "",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
|
||||||
},
|
},
|
||||||
"clearToDefault": "",
|
"clearToDefault": "",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": ""
|
"saveFailed": ""
|
||||||
},
|
},
|
||||||
"closeModal": "충돌 모달 닫기",
|
"closeModal": "충돌 모달 닫기",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "",
|
||||||
|
"commands": "",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "",
|
||||||
|
"eGPnpmBuild": "",
|
||||||
|
"eGPnpmTest": "",
|
||||||
|
"testCommand": ""
|
||||||
|
},
|
||||||
"conflictModalTitle": "설정 충돌 해결",
|
"conflictModalTitle": "설정 충돌 해결",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": "",
|
||||||
|
"featureFlags": ""
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "도움말",
|
"help": "도움말",
|
||||||
"version": "버전 {{version}}"
|
"version": "버전 {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "",
|
||||||
|
"200": "",
|
||||||
|
"3000": "",
|
||||||
|
"14Days": "",
|
||||||
|
"30Days": "",
|
||||||
|
"60Days": "",
|
||||||
|
"7Days": "",
|
||||||
|
"90Days": "",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": "",
|
||||||
|
"autoCleanupOldChats": "",
|
||||||
|
"autoPruneOldMail": "",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "",
|
||||||
|
"builtInWorkflows": "",
|
||||||
|
"capacityRiskBanner": "",
|
||||||
|
"chatHistory": "",
|
||||||
|
"chatRooms": "",
|
||||||
|
"completionDocumentationAutomation": "",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": "",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": "",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "",
|
||||||
|
"defaultTrackingModeForNewTasks": "",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": "",
|
||||||
|
"fN": "",
|
||||||
|
"general": "",
|
||||||
|
"gitHubTracking": "",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "",
|
||||||
"learnMore": "더 알아보기",
|
"learnMore": "더 알아보기",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": "",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "",
|
||||||
|
"off": "",
|
||||||
|
"offDefault": "",
|
||||||
|
"onForNewTasks": "",
|
||||||
|
"operationalLogRetention": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"prefixForNewTaskIDsEGKB": "",
|
||||||
|
"prefixMustBe110UppercaseLetters": "",
|
||||||
|
"projectDefaultTrackingRepo": "",
|
||||||
|
"recentVerbatimRoomMessages": "",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "",
|
||||||
|
"requireChangesetChangesetMd": "",
|
||||||
|
"roomCompactionFetchLimit": "",
|
||||||
|
"roomSummaryMaxCharacters": "",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": "",
|
||||||
"settingsSaved": "설정이 저장되었습니다",
|
"settingsSaved": "설정이 저장되었습니다",
|
||||||
|
"showCapacityRiskBanner": "",
|
||||||
|
"showQuickChatButton": "",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "",
|
||||||
|
"taskPrefix": "",
|
||||||
|
"todoThreshold": "",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": "",
|
||||||
"updateAvailablePrefix": "v{{version}} 사용 가능",
|
"updateAvailablePrefix": "v{{version}} 사용 가능",
|
||||||
"updateCheckFailed": "",
|
"updateCheckFailed": "",
|
||||||
"updateFailed": "",
|
"updateFailed": "",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "",
|
"updateSuccess": "",
|
||||||
"updateSuccessToast": "",
|
"updateSuccessToast": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upToDate": "최신 버전입니다 ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "",
|
||||||
|
"upToDate": "최신 버전입니다 ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": "",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": "",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": "",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": ""
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||||
|
"autoReloadDashboardOnVersionChange": "",
|
||||||
|
"checkForThe": "",
|
||||||
|
"checkForUpdatesAutomatically": "",
|
||||||
|
"cLIBinaryOnPATH": "",
|
||||||
|
"cLIBySpawning": "",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": "",
|
||||||
|
"dailyRecommended": "",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": "",
|
||||||
|
"frequency": "",
|
||||||
|
"general": "",
|
||||||
|
"globalDefaultTrackingRepo": "",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||||
|
"manualOnlyNeverAutoCheck": "",
|
||||||
|
"onStartupOncePerServerLaunch": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": "",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": "",
|
||||||
|
"saveAIThinkingForPermanentAgents": "",
|
||||||
|
"saveAIThinkingLogs": "",
|
||||||
|
"saveToolOutputInAgentLogs": "",
|
||||||
|
"updates": "",
|
||||||
|
"weekly": "",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": "",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": "",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": "",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": ""
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "",
|
||||||
|
"default": "",
|
||||||
|
"default2": "",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "",
|
||||||
|
"defaultModel": "",
|
||||||
|
"deny": "",
|
||||||
|
"fallbackModel": "",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": "",
|
||||||
|
"fusion": "",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": "",
|
||||||
|
"httpsRunfusionAi": "",
|
||||||
|
"latency": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"noFallback": "",
|
||||||
|
"openaiAnthropic": "",
|
||||||
|
"openRouterAdvanced": "",
|
||||||
|
"openRouterAllowFallbacks": "",
|
||||||
|
"openRouterHTTPReferer": "",
|
||||||
|
"openRouterOutputModalitiesFilter": "",
|
||||||
|
"openRouterRoutingIgnore": "",
|
||||||
|
"openRouterRoutingOnly": "",
|
||||||
|
"openRouterRoutingOrder": "",
|
||||||
|
"openRouterRoutingSort": "",
|
||||||
|
"openRouterSupportedParametersFilter": "",
|
||||||
|
"openRouterXTitle": "",
|
||||||
|
"price": "",
|
||||||
|
"providerName": "",
|
||||||
|
"requireParameters": "",
|
||||||
|
"startupModelSync": "",
|
||||||
|
"syncOpencodeGoModelListAtStartup": "",
|
||||||
|
"syncOpenRouterModelListAtStartup": "",
|
||||||
|
"text": "",
|
||||||
|
"thinkingEffort": "",
|
||||||
|
"throughput": "",
|
||||||
|
"toolsStructuredOutputs": "",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": "",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "원격 유지",
|
"keepRemote": "원격 유지",
|
||||||
"loading": "불러오는 중…",
|
"loading": "불러오는 중…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": "",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "",
|
||||||
|
"autoSummarizeMemory": "",
|
||||||
|
"bytesUpdated": "",
|
||||||
|
"cannotSave": "",
|
||||||
|
"checkingMemoryWriteAccess": "",
|
||||||
"compacting": "압축 중…",
|
"compacting": "압축 중…",
|
||||||
|
"compactionThresholdChars": "",
|
||||||
"compactSelectedFile": "선택한 파일 압축",
|
"compactSelectedFile": "선택한 파일 압축",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "",
|
||||||
|
"cronExpressionForDreamProcessing": "",
|
||||||
"dreamCompleted": "드림 처리가 완료되었습니다",
|
"dreamCompleted": "드림 처리가 완료되었습니다",
|
||||||
|
"dreaming": "",
|
||||||
"dreamNow": "지금 드림",
|
"dreamNow": "지금 드림",
|
||||||
|
"dreamSchedule": "",
|
||||||
|
"enableMemoryTools": "",
|
||||||
|
"failedToLoadBackendStatus": "",
|
||||||
|
"for": "",
|
||||||
"installing": "설치 중…",
|
"installing": "설치 중…",
|
||||||
"installQmd": "qmd 설치",
|
"installQmd": "qmd 설치",
|
||||||
|
"loadingMemory": "",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "",
|
||||||
|
"memory": "",
|
||||||
"memoryCompacted": "메모리 파일이 압축되었습니다",
|
"memoryCompacted": "메모리 파일이 압축되었습니다",
|
||||||
|
"memoryFile": "",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": "",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": "",
|
||||||
|
"memoryLivesIn": "",
|
||||||
"memorySaved": "메모리가 저장되었습니다",
|
"memorySaved": "메모리가 저장되었습니다",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "",
|
||||||
|
"noMatchingMemoryFound": "",
|
||||||
|
"processDreamsFromDailyMemory": "",
|
||||||
|
"qmd": "",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": "",
|
||||||
|
"result": "",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "",
|
||||||
"saveMemory": "메모리 저장",
|
"saveMemory": "메모리 저장",
|
||||||
|
"scheduleCron": "",
|
||||||
|
"searchMemoryWithQmd": "",
|
||||||
"testing": "테스트 중…",
|
"testing": "테스트 중…",
|
||||||
"testRetrieval": "검색 테스트"
|
"testRetrieval": "검색 테스트",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "",
|
||||||
|
"addFusionAsCoAuthorOnCommits": "",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": "",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "",
|
||||||
|
"aIMerge": "",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": "",
|
||||||
|
"aIOnly": "",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": "",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "",
|
||||||
|
"alwaysSquashDirectMerges": "",
|
||||||
|
"and": "",
|
||||||
|
"and2": "",
|
||||||
|
"areListedFirstOrChoose": "",
|
||||||
|
"autoCompletionMode": "",
|
||||||
|
"autoDetect": "",
|
||||||
|
"autoDetectOriginHEADMain": "",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": "",
|
||||||
|
"autoMergeCompletedTasks": "",
|
||||||
|
"autoMergeConflictRetries": "",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": "",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": "",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": "",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "",
|
||||||
|
"block": "",
|
||||||
|
"blockStrict": "",
|
||||||
|
"both": "",
|
||||||
|
"branchName": "",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "",
|
||||||
|
"coAuthorEmail": "",
|
||||||
|
"coAuthorName": "",
|
||||||
|
"conflictFallbackStrategy": "",
|
||||||
|
"controlsThePostMergeAuditGate": "",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": "",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "",
|
||||||
|
"custom": "",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": "",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": "",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "",
|
||||||
|
"directMergeCommitRouting": "",
|
||||||
|
"directMergeIntoTheCurrentBranch": "",
|
||||||
|
"emailUsedInThe": "",
|
||||||
|
"enableTestMode": "",
|
||||||
|
"fastForwardOfLocalMainFrom": "",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "",
|
||||||
|
"finalFallback": "",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": "",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "",
|
||||||
|
"fusion": "",
|
||||||
|
"gitHubAuthentication": "",
|
||||||
|
"gitHubAuthMode": "",
|
||||||
|
"gitHubCLIGhAuth": "",
|
||||||
|
"gitHubPersonalAccessToken": "",
|
||||||
|
"gitRemoteToPushToEGOrigin": "",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "",
|
||||||
|
"includeTaskIDInCommitScope": "",
|
||||||
|
"insteadOf": "",
|
||||||
|
"integrationBranch": "",
|
||||||
|
"integrationWorktree": "",
|
||||||
|
"inYourProjectRootWillShowTheNew": "",
|
||||||
|
"isTheLegacyBehavior": "",
|
||||||
|
"isTheStricterOptInModeThatRefuses": "",
|
||||||
|
"legacy": "",
|
||||||
|
"legacy2": "",
|
||||||
|
"legacyAutoMergeStamp": "",
|
||||||
|
"legacyAutoMergeStampCleanup": "",
|
||||||
|
"legacyIntegrationBranchMode": "",
|
||||||
|
"maxAIReviewPasses": "",
|
||||||
|
"merge": "",
|
||||||
|
"moreDetails": "",
|
||||||
|
"nameUsedInThe": "",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": "",
|
||||||
|
"noreplyRunfusionAi": "",
|
||||||
|
"off": "",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "",
|
||||||
|
"offSkipAudit": "",
|
||||||
|
"optionsStartWithABestEffort": "",
|
||||||
|
"origin": "",
|
||||||
|
"personalAccessToken": "",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "",
|
||||||
|
"postMergeAuditMode": "",
|
||||||
|
"pushRemote": "",
|
||||||
|
"pushToRemoteAfterMerge": "",
|
||||||
|
"readyToCleanUp": "",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": "",
|
||||||
|
"reuseTaskWorktreeDefault": "",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": "",
|
||||||
|
"smart": "",
|
||||||
|
"smartConflictResolution": "",
|
||||||
|
"smartPreferMain": "",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "",
|
||||||
|
"smartPreferMainOverlapGuard": "",
|
||||||
|
"smartPreferTask": "",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": "",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": "",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": "",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": "",
|
||||||
|
"soTheTaskBranchWinsFastButCan": "",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": "",
|
||||||
|
"symbolicRefFallback": "",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": "",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "",
|
||||||
|
"toResolveViaTheStandardCascade": "",
|
||||||
|
"toTypeABranchThatDoesnAposT": "",
|
||||||
|
"trailer": "",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": "",
|
||||||
|
"useDropdown": "",
|
||||||
|
"useProjectRootLegacy": "",
|
||||||
|
"uses": "",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": "",
|
||||||
|
"warn": "",
|
||||||
|
"warnDefaultLogFindingsContinue": "",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": ""
|
||||||
},
|
},
|
||||||
"mergeManually": "수동으로 병합",
|
"mergeManually": "수동으로 병합",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "이 프로젝트에만 적용됨"
|
"project": "이 프로젝트에만 적용됨"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "",
|
||||||
|
"defaultExecutionNode": "",
|
||||||
|
"fallBackToLocal": "",
|
||||||
|
"localExecutionNoDefaultNode": "",
|
||||||
|
"nodeRouting": "",
|
||||||
|
"selectedNode": "",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "",
|
||||||
|
"unavailableNodePolicy": "",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": ""
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "",
|
||||||
|
"conflictResolution": "",
|
||||||
|
"enableAutomaticSettingsSync": "",
|
||||||
|
"every15Minutes": "",
|
||||||
|
"every1Hour": "",
|
||||||
|
"every30Minutes": "",
|
||||||
|
"every5Minutes": "",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "",
|
||||||
|
"keepLocal": "",
|
||||||
|
"keepRemote": "",
|
||||||
|
"lastWriteWins": "",
|
||||||
|
"nodeSync": "",
|
||||||
|
"syncInterval": "",
|
||||||
|
"syncModelAuthCredentials": "",
|
||||||
"workflowSettingsNotSynced": ""
|
"workflowSettingsNotSynced": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "",
|
||||||
|
"advanced": "",
|
||||||
|
"allFailuresLegacy": "",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": "",
|
||||||
|
"customNtfyServerURLOptional": "",
|
||||||
|
"dashboardHostname": "",
|
||||||
|
"discord": "",
|
||||||
|
"enable": "",
|
||||||
|
"failureNotificationDelayMs": "",
|
||||||
|
"failureNotificationMode": "",
|
||||||
|
"format": "",
|
||||||
|
"general": "",
|
||||||
|
"generic": "",
|
||||||
|
"howLongAFailureMustPersistBeforeA": "",
|
||||||
|
"httpLocalhost3000": "",
|
||||||
|
"httpsHooksExampleCom": "",
|
||||||
|
"httpsNtfySh": "",
|
||||||
|
"learnMoreAboutNtfySh": "",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": "",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": "",
|
||||||
|
"messageInbox": "",
|
||||||
|
"mustBeAValidURLStartingWithHttp": "",
|
||||||
|
"myTopicName": "",
|
||||||
|
"notifications": "",
|
||||||
|
"notifyOnEvents": "",
|
||||||
|
"ntfy": "",
|
||||||
|
"ntfyTopic": "",
|
||||||
|
"roomReply": "",
|
||||||
"sending": "전송 중…",
|
"sending": "전송 중…",
|
||||||
|
"slack": "",
|
||||||
|
"stickyFailuresOnlyDefault": "",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "",
|
||||||
"testMessageInbox": "테스트 메시지 받은 편지함",
|
"testMessageInbox": "테스트 메시지 받은 편지함",
|
||||||
"testNotification": "테스트 알림",
|
"testNotification": "테스트 알림",
|
||||||
"testRoomReply": "테스트 룸 답장"
|
"testRoomReply": "테스트 룸 답장",
|
||||||
|
"tk": "",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": "",
|
||||||
|
"webhook": "",
|
||||||
|
"webhookNotifications": "",
|
||||||
|
"webhookURL": "",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": ""
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": "",
|
||||||
|
"piExtensions": "",
|
||||||
|
"pluginManagerType": "",
|
||||||
|
"plugins": ""
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": "",
|
||||||
|
"advancedWorkflowPolicy": "",
|
||||||
|
"aIMergeCommitSummaries": "",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": "",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": "",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": "",
|
||||||
|
"configuredPresets": "",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": "",
|
||||||
|
"defaultWorkflowModelLaneActions": "",
|
||||||
|
"defaultWorkflowModelLanes": "",
|
||||||
|
"delete": "",
|
||||||
|
"edit": "",
|
||||||
|
"executorModel": "",
|
||||||
|
"fallsBackTo": "",
|
||||||
|
"loadingAvailableModels": "",
|
||||||
|
"loadingWorkflowModelLanes": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"modelPresets": "",
|
||||||
|
"name": "",
|
||||||
|
"noCap": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "",
|
||||||
|
"noPreset": "",
|
||||||
|
"noPresetsConfiguredYet": "",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": "",
|
||||||
|
"presetEditor": "",
|
||||||
|
"reset": "",
|
||||||
|
"resetToDefaultNoCap": "",
|
||||||
|
"resetToInheritFromGlobal": "",
|
||||||
|
"resetToInheritFromWorkflow": "",
|
||||||
|
"reviewerModel": "",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": "",
|
||||||
|
"tokenCap": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"useWorkflowDefault": "",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": "",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": ""
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": "",
|
||||||
|
"advancedNamedTunnel": "",
|
||||||
|
"advancedSettings": "",
|
||||||
|
"authenticatedURL": "",
|
||||||
|
"authLinks": "",
|
||||||
|
"authLinkTokenType": "",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "",
|
||||||
|
"cloudflare": "",
|
||||||
"cloudflaredInstalled": "cloudflared가 성공적으로 설치되었습니다",
|
"cloudflaredInstalled": "cloudflared가 성공적으로 설치되었습니다",
|
||||||
|
"cloudflaredIsInstalled": "",
|
||||||
|
"cloudflaredIsNotInstalled": "",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "",
|
||||||
|
"enableShortLivedTokens": "",
|
||||||
|
"expiresAt": "",
|
||||||
|
"external": "",
|
||||||
|
"externalTunnelQRCode": "",
|
||||||
|
"generateQR": "",
|
||||||
|
"generateShortLivedToken": "",
|
||||||
|
"httpsYourDomainExample": "",
|
||||||
|
"ifHomebrewIsUnavailable": "",
|
||||||
|
"ingressURL": "",
|
||||||
|
"installCloudflared": "",
|
||||||
|
"installing": "",
|
||||||
|
"lastShortLivedTokenExpiresAt": "",
|
||||||
|
"manualInstall": "",
|
||||||
|
"ms": "",
|
||||||
|
"namedTunnelModeEnabled": "",
|
||||||
|
"noExpiry": "",
|
||||||
|
"persistentToken": "",
|
||||||
"persistentTokenRegenerated": "영구 토큰이 재생성되었습니다",
|
"persistentTokenRegenerated": "영구 토큰이 재생성되었습니다",
|
||||||
|
"qRSVGMarkup": "",
|
||||||
|
"regeneratePersistentToken": "",
|
||||||
|
"rememberLastRunningState": "",
|
||||||
|
"remoteAccess": "",
|
||||||
|
"remoteAccessCode": "",
|
||||||
|
"remoteAccessQRCode": "",
|
||||||
|
"remoteProvider": "",
|
||||||
"restarting": "재시작 중…",
|
"restarting": "재시작 중…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "",
|
||||||
|
"scanToConnect": "",
|
||||||
|
"scanToOpen": "",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "",
|
||||||
|
"shortLivedToken": "",
|
||||||
"shortLivedTokenGenerated": "단기 토큰이 생성되었습니다",
|
"shortLivedTokenGenerated": "단기 토큰이 생성되었습니다",
|
||||||
|
"shortLivedTTLMs": "",
|
||||||
|
"showURL": "",
|
||||||
"startFresh": "새로 시작",
|
"startFresh": "새로 시작",
|
||||||
"starting": "시작 중…",
|
"starting": "시작 중…",
|
||||||
"startTunnel": "터널 시작",
|
"startTunnel": "터널 시작",
|
||||||
"stopping": "중지 중…",
|
"stopping": "중지 중…",
|
||||||
"stopTunnel": "터널 중지",
|
"stopTunnel": "터널 중지",
|
||||||
|
"tailnetURL": "",
|
||||||
|
"tailscale": "",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "",
|
||||||
|
"tokenType": "",
|
||||||
|
"tunnelDetected": "",
|
||||||
|
"tunnelName": "",
|
||||||
"tunnelRestarted": "원격 터널이 재시작되었습니다",
|
"tunnelRestarted": "원격 터널이 재시작되었습니다",
|
||||||
"tunnelStarted": "원격 터널이 시작되었습니다",
|
"tunnelStarted": "원격 터널이 시작되었습니다",
|
||||||
"tunnelStopped": "원격 터널이 중지되었습니다",
|
"tunnelStopped": "원격 터널이 중지되었습니다",
|
||||||
"useExisting": "기존 사용"
|
"tunnelToken": "",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": "",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": "",
|
||||||
|
"useExisting": "기존 사용",
|
||||||
|
"usingQuickTunnel": ""
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "",
|
||||||
|
"alwaysOn": "",
|
||||||
|
"brave": "",
|
||||||
|
"builtInUsesAgentWebTools": "",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": "",
|
||||||
|
"customSearchEngineId": "",
|
||||||
|
"defaultMaxConcurrentRuns": "",
|
||||||
|
"defaultMaxDurationMs": "",
|
||||||
|
"defaultMaxSourcesPerRun": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"gitHub": "",
|
||||||
|
"googleCustomSearch": "",
|
||||||
|
"googleSearchCX": "",
|
||||||
|
"httpsSearxExampleCom": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxSynthesisRounds": "",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": "",
|
||||||
|
"openAuthentication": "",
|
||||||
|
"openAuthenticationSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"researchDefaults": "",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": "",
|
||||||
|
"searchProvider": "",
|
||||||
|
"searXNG": "",
|
||||||
|
"searXNGURL": "",
|
||||||
|
"tavily": "",
|
||||||
|
"webSearch": ""
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"enableResearchInThisProject": "",
|
||||||
|
"github": "",
|
||||||
|
"llmSynthesis": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxConcurrentRuns": "",
|
||||||
|
"maxDurationMs": "",
|
||||||
|
"maxSourcesPerRun": "",
|
||||||
|
"pageFetch": "",
|
||||||
|
"projectResearchSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"webSearch": "",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": ""
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "모두 해결: 로컬 유지",
|
"resolveAllLocal": "모두 해결: 로컬 유지",
|
||||||
"resolveAllRemote": "모두 해결: 원격 유지",
|
"resolveAllRemote": "모두 해결: 원격 유지",
|
||||||
"resolvedSuccess": "설정 충돌이 성공적으로 해결되었습니다",
|
"resolvedSuccess": "설정 충돌이 성공적으로 해결되었습니다",
|
||||||
"resolveFailed": "충돌 해결에 실패했습니다",
|
"resolveFailed": "충돌 해결에 실패했습니다",
|
||||||
"resolving": "해결 중...",
|
"resolving": "해결 중...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "",
|
||||||
|
"openClawRuntime": "",
|
||||||
|
"paperclipRuntime": ""
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "",
|
||||||
|
"disabled": "",
|
||||||
|
"enableScheduledEvalRunsForThisProject": "",
|
||||||
|
"evaluatorModel": "",
|
||||||
|
"evaluatorProvider": "",
|
||||||
|
"followUpPolicy": "",
|
||||||
|
"gpt5": "",
|
||||||
|
"intervalMs": "",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": "",
|
||||||
|
"openai": "",
|
||||||
|
"retentionDays": "",
|
||||||
|
"scheduledEvals": "",
|
||||||
|
"suggestOnly": ""
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": "",
|
||||||
|
"archiveAgentLog": "",
|
||||||
|
"archiveCompletedTasksAfterDays": "",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "",
|
||||||
|
"browse": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "",
|
||||||
|
"compactSummaryAndRecentEntries": "",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "",
|
||||||
|
"docs": "",
|
||||||
|
"doNotArchiveAgentLogs": "",
|
||||||
|
"enableAutomaticTaskArchiving": "",
|
||||||
|
"enablePlanStalenessEnforcement": "",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "",
|
||||||
|
"fullAgentLog": "",
|
||||||
|
"globalMaxConcurrent": "",
|
||||||
|
"heartbeatScopeDiscipline": "",
|
||||||
|
"ignoredOverlapPaths": "",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": "",
|
||||||
|
"lite": "",
|
||||||
|
"maxConcurrentTasks": "",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "",
|
||||||
|
"maximumConcurrentPlanningAgents": "",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "",
|
||||||
|
"maxStuckRetries": "",
|
||||||
|
"maxTriageConcurrent": "",
|
||||||
|
"numberOfDaysATaskCanStayIn": "",
|
||||||
|
"off": "",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": "",
|
||||||
|
"or": "",
|
||||||
|
"pollIntervalMs": "",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": "",
|
||||||
|
"remove": "",
|
||||||
|
"scheduling": "",
|
||||||
"selectCurrentDir": "현재 디렉터리 선택",
|
"selectCurrentDir": "현재 디렉터리 선택",
|
||||||
"selectIgnoredOverlapPath": "무시된 겹침 경로 선택"
|
"selectIgnoredOverlapPath": "무시된 겹침 경로 선택",
|
||||||
|
"serializeTasksWithOverlappingFiles": "",
|
||||||
|
"staleHighFanOutEscalationHours": "",
|
||||||
|
"staleSpecThresholdHours": "",
|
||||||
|
"stepExecution": "",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "",
|
||||||
|
"strictDefault": "",
|
||||||
|
"stuckTaskTimeoutMinutes": "",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
|
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "설정",
|
"title": "설정",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": "",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": "",
|
||||||
|
"and": "",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": "",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "",
|
||||||
"awaitingApproval": "승인 대기 중 — 승인을 열어 계속하세요.",
|
"awaitingApproval": "승인 대기 중 — 승인을 열어 계속하세요.",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": "",
|
||||||
|
"browse": "",
|
||||||
|
"browseWorktreesDirectory": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"defaultsTo": "",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": "",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": "",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "",
|
||||||
|
"enableWorktrunkIntegration": "",
|
||||||
|
"failAndPauseTheTaskDefault": "",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": "",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": "",
|
||||||
|
"installedAt": "",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "",
|
||||||
"installWorktrunk": "worktrunk 바이너리 설치",
|
"installWorktrunk": "worktrunk 바이너리 설치",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": "",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "",
|
||||||
|
"maxWorktrees": "",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "",
|
||||||
"openApprovals": "승인 열기",
|
"openApprovals": "승인 열기",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "",
|
||||||
|
"optionalSupports": "",
|
||||||
|
"pnpmInstallFrozenLockfile": "",
|
||||||
|
"randomNamesEGSwiftFalcon": "",
|
||||||
|
"rebaseFromRemoteBeforeMerge": "",
|
||||||
|
"rebaseRemote": "",
|
||||||
|
"recycleWorktrees": "",
|
||||||
"selectWorktreesDir": "Worktrees 디렉터리 선택",
|
"selectWorktreesDir": "Worktrees 디렉터리 선택",
|
||||||
"tryAgain": "다시 시도"
|
"shellCommandToRunInEachNewWorktree": "",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": "",
|
||||||
|
"taskIDEGFN042": "",
|
||||||
|
"taskTitleEGFixLoginBug": "",
|
||||||
|
"tryAgain": "다시 시도",
|
||||||
|
"useGitDefault": "",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": "",
|
||||||
|
"whichRemoteToFetchForThePreMerge": "",
|
||||||
|
"worktreeInitCommand": "",
|
||||||
|
"worktreeNamingStyle": "",
|
||||||
|
"worktrees": "",
|
||||||
|
"worktreesDirectory": "",
|
||||||
|
"worktrunk": "",
|
||||||
|
"worktrunkBinaryPath": "",
|
||||||
|
"worktrunkFailureBehavior": "",
|
||||||
|
"worktrunkIntegration": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": ""
|
"notConfigured": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "",
|
||||||
|
"statusError": "",
|
||||||
|
"statusOffline": "",
|
||||||
|
"statusOnline": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "",
|
"apiKeyFormatError": "",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "每月几号(1–28)",
|
"resetDayMonthly": "每月几号(1–28)",
|
||||||
"resetDayWeekly": "星期几(0=周日)",
|
"resetDayWeekly": "星期几(0=周日)",
|
||||||
"resetting": "正在重置...",
|
"resetting": "正在重置...",
|
||||||
|
"resizeSidebar": "",
|
||||||
"result": "结果",
|
"result": "结果",
|
||||||
"resultCreated_one": "",
|
"resultCreated_one": "",
|
||||||
"resultCreated_other": "",
|
"resultCreated_other": "",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "创建房间"
|
"title": "创建房间"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "初始化仪表板...",
|
"initializingDashboard": "初始化仪表板...",
|
||||||
"loadingMessage": "加载 Fusion 仪表板",
|
"loadingMessage": "加载 Fusion 仪表板",
|
||||||
"loadingProgress": "仪表板加载进度",
|
"loadingProgress": "仪表板加载进度",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"save": "保存"
|
"save": "保存"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "",
|
||||||
|
"agentProvisioningApprovals": "",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": "",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": ""
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"languageAuto": "自动",
|
"languageAuto": "自动",
|
||||||
"languageAutoHint": "跟随浏览器语言",
|
"languageAutoHint": "跟随浏览器语言",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": "",
|
||||||
"title": "外观"
|
"title": "外观"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "认证",
|
"title": "认证",
|
||||||
"waitingForLogin": "等待登录…"
|
"waitingForLogin": "等待登录…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "",
|
||||||
|
"key": ""
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "",
|
||||||
|
"03": "",
|
||||||
|
"agentsOnlyFusionAgentMemory": "",
|
||||||
|
"allProjectAgents": "",
|
||||||
|
"and": "",
|
||||||
"backupCreated": "备份创建成功",
|
"backupCreated": "备份创建成功",
|
||||||
|
"backupDirectory": "",
|
||||||
"backupNow": "立即备份",
|
"backupNow": "立即备份",
|
||||||
"creating": "创建中…"
|
"backups": "",
|
||||||
|
"backupS": "",
|
||||||
|
"backupScheduleCron": "",
|
||||||
|
"creating": "创建中…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": "",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "",
|
||||||
|
"currentBackups": "",
|
||||||
|
"databaseBackups": "",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "",
|
||||||
|
"enableAutomaticDatabaseBackups": "",
|
||||||
|
"enableAutomaticMemoryBackups": "",
|
||||||
|
"fusionBackups": "",
|
||||||
|
"fusionBackupsMemory": "",
|
||||||
|
"invalidCronExpressionFormat": "",
|
||||||
|
"loadingBackupInfo": "",
|
||||||
|
"memoryBackupDirectory": "",
|
||||||
|
"memoryBackups": "",
|
||||||
|
"memoryBackupScheduleCron": "",
|
||||||
|
"memoryBackupScope": "",
|
||||||
|
"memoryRetentionCount": "",
|
||||||
|
"more": "",
|
||||||
|
"mustBeBetween1And100": "",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "",
|
||||||
|
"projectOnlyFusionMemory": "",
|
||||||
|
"retentionCount": "",
|
||||||
|
"totalSize": "",
|
||||||
|
"view": "",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
|
||||||
},
|
},
|
||||||
"clearToDefault": "",
|
"clearToDefault": "",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": ""
|
"saveFailed": ""
|
||||||
},
|
},
|
||||||
"closeModal": "关闭冲突模式",
|
"closeModal": "关闭冲突模式",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "",
|
||||||
|
"commands": "",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "",
|
||||||
|
"eGPnpmBuild": "",
|
||||||
|
"eGPnpmTest": "",
|
||||||
|
"testCommand": ""
|
||||||
|
},
|
||||||
"conflictModalTitle": "解决设置冲突",
|
"conflictModalTitle": "解决设置冲突",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": "",
|
||||||
|
"featureFlags": ""
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"version": "版本 {{version}}"
|
"version": "版本 {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "",
|
||||||
|
"200": "",
|
||||||
|
"3000": "",
|
||||||
|
"14Days": "",
|
||||||
|
"30Days": "",
|
||||||
|
"60Days": "",
|
||||||
|
"7Days": "",
|
||||||
|
"90Days": "",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": "",
|
||||||
|
"autoCleanupOldChats": "",
|
||||||
|
"autoPruneOldMail": "",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "",
|
||||||
|
"builtInWorkflows": "",
|
||||||
|
"capacityRiskBanner": "",
|
||||||
|
"chatHistory": "",
|
||||||
|
"chatRooms": "",
|
||||||
|
"completionDocumentationAutomation": "",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": "",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": "",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "",
|
||||||
|
"defaultTrackingModeForNewTasks": "",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": "",
|
||||||
|
"fN": "",
|
||||||
|
"general": "",
|
||||||
|
"gitHubTracking": "",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "",
|
||||||
"learnMore": "了解更多",
|
"learnMore": "了解更多",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": "",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "",
|
||||||
|
"off": "",
|
||||||
|
"offDefault": "",
|
||||||
|
"onForNewTasks": "",
|
||||||
|
"operationalLogRetention": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"prefixForNewTaskIDsEGKB": "",
|
||||||
|
"prefixMustBe110UppercaseLetters": "",
|
||||||
|
"projectDefaultTrackingRepo": "",
|
||||||
|
"recentVerbatimRoomMessages": "",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "",
|
||||||
|
"requireChangesetChangesetMd": "",
|
||||||
|
"roomCompactionFetchLimit": "",
|
||||||
|
"roomSummaryMaxCharacters": "",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": "",
|
||||||
"settingsSaved": "设置已保存",
|
"settingsSaved": "设置已保存",
|
||||||
|
"showCapacityRiskBanner": "",
|
||||||
|
"showQuickChatButton": "",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "",
|
||||||
|
"taskPrefix": "",
|
||||||
|
"todoThreshold": "",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": "",
|
||||||
"updateAvailablePrefix": "v{{version}} 可用",
|
"updateAvailablePrefix": "v{{version}} 可用",
|
||||||
"updateCheckFailed": "",
|
"updateCheckFailed": "",
|
||||||
"updateFailed": "",
|
"updateFailed": "",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "",
|
"updateSuccess": "",
|
||||||
"updateSuccessToast": "",
|
"updateSuccessToast": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upToDate": "您已是最新版本 ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "",
|
||||||
|
"upToDate": "您已是最新版本 ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": "",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": "",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": "",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": ""
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||||
|
"autoReloadDashboardOnVersionChange": "",
|
||||||
|
"checkForThe": "",
|
||||||
|
"checkForUpdatesAutomatically": "",
|
||||||
|
"cLIBinaryOnPATH": "",
|
||||||
|
"cLIBySpawning": "",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": "",
|
||||||
|
"dailyRecommended": "",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": "",
|
||||||
|
"frequency": "",
|
||||||
|
"general": "",
|
||||||
|
"globalDefaultTrackingRepo": "",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||||
|
"manualOnlyNeverAutoCheck": "",
|
||||||
|
"onStartupOncePerServerLaunch": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": "",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": "",
|
||||||
|
"saveAIThinkingForPermanentAgents": "",
|
||||||
|
"saveAIThinkingLogs": "",
|
||||||
|
"saveToolOutputInAgentLogs": "",
|
||||||
|
"updates": "",
|
||||||
|
"weekly": "",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": "",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": "",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": "",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": ""
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "",
|
||||||
|
"default": "",
|
||||||
|
"default2": "",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "",
|
||||||
|
"defaultModel": "",
|
||||||
|
"deny": "",
|
||||||
|
"fallbackModel": "",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": "",
|
||||||
|
"fusion": "",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": "",
|
||||||
|
"httpsRunfusionAi": "",
|
||||||
|
"latency": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"noFallback": "",
|
||||||
|
"openaiAnthropic": "",
|
||||||
|
"openRouterAdvanced": "",
|
||||||
|
"openRouterAllowFallbacks": "",
|
||||||
|
"openRouterHTTPReferer": "",
|
||||||
|
"openRouterOutputModalitiesFilter": "",
|
||||||
|
"openRouterRoutingIgnore": "",
|
||||||
|
"openRouterRoutingOnly": "",
|
||||||
|
"openRouterRoutingOrder": "",
|
||||||
|
"openRouterRoutingSort": "",
|
||||||
|
"openRouterSupportedParametersFilter": "",
|
||||||
|
"openRouterXTitle": "",
|
||||||
|
"price": "",
|
||||||
|
"providerName": "",
|
||||||
|
"requireParameters": "",
|
||||||
|
"startupModelSync": "",
|
||||||
|
"syncOpencodeGoModelListAtStartup": "",
|
||||||
|
"syncOpenRouterModelListAtStartup": "",
|
||||||
|
"text": "",
|
||||||
|
"thinkingEffort": "",
|
||||||
|
"throughput": "",
|
||||||
|
"toolsStructuredOutputs": "",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": "",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "保留远程",
|
"keepRemote": "保留远程",
|
||||||
"loading": "加载中…",
|
"loading": "加载中…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": "",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "",
|
||||||
|
"autoSummarizeMemory": "",
|
||||||
|
"bytesUpdated": "",
|
||||||
|
"cannotSave": "",
|
||||||
|
"checkingMemoryWriteAccess": "",
|
||||||
"compacting": "压缩中…",
|
"compacting": "压缩中…",
|
||||||
|
"compactionThresholdChars": "",
|
||||||
"compactSelectedFile": "压缩选定文件",
|
"compactSelectedFile": "压缩选定文件",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "",
|
||||||
|
"cronExpressionForDreamProcessing": "",
|
||||||
"dreamCompleted": "梦境处理完成",
|
"dreamCompleted": "梦境处理完成",
|
||||||
|
"dreaming": "",
|
||||||
"dreamNow": "立即触发梦境",
|
"dreamNow": "立即触发梦境",
|
||||||
|
"dreamSchedule": "",
|
||||||
|
"enableMemoryTools": "",
|
||||||
|
"failedToLoadBackendStatus": "",
|
||||||
|
"for": "",
|
||||||
"installing": "安装中…",
|
"installing": "安装中…",
|
||||||
"installQmd": "安装 qmd",
|
"installQmd": "安装 qmd",
|
||||||
|
"loadingMemory": "",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "",
|
||||||
|
"memory": "",
|
||||||
"memoryCompacted": "记忆文件已压缩",
|
"memoryCompacted": "记忆文件已压缩",
|
||||||
|
"memoryFile": "",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": "",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": "",
|
||||||
|
"memoryLivesIn": "",
|
||||||
"memorySaved": "记忆已保存",
|
"memorySaved": "记忆已保存",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "",
|
||||||
|
"noMatchingMemoryFound": "",
|
||||||
|
"processDreamsFromDailyMemory": "",
|
||||||
|
"qmd": "",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": "",
|
||||||
|
"result": "",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "",
|
||||||
"saveMemory": "保存记忆",
|
"saveMemory": "保存记忆",
|
||||||
|
"scheduleCron": "",
|
||||||
|
"searchMemoryWithQmd": "",
|
||||||
"testing": "测试中…",
|
"testing": "测试中…",
|
||||||
"testRetrieval": "测试检索"
|
"testRetrieval": "测试检索",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "",
|
||||||
|
"addFusionAsCoAuthorOnCommits": "",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": "",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "",
|
||||||
|
"aIMerge": "",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": "",
|
||||||
|
"aIOnly": "",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": "",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "",
|
||||||
|
"alwaysSquashDirectMerges": "",
|
||||||
|
"and": "",
|
||||||
|
"and2": "",
|
||||||
|
"areListedFirstOrChoose": "",
|
||||||
|
"autoCompletionMode": "",
|
||||||
|
"autoDetect": "",
|
||||||
|
"autoDetectOriginHEADMain": "",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": "",
|
||||||
|
"autoMergeCompletedTasks": "",
|
||||||
|
"autoMergeConflictRetries": "",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": "",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": "",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": "",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "",
|
||||||
|
"block": "",
|
||||||
|
"blockStrict": "",
|
||||||
|
"both": "",
|
||||||
|
"branchName": "",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "",
|
||||||
|
"coAuthorEmail": "",
|
||||||
|
"coAuthorName": "",
|
||||||
|
"conflictFallbackStrategy": "",
|
||||||
|
"controlsThePostMergeAuditGate": "",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": "",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "",
|
||||||
|
"custom": "",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": "",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": "",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "",
|
||||||
|
"directMergeCommitRouting": "",
|
||||||
|
"directMergeIntoTheCurrentBranch": "",
|
||||||
|
"emailUsedInThe": "",
|
||||||
|
"enableTestMode": "",
|
||||||
|
"fastForwardOfLocalMainFrom": "",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "",
|
||||||
|
"finalFallback": "",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": "",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "",
|
||||||
|
"fusion": "",
|
||||||
|
"gitHubAuthentication": "",
|
||||||
|
"gitHubAuthMode": "",
|
||||||
|
"gitHubCLIGhAuth": "",
|
||||||
|
"gitHubPersonalAccessToken": "",
|
||||||
|
"gitRemoteToPushToEGOrigin": "",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "",
|
||||||
|
"includeTaskIDInCommitScope": "",
|
||||||
|
"insteadOf": "",
|
||||||
|
"integrationBranch": "",
|
||||||
|
"integrationWorktree": "",
|
||||||
|
"inYourProjectRootWillShowTheNew": "",
|
||||||
|
"isTheLegacyBehavior": "",
|
||||||
|
"isTheStricterOptInModeThatRefuses": "",
|
||||||
|
"legacy": "",
|
||||||
|
"legacy2": "",
|
||||||
|
"legacyAutoMergeStamp": "",
|
||||||
|
"legacyAutoMergeStampCleanup": "",
|
||||||
|
"legacyIntegrationBranchMode": "",
|
||||||
|
"maxAIReviewPasses": "",
|
||||||
|
"merge": "",
|
||||||
|
"moreDetails": "",
|
||||||
|
"nameUsedInThe": "",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": "",
|
||||||
|
"noreplyRunfusionAi": "",
|
||||||
|
"off": "",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "",
|
||||||
|
"offSkipAudit": "",
|
||||||
|
"optionsStartWithABestEffort": "",
|
||||||
|
"origin": "",
|
||||||
|
"personalAccessToken": "",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "",
|
||||||
|
"postMergeAuditMode": "",
|
||||||
|
"pushRemote": "",
|
||||||
|
"pushToRemoteAfterMerge": "",
|
||||||
|
"readyToCleanUp": "",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": "",
|
||||||
|
"reuseTaskWorktreeDefault": "",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": "",
|
||||||
|
"smart": "",
|
||||||
|
"smartConflictResolution": "",
|
||||||
|
"smartPreferMain": "",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "",
|
||||||
|
"smartPreferMainOverlapGuard": "",
|
||||||
|
"smartPreferTask": "",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": "",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": "",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": "",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": "",
|
||||||
|
"soTheTaskBranchWinsFastButCan": "",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": "",
|
||||||
|
"symbolicRefFallback": "",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": "",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "",
|
||||||
|
"toResolveViaTheStandardCascade": "",
|
||||||
|
"toTypeABranchThatDoesnAposT": "",
|
||||||
|
"trailer": "",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": "",
|
||||||
|
"useDropdown": "",
|
||||||
|
"useProjectRootLegacy": "",
|
||||||
|
"uses": "",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": "",
|
||||||
|
"warn": "",
|
||||||
|
"warnDefaultLogFindingsContinue": "",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": ""
|
||||||
},
|
},
|
||||||
"mergeManually": "手动合并",
|
"mergeManually": "手动合并",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "特定于此项目"
|
"project": "特定于此项目"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "",
|
||||||
|
"defaultExecutionNode": "",
|
||||||
|
"fallBackToLocal": "",
|
||||||
|
"localExecutionNoDefaultNode": "",
|
||||||
|
"nodeRouting": "",
|
||||||
|
"selectedNode": "",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "",
|
||||||
|
"unavailableNodePolicy": "",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": ""
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "",
|
||||||
|
"conflictResolution": "",
|
||||||
|
"enableAutomaticSettingsSync": "",
|
||||||
|
"every15Minutes": "",
|
||||||
|
"every1Hour": "",
|
||||||
|
"every30Minutes": "",
|
||||||
|
"every5Minutes": "",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "",
|
||||||
|
"keepLocal": "",
|
||||||
|
"keepRemote": "",
|
||||||
|
"lastWriteWins": "",
|
||||||
|
"nodeSync": "",
|
||||||
|
"syncInterval": "",
|
||||||
|
"syncModelAuthCredentials": "",
|
||||||
"workflowSettingsNotSynced": ""
|
"workflowSettingsNotSynced": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "",
|
||||||
|
"advanced": "",
|
||||||
|
"allFailuresLegacy": "",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": "",
|
||||||
|
"customNtfyServerURLOptional": "",
|
||||||
|
"dashboardHostname": "",
|
||||||
|
"discord": "",
|
||||||
|
"enable": "",
|
||||||
|
"failureNotificationDelayMs": "",
|
||||||
|
"failureNotificationMode": "",
|
||||||
|
"format": "",
|
||||||
|
"general": "",
|
||||||
|
"generic": "",
|
||||||
|
"howLongAFailureMustPersistBeforeA": "",
|
||||||
|
"httpLocalhost3000": "",
|
||||||
|
"httpsHooksExampleCom": "",
|
||||||
|
"httpsNtfySh": "",
|
||||||
|
"learnMoreAboutNtfySh": "",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": "",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": "",
|
||||||
|
"messageInbox": "",
|
||||||
|
"mustBeAValidURLStartingWithHttp": "",
|
||||||
|
"myTopicName": "",
|
||||||
|
"notifications": "",
|
||||||
|
"notifyOnEvents": "",
|
||||||
|
"ntfy": "",
|
||||||
|
"ntfyTopic": "",
|
||||||
|
"roomReply": "",
|
||||||
"sending": "发送中…",
|
"sending": "发送中…",
|
||||||
|
"slack": "",
|
||||||
|
"stickyFailuresOnlyDefault": "",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "",
|
||||||
"testMessageInbox": "测试消息收件箱",
|
"testMessageInbox": "测试消息收件箱",
|
||||||
"testNotification": "测试通知",
|
"testNotification": "测试通知",
|
||||||
"testRoomReply": "测试房间回复"
|
"testRoomReply": "测试房间回复",
|
||||||
|
"tk": "",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": "",
|
||||||
|
"webhook": "",
|
||||||
|
"webhookNotifications": "",
|
||||||
|
"webhookURL": "",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": ""
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": "",
|
||||||
|
"piExtensions": "",
|
||||||
|
"pluginManagerType": "",
|
||||||
|
"plugins": ""
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": "",
|
||||||
|
"advancedWorkflowPolicy": "",
|
||||||
|
"aIMergeCommitSummaries": "",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": "",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": "",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": "",
|
||||||
|
"configuredPresets": "",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": "",
|
||||||
|
"defaultWorkflowModelLaneActions": "",
|
||||||
|
"defaultWorkflowModelLanes": "",
|
||||||
|
"delete": "",
|
||||||
|
"edit": "",
|
||||||
|
"executorModel": "",
|
||||||
|
"fallsBackTo": "",
|
||||||
|
"loadingAvailableModels": "",
|
||||||
|
"loadingWorkflowModelLanes": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"modelPresets": "",
|
||||||
|
"name": "",
|
||||||
|
"noCap": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "",
|
||||||
|
"noPreset": "",
|
||||||
|
"noPresetsConfiguredYet": "",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": "",
|
||||||
|
"presetEditor": "",
|
||||||
|
"reset": "",
|
||||||
|
"resetToDefaultNoCap": "",
|
||||||
|
"resetToInheritFromGlobal": "",
|
||||||
|
"resetToInheritFromWorkflow": "",
|
||||||
|
"reviewerModel": "",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": "",
|
||||||
|
"tokenCap": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"useWorkflowDefault": "",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": "",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": ""
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": "",
|
||||||
|
"advancedNamedTunnel": "",
|
||||||
|
"advancedSettings": "",
|
||||||
|
"authenticatedURL": "",
|
||||||
|
"authLinks": "",
|
||||||
|
"authLinkTokenType": "",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "",
|
||||||
|
"cloudflare": "",
|
||||||
"cloudflaredInstalled": "cloudflared 安装成功",
|
"cloudflaredInstalled": "cloudflared 安装成功",
|
||||||
|
"cloudflaredIsInstalled": "",
|
||||||
|
"cloudflaredIsNotInstalled": "",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "",
|
||||||
|
"enableShortLivedTokens": "",
|
||||||
|
"expiresAt": "",
|
||||||
|
"external": "",
|
||||||
|
"externalTunnelQRCode": "",
|
||||||
|
"generateQR": "",
|
||||||
|
"generateShortLivedToken": "",
|
||||||
|
"httpsYourDomainExample": "",
|
||||||
|
"ifHomebrewIsUnavailable": "",
|
||||||
|
"ingressURL": "",
|
||||||
|
"installCloudflared": "",
|
||||||
|
"installing": "",
|
||||||
|
"lastShortLivedTokenExpiresAt": "",
|
||||||
|
"manualInstall": "",
|
||||||
|
"ms": "",
|
||||||
|
"namedTunnelModeEnabled": "",
|
||||||
|
"noExpiry": "",
|
||||||
|
"persistentToken": "",
|
||||||
"persistentTokenRegenerated": "持久令牌已重新生成",
|
"persistentTokenRegenerated": "持久令牌已重新生成",
|
||||||
|
"qRSVGMarkup": "",
|
||||||
|
"regeneratePersistentToken": "",
|
||||||
|
"rememberLastRunningState": "",
|
||||||
|
"remoteAccess": "",
|
||||||
|
"remoteAccessCode": "",
|
||||||
|
"remoteAccessQRCode": "",
|
||||||
|
"remoteProvider": "",
|
||||||
"restarting": "重启中…",
|
"restarting": "重启中…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "",
|
||||||
|
"scanToConnect": "",
|
||||||
|
"scanToOpen": "",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "",
|
||||||
|
"shortLivedToken": "",
|
||||||
"shortLivedTokenGenerated": "短期令牌已生成",
|
"shortLivedTokenGenerated": "短期令牌已生成",
|
||||||
|
"shortLivedTTLMs": "",
|
||||||
|
"showURL": "",
|
||||||
"startFresh": "重新启动",
|
"startFresh": "重新启动",
|
||||||
"starting": "启动中…",
|
"starting": "启动中…",
|
||||||
"startTunnel": "启动隧道",
|
"startTunnel": "启动隧道",
|
||||||
"stopping": "停止中…",
|
"stopping": "停止中…",
|
||||||
"stopTunnel": "停止隧道",
|
"stopTunnel": "停止隧道",
|
||||||
|
"tailnetURL": "",
|
||||||
|
"tailscale": "",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "",
|
||||||
|
"tokenType": "",
|
||||||
|
"tunnelDetected": "",
|
||||||
|
"tunnelName": "",
|
||||||
"tunnelRestarted": "远程隧道已重启",
|
"tunnelRestarted": "远程隧道已重启",
|
||||||
"tunnelStarted": "远程隧道已启动",
|
"tunnelStarted": "远程隧道已启动",
|
||||||
"tunnelStopped": "远程隧道已停止",
|
"tunnelStopped": "远程隧道已停止",
|
||||||
"useExisting": "使用现有"
|
"tunnelToken": "",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": "",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": "",
|
||||||
|
"useExisting": "使用现有",
|
||||||
|
"usingQuickTunnel": ""
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "",
|
||||||
|
"alwaysOn": "",
|
||||||
|
"brave": "",
|
||||||
|
"builtInUsesAgentWebTools": "",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": "",
|
||||||
|
"customSearchEngineId": "",
|
||||||
|
"defaultMaxConcurrentRuns": "",
|
||||||
|
"defaultMaxDurationMs": "",
|
||||||
|
"defaultMaxSourcesPerRun": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"gitHub": "",
|
||||||
|
"googleCustomSearch": "",
|
||||||
|
"googleSearchCX": "",
|
||||||
|
"httpsSearxExampleCom": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxSynthesisRounds": "",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": "",
|
||||||
|
"openAuthentication": "",
|
||||||
|
"openAuthenticationSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"researchDefaults": "",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": "",
|
||||||
|
"searchProvider": "",
|
||||||
|
"searXNG": "",
|
||||||
|
"searXNGURL": "",
|
||||||
|
"tavily": "",
|
||||||
|
"webSearch": ""
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"enableResearchInThisProject": "",
|
||||||
|
"github": "",
|
||||||
|
"llmSynthesis": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxConcurrentRuns": "",
|
||||||
|
"maxDurationMs": "",
|
||||||
|
"maxSourcesPerRun": "",
|
||||||
|
"pageFetch": "",
|
||||||
|
"projectResearchSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"webSearch": "",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": ""
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "全部解决:保留本地",
|
"resolveAllLocal": "全部解决:保留本地",
|
||||||
"resolveAllRemote": "全部解决:保留远程",
|
"resolveAllRemote": "全部解决:保留远程",
|
||||||
"resolvedSuccess": "设置冲突已成功解决",
|
"resolvedSuccess": "设置冲突已成功解决",
|
||||||
"resolveFailed": "无法解决冲突",
|
"resolveFailed": "无法解决冲突",
|
||||||
"resolving": "解决中...",
|
"resolving": "解决中...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "",
|
||||||
|
"openClawRuntime": "",
|
||||||
|
"paperclipRuntime": ""
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "",
|
||||||
|
"disabled": "",
|
||||||
|
"enableScheduledEvalRunsForThisProject": "",
|
||||||
|
"evaluatorModel": "",
|
||||||
|
"evaluatorProvider": "",
|
||||||
|
"followUpPolicy": "",
|
||||||
|
"gpt5": "",
|
||||||
|
"intervalMs": "",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": "",
|
||||||
|
"openai": "",
|
||||||
|
"retentionDays": "",
|
||||||
|
"scheduledEvals": "",
|
||||||
|
"suggestOnly": ""
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": "",
|
||||||
|
"archiveAgentLog": "",
|
||||||
|
"archiveCompletedTasksAfterDays": "",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "",
|
||||||
|
"browse": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "",
|
||||||
|
"compactSummaryAndRecentEntries": "",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "",
|
||||||
|
"docs": "",
|
||||||
|
"doNotArchiveAgentLogs": "",
|
||||||
|
"enableAutomaticTaskArchiving": "",
|
||||||
|
"enablePlanStalenessEnforcement": "",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "",
|
||||||
|
"fullAgentLog": "",
|
||||||
|
"globalMaxConcurrent": "",
|
||||||
|
"heartbeatScopeDiscipline": "",
|
||||||
|
"ignoredOverlapPaths": "",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": "",
|
||||||
|
"lite": "",
|
||||||
|
"maxConcurrentTasks": "",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "",
|
||||||
|
"maximumConcurrentPlanningAgents": "",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "",
|
||||||
|
"maxStuckRetries": "",
|
||||||
|
"maxTriageConcurrent": "",
|
||||||
|
"numberOfDaysATaskCanStayIn": "",
|
||||||
|
"off": "",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": "",
|
||||||
|
"or": "",
|
||||||
|
"pollIntervalMs": "",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": "",
|
||||||
|
"remove": "",
|
||||||
|
"scheduling": "",
|
||||||
"selectCurrentDir": "选择当前目录",
|
"selectCurrentDir": "选择当前目录",
|
||||||
"selectIgnoredOverlapPath": "选择忽略的重叠路径"
|
"selectIgnoredOverlapPath": "选择忽略的重叠路径",
|
||||||
|
"serializeTasksWithOverlappingFiles": "",
|
||||||
|
"staleHighFanOutEscalationHours": "",
|
||||||
|
"staleSpecThresholdHours": "",
|
||||||
|
"stepExecution": "",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "",
|
||||||
|
"strictDefault": "",
|
||||||
|
"stuckTaskTimeoutMinutes": "",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "这些设置在所有 Fusion 项目中共享。",
|
"globalBanner": "这些设置在所有 Fusion 项目中共享。",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": "",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": "",
|
||||||
|
"and": "",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": "",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "",
|
||||||
"awaitingApproval": "等待审批——打开审批以继续。",
|
"awaitingApproval": "等待审批——打开审批以继续。",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": "",
|
||||||
|
"browse": "",
|
||||||
|
"browseWorktreesDirectory": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"defaultsTo": "",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": "",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": "",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "",
|
||||||
|
"enableWorktrunkIntegration": "",
|
||||||
|
"failAndPauseTheTaskDefault": "",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": "",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": "",
|
||||||
|
"installedAt": "",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "",
|
||||||
"installWorktrunk": "安装 worktrunk 二进制文件",
|
"installWorktrunk": "安装 worktrunk 二进制文件",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": "",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "",
|
||||||
|
"maxWorktrees": "",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "",
|
||||||
"openApprovals": "打开审批",
|
"openApprovals": "打开审批",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "",
|
||||||
|
"optionalSupports": "",
|
||||||
|
"pnpmInstallFrozenLockfile": "",
|
||||||
|
"randomNamesEGSwiftFalcon": "",
|
||||||
|
"rebaseFromRemoteBeforeMerge": "",
|
||||||
|
"rebaseRemote": "",
|
||||||
|
"recycleWorktrees": "",
|
||||||
"selectWorktreesDir": "选择工作树目录",
|
"selectWorktreesDir": "选择工作树目录",
|
||||||
"tryAgain": "重试"
|
"shellCommandToRunInEachNewWorktree": "",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": "",
|
||||||
|
"taskIDEGFN042": "",
|
||||||
|
"taskTitleEGFixLoginBug": "",
|
||||||
|
"tryAgain": "重试",
|
||||||
|
"useGitDefault": "",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": "",
|
||||||
|
"whichRemoteToFetchForThePreMerge": "",
|
||||||
|
"worktreeInitCommand": "",
|
||||||
|
"worktreeNamingStyle": "",
|
||||||
|
"worktrees": "",
|
||||||
|
"worktreesDirectory": "",
|
||||||
|
"worktrunk": "",
|
||||||
|
"worktrunkBinaryPath": "",
|
||||||
|
"worktrunkFailureBehavior": "",
|
||||||
|
"worktrunkIntegration": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": ""
|
"notConfigured": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "",
|
||||||
|
"statusError": "",
|
||||||
|
"statusOffline": "",
|
||||||
|
"statusOnline": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "",
|
"apiKeyFormatError": "",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
@@ -872,6 +872,7 @@
|
|||||||
"resetDayMonthly": "每月幾號(1–28)",
|
"resetDayMonthly": "每月幾號(1–28)",
|
||||||
"resetDayWeekly": "星期幾(0=週日)",
|
"resetDayWeekly": "星期幾(0=週日)",
|
||||||
"resetting": "正在重置...",
|
"resetting": "正在重置...",
|
||||||
|
"resizeSidebar": "",
|
||||||
"result": "結果",
|
"result": "結果",
|
||||||
"resultCreated_one": "",
|
"resultCreated_one": "",
|
||||||
"resultCreated_other": "",
|
"resultCreated_other": "",
|
||||||
@@ -1907,7 +1908,6 @@
|
|||||||
"title": "建立房間"
|
"title": "建立房間"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "初始化儀表板...",
|
"initializingDashboard": "初始化儀表板...",
|
||||||
"loadingMessage": "載入 Fusion 儀表板",
|
"loadingMessage": "載入 Fusion 儀表板",
|
||||||
"loadingProgress": "儀表板加載進度",
|
"loadingProgress": "儀表板加載進度",
|
||||||
@@ -5458,10 +5458,18 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"save": "儲存"
|
"save": "儲存"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "",
|
||||||
|
"agentProvisioningApprovals": "",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": "",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": ""
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"languageAuto": "自動",
|
"languageAuto": "自動",
|
||||||
"languageAutoHint": "跟隨瀏覽器語言",
|
"languageAutoHint": "跟隨瀏覽器語言",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": "",
|
||||||
"title": "外觀"
|
"title": "外觀"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5501,10 +5509,51 @@
|
|||||||
"title": "驗證",
|
"title": "驗證",
|
||||||
"waitingForLogin": "等待登入…"
|
"waitingForLogin": "等待登入…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "",
|
||||||
|
"key": ""
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "",
|
||||||
|
"03": "",
|
||||||
|
"agentsOnlyFusionAgentMemory": "",
|
||||||
|
"allProjectAgents": "",
|
||||||
|
"and": "",
|
||||||
"backupCreated": "備份建立成功",
|
"backupCreated": "備份建立成功",
|
||||||
|
"backupDirectory": "",
|
||||||
"backupNow": "立即備份",
|
"backupNow": "立即備份",
|
||||||
"creating": "建立中…"
|
"backups": "",
|
||||||
|
"backupS": "",
|
||||||
|
"backupScheduleCron": "",
|
||||||
|
"creating": "建立中…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": "",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "",
|
||||||
|
"currentBackups": "",
|
||||||
|
"databaseBackups": "",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "",
|
||||||
|
"enableAutomaticDatabaseBackups": "",
|
||||||
|
"enableAutomaticMemoryBackups": "",
|
||||||
|
"fusionBackups": "",
|
||||||
|
"fusionBackupsMemory": "",
|
||||||
|
"invalidCronExpressionFormat": "",
|
||||||
|
"loadingBackupInfo": "",
|
||||||
|
"memoryBackupDirectory": "",
|
||||||
|
"memoryBackups": "",
|
||||||
|
"memoryBackupScheduleCron": "",
|
||||||
|
"memoryBackupScope": "",
|
||||||
|
"memoryRetentionCount": "",
|
||||||
|
"more": "",
|
||||||
|
"mustBeBetween1And100": "",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "",
|
||||||
|
"projectOnlyFusionMemory": "",
|
||||||
|
"retentionCount": "",
|
||||||
|
"totalSize": "",
|
||||||
|
"view": "",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
|
||||||
},
|
},
|
||||||
"clearToDefault": "",
|
"clearToDefault": "",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5531,14 +5580,80 @@
|
|||||||
"saveFailed": ""
|
"saveFailed": ""
|
||||||
},
|
},
|
||||||
"closeModal": "關閉衝突模式",
|
"closeModal": "關閉衝突模式",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "",
|
||||||
|
"commands": "",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "",
|
||||||
|
"eGPnpmBuild": "",
|
||||||
|
"eGPnpmTest": "",
|
||||||
|
"testCommand": ""
|
||||||
|
},
|
||||||
"conflictModalTitle": "解決設定衝突",
|
"conflictModalTitle": "解決設定衝突",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": "",
|
||||||
|
"featureFlags": ""
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "說明",
|
"help": "說明",
|
||||||
"version": "版本 {{version}}"
|
"version": "版本 {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"25": "",
|
||||||
|
"200": "",
|
||||||
|
"3000": "",
|
||||||
|
"14Days": "",
|
||||||
|
"30Days": "",
|
||||||
|
"60Days": "",
|
||||||
|
"7Days": "",
|
||||||
|
"90Days": "",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": "",
|
||||||
|
"autoCleanupOldChats": "",
|
||||||
|
"autoPruneOldMail": "",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "",
|
||||||
|
"builtInWorkflows": "",
|
||||||
|
"capacityRiskBanner": "",
|
||||||
|
"chatHistory": "",
|
||||||
|
"chatRooms": "",
|
||||||
|
"completionDocumentationAutomation": "",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": "",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": "",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "",
|
||||||
|
"defaultTrackingModeForNewTasks": "",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": "",
|
||||||
|
"fN": "",
|
||||||
|
"general": "",
|
||||||
|
"gitHubTracking": "",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "",
|
||||||
"learnMore": "了解更多",
|
"learnMore": "了解更多",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": "",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "",
|
||||||
|
"off": "",
|
||||||
|
"offDefault": "",
|
||||||
|
"onForNewTasks": "",
|
||||||
|
"operationalLogRetention": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"prefixForNewTaskIDsEGKB": "",
|
||||||
|
"prefixMustBe110UppercaseLetters": "",
|
||||||
|
"projectDefaultTrackingRepo": "",
|
||||||
|
"recentVerbatimRoomMessages": "",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "",
|
||||||
|
"requireChangesetChangesetMd": "",
|
||||||
|
"roomCompactionFetchLimit": "",
|
||||||
|
"roomSummaryMaxCharacters": "",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": "",
|
||||||
"settingsSaved": "設定已儲存",
|
"settingsSaved": "設定已儲存",
|
||||||
|
"showCapacityRiskBanner": "",
|
||||||
|
"showQuickChatButton": "",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "",
|
||||||
|
"taskPrefix": "",
|
||||||
|
"todoThreshold": "",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": "",
|
||||||
"updateAvailablePrefix": "v{{version}} 可用",
|
"updateAvailablePrefix": "v{{version}} 可用",
|
||||||
"updateCheckFailed": "",
|
"updateCheckFailed": "",
|
||||||
"updateFailed": "",
|
"updateFailed": "",
|
||||||
@@ -5547,7 +5662,88 @@
|
|||||||
"updateSuccess": "",
|
"updateSuccess": "",
|
||||||
"updateSuccessToast": "",
|
"updateSuccessToast": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upToDate": "您已是最新版本 ✓"
|
"upperBoundOnMessagesFetchedFromTheRoom": "",
|
||||||
|
"upToDate": "您已是最新版本 ✓",
|
||||||
|
"useEphemeralTaskWorkerAgents": "",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": "",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": "",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": ""
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||||
|
"autoReloadDashboardOnVersionChange": "",
|
||||||
|
"checkForThe": "",
|
||||||
|
"checkForUpdatesAutomatically": "",
|
||||||
|
"cLIBinaryOnPATH": "",
|
||||||
|
"cLIBySpawning": "",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": "",
|
||||||
|
"dailyRecommended": "",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": "",
|
||||||
|
"frequency": "",
|
||||||
|
"general": "",
|
||||||
|
"globalDefaultTrackingRepo": "",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||||
|
"manualOnlyNeverAutoCheck": "",
|
||||||
|
"onStartupOncePerServerLaunch": "",
|
||||||
|
"ownerRepo": "",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": "",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": "",
|
||||||
|
"saveAIThinkingForPermanentAgents": "",
|
||||||
|
"saveAIThinkingLogs": "",
|
||||||
|
"saveToolOutputInAgentLogs": "",
|
||||||
|
"updates": "",
|
||||||
|
"weekly": "",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": "",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": "",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": "",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": ""
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "",
|
||||||
|
"default": "",
|
||||||
|
"default2": "",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "",
|
||||||
|
"defaultModel": "",
|
||||||
|
"deny": "",
|
||||||
|
"fallbackModel": "",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": "",
|
||||||
|
"fusion": "",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": "",
|
||||||
|
"httpsRunfusionAi": "",
|
||||||
|
"latency": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"noFallback": "",
|
||||||
|
"openaiAnthropic": "",
|
||||||
|
"openRouterAdvanced": "",
|
||||||
|
"openRouterAllowFallbacks": "",
|
||||||
|
"openRouterHTTPReferer": "",
|
||||||
|
"openRouterOutputModalitiesFilter": "",
|
||||||
|
"openRouterRoutingIgnore": "",
|
||||||
|
"openRouterRoutingOnly": "",
|
||||||
|
"openRouterRoutingOrder": "",
|
||||||
|
"openRouterRoutingSort": "",
|
||||||
|
"openRouterSupportedParametersFilter": "",
|
||||||
|
"openRouterXTitle": "",
|
||||||
|
"price": "",
|
||||||
|
"providerName": "",
|
||||||
|
"requireParameters": "",
|
||||||
|
"startupModelSync": "",
|
||||||
|
"syncOpencodeGoModelListAtStartup": "",
|
||||||
|
"syncOpenRouterModelListAtStartup": "",
|
||||||
|
"text": "",
|
||||||
|
"thinkingEffort": "",
|
||||||
|
"throughput": "",
|
||||||
|
"toolsStructuredOutputs": "",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": "",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5567,17 +5763,179 @@
|
|||||||
"keepRemote": "保留遠端",
|
"keepRemote": "保留遠端",
|
||||||
"loading": "載入中…",
|
"loading": "載入中…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": "",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "",
|
||||||
|
"autoSummarizeMemory": "",
|
||||||
|
"bytesUpdated": "",
|
||||||
|
"cannotSave": "",
|
||||||
|
"checkingMemoryWriteAccess": "",
|
||||||
"compacting": "壓縮中…",
|
"compacting": "壓縮中…",
|
||||||
|
"compactionThresholdChars": "",
|
||||||
"compactSelectedFile": "壓縮選定檔案",
|
"compactSelectedFile": "壓縮選定檔案",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "",
|
||||||
|
"cronExpressionForDreamProcessing": "",
|
||||||
"dreamCompleted": "夢境處理完成",
|
"dreamCompleted": "夢境處理完成",
|
||||||
|
"dreaming": "",
|
||||||
"dreamNow": "立即觸發夢境",
|
"dreamNow": "立即觸發夢境",
|
||||||
|
"dreamSchedule": "",
|
||||||
|
"enableMemoryTools": "",
|
||||||
|
"failedToLoadBackendStatus": "",
|
||||||
|
"for": "",
|
||||||
"installing": "安裝中…",
|
"installing": "安裝中…",
|
||||||
"installQmd": "安裝 qmd",
|
"installQmd": "安裝 qmd",
|
||||||
|
"loadingMemory": "",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "",
|
||||||
|
"memory": "",
|
||||||
"memoryCompacted": "記憶檔案已壓縮",
|
"memoryCompacted": "記憶檔案已壓縮",
|
||||||
|
"memoryFile": "",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": "",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": "",
|
||||||
|
"memoryLivesIn": "",
|
||||||
"memorySaved": "記憶已儲存",
|
"memorySaved": "記憶已儲存",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "",
|
||||||
|
"noMatchingMemoryFound": "",
|
||||||
|
"processDreamsFromDailyMemory": "",
|
||||||
|
"qmd": "",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": "",
|
||||||
|
"result": "",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "",
|
||||||
"saveMemory": "儲存記憶",
|
"saveMemory": "儲存記憶",
|
||||||
|
"scheduleCron": "",
|
||||||
|
"searchMemoryWithQmd": "",
|
||||||
"testing": "測試中…",
|
"testing": "測試中…",
|
||||||
"testRetrieval": "測試擷取"
|
"testRetrieval": "測試擷取",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"abort": "",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "",
|
||||||
|
"addFusionAsCoAuthorOnCommits": "",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": "",
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "",
|
||||||
|
"aIMerge": "",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": "",
|
||||||
|
"aIOnly": "",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": "",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "",
|
||||||
|
"alwaysSquashDirectMerges": "",
|
||||||
|
"and": "",
|
||||||
|
"and2": "",
|
||||||
|
"areListedFirstOrChoose": "",
|
||||||
|
"autoCompletionMode": "",
|
||||||
|
"autoDetect": "",
|
||||||
|
"autoDetectOriginHEADMain": "",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": "",
|
||||||
|
"autoMergeCompletedTasks": "",
|
||||||
|
"autoMergeConflictRetries": "",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": "",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": "",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": "",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "",
|
||||||
|
"block": "",
|
||||||
|
"blockStrict": "",
|
||||||
|
"both": "",
|
||||||
|
"branchName": "",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "",
|
||||||
|
"coAuthorEmail": "",
|
||||||
|
"coAuthorName": "",
|
||||||
|
"conflictFallbackStrategy": "",
|
||||||
|
"controlsThePostMergeAuditGate": "",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": "",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "",
|
||||||
|
"custom": "",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": "",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": "",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "",
|
||||||
|
"directMergeCommitRouting": "",
|
||||||
|
"directMergeIntoTheCurrentBranch": "",
|
||||||
|
"emailUsedInThe": "",
|
||||||
|
"enableTestMode": "",
|
||||||
|
"fastForwardOfLocalMainFrom": "",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "",
|
||||||
|
"finalFallback": "",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": "",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "",
|
||||||
|
"fusion": "",
|
||||||
|
"gitHubAuthentication": "",
|
||||||
|
"gitHubAuthMode": "",
|
||||||
|
"gitHubCLIGhAuth": "",
|
||||||
|
"gitHubPersonalAccessToken": "",
|
||||||
|
"gitRemoteToPushToEGOrigin": "",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "",
|
||||||
|
"includeTaskIDInCommitScope": "",
|
||||||
|
"insteadOf": "",
|
||||||
|
"integrationBranch": "",
|
||||||
|
"integrationWorktree": "",
|
||||||
|
"inYourProjectRootWillShowTheNew": "",
|
||||||
|
"isTheLegacyBehavior": "",
|
||||||
|
"isTheStricterOptInModeThatRefuses": "",
|
||||||
|
"legacy": "",
|
||||||
|
"legacy2": "",
|
||||||
|
"legacyAutoMergeStamp": "",
|
||||||
|
"legacyAutoMergeStampCleanup": "",
|
||||||
|
"legacyIntegrationBranchMode": "",
|
||||||
|
"maxAIReviewPasses": "",
|
||||||
|
"merge": "",
|
||||||
|
"moreDetails": "",
|
||||||
|
"nameUsedInThe": "",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": "",
|
||||||
|
"noreplyRunfusionAi": "",
|
||||||
|
"off": "",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "",
|
||||||
|
"offSkipAudit": "",
|
||||||
|
"optionsStartWithABestEffort": "",
|
||||||
|
"origin": "",
|
||||||
|
"personalAccessToken": "",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "",
|
||||||
|
"postMergeAuditMode": "",
|
||||||
|
"pushRemote": "",
|
||||||
|
"pushToRemoteAfterMerge": "",
|
||||||
|
"readyToCleanUp": "",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": "",
|
||||||
|
"reuseTaskWorktreeDefault": "",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": "",
|
||||||
|
"smart": "",
|
||||||
|
"smartConflictResolution": "",
|
||||||
|
"smartPreferMain": "",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "",
|
||||||
|
"smartPreferMainOverlapGuard": "",
|
||||||
|
"smartPreferTask": "",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": "",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": "",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": "",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": "",
|
||||||
|
"soTheTaskBranchWinsFastButCan": "",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": "",
|
||||||
|
"symbolicRefFallback": "",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": "",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "",
|
||||||
|
"toResolveViaTheStandardCascade": "",
|
||||||
|
"toTypeABranchThatDoesnAposT": "",
|
||||||
|
"trailer": "",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": "",
|
||||||
|
"useDropdown": "",
|
||||||
|
"useProjectRootLegacy": "",
|
||||||
|
"uses": "",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": "",
|
||||||
|
"warn": "",
|
||||||
|
"warnDefaultLogFindingsContinue": "",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": ""
|
||||||
},
|
},
|
||||||
"mergeManually": "手動合併",
|
"mergeManually": "手動合併",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5610,38 +5968,314 @@
|
|||||||
"project": "特定於此專案"
|
"project": "特定於此專案"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "",
|
||||||
|
"defaultExecutionNode": "",
|
||||||
|
"fallBackToLocal": "",
|
||||||
|
"localExecutionNoDefaultNode": "",
|
||||||
|
"nodeRouting": "",
|
||||||
|
"selectedNode": "",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "",
|
||||||
|
"unavailableNodePolicy": "",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": ""
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "",
|
||||||
|
"conflictResolution": "",
|
||||||
|
"enableAutomaticSettingsSync": "",
|
||||||
|
"every15Minutes": "",
|
||||||
|
"every1Hour": "",
|
||||||
|
"every30Minutes": "",
|
||||||
|
"every5Minutes": "",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "",
|
||||||
|
"keepLocal": "",
|
||||||
|
"keepRemote": "",
|
||||||
|
"lastWriteWins": "",
|
||||||
|
"nodeSync": "",
|
||||||
|
"syncInterval": "",
|
||||||
|
"syncModelAuthCredentials": "",
|
||||||
"workflowSettingsNotSynced": ""
|
"workflowSettingsNotSynced": ""
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "",
|
||||||
|
"advanced": "",
|
||||||
|
"allFailuresLegacy": "",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": "",
|
||||||
|
"customNtfyServerURLOptional": "",
|
||||||
|
"dashboardHostname": "",
|
||||||
|
"discord": "",
|
||||||
|
"enable": "",
|
||||||
|
"failureNotificationDelayMs": "",
|
||||||
|
"failureNotificationMode": "",
|
||||||
|
"format": "",
|
||||||
|
"general": "",
|
||||||
|
"generic": "",
|
||||||
|
"howLongAFailureMustPersistBeforeA": "",
|
||||||
|
"httpLocalhost3000": "",
|
||||||
|
"httpsHooksExampleCom": "",
|
||||||
|
"httpsNtfySh": "",
|
||||||
|
"learnMoreAboutNtfySh": "",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": "",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": "",
|
||||||
|
"messageInbox": "",
|
||||||
|
"mustBeAValidURLStartingWithHttp": "",
|
||||||
|
"myTopicName": "",
|
||||||
|
"notifications": "",
|
||||||
|
"notifyOnEvents": "",
|
||||||
|
"ntfy": "",
|
||||||
|
"ntfyTopic": "",
|
||||||
|
"roomReply": "",
|
||||||
"sending": "發送中…",
|
"sending": "發送中…",
|
||||||
|
"slack": "",
|
||||||
|
"stickyFailuresOnlyDefault": "",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "",
|
||||||
"testMessageInbox": "測試訊息收件匣",
|
"testMessageInbox": "測試訊息收件匣",
|
||||||
"testNotification": "測試通知",
|
"testNotification": "測試通知",
|
||||||
"testRoomReply": "測試房間回覆"
|
"testRoomReply": "測試房間回覆",
|
||||||
|
"tk": "",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": "",
|
||||||
|
"webhook": "",
|
||||||
|
"webhookNotifications": "",
|
||||||
|
"webhookURL": "",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": ""
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": "",
|
||||||
|
"piExtensions": "",
|
||||||
|
"pluginManagerType": "",
|
||||||
|
"plugins": ""
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"addPreset": "",
|
||||||
|
"advancedWorkflowPolicy": "",
|
||||||
|
"aIMergeCommitSummaries": "",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": "",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": "",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": "",
|
||||||
|
"configuredPresets": "",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": "",
|
||||||
|
"defaultWorkflowModelLaneActions": "",
|
||||||
|
"defaultWorkflowModelLanes": "",
|
||||||
|
"delete": "",
|
||||||
|
"edit": "",
|
||||||
|
"executorModel": "",
|
||||||
|
"fallsBackTo": "",
|
||||||
|
"loadingAvailableModels": "",
|
||||||
|
"loadingWorkflowModelLanes": "",
|
||||||
|
"modelLanes": "",
|
||||||
|
"modelPresets": "",
|
||||||
|
"name": "",
|
||||||
|
"noCap": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": "",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "",
|
||||||
|
"noPreset": "",
|
||||||
|
"noPresetsConfiguredYet": "",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": "",
|
||||||
|
"presetEditor": "",
|
||||||
|
"reset": "",
|
||||||
|
"resetToDefaultNoCap": "",
|
||||||
|
"resetToInheritFromGlobal": "",
|
||||||
|
"resetToInheritFromWorkflow": "",
|
||||||
|
"reviewerModel": "",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": "",
|
||||||
|
"tokenCap": "",
|
||||||
|
"useDefault": "",
|
||||||
|
"useWorkflowDefault": "",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": "",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": ""
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": "",
|
||||||
|
"advancedNamedTunnel": "",
|
||||||
|
"advancedSettings": "",
|
||||||
|
"authenticatedURL": "",
|
||||||
|
"authLinks": "",
|
||||||
|
"authLinkTokenType": "",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "",
|
||||||
|
"cloudflare": "",
|
||||||
"cloudflaredInstalled": "cloudflared 安裝成功",
|
"cloudflaredInstalled": "cloudflared 安裝成功",
|
||||||
|
"cloudflaredIsInstalled": "",
|
||||||
|
"cloudflaredIsNotInstalled": "",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "",
|
||||||
|
"enableShortLivedTokens": "",
|
||||||
|
"expiresAt": "",
|
||||||
|
"external": "",
|
||||||
|
"externalTunnelQRCode": "",
|
||||||
|
"generateQR": "",
|
||||||
|
"generateShortLivedToken": "",
|
||||||
|
"httpsYourDomainExample": "",
|
||||||
|
"ifHomebrewIsUnavailable": "",
|
||||||
|
"ingressURL": "",
|
||||||
|
"installCloudflared": "",
|
||||||
|
"installing": "",
|
||||||
|
"lastShortLivedTokenExpiresAt": "",
|
||||||
|
"manualInstall": "",
|
||||||
|
"ms": "",
|
||||||
|
"namedTunnelModeEnabled": "",
|
||||||
|
"noExpiry": "",
|
||||||
|
"persistentToken": "",
|
||||||
"persistentTokenRegenerated": "持久權杖已重新產生",
|
"persistentTokenRegenerated": "持久權杖已重新產生",
|
||||||
|
"qRSVGMarkup": "",
|
||||||
|
"regeneratePersistentToken": "",
|
||||||
|
"rememberLastRunningState": "",
|
||||||
|
"remoteAccess": "",
|
||||||
|
"remoteAccessCode": "",
|
||||||
|
"remoteAccessQRCode": "",
|
||||||
|
"remoteProvider": "",
|
||||||
"restarting": "重啟中…",
|
"restarting": "重啟中…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "",
|
||||||
|
"scanToConnect": "",
|
||||||
|
"scanToOpen": "",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "",
|
||||||
|
"shortLivedToken": "",
|
||||||
"shortLivedTokenGenerated": "短期權杖已產生",
|
"shortLivedTokenGenerated": "短期權杖已產生",
|
||||||
|
"shortLivedTTLMs": "",
|
||||||
|
"showURL": "",
|
||||||
"startFresh": "重新啟動",
|
"startFresh": "重新啟動",
|
||||||
"starting": "啟動中…",
|
"starting": "啟動中…",
|
||||||
"startTunnel": "啟動隧道",
|
"startTunnel": "啟動隧道",
|
||||||
"stopping": "停止中…",
|
"stopping": "停止中…",
|
||||||
"stopTunnel": "停止隧道",
|
"stopTunnel": "停止隧道",
|
||||||
|
"tailnetURL": "",
|
||||||
|
"tailscale": "",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "",
|
||||||
|
"tokenType": "",
|
||||||
|
"tunnelDetected": "",
|
||||||
|
"tunnelName": "",
|
||||||
"tunnelRestarted": "遠端隧道已重啟",
|
"tunnelRestarted": "遠端隧道已重啟",
|
||||||
"tunnelStarted": "遠端隧道已啟動",
|
"tunnelStarted": "遠端隧道已啟動",
|
||||||
"tunnelStopped": "遠端隧道已停止",
|
"tunnelStopped": "遠端隧道已停止",
|
||||||
"useExisting": "使用現有"
|
"tunnelToken": "",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": "",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": "",
|
||||||
|
"useExisting": "使用現有",
|
||||||
|
"usingQuickTunnel": ""
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "",
|
||||||
|
"alwaysOn": "",
|
||||||
|
"brave": "",
|
||||||
|
"builtInUsesAgentWebTools": "",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": "",
|
||||||
|
"customSearchEngineId": "",
|
||||||
|
"defaultMaxConcurrentRuns": "",
|
||||||
|
"defaultMaxDurationMs": "",
|
||||||
|
"defaultMaxSourcesPerRun": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"gitHub": "",
|
||||||
|
"googleCustomSearch": "",
|
||||||
|
"googleSearchCX": "",
|
||||||
|
"httpsSearxExampleCom": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxSynthesisRounds": "",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": "",
|
||||||
|
"openAuthentication": "",
|
||||||
|
"openAuthenticationSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"researchDefaults": "",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": "",
|
||||||
|
"searchProvider": "",
|
||||||
|
"searXNG": "",
|
||||||
|
"searXNGURL": "",
|
||||||
|
"tavily": "",
|
||||||
|
"webSearch": ""
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "",
|
||||||
|
"enabledSources": "",
|
||||||
|
"enableResearchInThisProject": "",
|
||||||
|
"github": "",
|
||||||
|
"llmSynthesis": "",
|
||||||
|
"localDocs": "",
|
||||||
|
"maxConcurrentRuns": "",
|
||||||
|
"maxDurationMs": "",
|
||||||
|
"maxSourcesPerRun": "",
|
||||||
|
"pageFetch": "",
|
||||||
|
"projectResearchSettings": "",
|
||||||
|
"requestTimeoutMs": "",
|
||||||
|
"webSearch": "",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": ""
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "全部解決:保留本機",
|
"resolveAllLocal": "全部解決:保留本機",
|
||||||
"resolveAllRemote": "全部解決:保留遠端",
|
"resolveAllRemote": "全部解決:保留遠端",
|
||||||
"resolvedSuccess": "設定衝突已成功解決",
|
"resolvedSuccess": "設定衝突已成功解決",
|
||||||
"resolveFailed": "無法解決衝突",
|
"resolveFailed": "無法解決衝突",
|
||||||
"resolving": "解決中...",
|
"resolving": "解決中...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "",
|
||||||
|
"openClawRuntime": "",
|
||||||
|
"paperclipRuntime": ""
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "",
|
||||||
|
"disabled": "",
|
||||||
|
"enableScheduledEvalRunsForThisProject": "",
|
||||||
|
"evaluatorModel": "",
|
||||||
|
"evaluatorProvider": "",
|
||||||
|
"followUpPolicy": "",
|
||||||
|
"gpt5": "",
|
||||||
|
"intervalMs": "",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": "",
|
||||||
|
"openai": "",
|
||||||
|
"retentionDays": "",
|
||||||
|
"scheduledEvals": "",
|
||||||
|
"suggestOnly": ""
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": "",
|
||||||
|
"archiveAgentLog": "",
|
||||||
|
"archiveCompletedTasksAfterDays": "",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "",
|
||||||
|
"browse": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "",
|
||||||
|
"compactSummaryAndRecentEntries": "",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "",
|
||||||
|
"docs": "",
|
||||||
|
"doNotArchiveAgentLogs": "",
|
||||||
|
"enableAutomaticTaskArchiving": "",
|
||||||
|
"enablePlanStalenessEnforcement": "",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "",
|
||||||
|
"fullAgentLog": "",
|
||||||
|
"globalMaxConcurrent": "",
|
||||||
|
"heartbeatScopeDiscipline": "",
|
||||||
|
"ignoredOverlapPaths": "",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": "",
|
||||||
|
"lite": "",
|
||||||
|
"maxConcurrentTasks": "",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "",
|
||||||
|
"maximumConcurrentPlanningAgents": "",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "",
|
||||||
|
"maxStuckRetries": "",
|
||||||
|
"maxTriageConcurrent": "",
|
||||||
|
"numberOfDaysATaskCanStayIn": "",
|
||||||
|
"off": "",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": "",
|
||||||
|
"or": "",
|
||||||
|
"pollIntervalMs": "",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": "",
|
||||||
|
"remove": "",
|
||||||
|
"scheduling": "",
|
||||||
"selectCurrentDir": "選取目前目錄",
|
"selectCurrentDir": "選取目前目錄",
|
||||||
"selectIgnoredOverlapPath": "選取忽略的重疊路徑"
|
"selectIgnoredOverlapPath": "選取忽略的重疊路徑",
|
||||||
|
"serializeTasksWithOverlappingFiles": "",
|
||||||
|
"staleHighFanOutEscalationHours": "",
|
||||||
|
"staleSpecThresholdHours": "",
|
||||||
|
"stepExecution": "",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "",
|
||||||
|
"strictDefault": "",
|
||||||
|
"stuckTaskTimeoutMinutes": "",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "這些設定在所有 Fusion 專案中共用。",
|
"globalBanner": "這些設定在所有 Fusion 專案中共用。",
|
||||||
@@ -5649,11 +6283,59 @@
|
|||||||
},
|
},
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": "",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": "",
|
||||||
|
"and": "",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": "",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "",
|
||||||
"awaitingApproval": "等待審批——開啟審批以繼續。",
|
"awaitingApproval": "等待審批——開啟審批以繼續。",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": "",
|
||||||
|
"browse": "",
|
||||||
|
"browseWorktreesDirectory": "",
|
||||||
|
"closeParenPeriod": "",
|
||||||
|
"defaultsTo": "",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": "",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": "",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "",
|
||||||
|
"enableWorktrunkIntegration": "",
|
||||||
|
"failAndPauseTheTaskDefault": "",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": "",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": "",
|
||||||
|
"installedAt": "",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "",
|
||||||
"installWorktrunk": "安裝 worktrunk 二進位檔",
|
"installWorktrunk": "安裝 worktrunk 二進位檔",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": "",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "",
|
||||||
|
"maxWorktrees": "",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "",
|
||||||
"openApprovals": "開啟審批",
|
"openApprovals": "開啟審批",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "",
|
||||||
|
"optionalSupports": "",
|
||||||
|
"pnpmInstallFrozenLockfile": "",
|
||||||
|
"randomNamesEGSwiftFalcon": "",
|
||||||
|
"rebaseFromRemoteBeforeMerge": "",
|
||||||
|
"rebaseRemote": "",
|
||||||
|
"recycleWorktrees": "",
|
||||||
"selectWorktreesDir": "選取工作樹目錄",
|
"selectWorktreesDir": "選取工作樹目錄",
|
||||||
"tryAgain": "重試"
|
"shellCommandToRunInEachNewWorktree": "",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": "",
|
||||||
|
"taskIDEGFN042": "",
|
||||||
|
"taskTitleEGFixLoginBug": "",
|
||||||
|
"tryAgain": "重試",
|
||||||
|
"useGitDefault": "",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": "",
|
||||||
|
"whichRemoteToFetchForThePreMerge": "",
|
||||||
|
"worktreeInitCommand": "",
|
||||||
|
"worktreeNamingStyle": "",
|
||||||
|
"worktrees": "",
|
||||||
|
"worktreesDirectory": "",
|
||||||
|
"worktrunk": "",
|
||||||
|
"worktrunkBinaryPath": "",
|
||||||
|
"worktrunkFailureBehavior": "",
|
||||||
|
"worktrunkIntegration": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -142,6 +142,14 @@
|
|||||||
"notConfigured": ""
|
"notConfigured": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "",
|
||||||
|
"statusError": "",
|
||||||
|
"statusOffline": "",
|
||||||
|
"statusOnline": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "",
|
"apiKeyFormatError": "",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
706
packages/i18n/src/resources.d.ts
vendored
706
packages/i18n/src/resources.d.ts
vendored
@@ -874,6 +874,7 @@ export default interface Resources {
|
|||||||
"resetDayMonthly": "Day of month (1–28)",
|
"resetDayMonthly": "Day of month (1–28)",
|
||||||
"resetDayWeekly": "Day of week (0=Sun)",
|
"resetDayWeekly": "Day of week (0=Sun)",
|
||||||
"resetting": "Resetting...",
|
"resetting": "Resetting...",
|
||||||
|
"resizeSidebar": "Resize agents sidebar",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
"resultCreated_one": "{{count}} created",
|
"resultCreated_one": "{{count}} created",
|
||||||
"resultCreated_other": "{{count}} created",
|
"resultCreated_other": "{{count}} created",
|
||||||
@@ -1909,7 +1910,6 @@ export default interface Resources {
|
|||||||
"title": "Create room"
|
"title": "Create room"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"brandName": "Fusion",
|
|
||||||
"initializingDashboard": "Initializing dashboard...",
|
"initializingDashboard": "Initializing dashboard...",
|
||||||
"loadingMessage": "Loading Fusion dashboard",
|
"loadingMessage": "Loading Fusion dashboard",
|
||||||
"loadingProgress": "Dashboard loading progress",
|
"loadingProgress": "Dashboard loading progress",
|
||||||
@@ -5460,10 +5460,18 @@ export default interface Resources {
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save"
|
"save": "Save"
|
||||||
},
|
},
|
||||||
|
"agentPermissions": {
|
||||||
|
"agentPermissions": "Agent Permissions",
|
||||||
|
"agentProvisioningApprovals": "Agent Provisioning Approvals",
|
||||||
|
"configureProjectLevelApprovalBehaviorForDurableProvisioning": " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). ",
|
||||||
|
"perAgentSettingsOverrideProjectDefaultsEachCategory": "Per-agent settings override project defaults. Each category controls a separate approval gate."
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
"hideAISessionNotificationBanners": "Hide AI session notification banners",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"languageAuto": "Auto",
|
"languageAuto": "Auto",
|
||||||
"languageAutoHint": "Follow the browser language",
|
"languageAutoHint": "Follow the browser language",
|
||||||
|
"suppressTheLdquoNeedsYourInputRdquoBanner": " Suppress the “needs your input” banner that appears when AI sessions are awaiting input or have failed. ",
|
||||||
"title": "Appearance"
|
"title": "Appearance"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -5503,10 +5511,51 @@ export default interface Resources {
|
|||||||
"title": "Authentication",
|
"title": "Authentication",
|
||||||
"waitingForLogin": "Waiting for login…"
|
"waitingForLogin": "Waiting for login…"
|
||||||
},
|
},
|
||||||
|
"authentication": {
|
||||||
|
"enterAPIKey": "Enter API key",
|
||||||
|
"key": "Key: "
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
|
"02": "0 2 * * *",
|
||||||
|
"03": "0 3 * * *",
|
||||||
|
"agentsOnlyFusionAgentMemory": "Agents only (.fusion/agent-memory)",
|
||||||
|
"allProjectAgents": "All (project + agents)",
|
||||||
|
"and": "...and ",
|
||||||
"backupCreated": "Backup created successfully",
|
"backupCreated": "Backup created successfully",
|
||||||
|
"backupDirectory": "Backup Directory",
|
||||||
"backupNow": "Backup Now",
|
"backupNow": "Backup Now",
|
||||||
"creating": "Creating…"
|
"backupS": " backup(s)",
|
||||||
|
"backupScheduleCron": "Backup Schedule (Cron)",
|
||||||
|
"backups": "backups",
|
||||||
|
"creating": "Creating…",
|
||||||
|
"cronExpressionForBackupTimingDefault02": " Cron expression for backup timing. Default: 0 2 * * * (daily at 2 AM). Examples: 0 * * * * (hourly), 0 0 * * 0 (weekly), */15 * * * * (every 15 min) ",
|
||||||
|
"cronExpressionForMemoryBackupTimingDefault0": "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).",
|
||||||
|
"currentBackups": "Current Backups",
|
||||||
|
"databaseBackups": "Database Backups",
|
||||||
|
"directoryForBackupFilesRelativeToProjectRoot": "Directory for backup files, relative to project root",
|
||||||
|
"directoryForMemoryBackupsRelativeToProjectRoot": "Directory for memory backups, relative to project root.",
|
||||||
|
"enableAutomaticDatabaseBackups": " Enable automatic database backups ",
|
||||||
|
"enableAutomaticMemoryBackups": " Enable automatic memory backups ",
|
||||||
|
"fusionBackups": ".fusion/backups",
|
||||||
|
"fusionBackupsMemory": ".fusion/backups/memory",
|
||||||
|
"invalidCronExpressionFormat": "Invalid cron expression format",
|
||||||
|
"loadingBackupInfo": "Loading backup info…",
|
||||||
|
"memoryBackupDirectory": "Memory Backup Directory",
|
||||||
|
"memoryBackupScheduleCron": "Memory Backup Schedule (Cron)",
|
||||||
|
"memoryBackupScope": "Memory Backup Scope",
|
||||||
|
"memoryBackups": "Memory Backups",
|
||||||
|
"memoryRetentionCount": "Memory Retention Count",
|
||||||
|
"more": " more",
|
||||||
|
"mustBeBetween1And100": "Must be between 1 and 100",
|
||||||
|
"numberOfBackupFilesToKeepOldestAre": "Number of backup files to keep (oldest are deleted first). Range: 1-100.",
|
||||||
|
"numberOfMemoryBackupsToKeepOldestAre": "Number of memory backups to keep (oldest are deleted first). Range: 1-100.",
|
||||||
|
"pathCannotContainParentDirectoryTraversal": "Path cannot contain parent directory traversal (..)",
|
||||||
|
"projectOnlyFusionMemory": "Project only (.fusion/memory)",
|
||||||
|
"retentionCount": "Retention Count",
|
||||||
|
"totalSize": "total size",
|
||||||
|
"view": "View ",
|
||||||
|
"whenEnabledProjectAndAgentMemoryFilesAre": "When enabled, project and agent memory files are backed up automatically on a schedule.",
|
||||||
|
"whenEnabledTheDatabaseIsBackedUpAutomatically": "When enabled, the database is backed up automatically on a schedule"
|
||||||
},
|
},
|
||||||
"clearToDefault": "Reset to default",
|
"clearToDefault": "Reset to default",
|
||||||
"cliAgents": {
|
"cliAgents": {
|
||||||
@@ -5533,14 +5582,80 @@ export default interface Resources {
|
|||||||
"saveFailed": "Failed to save CLI agent settings"
|
"saveFailed": "Failed to save CLI agent settings"
|
||||||
},
|
},
|
||||||
"closeModal": "Close conflict modal",
|
"closeModal": "Close conflict modal",
|
||||||
|
"commands": {
|
||||||
|
"buildCommand": "Build Command",
|
||||||
|
"commandUsedToBuildTheProjectInjectedInto": "Command used to build the project — injected into generated task specs",
|
||||||
|
"commandUsedToRunTestsInjectedIntoGenerated": "Command used to run tests — injected into generated task specs",
|
||||||
|
"commands": "Commands",
|
||||||
|
"eGPnpmBuild": "e.g. pnpm build",
|
||||||
|
"eGPnpmTest": "e.g. pnpm test",
|
||||||
|
"testCommand": "Test Command"
|
||||||
|
},
|
||||||
"conflictModalTitle": "Resolve Settings Conflicts",
|
"conflictModalTitle": "Resolve Settings Conflicts",
|
||||||
|
"experimental": {
|
||||||
|
"experimentalFeatures": "Experimental Features",
|
||||||
|
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. ",
|
||||||
|
"featureFlags": "Feature Flags"
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"version": "Version {{version}}"
|
"version": "Version {{version}}"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
|
"14Days": "14 days",
|
||||||
|
"200": "200",
|
||||||
|
"25": "25",
|
||||||
|
"3000": "3000",
|
||||||
|
"30Days": "30 days",
|
||||||
|
"60Days": "60 days",
|
||||||
|
"7Days": "7 days",
|
||||||
|
"90Days": "90 days",
|
||||||
|
"agentsToRunEachTaskWhenDisabledOnly": " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ",
|
||||||
|
"autoCleanupOldChats": "Auto-cleanup old chats",
|
||||||
|
"autoPruneOldMail": "Auto-prune old mail",
|
||||||
|
"bannerFiresWhenTodoCountIsStrictlyGreater": "Banner fires when todo count is strictly greater than this value (default 20). Applies when the banner is enabled.",
|
||||||
|
"builtInWorkflows": "Built-in workflows",
|
||||||
|
"capacityRiskBanner": "Capacity Risk Banner",
|
||||||
|
"chatHistory": "Chat history",
|
||||||
|
"chatRooms": "Chat Rooms",
|
||||||
|
"completionDocumentationAutomation": "Completion Documentation Automation",
|
||||||
|
"controlsHowFutureTaskSpecsHandleReleaseNote": " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ",
|
||||||
|
"controlsWhetherNewlyCreatedTasksHaveGitHubIssue": " Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal. ",
|
||||||
|
"defaultRepoUsedWhenCreatingGitHubIssuesFor": "Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.",
|
||||||
|
"defaultTrackingModeForNewTasks": "Default tracking mode for new tasks",
|
||||||
|
"deleteChatSessionsAndRoomsThatHaveBeen": "Delete chat sessions and rooms that have been idle for this many days. Default: Off.",
|
||||||
|
"deleteInboxOutboxMessagesOlderThanThisMany": "Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.",
|
||||||
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.",
|
||||||
|
"enableSummarizationInProjectModelsToConfigureThatModel": " Enable summarization in Project Models to configure that model.",
|
||||||
|
"fN": "FN",
|
||||||
|
"general": "General",
|
||||||
|
"gitHubTracking": "GitHub Tracking",
|
||||||
|
"hardCapOnTheSynthesizedEarlierRoomContext": "Hard cap on the synthesized \"Earlier room context\" summary block. Default: 3000.",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
||||||
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "New tasks inherit this custom workflow's steps (overridable per task)",
|
||||||
|
"numberOfMostRecentChatRoomMessagesKept": "Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.",
|
||||||
|
"off": "Off",
|
||||||
|
"offDefault": "Off (default)",
|
||||||
|
"onForNewTasks": "On for new tasks",
|
||||||
|
"operationalLogRetention": "Operational log retention",
|
||||||
|
"ownerRepo": "owner/repo",
|
||||||
|
"prefixForNewTaskIDsEGKB": "Prefix for new task IDs (e.g. KB, PROJ)",
|
||||||
|
"prefixMustBe110UppercaseLetters": "Prefix must be 1–10 uppercase letters",
|
||||||
|
"projectDefaultTrackingRepo": "Project default tracking repo",
|
||||||
|
"recentVerbatimRoomMessages": "Recent verbatim room messages",
|
||||||
|
"requireChangelogUpdateExistingChangelog": "Require changelog update (existing changelog)",
|
||||||
|
"requireChangesetChangesetMd": "Require changeset (.changeset/*.md)",
|
||||||
|
"roomCompactionFetchLimit": "Room compaction fetch limit",
|
||||||
|
"roomSummaryMaxCharacters": "Room summary max characters",
|
||||||
|
"searchTheTrackingRepoForLikelyDuplicatesBefore": " Search the tracking repo for likely duplicates before opening a new issue ",
|
||||||
"settingsSaved": "Settings saved",
|
"settingsSaved": "Settings saved",
|
||||||
|
"showCapacityRiskBanner": " Show capacity risk banner ",
|
||||||
|
"showQuickChatButton": " Show quick chat button ",
|
||||||
|
"showTheFloatingChatButtonInTheDashboard": "Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.",
|
||||||
|
"taskPrefix": "Task Prefix",
|
||||||
|
"todoThreshold": "Todo threshold",
|
||||||
|
"trackingIssuesUseThisTaskAposSTitle": " Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models. ",
|
||||||
"upToDate": "You're up to date ✓",
|
"upToDate": "You're up to date ✓",
|
||||||
"updateAvailablePrefix": "v{{version}} available",
|
"updateAvailablePrefix": "v{{version}} available",
|
||||||
"updateCheckFailed": "Failed to check for updates",
|
"updateCheckFailed": "Failed to check for updates",
|
||||||
@@ -5549,7 +5664,88 @@ export default interface Resources {
|
|||||||
"updateNow": "Update now",
|
"updateNow": "Update now",
|
||||||
"updateSuccess": "Updated to v{{version}} — restart Fusion to apply",
|
"updateSuccess": "Updated to v{{version}} — restart Fusion to apply",
|
||||||
"updateSuccessToast": "Update installed. Restart Fusion to apply it.",
|
"updateSuccessToast": "Update installed. Restart Fusion to apply it.",
|
||||||
"updating": "Updating…"
|
"updating": "Updating…",
|
||||||
|
"upperBoundOnMessagesFetchedFromTheRoom": "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.",
|
||||||
|
"useEphemeralTaskWorkerAgents": " Use ephemeral task-worker agents ",
|
||||||
|
"warnOnTheBoardWhenTodoWorkExceeds": "Warn on the board when todo work exceeds the threshold and no idle agents are available.",
|
||||||
|
"whenEnabledDefaultFusionSpawnsShortLived": " When enabled (default), Fusion spawns short-lived ",
|
||||||
|
"whenEnabledFusionChecksOpenAndClosedIssues": " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. ",
|
||||||
|
"workflowsOrChangelogModeWhenContributorsShouldUpdate": " workflows, or changelog mode when contributors should update an existing changelog file. "
|
||||||
|
},
|
||||||
|
"globalGeneral": {
|
||||||
|
"andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. ",
|
||||||
|
"autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ",
|
||||||
|
"cLIBinaryOnPATH": " CLI binary on PATH ",
|
||||||
|
"cLIBySpawning": " CLI by spawning",
|
||||||
|
"checkForThe": " Check for the ",
|
||||||
|
"checkForUpdatesAutomatically": " Check for updates automatically ",
|
||||||
|
"controlsHowOftenTheDashboardReFetchesThe": " Controls how often the dashboard re-fetches the npm registry. Use the version + refresh control in the header to trigger an immediate check at any time. ",
|
||||||
|
"dailyRecommended": "Daily (recommended)",
|
||||||
|
"disableThisIfYourLocalDevProcessIs": ". Disable this if your local dev process is the source of truth and you don't want any outdated globally-installed binary executed during the probe. ",
|
||||||
|
"frequency": "Frequency",
|
||||||
|
"general": "General",
|
||||||
|
"globalDefaultTrackingRepo": "Global default tracking repo",
|
||||||
|
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
|
||||||
|
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
|
||||||
|
"onStartupOncePerServerLaunch": "On startup — once per server launch",
|
||||||
|
"ownerRepo": "owner/repo",
|
||||||
|
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo.",
|
||||||
|
"rowsAndDoesNotAffectAssistantTextOr": " rows and does not affect assistant text or tool rows. ",
|
||||||
|
"saveAIThinkingForEphemeralTaskWorkerAgents": " Save AI thinking for ephemeral / task-worker agents ",
|
||||||
|
"saveAIThinkingForPermanentAgents": " Save AI thinking for permanent agents ",
|
||||||
|
"saveAIThinkingLogs": "Save AI thinking logs",
|
||||||
|
"saveToolOutputInAgentLogs": " Save tool output in agent logs ",
|
||||||
|
"updates": "Updates",
|
||||||
|
"weekly": "Weekly",
|
||||||
|
"whenDisabledToolRowsAreStillLoggedBut": " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. ",
|
||||||
|
"whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen": " When enabled (default), the dashboard automatically reloads when it detects a new build version — either from server rebuilds or service worker updates. Disable this to stay on the current version until you manually refresh. ",
|
||||||
|
"whenEnabledFusionChecksNpmForNewVersions": " When enabled, Fusion checks npm for new versions of",
|
||||||
|
"whenEnabledTheDashboardProbesForAGlobally": " When enabled, the dashboard probes for a globally-installed"
|
||||||
|
},
|
||||||
|
"globalModels": {
|
||||||
|
"allow": "allow",
|
||||||
|
"commaSeparatedValuesSentToOpenRouterModelSync": "Comma-separated values sent to OpenRouter model sync.",
|
||||||
|
"controlsHowMuchReasoningEffortTheAIModel": "Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.",
|
||||||
|
"default": "Default",
|
||||||
|
"default2": "default",
|
||||||
|
"defaultAIModelUsedForTaskExecutionWhen": "Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.",
|
||||||
|
"defaultModel": "Default Model",
|
||||||
|
"deny": "deny",
|
||||||
|
"fallbackModel": "Fallback Model",
|
||||||
|
"flowAndPublishesThemUnderTheOpencodeGo": " flow and publishes them under the opencode-go provider in model pickers. ",
|
||||||
|
"fusion": "Fusion",
|
||||||
|
"globalBaselineModelsForEachAIRoleProject": " Global baseline models for each AI role. Project settings can override these per-project. ",
|
||||||
|
"httpsRunfusionAi": "https://runfusion.ai",
|
||||||
|
"latency": "latency",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultFusion": "Leave empty to omit this header. Default: Fusion.",
|
||||||
|
"leaveEmptyToOmitThisHeaderDefaultHttps": "Leave empty to omit this header. Default: https://runfusion.ai.",
|
||||||
|
"modelLanes": "Model Lanes",
|
||||||
|
"noFallback": "No fallback",
|
||||||
|
"openRouterAdvanced": "OpenRouter advanced",
|
||||||
|
"openRouterAllowFallbacks": "OpenRouter allow fallbacks",
|
||||||
|
"openRouterHTTPReferer": "OpenRouter HTTP-Referer",
|
||||||
|
"openRouterOutputModalitiesFilter": "OpenRouter output_modalities filter",
|
||||||
|
"openRouterRoutingIgnore": "OpenRouter routing ignore",
|
||||||
|
"openRouterRoutingOnly": "OpenRouter routing only",
|
||||||
|
"openRouterRoutingOrder": "OpenRouter routing order",
|
||||||
|
"openRouterRoutingSort": "OpenRouter routing sort",
|
||||||
|
"openRouterSupportedParametersFilter": "OpenRouter supported_parameters filter",
|
||||||
|
"openRouterXTitle": "OpenRouter X-Title",
|
||||||
|
"openaiAnthropic": "openai, anthropic",
|
||||||
|
"price": "price",
|
||||||
|
"providerName": "provider-name",
|
||||||
|
"requireParameters": " Require parameters ",
|
||||||
|
"startupModelSync": "Startup Model Sync",
|
||||||
|
"syncOpenRouterModelListAtStartup": " Sync OpenRouter model list at startup ",
|
||||||
|
"syncOpencodeGoModelListAtStartup": " Sync opencode-go model list at startup ",
|
||||||
|
"text": "text",
|
||||||
|
"thinkingEffort": "Thinking Effort",
|
||||||
|
"throughput": "throughput",
|
||||||
|
"toolsStructuredOutputs": "tools, structured_outputs",
|
||||||
|
"useDefault": "Use default",
|
||||||
|
"usedAutomaticallyIfThePrimaryDefaultModelHits": "Used automatically if the primary default model hits a retryable provider error like rate limiting or overload.",
|
||||||
|
"whenEnabledStartupFetchesTheLatestAvailableModels": " When enabled, startup fetches the latest available models from the OpenRouter API so model pickers always include the newest catalog. ",
|
||||||
|
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
@@ -5569,17 +5765,179 @@ export default interface Resources {
|
|||||||
"keepRemote": "Keep Remote",
|
"keepRemote": "Keep Remote",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"memory": {
|
"memory": {
|
||||||
|
"03": "0 3 * * *",
|
||||||
|
"agentsGetMemorySearchMemoryGetAndMemory": "Agents get memory_search, memory_get, and memory_append tools. Search defaults to qmd with a local file fallback.",
|
||||||
|
"agentsSearchWithQmdFirstFallBackTo": ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ",
|
||||||
|
"autoSummarizeMemory": " Auto-Summarize Memory ",
|
||||||
|
"automaticallyCompactMemoryWhenItExceedsTheThreshold": "Automatically compact memory when it exceeds the threshold on a schedule",
|
||||||
|
"bytesUpdated": " bytes · updated ",
|
||||||
|
"cannotSave": "Cannot save: ",
|
||||||
|
"checkingMemoryWriteAccess": "Checking memory write access...",
|
||||||
"compactSelectedFile": "Compact Selected File",
|
"compactSelectedFile": "Compact Selected File",
|
||||||
"compacting": "Compacting…",
|
"compacting": "Compacting…",
|
||||||
|
"compactionThresholdChars": "Compaction Threshold (chars)",
|
||||||
|
"cronExpressionForAutoSummarizeScheduleDefaultDaily": "Cron expression for auto-summarize schedule (default: daily at 3 AM)",
|
||||||
|
"cronExpressionForDreamProcessing": "Cron expression for dream processing.",
|
||||||
"dreamCompleted": "Dream processing completed",
|
"dreamCompleted": "Dream processing completed",
|
||||||
"dreamNow": "Dream Now",
|
"dreamNow": "Dream Now",
|
||||||
|
"dreamSchedule": "Dream Schedule",
|
||||||
|
"dreaming": " Dreaming… ",
|
||||||
|
"enableMemoryTools": " Enable memory tools ",
|
||||||
|
"failedToLoadBackendStatus": "Failed to load backend status: ",
|
||||||
|
"for": "for \"",
|
||||||
"installQmd": "Install qmd",
|
"installQmd": "Install qmd",
|
||||||
"installing": "Installing…",
|
"installing": "Installing…",
|
||||||
|
"loadingMemory": "Loading memory…",
|
||||||
|
"manuallyTriggerDreamProcessingNow": "Manually trigger dream processing now.",
|
||||||
|
"memory": "Memory",
|
||||||
"memoryCompacted": "Memory file compacted",
|
"memoryCompacted": "Memory file compacted",
|
||||||
|
"memoryFile": "Memory File",
|
||||||
|
"memoryIsConfiguredWithAReadOnlyBackend": " Memory is configured with a read-only backend. You can view the file, but saving is disabled. ",
|
||||||
|
"memoryIsCurrentlyDisabledYouCanViewThe": " Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled. ",
|
||||||
|
"memoryLivesIn": " Memory lives in ",
|
||||||
"memorySaved": "Memory saved",
|
"memorySaved": "Memory saved",
|
||||||
|
"memoryWillBeCompactedWhenItExceedsThis": "Memory will be compacted when it exceeds this character count",
|
||||||
|
"noMatchingMemoryFound": "No matching memory found.",
|
||||||
|
"processDreamsFromDailyMemory": " Process dreams from daily memory ",
|
||||||
|
"qmd": " qmd ",
|
||||||
|
"qmdIsNotInstalledSearchWillUseLocal": " qmd is not installed. Search will use local files. Install indexed retrieval: ",
|
||||||
|
"result": " result",
|
||||||
|
"runsTheSameQmdBackedMemorySearchPath": "Runs the same qmd-backed memory_search path agents use.",
|
||||||
"saveMemory": "Save Memory",
|
"saveMemory": "Save Memory",
|
||||||
|
"scheduleCron": "Schedule (cron)",
|
||||||
|
"searchMemoryWithQmd": "Search memory with qmd",
|
||||||
"testRetrieval": "Test Retrieval",
|
"testRetrieval": "Test Retrieval",
|
||||||
"testing": "Testing…"
|
"testing": "Testing…",
|
||||||
|
"turnsDailyNotesIntoDREAMSMdAndPromotes": "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md."
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"aICorrectiveRoundsBeforeLandingTheBestResult": "AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3. The reviewer uses your project's reviewer/validator model.",
|
||||||
|
"aIMerge": "AI merge",
|
||||||
|
"aIMergeDefaultAIMergesInAClean": "AI merge (default) — AI merges in a clean room, an AI reviewer audits with retries, then lands",
|
||||||
|
"aIModeMergesTheTaskBranchIntoAn": " AI mode merges the task branch into an isolated clean-room checkout at the target branch's tip, has an AI reviewer audit the squash (with corrective retries — advisory concerns land with a logged warning, an unfixable correctness concern hard-fails), then fast-forwards the target branch and syncs your local checkout (AI reconciles a conflicting restore). Each task merges to its own target branch, or the default integration branch. ",
|
||||||
|
"aIOnly": "AI only",
|
||||||
|
"aIOnlyAIAutoResolveAIRetryNever": "AI only — AI → auto-resolve → AI retry; never silently pick a side",
|
||||||
|
"abort": "Abort",
|
||||||
|
"abortOneAIAttemptRequireManualResolutionIf": "Abort — one AI attempt; require manual resolution if it fails",
|
||||||
|
"addFusionAsCoAuthorOnCommits": " Add Fusion as co-author on commits ",
|
||||||
|
"afterFusionAdvancesTheIntegrationBranchRefThe": " After Fusion advances the integration branch ref, the merger can auto-sync other worktrees still checked out on that branch (typically your project-root checkout). ",
|
||||||
|
"allowAIMergeToSyncADirtyChecked": " Allow AI merge to sync a dirty checked-out integration branch ",
|
||||||
|
"alwaysPreserveDirectMergeCommitHistory": "Always preserve direct-merge commit history",
|
||||||
|
"alwaysSquashDirectMerges": "Always squash direct merges",
|
||||||
|
"and": ", and ",
|
||||||
|
"and2": " and ",
|
||||||
|
"areListedFirstOrChoose": " are listed first — or choose ",
|
||||||
|
"autoCompletionMode": "Auto-completion mode",
|
||||||
|
"autoDetect": "auto-detect",
|
||||||
|
"autoDetectOriginHEADMain": "(auto-detect — origin/HEAD → main)",
|
||||||
|
"autoKeepsTodayAposSSquashBehaviorFor": " Auto keeps today's squash behavior for branches with zero or one substantive commit, but switches multi-substantive branches to a history-preserving rebase-and-merge path. Individual tasks can override this in PROMPT.md with ",
|
||||||
|
"autoMergeCompletedTasks": " Auto-merge completed tasks ",
|
||||||
|
"autoMergeConflictRetries": "Auto-merge conflict retries",
|
||||||
|
"autoMergeRunsInTheTaskWorktreeBy": " Auto-merge runs in the task worktree by default. Switch to the legacy project-root path only if you need the pre-FN-5279 fallback; worktrunk-managed projects still defer to worktrunk. ",
|
||||||
|
"autoMergeWillRunRebaseConflictResolutionAnd": " Auto-merge will run rebase, conflict resolution, and squash commits inside the project root (the user's checked-out integration-branch worktree) instead of the task worktree. Fusion assumes that directory is already on the integration branch and clean; if it isn't, merges may fail or touch the user's working tree. Reuse-task-worktree is the recommended default (FN-5279). Switch back unless you have a specific reason to opt in (FN-5348). ",
|
||||||
|
"autoResolveConflictsInLockFilesAndGenerated": " Auto-resolve conflicts in lock files and generated files ",
|
||||||
|
"autoSquashSingleSubstantiveBranchesPreserveMultiSubstantive": "Auto — squash single-substantive branches, preserve multi-substantive history",
|
||||||
|
"autoSyncProjectCheckoutAfterMerge": "Auto-sync project checkout after merge",
|
||||||
|
"block": "Block",
|
||||||
|
"blockStrict": "Block (strict)",
|
||||||
|
"both": " Both ",
|
||||||
|
"branchName": "branch name",
|
||||||
|
"checkingForLegacyAutoMergeStamps": "Checking for legacy auto-merge stamps…",
|
||||||
|
"coAuthorEmail": "Co-author Email",
|
||||||
|
"coAuthorName": "Co-author Name",
|
||||||
|
"conflictFallbackStrategy": "Conflict Fallback Strategy",
|
||||||
|
"controlsThePostMergeAuditGate": " Controls the post-merge audit gate. ",
|
||||||
|
"controlsWhatHappensAfterATaskReachesIn": " Controls what happens after a task reaches In Review. Direct mode merges into the current branch locally. Pull request mode keeps the task in In Review while Fusion waits for GitHub reviews and required checks before merging the PR. ",
|
||||||
|
"createMonitorAndMergeAGitHubPullRequest": "Create, monitor, and merge a GitHub pull request",
|
||||||
|
"custom": "Custom…",
|
||||||
|
"dangerousCompatibilityEscapeHatchLeaveOffUnlessYou": " Dangerous compatibility escape hatch. Leave off unless you explicitly want the legacy stash → fast-forward → restore behavior when your checked-out integration branch has unrelated local edits. When off, AI merge blocks before advancing the branch so dirty project-root edits cannot contaminate a completed merge. ",
|
||||||
|
"defaultLogsFindingsButAutoCompletesTheMerge": " (default) logs findings but auto-completes the merge. ",
|
||||||
|
"deterministicLegacyRebaseConflictStrategyAuditPipeline": "Deterministic (legacy) — rebase / conflict-strategy / audit pipeline",
|
||||||
|
"directMergeCommitRouting": "Direct merge commit routing",
|
||||||
|
"directMergeIntoTheCurrentBranch": "Direct merge into the current branch",
|
||||||
|
"emailUsedInThe": "Email used in the ",
|
||||||
|
"enableTestMode": " Enable test mode ",
|
||||||
|
"fastForwardOfLocalMainFrom": " + fast-forward of local main from ",
|
||||||
|
"fastForwardOnlySkipDirtyWorktrees": "Fast-forward only — skip dirty worktrees",
|
||||||
|
"finalFallback": "final fallback",
|
||||||
|
"findsInReviewTasksWhoseAutoMergeValue": " Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp. Dry-run is automatic; applying delegates to the store cleanup and preserves genuine per-task overrides. ",
|
||||||
|
"flipOverlappingFilesToPreferTheTaskBranch": "Flip overlapping files to prefer the task branch (default)",
|
||||||
|
"forcesAllAILanesToUseTheDeterministic": "Forces all AI lanes to use the deterministic mock provider. No network calls, zero token cost.",
|
||||||
|
"fusion": "Fusion",
|
||||||
|
"gitHubAuthMode": "GitHub auth mode",
|
||||||
|
"gitHubAuthentication": "GitHub Authentication",
|
||||||
|
"gitHubCLIGhAuth": "GitHub CLI (gh auth)",
|
||||||
|
"gitHubPersonalAccessToken": "GitHub personal access token",
|
||||||
|
"gitRemoteToPushToEGOrigin": "Git remote to push to (e.g. \"origin\"). Can include branch name (e.g. \"origin main\"). Default: \"origin\".",
|
||||||
|
"ignoreOverlapDetectionPreserveLegacyBehavior": "Ignore overlap detection — preserve legacy behavior",
|
||||||
|
"inYourProjectRootWillShowTheNew": " in your project root will show the new commits inverted as "staged changes" until you pull manually. Only applies to direct merges. ",
|
||||||
|
"includeTaskIDInCommitScope": " Include task ID in commit scope ",
|
||||||
|
"insteadOf": " instead of ",
|
||||||
|
"integrationBranch": "Integration branch",
|
||||||
|
"integrationWorktree": "Integration worktree",
|
||||||
|
"isTheLegacyBehavior": " is the legacy behavior: ",
|
||||||
|
"isTheStricterOptInModeThatRefuses": " is the stricter opt-in mode that refuses to auto-complete merges with duplicate-subject or touched-file overlap risks. ",
|
||||||
|
"legacy": " → legacy ",
|
||||||
|
"legacy2": "Legacy ",
|
||||||
|
"legacyAutoMergeStamp": " legacy auto-merge stamp",
|
||||||
|
"legacyAutoMergeStampCleanup": "Legacy auto-merge stamp cleanup",
|
||||||
|
"legacyIntegrationBranchMode": "Legacy integration-branch mode.",
|
||||||
|
"maxAIReviewPasses": "Max AI review passes",
|
||||||
|
"merge": "Merge",
|
||||||
|
"moreDetails": "More details",
|
||||||
|
"nameUsedInThe": "Name used in the ",
|
||||||
|
"noLegacyAutoMergeStampsToCleanUp": " No legacy auto-merge stamps to clean up. ",
|
||||||
|
"noreplyRunfusionAi": "noreply@runfusion.ai",
|
||||||
|
"off": "Off",
|
||||||
|
"offLeaveTheProjectRootStaleLegacyBehavior": "Off — leave the project root stale (legacy behavior)",
|
||||||
|
"offSkipAudit": "Off (skip audit)",
|
||||||
|
"optionsStartWithABestEffort": " options start with a best-effort ",
|
||||||
|
"origin": "origin",
|
||||||
|
"personalAccessToken": "Personal access token",
|
||||||
|
"pickALocalBranchFromTheDropdownCommon": "). Pick a local branch from the dropdown — common integration names like ",
|
||||||
|
"positiveIntegerRetryCapForAutoMergeConflict": "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.",
|
||||||
|
"postMergeAuditMode": "Post-merge audit mode",
|
||||||
|
"pushRemote": "Push Remote",
|
||||||
|
"pushToRemoteAfterMerge": " Push to remote after merge ",
|
||||||
|
"readyToCleanUp": " ready to clean up.",
|
||||||
|
"retriesTheAIAgentRatherThanAutoPicking": " retries the AI agent rather than auto-picking a side. ",
|
||||||
|
"reuseTaskWorktreeDefault": "Reuse task worktree (default)",
|
||||||
|
"skipsTheAuditEntirelySwitchingToOffIs": " skips the audit entirely. Switching to Off is recommended only if you trust your branches don't silently drop edits. ",
|
||||||
|
"smart": "Smart",
|
||||||
|
"smartConflictResolution": " Smart conflict resolution ",
|
||||||
|
"smartPreferMain": "Smart, prefer main",
|
||||||
|
"smartPreferMainOnFallbackFetchFfOrigin": "Smart, prefer main on fallback — fetch+ff origin → AI → auto-resolve → -X ours (default; protects just-merged sibling work)",
|
||||||
|
"smartPreferMainOverlapGuard": "Smart Prefer Main Overlap Guard",
|
||||||
|
"smartPreferTask": "Smart, prefer task",
|
||||||
|
"smartPreferTaskOnFallbackFetchFfOrigin": "Smart, prefer task on fallback — fetch+ff origin → AI → auto-resolve → -X theirs (legacy \"smart\" behavior; task branch wins)",
|
||||||
|
"snapsCleanlyWhenTheWorktreeHasNoEdits": " snaps cleanly when the worktree has no edits and skips otherwise. ",
|
||||||
|
"snapshotsRealLocalEditsAsAPatchAgainst": " snapshots real local edits as a patch against the previous tip, snaps the worktree to the new tip, then reapplies the patch — untracked files that collide with newly-tracked paths are left in a temp dir for manual recovery. ",
|
||||||
|
"soAFreshlyPushedSiblingCommitDoesntGet": " (so a freshly-pushed sibling commit doesn't get clobbered), then run an AI agent, then auto-resolve handles lock/generated/trivial files. They differ only in the ",
|
||||||
|
"soMainWinsProtectsJustMergedSiblingWork": " so main wins — protects just-merged sibling work and is the new default. ",
|
||||||
|
"soTheTaskBranchWinsFastButCan": " so the task branch wins — fast, but can resurrect code an earlier sibling task deleted (the FN-2887 class of regression). ",
|
||||||
|
"stashFastForwardDefaultPreserveLocalEdits": "Stash + fast-forward (default) — preserve local edits",
|
||||||
|
"stopsAfterTheFirstAIAttemptAndWaits": " stops after the first AI attempt and waits for a human. ",
|
||||||
|
"symbolicRefFallback": " symbolic ref → fallback ",
|
||||||
|
"theCanonicalBranchFusionMergesTasksIntoAnd": " The canonical branch Fusion merges tasks into and uses as the reference for all ahead/behind / overlap / pre-rebase computations. Leave on ",
|
||||||
|
"theLegacyMergeSettingsBelowDoNotApply": "The legacy merge settings below do not apply while AI merge is on.",
|
||||||
|
"toResolveViaTheStandardCascade": " to resolve via the standard cascade (",
|
||||||
|
"toTypeABranchThatDoesnAposT": " to type a branch that doesn't exist locally yet. Applies to both direct merges and pull-request mode; individual tasks can still override via task metadata. ",
|
||||||
|
"trailer": " trailer",
|
||||||
|
"trailerCreditingFusionRecognizedByGitHubForShared": " trailer crediting Fusion (recognized by GitHub for shared attribution). ",
|
||||||
|
"useDropdown": " Use dropdown ",
|
||||||
|
"useProjectRootLegacy": "Use project root (legacy)",
|
||||||
|
"uses": " uses ",
|
||||||
|
"valuesFromOlderSettingsAreMigratedAutomatically": " values from older settings are migrated automatically.",
|
||||||
|
"warn": "Warn",
|
||||||
|
"warnDefaultLogFindingsContinue": "Warn (default; log findings, continue)",
|
||||||
|
"warnOnlyKeepLegacyMainWinsFallback": "Warn only — keep legacy main-wins fallback",
|
||||||
|
"whenDisabledMergeCommitMessagesOmitTheTask": "When disabled, merge commit messages omit the task ID from the scope (e.g. ",
|
||||||
|
"whenEnabledCommitsMadeByFusionKeepYour": " When enabled, commits made by Fusion keep your git identity as the primary author and append a ",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm": "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.), generated files (dist/*, *.gen.ts), and trivial whitespace conflicts are resolved automatically without AI intervention. Complex code conflicts still require AI review.",
|
||||||
|
"whenEnabledLockFilesPackageLockJsonPnpm2": "When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.",
|
||||||
|
"whenEnabledTasksThatPassReviewAreAutomatically": "When enabled, tasks that pass review are automatically merged into the main branch",
|
||||||
|
"whenEnabledTheMergedResultIsAutomaticallyPushed": "When enabled, the merged result is automatically pushed to the configured git remote. This includes pulling the latest from the remote first (rebase) and resolving any conflicts with AI if needed.",
|
||||||
|
"whenUsingSmartPreferMainAutomaticallyPreferThe": " When using smart-prefer-main, automatically prefer the branch side for files that main has recently modified to avoid silently discarding branch work. "
|
||||||
},
|
},
|
||||||
"mergeManually": "Merge Manually",
|
"mergeManually": "Merge Manually",
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
@@ -5612,38 +5970,314 @@ export default interface Resources {
|
|||||||
"project": "Specific to this project"
|
"project": "Specific to this project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeRouting": {
|
||||||
|
"blockExecution": "Block execution",
|
||||||
|
"configureHowTasksAreRoutedToExecutionNodes": "Configure how tasks are routed to execution nodes.",
|
||||||
|
"defaultExecutionNode": "Default Execution Node",
|
||||||
|
"fallBackToLocal": "Fall back to local",
|
||||||
|
"localExecutionNoDefaultNode": "Local execution (no default node)",
|
||||||
|
"nodeRouting": "Node Routing",
|
||||||
|
"selectedNode": "Selected node:",
|
||||||
|
"theseSettingsApplyAtTheProjectLevel": "These settings apply at the project level.",
|
||||||
|
"unavailableNodePolicy": "Unavailable Node Policy",
|
||||||
|
"usedWhenATaskHasNoNodeOverride": "Used when a task has no node override. Node status is shown for safer routing selection."
|
||||||
|
},
|
||||||
"nodeSync": {
|
"nodeSync": {
|
||||||
|
"alwaysAsk": "Always ask",
|
||||||
|
"automaticallySynchronizeSettingsBetweenThisNodeAndConnected": "Automatically synchronize settings between this node and connected remote nodes",
|
||||||
|
"conflictResolution": "Conflict resolution",
|
||||||
|
"enableAutomaticSettingsSync": " Enable automatic settings sync ",
|
||||||
|
"every15Minutes": "Every 15 minutes",
|
||||||
|
"every1Hour": "Every 1 hour",
|
||||||
|
"every30Minutes": "Every 30 minutes",
|
||||||
|
"every5Minutes": "Every 5 minutes",
|
||||||
|
"includeAPIKeysAndOAuthTokensInSync": "Include API keys and OAuth tokens in sync operations",
|
||||||
|
"keepLocal": "Keep local",
|
||||||
|
"keepRemote": "Keep remote",
|
||||||
|
"lastWriteWins": "Last write wins",
|
||||||
|
"nodeSync": "Node Sync",
|
||||||
|
"syncInterval": "Sync interval",
|
||||||
|
"syncModelAuthCredentials": " Sync model auth credentials ",
|
||||||
"workflowSettingsNotSynced": "Workflow settings are not synced across nodes yet."
|
"workflowSettingsNotSynced": "Workflow settings are not synced across nodes yet."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"accessTokenOptional": "Access token (optional)",
|
||||||
|
"advanced": "Advanced",
|
||||||
|
"allFailuresLegacy": "All failures (legacy)",
|
||||||
|
"baseURLForDeepLinksInNotificationsWhen": " Base URL for deep links in notifications. When set, clicking a notification opens the dashboard directly to the task. ",
|
||||||
|
"customNtfyServerURLOptional": "Custom ntfy server URL (optional)",
|
||||||
|
"dashboardHostname": "Dashboard Hostname",
|
||||||
|
"discord": "Discord",
|
||||||
|
"enable": " Enable ",
|
||||||
|
"failureNotificationDelayMs": "Failure notification delay (ms)",
|
||||||
|
"failureNotificationMode": "Failure notification mode",
|
||||||
|
"format": "Format",
|
||||||
|
"general": " General: ",
|
||||||
|
"generic": "Generic",
|
||||||
|
"howLongAFailureMustPersistBeforeA": " How long a failure must persist before a push notification is sent. 0 = notify immediately. ",
|
||||||
|
"httpLocalhost3000": "http://localhost:3000",
|
||||||
|
"httpsHooksExampleCom": "https://hooks.example.com/...",
|
||||||
|
"httpsNtfySh": "https://ntfy.sh",
|
||||||
|
"learnMoreAboutNtfySh": " Learn more about ntfy.sh ",
|
||||||
|
"leaveBlankToKeepTheDefaultServerHttps": " Leave blank to keep the default server: https://ntfy.sh. Custom servers must use http:// or https://. ",
|
||||||
|
"leaveBlankToPublishWithoutAuthenticationWhenSet": " Leave blank to publish without authentication. When set, Fusion sends an Authorization Bearer header with ntfy requests. ",
|
||||||
|
"messageInbox": " Message inbox: ",
|
||||||
|
"mustBeAValidURLStartingWithHttp": " Must be a valid URL starting with http:// or https:// ",
|
||||||
|
"myTopicName": "my-topic-name",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"notifyOnEvents": "Notify on events",
|
||||||
|
"ntfy": "ntfy",
|
||||||
|
"ntfyTopic": "ntfy Topic",
|
||||||
|
"roomReply": " Room reply: ",
|
||||||
"sending": "Sending…",
|
"sending": "Sending…",
|
||||||
|
"slack": "Slack",
|
||||||
|
"stickyFailuresOnlyDefault": "Sticky failures only (default)",
|
||||||
|
"stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits": "Sticky-only suppresses recovered failures; terminal-only waits for paused/in-review failed tasks; all restores legacy alerts.",
|
||||||
|
"terminalFailuresOnlySuppressAutoRetried": "Terminal failures only (suppress auto-retried)",
|
||||||
"testMessageInbox": "Test message inbox",
|
"testMessageInbox": "Test message inbox",
|
||||||
"testNotification": "Test notification",
|
"testNotification": "Test notification",
|
||||||
"testRoomReply": "Test room reply"
|
"testRoomReply": "Test room reply",
|
||||||
|
"tk": "tk_...",
|
||||||
|
"topicMustBe164AlphanumericHyphenOr": " Topic must be 1–64 alphanumeric, hyphen, or underscore characters ",
|
||||||
|
"webhook": "Webhook",
|
||||||
|
"webhookNotifications": " Webhook notifications ",
|
||||||
|
"webhookURL": "Webhook URL",
|
||||||
|
"yourNtfyShTopicName164Alphanumeric": " Your ntfy.sh topic name (1–64 alphanumeric/hyphen/underscore characters)."
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"fusionPlugins": " Fusion Plugins ",
|
||||||
|
"piExtensions": " Pi Extensions ",
|
||||||
|
"pluginManagerType": "Plugin manager type",
|
||||||
|
"plugins": "Plugins"
|
||||||
|
},
|
||||||
|
"projectModels": {
|
||||||
|
"aIMergeCommitSummaries": " AI merge commit summaries ",
|
||||||
|
"aITitleAndGitCommitMessageSummarization": " AI Title and Git Commit Message Summarization ",
|
||||||
|
"addPreset": " Add Preset ",
|
||||||
|
"advancedWorkflowPolicy": " Advanced workflow policy ",
|
||||||
|
"autoSelectPresetBasedOnTaskSize": " Auto-select preset based on task size ",
|
||||||
|
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize long descriptions as titles ",
|
||||||
|
"automaticallyCompactContextWhenApproachingThisTokenCount": "Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count.",
|
||||||
|
"configuredPresets": "Configured presets",
|
||||||
|
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ",
|
||||||
|
"defaultWorkflowModelLaneActions": "Default workflow model lane actions",
|
||||||
|
"defaultWorkflowModelLanes": "Default workflow model lanes",
|
||||||
|
"delete": " Delete ",
|
||||||
|
"edit": " Edit ",
|
||||||
|
"executorModel": "Executor model",
|
||||||
|
"fallsBackTo": " Falls back to: ",
|
||||||
|
"loadingAvailableModels": "Loading available models…",
|
||||||
|
"loadingWorkflowModelLanes": "Loading workflow model lanes…",
|
||||||
|
"modelLanes": "Model Lanes",
|
||||||
|
"modelPresets": "Model Presets",
|
||||||
|
"name": "Name",
|
||||||
|
"noCap": "No cap",
|
||||||
|
"noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow": " No models available. Configure authentication before selecting workflow model lanes. ",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst": " No models available. Configure authentication first. ",
|
||||||
|
"noModelsAvailableConfigureAuthenticationFirst2": "No models available. Configure authentication first.",
|
||||||
|
"noPreset": "No preset",
|
||||||
|
"noPresetsConfiguredYet": "No presets configured yet.",
|
||||||
|
"openAProjectToEditWorkflowModelLanes": "Open a project to edit workflow model lanes.",
|
||||||
|
"overrideGlobalModelSettingsAtTheProjectLevel": " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ",
|
||||||
|
"presetEditor": "Preset editor",
|
||||||
|
"reset": " Reset ",
|
||||||
|
"resetToDefaultNoCap": "Reset to default (no cap)",
|
||||||
|
"resetToInheritFromGlobal": "Reset to inherit from global",
|
||||||
|
"resetToInheritFromWorkflow": "Reset to inherit from workflow",
|
||||||
|
"reviewerModel": "Reviewer model",
|
||||||
|
"theseProjectOverridesApplyToTheActiveDefault": " These project overrides apply to the active default workflow. ",
|
||||||
|
"tokenCap": "Token Cap",
|
||||||
|
"useDefault": "Use default",
|
||||||
|
"useWorkflowDefault": "Use workflow default",
|
||||||
|
"whenEnabledMergeCommitMessagesIncludeAnAI": " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. ",
|
||||||
|
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. "
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"acceptRoutes": " Accept routes ",
|
||||||
|
"advancedNamedTunnel": "Advanced (Named Tunnel)",
|
||||||
|
"advancedSettings": "Advanced Settings",
|
||||||
|
"authLinkTokenType": "Auth link token type",
|
||||||
|
"authLinks": "Auth Links",
|
||||||
|
"authenticatedURL": "Authenticated URL:",
|
||||||
|
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped.",
|
||||||
|
"cloudflare": "Cloudflare",
|
||||||
"cloudflaredInstalled": "cloudflared installed successfully",
|
"cloudflaredInstalled": "cloudflared installed successfully",
|
||||||
|
"cloudflaredIsInstalled": "cloudflared is installed",
|
||||||
|
"cloudflaredIsNotInstalled": "cloudflared is not installed",
|
||||||
|
"cloudflaredMustBeInstalledToStartTheTunnel": "cloudflared must be installed to start the tunnel",
|
||||||
|
"enableShortLivedTokens": " Enable short-lived tokens ",
|
||||||
|
"expiresAt": " · Expires at {{expiresAt}}",
|
||||||
|
"external": "External ",
|
||||||
|
"externalTunnelQRCode": "External tunnel QR code",
|
||||||
|
"generateQR": "Generate QR",
|
||||||
|
"generateShortLivedToken": "Generate short-lived token",
|
||||||
|
"httpsYourDomainExample": "https://your-domain.example",
|
||||||
|
"ifHomebrewIsUnavailable": "If Homebrew is unavailable: ",
|
||||||
|
"ingressURL": "Ingress URL",
|
||||||
|
"installCloudflared": "Install cloudflared",
|
||||||
|
"installing": "Installing…",
|
||||||
|
"lastShortLivedTokenExpiresAt": "Last short-lived token expires at ",
|
||||||
|
"manualInstall": "Manual install: ",
|
||||||
|
"ms": "ms)",
|
||||||
|
"namedTunnelModeEnabled": "Named Tunnel mode enabled — configure tunnel name, token, and ingress URL below.",
|
||||||
|
"noExpiry": " · No expiry",
|
||||||
|
"persistentToken": "Persistent token",
|
||||||
"persistentTokenRegenerated": "Persistent token regenerated",
|
"persistentTokenRegenerated": "Persistent token regenerated",
|
||||||
|
"qRSVGMarkup": "QR SVG markup",
|
||||||
|
"regeneratePersistentToken": "Regenerate persistent token",
|
||||||
|
"rememberLastRunningState": " Remember last running state ",
|
||||||
|
"remoteAccess": "Remote Access",
|
||||||
|
"remoteAccessCode": "Remote access code:",
|
||||||
|
"remoteAccessQRCode": "Remote access QR code",
|
||||||
|
"remoteProvider": "Remote provider",
|
||||||
"restarting": "Restarting…",
|
"restarting": "Restarting…",
|
||||||
|
"scanThisQRCodeOnYourPhone": "Scan this QR code on your phone",
|
||||||
|
"scanToConnect": "Scan to connect:",
|
||||||
|
"scanToOpen": "Scan to open:",
|
||||||
|
"selectAProviderAboveToConfigureRemoteAccess": "Select a provider above to configure remote access.",
|
||||||
|
"shortLivedTTLMs": "Short-lived TTL (ms)",
|
||||||
|
"shortLivedToken": "Short-lived token",
|
||||||
"shortLivedTokenGenerated": "Short-lived token generated",
|
"shortLivedTokenGenerated": "Short-lived token generated",
|
||||||
|
"showURL": "Show URL",
|
||||||
"startFresh": "Start Fresh",
|
"startFresh": "Start Fresh",
|
||||||
"startTunnel": "Start Tunnel",
|
"startTunnel": "Start Tunnel",
|
||||||
"starting": "Starting…",
|
"starting": "Starting…",
|
||||||
"stopTunnel": "Stop Tunnel",
|
"stopTunnel": "Stop Tunnel",
|
||||||
"stopping": "Stopping…",
|
"stopping": "Stopping…",
|
||||||
|
"tailnetURL": "Tailnet URL:",
|
||||||
|
"tailscale": "Tailscale",
|
||||||
|
"tailscaleFunnelWillExposeThisDashboardOnYour": "Tailscale Funnel will expose this dashboard on your tailnet's public ",
|
||||||
|
"tokenType": " Token type: ",
|
||||||
|
"tunnelDetected": " tunnel detected",
|
||||||
|
"tunnelName": "Tunnel name",
|
||||||
"tunnelRestarted": "Remote tunnel restarted",
|
"tunnelRestarted": "Remote tunnel restarted",
|
||||||
"tunnelStarted": "Remote tunnel started",
|
"tunnelStarted": "Remote tunnel started",
|
||||||
"tunnelStopped": "Remote tunnel stopped",
|
"tunnelStopped": "Remote tunnel stopped",
|
||||||
"useExisting": "Use Existing"
|
"tunnelToken": "Tunnel token",
|
||||||
|
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
|
||||||
|
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
|
||||||
|
"useExisting": "Use Existing",
|
||||||
|
"usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed."
|
||||||
|
},
|
||||||
|
"researchGlobal": {
|
||||||
|
"advancedExternalSearchProviders": "Advanced — external search providers",
|
||||||
|
"alwaysOn": "Always on",
|
||||||
|
"brave": "Brave",
|
||||||
|
"builtInUsesAgentWebTools": " Built-in (uses agent web tools) ",
|
||||||
|
"configureBraveTavilyAndGoogleAPIKeysIn": " Configure Brave, Tavily, and Google API keys in Authentication. ",
|
||||||
|
"customSearchEngineId": "custom-search-engine-id",
|
||||||
|
"defaultMaxConcurrentRuns": "Default Max Concurrent Runs",
|
||||||
|
"defaultMaxDurationMs": "Default Max Duration (ms)",
|
||||||
|
"defaultMaxSourcesPerRun": "Default Max Sources Per Run",
|
||||||
|
"enabledSources": "Enabled Sources",
|
||||||
|
"gitHub": " GitHub ",
|
||||||
|
"googleCustomSearch": "Google Custom Search",
|
||||||
|
"googleSearchCX": "Google Search CX",
|
||||||
|
"httpsSearxExampleCom": "https://searx.example.com",
|
||||||
|
"localDocs": " Local Docs ",
|
||||||
|
"maxSynthesisRounds": "Max Synthesis Rounds",
|
||||||
|
"missingCredentialsForTheSelectedResearchProvider": " Missing credentials for the selected research provider. ",
|
||||||
|
"openAuthentication": " Open Authentication ",
|
||||||
|
"openAuthenticationSettings": " Open Authentication Settings ",
|
||||||
|
"requestTimeoutMs": "Request Timeout (ms)",
|
||||||
|
"researchDefaults": "Research Defaults",
|
||||||
|
"searXNG": "SearXNG",
|
||||||
|
"searXNGURL": "SearXNG URL",
|
||||||
|
"searchProvider": "Search Provider",
|
||||||
|
"searchesAndFetchesUseTheAgentsNativeWebSearch": " Searches and fetches use the agent's native WebSearch/WebFetch tools. No API key required. ",
|
||||||
|
"tavily": "Tavily",
|
||||||
|
"webSearch": " Web Search "
|
||||||
|
},
|
||||||
|
"researchProject": {
|
||||||
|
"alwaysOn": "Always on",
|
||||||
|
"enableResearchInThisProject": " Enable research in this project ",
|
||||||
|
"enabledSources": "Enabled Sources",
|
||||||
|
"github": "GitHub",
|
||||||
|
"llmSynthesis": "LLM Synthesis",
|
||||||
|
"localDocs": "Local Docs",
|
||||||
|
"maxConcurrentRuns": "Max Concurrent Runs",
|
||||||
|
"maxDurationMs": "Max Duration (ms)",
|
||||||
|
"maxSourcesPerRun": "Max Sources Per Run",
|
||||||
|
"pageFetch": "Page Fetch",
|
||||||
|
"projectResearchSettings": "Project Research Settings",
|
||||||
|
"requestTimeoutMs": "Request Timeout (ms)",
|
||||||
|
"webSearch": " Web Search ",
|
||||||
|
"webSearchIsAlwaysEnabledConfigureTheSearch": " Web search is always enabled. Configure the search provider under Research Defaults. "
|
||||||
},
|
},
|
||||||
"resolveAllLocal": "Resolve All: Keep Local",
|
"resolveAllLocal": "Resolve All: Keep Local",
|
||||||
"resolveAllRemote": "Resolve All: Keep Remote",
|
"resolveAllRemote": "Resolve All: Keep Remote",
|
||||||
"resolveFailed": "Failed to resolve conflicts",
|
"resolveFailed": "Failed to resolve conflicts",
|
||||||
"resolvedSuccess": "Settings conflicts resolved successfully",
|
"resolvedSuccess": "Settings conflicts resolved successfully",
|
||||||
"resolving": "Resolving...",
|
"resolving": "Resolving...",
|
||||||
|
"runtimesRuntimes": {
|
||||||
|
"hermesRuntime": "Hermes Runtime",
|
||||||
|
"openClawRuntime": "OpenClaw Runtime",
|
||||||
|
"paperclipRuntime": "Paperclip Runtime"
|
||||||
|
},
|
||||||
|
"scheduledEvals": {
|
||||||
|
"autoCreateTasks": "Auto-create tasks",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"enableScheduledEvalRunsForThisProject": " Enable scheduled eval runs for this project ",
|
||||||
|
"evaluatorModel": "Evaluator Model",
|
||||||
|
"evaluatorProvider": "Evaluator Provider",
|
||||||
|
"followUpPolicy": "Follow-up Policy",
|
||||||
|
"gpt5": "gpt-5",
|
||||||
|
"intervalMs": "Interval (ms)",
|
||||||
|
"leaveProviderAndModelBlankToInheritThe": " Leave provider and model blank to inherit the project validator lane model settings. ",
|
||||||
|
"openai": "openai",
|
||||||
|
"retentionDays": "Retention (days)",
|
||||||
|
"scheduledEvals": "Scheduled Evals",
|
||||||
|
"suggestOnly": "Suggest only"
|
||||||
|
},
|
||||||
"scheduling": {
|
"scheduling": {
|
||||||
|
"addIgnoredPath": " Add ignored path ",
|
||||||
|
"archiveAgentLog": "Archive Agent Log",
|
||||||
|
"archiveCompletedTasksAfterDays": "Archive Completed Tasks After (days)",
|
||||||
|
"backlogNoTaskAutoClaimIsExecutorOnly": "Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.",
|
||||||
|
"browse": " Browse ",
|
||||||
|
"closeParenPeriod": ").",
|
||||||
|
"compactModeKeepsArchiveSizeLowWhilePreserving": "Compact mode keeps archive size low while preserving recent agent activity for context.",
|
||||||
|
"compactSummaryAndRecentEntries": "Compact summary and recent entries",
|
||||||
|
"completedTasksOlderThanTheThresholdAreMoved": "Completed tasks older than the threshold are moved out of the active task database.",
|
||||||
|
"doNotArchiveAgentLogs": "Do not archive agent logs",
|
||||||
|
"docs": "docs/",
|
||||||
|
"enableAutomaticTaskArchiving": " Enable automatic task archiving ",
|
||||||
|
"enablePlanStalenessEnforcement": " Enable plan staleness enforcement ",
|
||||||
|
"escalateHighFanOutBlockersOnlyAfterThey": "Escalate high fan-out blockers only after they remain in in-progress or in-review for this many hours (age source: columnMovedAt, fallback updatedAt). Default: 2 hours.",
|
||||||
|
"fullAgentLog": "Full agent log",
|
||||||
|
"globalMaxConcurrent": "Global Max Concurrent",
|
||||||
|
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
||||||
|
"ignoredOverlapPaths": "Ignored overlap paths",
|
||||||
|
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
||||||
|
"lite": "Lite",
|
||||||
|
"maxConcurrentTasks": "Max Concurrent Tasks",
|
||||||
|
"maxStuckRetries": "Max Stuck Retries",
|
||||||
|
"maxTriageConcurrent": "Max Triage Concurrent",
|
||||||
|
"maximumAgeInHoursBeforeAPlanIs": "Maximum age in hours before a plan is considered stale. Default: 6 hours.",
|
||||||
|
"maximumConcurrentAgentsAcrossAllProjects": "Maximum concurrent agents across all projects",
|
||||||
|
"maximumConcurrentPlanningAgents": "Maximum concurrent planning agents",
|
||||||
|
"maximumStuckDetectorRetriesBeforeATaskIs": "Maximum stuck-detector retries before a task is marked failed. Default: 6.",
|
||||||
|
"numberOfDaysATaskCanStayIn": "Number of days a task can stay in Done before it is archived. Default: 2 days (48 hours).",
|
||||||
|
"off": "Off",
|
||||||
|
"optionalFileOrDirectoryPathsToIgnoreWhen": " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ",
|
||||||
|
"or": " or ",
|
||||||
|
"pollIntervalMs": "Poll Interval (ms)",
|
||||||
|
"preserveStepProgressOnStuckTaskRequeue": " Preserve step progress on stuck-task requeue ",
|
||||||
|
"remove": " Remove ",
|
||||||
|
"scheduling": "Scheduling",
|
||||||
"selectCurrentDir": "Select current directory",
|
"selectCurrentDir": "Select current directory",
|
||||||
"selectIgnoredOverlapPath": "Select ignored overlap path"
|
"selectIgnoredOverlapPath": "Select ignored overlap path",
|
||||||
|
"serializeTasksWithOverlappingFiles": " Serialize tasks with overlapping files ",
|
||||||
|
"staleHighFanOutEscalationHours": "Stale High Fan-out Escalation (hours)",
|
||||||
|
"staleSpecThresholdHours": "Stale Spec Threshold (hours)",
|
||||||
|
"stepExecution": "Step Execution",
|
||||||
|
"strictCoordinationFocusedHigherPerTickTokensLite": "Strict — coordination-focused; higher per-tick tokens. Lite — pre-2026-05-11 behavior. Off — minimal procedure.",
|
||||||
|
"strictDefault": "Strict (default)",
|
||||||
|
"stuckTaskTimeoutMinutes": "Stuck Task Timeout (minutes)",
|
||||||
|
"timeoutInMinutesForDetectingStuckTasksWhen": "Timeout in minutes for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.",
|
||||||
|
"whenEnabledTasksThatModifyTheSameFiles": "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts",
|
||||||
|
"whenEnabledTasksWithStalePlansPROMPTMd": "When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning",
|
||||||
|
"whenTheStuckDetectorKillsAndReQueues": "When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled."
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "These settings are shared across all your Fusion projects.",
|
"globalBanner": "These settings are shared across all your Fusion projects.",
|
||||||
@@ -5651,11 +6285,59 @@ export default interface Resources {
|
|||||||
},
|
},
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Allow silent sibling branch rename during executor conflicts ",
|
||||||
|
"alsoRebaseOntoLocalDefaultBranchHEAD": " Also rebase onto local default-branch HEAD ",
|
||||||
|
"and": " and ",
|
||||||
|
"andCanHidePriorCommitsFromTheDefault": " and can hide prior commits from the default recovery flow. ",
|
||||||
|
"autoDetectFusionBinWorktrunkOrPATH": "auto-detect (~/.fusion/bin/worktrunk or $PATH)",
|
||||||
"awaitingApproval": "Awaiting approval — open Approvals to continue.",
|
"awaitingApproval": "Awaiting approval — open Approvals to continue.",
|
||||||
|
"branchCollisionSilentlyForksWorkOntoSiblingBranches": " branch collision silently forks work onto sibling branches like ",
|
||||||
|
"browse": " Browse ",
|
||||||
|
"browseWorktreesDirectory": "Browse worktrees directory",
|
||||||
|
"closeParenPeriod": ").",
|
||||||
|
"defaultsTo": ". Defaults to ",
|
||||||
|
"defaultsToWorktreesLeaveEmptyUnlessOverriding": "Defaults to .worktrees — leave empty unless overriding",
|
||||||
|
"disabledByDefaultOptInWhenEnabledFusion": " Disabled by default (opt-in). When enabled, Fusion shells out to ",
|
||||||
|
"discouragedThisRestoresTheLegacyBehaviorWhereA": " Discouraged. This restores the legacy behavior where a live ",
|
||||||
|
"enableWorktrunkAndRequestApprovalToInstallThe": "Enable worktrunk and request approval to install the pinned release.",
|
||||||
|
"enableWorktrunkIntegration": " Enable worktrunk integration ",
|
||||||
|
"failAndPauseTheTaskDefault": "Fail and pause the task (default)",
|
||||||
|
"fallBackToFusionsNativeWorktreeBackend": "Fall back to Fusion's native worktree backend",
|
||||||
|
"forWorktreeCreateSyncPruneAndRemoveOperations": " for worktree create, sync, prune, and remove operations and follows worktrunk's directory layout. ",
|
||||||
|
"inAdditionToTheRemoteRebaseAboveAlso": " In addition to the remote rebase above, also rebase the task branch onto the local default-branch HEAD (rootDir). This catches sibling tasks that merged locally but haven't been pushed yet — without it, two concurrent tasks where one deletes code can have the other silently re-introduce it via the fallback strategy. Enabled by default; only disable if it causes issues with your workflow. ",
|
||||||
|
"installTheWorktrunkBinaryBelowToEnableThis": "Install the worktrunk binary below to enable this integration.",
|
||||||
"installWorktrunk": "Install worktrunk binary",
|
"installWorktrunk": "Install worktrunk binary",
|
||||||
|
"installedAt": " installed at ",
|
||||||
|
"keepsProgressMovingBySwitchingToFusionApos": " keeps progress moving by switching to Fusion's built-in worktree backend. ",
|
||||||
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "Limits total git worktrees including in-review tasks",
|
||||||
|
"maxWorktrees": "Max Worktrees",
|
||||||
|
"offByDefaultOptInWhenEnabledCompleted": "Off by default (opt-in). When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup",
|
||||||
"openApprovals": "Open Approvals",
|
"openApprovals": "Open Approvals",
|
||||||
|
"optionalLeaveBlankToAutoResolveFusionWill": "Optional. Leave blank to auto-resolve; Fusion will offer to install on first use.",
|
||||||
|
"optionalSupports": " Optional. Supports ",
|
||||||
|
"pnpmInstallFrozenLockfile": "pnpm install --frozen-lockfile",
|
||||||
|
"randomNamesEGSwiftFalcon": "Random names (e.g., swift-falcon)",
|
||||||
|
"rebaseFromRemoteBeforeMerge": " Rebase from remote before merge ",
|
||||||
|
"rebaseRemote": "Rebase Remote",
|
||||||
|
"recycleWorktrees": " Recycle worktrees ",
|
||||||
"selectWorktreesDir": "Select worktrees directory",
|
"selectWorktreesDir": "Select worktrees directory",
|
||||||
"tryAgain": "Try again"
|
"shellCommandToRunInEachNewWorktree": "Shell command to run in each new worktree after creation",
|
||||||
|
"stopsOnWorktrunkErrorsForExplicitOperatorRecovery": " stops on worktrunk errors for explicit operator recovery; ",
|
||||||
|
"taskIDEGFN042": "Task ID (e.g., FN-042)",
|
||||||
|
"taskTitleEGFixLoginBug": "Task title (e.g., fix-login-bug)",
|
||||||
|
"tryAgain": "Try again",
|
||||||
|
"useGitDefault": "Use git default",
|
||||||
|
"whenEnabledTheMergerFetchesFromTheConfigured": "When enabled, the merger fetches from the configured remote and rebases the task branch onto the latest default-branch tip before merging — catching concurrent pushes from other collaborators or fusion workers. Any conflicts the rebase surfaces flow into the existing smart/AI resolve pipeline.",
|
||||||
|
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": " when unset. Only affects newly-created worktrees. ",
|
||||||
|
"whichRemoteToFetchForThePreMerge": " Which remote to fetch for the pre-merge rebase. \"Use git default\" falls back to the remote configured for the default branch (typically ",
|
||||||
|
"worktreeInitCommand": "Worktree Init Command",
|
||||||
|
"worktreeNamingStyle": "Worktree Naming Style",
|
||||||
|
"worktrees": "Worktrees",
|
||||||
|
"worktreesDirectory": "Worktrees Directory",
|
||||||
|
"worktrunk": " worktrunk ",
|
||||||
|
"worktrunkBinaryPath": "Worktrunk binary path",
|
||||||
|
"worktrunkFailureBehavior": "Worktrunk failure behavior",
|
||||||
|
"worktrunkIntegration": "Worktrunk integration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
@@ -7980,6 +8662,14 @@ export default interface Resources {
|
|||||||
"notConfigured": "Not configured"
|
"notConfigured": "Not configured"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"nodeRouting": {
|
||||||
|
"statusConnecting": "Connecting",
|
||||||
|
"statusError": "Error",
|
||||||
|
"statusOffline": "Offline",
|
||||||
|
"statusOnline": "Online"
|
||||||
|
}
|
||||||
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"apiKeyFormatError": "{{providerName}} keys should follow this format: {{hint}} (e.g. {{example}})",
|
"apiKeyFormatError": "{{providerName}} keys should follow this format: {{hint}} (e.g. {{example}})",
|
||||||
"apiKeyLabel": {
|
"apiKeyLabel": {
|
||||||
|
|||||||
Reference in New Issue
Block a user