FN-6770: localize dashboard workflow and task UI strings

Localize dashboard workflow, task, setup, and pull request UI copy through the shared i18n resources.

- Replace hardcoded dashboard strings in workflow, task detail/chat/comment, setup wizard, PR, and related modal components with translation lookups.
- Add app/common namespace keys across English and secondary locale catalogs, plus regenerated i18n resource types.
- Add the published package changeset and clear the now-localized component files from the i18n lint deferral list.

Files changed:
 .changeset/fn-6770-localize-workflow-task-pr.md    |   5 +
 i18next.config.ts                                  |  17 +-
 packages/dashboard/app/components/Board.tsx        |  10 +-
 .../dashboard/app/components/PrCreateModal.tsx     |  38 +--
 packages/dashboard/app/components/PrPanel.tsx      |   2 +-
 .../dashboard/app/components/PullRequestView.tsx   |  51 ++--
 .../dashboard/app/components/SettingsModal.tsx     |  92 +++----
 .../dashboard/app/components/SetupWizardModal.tsx  |   8 +-
 packages/dashboard/app/components/TaskChatTab.tsx  | 107 ++++----
 packages/dashboard/app/components/TaskComments.tsx |   2 +-
 .../dashboard/app/components/TaskDetailModal.tsx   |  54 ++--
 .../app/components/WorkflowNodeEditor.tsx          | 109 +++++----
 .../app/components/WorkflowResultsTab.tsx          | 190 +++++++-------
 .../dashboard/app/components/WorkflowSelector.tsx  |  20 +-
 packages/i18n/locales/en/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/en/common.json               |  28 +--
 packages/i18n/locales/es/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/es/common.json               |  28 +--
 packages/i18n/locales/fr/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/fr/common.json               |  28 +--
 packages/i18n/locales/ko/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/ko/common.json               |  28 +--
 packages/i18n/locales/zh-CN/app.json               | 268 ++++++++++++++++++--
 packages/i18n/locales/zh-CN/common.json            |  28 +--
 packages/i18n/locales/zh-TW/app.json               | 268 ++++++++++++++++++--
 packages/i18n/locales/zh-TW/common.json            |  28 +--
 packages/i18n/src/resources.d.ts                   | 272 ++++++++++++++++++---
 27 files changed, 2144 insertions(+), 609 deletions(-)

Fusion-Task-Id: FN-6770

Fusion-Task-Lineage: 393b6b8a-1902-4e46-a53e-b4a0793022ca
This commit is contained in:
gsxdsm
2026-06-20 10:35:40 -07:00
parent c0d78f19c5
commit bdf95f8d65
27 changed files with 2144 additions and 609 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Localized the dashboard workflow/task/setup/PR component cluster and removed the obsolete i18n lint deferrals for those files so the hardcoded-string guardrail scans them again.

View File

@@ -6,23 +6,8 @@ import {
const DEFERRED_I18N_LINT_FILES = [ const DEFERRED_I18N_LINT_FILES = [
// 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.
// Keep the deferral file-scoped and remove entries as those follow-ups localize each cluster.
// FNXC:i18n-LintBaseline 2026-06-20-00:00: // 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. // FN-6770 and FN-6771 localized the remaining workflow/task/setup/PR and settings/sections clusters, so no dashboard component files remain deferred from hardcoded-string lint.
"packages/dashboard/app/components/WorkflowSelector.tsx",
"packages/dashboard/app/components/WorkflowResultsTab.tsx",
"packages/dashboard/app/components/WorkflowNodeEditor.tsx",
"packages/dashboard/app/components/TaskDetailModal.tsx",
"packages/dashboard/app/components/TaskComments.tsx",
"packages/dashboard/app/components/TaskChatTab.tsx",
"packages/dashboard/app/components/SetupWizardModal.tsx",
"packages/dashboard/app/components/SettingsModal.tsx",
"packages/dashboard/app/components/PullRequestView.tsx",
"packages/dashboard/app/components/PrPanel.tsx",
"packages/dashboard/app/components/PrCreateModal.tsx",
"packages/dashboard/app/components/Board.tsx",
] as const; ] as const;
/** /**

View File

@@ -5,6 +5,7 @@ import { Column } from "./Column";
import "./Lane.css"; import "./Lane.css";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Pencil, Plus } from "lucide-react"; import { Pencil, Plus } from "lucide-react";
import { fetchWorkflowSteps, fetchBoardWorkflows, promoteTask, type ModelInfo, type BoardWorkflowDefinition, type BoardWorkflowsPayload } from "../api"; import { fetchWorkflowSteps, fetchBoardWorkflows, promoteTask, type ModelInfo, type BoardWorkflowDefinition, type BoardWorkflowsPayload } from "../api";
import { useBlockerFanout } from "../hooks/useBlockerFanout"; import { useBlockerFanout } from "../hooks/useBlockerFanout";
@@ -124,6 +125,7 @@ function areWorkflowNameLookupsEqual(previous: ReadonlyMap<string, string>, next
} }
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow }: BoardProps) { export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow }: BoardProps) {
const { t } = useTranslation("app");
const [archivedCollapsed, setArchivedCollapsed] = useState(true); const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const archivedLoadedRef = useRef(false); const archivedLoadedRef = useRef(false);
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP); const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
@@ -527,8 +529,8 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
type="button" type="button"
className="btn btn-icon btn-sm board-workflow-edit-btn" className="btn btn-icon btn-sm board-workflow-edit-btn"
onClick={() => onOpenWorkflowEditor(selectedWorkflow.id)} onClick={() => onOpenWorkflowEditor(selectedWorkflow.id)}
title="Edit workflows" title={t("board.workflow.edit", "Edit workflows")}
aria-label="Edit workflows" aria-label={t("board.workflow.edit", "Edit workflows")}
> >
<Pencil size={15} /> <Pencil size={15} />
</button> </button>
@@ -538,8 +540,8 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
type="button" type="button"
className="btn btn-icon btn-sm board-workflow-create-btn" className="btn btn-icon btn-sm board-workflow-create-btn"
onClick={onCreateWorkflow} onClick={onCreateWorkflow}
title="New workflow" title={t("board.workflow.new", "New workflow")}
aria-label="New workflow" aria-label={t("board.workflow.new", "New workflow")}
> >
<Plus size={15} /> <Plus size={15} />
</button> </button>

View File

@@ -488,7 +488,7 @@ export function PrCreateModal({
<> <>
<section className="pr-create-modal__section"> <section className="pr-create-modal__section">
<h3 className="pr-create-modal__section-title">{t("pr.preflightChecks", "Pre-flight checks")}</h3> <h3 className="pr-create-modal__section-title">{t("pr.preflightChecks", "Pre-flight checks")}</h3>
{preflightLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Loading pre-flight checks…</div> : null} {preflightLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.loadingPreflight", "Loading pre-flight checks…")}</div> : null}
{preflightError ? <div className="form-error pr-error" role="alert"><p>{preflightError}</p></div> : null} {preflightError ? <div className="form-error pr-error" role="alert"><p>{preflightError}</p></div> : null}
{!preflightLoading && !preflightError ? ( {!preflightLoading && !preflightError ? (
<> <>
@@ -511,8 +511,8 @@ export function PrCreateModal({
{!preflight?.branchOnRemote ? ( {!preflight?.branchOnRemote ? (
<div className="card pr-create-modal__preflight-remediation"> <div className="card pr-create-modal__preflight-remediation">
<div className="pr-create-modal__conflict-copy"> <div className="pr-create-modal__conflict-copy">
<p className="pr-create-modal__conflict-title">Push branch to remote</p> <p className="pr-create-modal__conflict-title">{t("pr.pushBranch.title", "Push branch to remote")}</p>
<p className="pr-create-modal__conflict-message">Fusion will push this task&apos;s branch to origin so the PR can be created.</p> <p className="pr-create-modal__conflict-message">{t("pr.pushBranch.message", "Fusion will push this task's branch to origin so the PR can be created.")}</p>
</div> </div>
<button <button
type="button" type="button"
@@ -521,15 +521,15 @@ export function PrCreateModal({
disabled={pushingBranch || preflightLoading || !baseBranch} disabled={pushingBranch || preflightLoading || !baseBranch}
> >
{pushingBranch ? <RefreshCw size={14} className="spin" /> : null} {pushingBranch ? <RefreshCw size={14} className="spin" /> : null}
Push branch to remote {t("pr.pushBranch.button", "Push branch to remote")}
</button> </button>
</div> </div>
) : null} ) : null}
{preflight?.conflictsWithBase ? ( {preflight?.conflictsWithBase ? (
<div className="card pr-create-modal__conflict-resolution"> <div className="card pr-create-modal__conflict-resolution">
<div className="pr-create-modal__conflict-copy"> <div className="pr-create-modal__conflict-copy">
<p className="pr-create-modal__conflict-title">Resolve conflicts with AI</p> <p className="pr-create-modal__conflict-title">{t("pr.resolveConflicts.title", "Resolve conflicts with AI")}</p>
<p className="pr-create-modal__conflict-message">Fusion will use AI to resolve conflicts on this branch and push it.</p> <p className="pr-create-modal__conflict-message">{t("pr.resolveConflicts.message", "Fusion will use AI to resolve conflicts on this branch and push it.")}</p>
</div> </div>
<button <button
type="button" type="button"
@@ -538,7 +538,7 @@ export function PrCreateModal({
disabled={resolvingConflicts || preflightLoading || !baseBranch} disabled={resolvingConflicts || preflightLoading || !baseBranch}
> >
{resolvingConflicts ? <RefreshCw size={14} className="spin" /> : null} {resolvingConflicts ? <RefreshCw size={14} className="spin" /> : null}
Resolve conflicts with AI {t("pr.resolveConflicts.button", "Resolve conflicts with AI")}
</button> </button>
</div> </div>
) : null} ) : null}
@@ -554,7 +554,7 @@ export function PrCreateModal({
{userEditedTitle && <button type="button" className="btn btn-sm" onClick={() => { setTitle(aiTitle); setUserEditedTitle(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>} {userEditedTitle && <button type="button" className="btn btn-sm" onClick={() => { setTitle(aiTitle); setUserEditedTitle(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>}
</div> </div>
</div> </div>
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Generating AI title…</div> : null} {metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.generatingTitle", "Generating AI title…")}</div> : null}
{metadataError ? <div className="form-error pr-error" role="alert"><p>{metadataError}</p></div> : null} {metadataError ? <div className="form-error pr-error" role="alert"><p>{metadataError}</p></div> : null}
<input id="pr-create-modal-title" className="input" value={title} onChange={(event) => { setTitle(event.target.value); setUserEditedTitle(true); }} /> <input id="pr-create-modal-title" className="input" value={title} onChange={(event) => { setTitle(event.target.value); setUserEditedTitle(true); }} />
</section> </section>
@@ -567,7 +567,7 @@ export function PrCreateModal({
{userEditedBody && <button type="button" className="btn btn-sm" onClick={() => { setBody(aiBody); setUserEditedBody(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>} {userEditedBody && <button type="button" className="btn btn-sm" onClick={() => { setBody(aiBody); setUserEditedBody(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>}
</div> </div>
</div> </div>
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Generating AI body…</div> : null} {metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.generatingBody", "Generating AI body…")}</div> : null}
<textarea id="pr-create-modal-body" className="input pr-create-modal__body-input" value={body} onChange={(event) => { setBody(event.target.value); setUserEditedBody(true); }} rows={8} /> <textarea id="pr-create-modal-body" className="input pr-create-modal__body-input" value={body} onChange={(event) => { setBody(event.target.value); setUserEditedBody(true); }} rows={8} />
{templateUsed && <p className="pr-create-template-hint">{t("pr.usingTemplate", "Using <code>.github/pull_request_template.md</code>")}</p>} {templateUsed && <p className="pr-create-template-hint">{t("pr.usingTemplate", "Using <code>.github/pull_request_template.md</code>")}</p>}
</section> </section>
@@ -575,7 +575,7 @@ export function PrCreateModal({
<section className="pr-create-modal__section pr-create-modal__grid-two"> <section className="pr-create-modal__section pr-create-modal__grid-two">
<div> <div>
<label className="pr-create-modal__label" htmlFor="pr-create-modal-base">{t("pr.baseBranch", "Base branch")}</label> <label className="pr-create-modal__label" htmlFor="pr-create-modal-base">{t("pr.baseBranch", "Base branch")}</label>
{optionsLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Loading PR options…</div> : null} {optionsLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.loadingOptions", "Loading PR options…")}</div> : null}
{optionsError ? <div className="form-error pr-error" role="alert"><p>{optionsError}</p></div> : null} {optionsError ? <div className="form-error pr-error" role="alert"><p>{optionsError}</p></div> : null}
<select id="pr-create-modal-base" className="select" value={baseBranch} onChange={(event) => void handleBaseChange(event.target.value)} disabled={optionsLoading || Boolean(optionsError) || (options?.baseBranches?.length ?? 0) === 0}> <select id="pr-create-modal-base" className="select" value={baseBranch} onChange={(event) => void handleBaseChange(event.target.value)} disabled={optionsLoading || Boolean(optionsError) || (options?.baseBranches?.length ?? 0) === 0}>
{(options?.baseBranches ?? []).map((branch) => <option key={branch} value={branch}>{branch}</option>)} {(options?.baseBranches ?? []).map((branch) => <option key={branch} value={branch}>{branch}</option>)}
@@ -588,7 +588,7 @@ export function PrCreateModal({
</section> </section>
<OptionChips <OptionChips
label="Reviewers" label={t("pr.reviewers", "Reviewers")}
options={options?.reviewers ?? []} options={options?.reviewers ?? []}
selected={reviewers} selected={reviewers}
onChange={setReviewers} onChange={setReviewers}
@@ -596,7 +596,7 @@ export function PrCreateModal({
getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`} getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`}
/> />
<OptionChips <OptionChips
label="Assignees" label={t("pr.assignees", "Assignees")}
options={options?.assignees ?? []} options={options?.assignees ?? []}
selected={assignees} selected={assignees}
onChange={setAssignees} onChange={setAssignees}
@@ -604,7 +604,7 @@ export function PrCreateModal({
getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`} getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`}
/> />
<OptionChips <OptionChips
label="Labels" label={t("pr.labels", "Labels")}
options={options?.labels ?? []} options={options?.labels ?? []}
selected={labels} selected={labels}
onChange={setLabels} onChange={setLabels}
@@ -617,7 +617,7 @@ export function PrCreateModal({
<summary>{t("pr.previewTitle", "Diff & commit preview")}</summary> <summary>{t("pr.previewTitle", "Diff & commit preview")}</summary>
<div className="pr-create-modal__preview"> <div className="pr-create-modal__preview">
<h4>{t("pr.commitsLabel", "Commits")}</h4> <h4>{t("pr.commitsLabel", "Commits")}</h4>
{!preflightLoading && !preflightError && (preflight?.commits?.length ?? 0) === 0 ? <p className="pr-create-template-hint">No commits found.</p> : null} {!preflightLoading && !preflightError && (preflight?.commits?.length ?? 0) === 0 ? <p className="pr-create-template-hint">{t("pr.noCommits", "No commits found.")}</p> : null}
{(preflight?.commits ?? []).map((commit) => ( {(preflight?.commits ?? []).map((commit) => (
<div className="pr-create-modal__commit-row" key={commit.sha}> <div className="pr-create-modal__commit-row" key={commit.sha}>
<code>{commit.sha.slice(0, 7)}</code> <code>{commit.sha.slice(0, 7)}</code>
@@ -626,7 +626,7 @@ export function PrCreateModal({
</div> </div>
))} ))}
<h4>{t("pr.changedFilesLabel", "Changed files")}</h4> <h4>{t("pr.changedFilesLabel", "Changed files")}</h4>
{!preflightLoading && !preflightError && (preflight?.changedFiles?.length ?? 0) === 0 ? <p className="pr-create-template-hint">No changed files detected.</p> : null} {!preflightLoading && !preflightError && (preflight?.changedFiles?.length ?? 0) === 0 ? <p className="pr-create-template-hint">{t("pr.noChangedFiles", "No changed files detected.")}</p> : null}
{(preflight?.changedFiles ?? []).map((file) => ( {(preflight?.changedFiles ?? []).map((file) => (
<div className="pr-create-modal__file-row" key={file.path}> <div className="pr-create-modal__file-row" key={file.path}>
<span>{file.path}</span> <span>{file.path}</span>
@@ -641,7 +641,7 @@ export function PrCreateModal({
<div className="form-error pr-error" role="alert"> <div className="form-error pr-error" role="alert">
<p>{pushBranchError}</p> <p>{pushBranchError}</p>
<div className="pr-error__actions"> <div className="pr-error__actions">
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setPushBranchError(null)} aria-label="Dismiss push branch error">×</button> <button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setPushBranchError(null)} aria-label={t("pr.dismissPushBranchError", "Dismiss push branch error")}>×</button>
</div> </div>
</div> </div>
) : null} ) : null}
@@ -650,7 +650,7 @@ export function PrCreateModal({
<div className="form-error pr-error" role="alert"> <div className="form-error pr-error" role="alert">
<p>{resolveConflictError}</p> <p>{resolveConflictError}</p>
<div className="pr-error__actions"> <div className="pr-error__actions">
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setResolveConflictError(null)} aria-label="Dismiss conflict resolution error">×</button> <button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setResolveConflictError(null)} aria-label={t("pr.dismissConflictResolutionError", "Dismiss conflict resolution error")}>×</button>
</div> </div>
</div> </div>
) : null} ) : null}
@@ -660,8 +660,8 @@ export function PrCreateModal({
<p>{submitError}</p> <p>{submitError}</p>
{lastGhError?.hint ? <p className="pr-error__hint">{lastGhError.hint}</p> : null} {lastGhError?.hint ? <p className="pr-error__hint">{lastGhError.hint}</p> : null}
<div className="pr-error__actions"> <div className="pr-error__actions">
{lastGhError?.action?.kind === "shell" ? <p>Action: run <code>{lastGhError.action.command}</code></p> : null} {lastGhError?.action?.kind === "shell" ? <p>{t("pr.error.actionRun", "Action: run")} <code>{lastGhError.action.command}</code></p> : null}
{lastGhError?.action?.kind === "open" ? <p>Action: open <a href={lastGhError.action.url} target="_blank" rel="noreferrer">docs</a></p> : null} {lastGhError?.action?.kind === "open" ? <p>{t("pr.error.actionOpen", "Action: open")} <a href={lastGhError.action.url} target="_blank" rel="noreferrer">{t("pr.error.docs", "docs")}</a></p> : null}
{lastGhError?.retryable ? <button type="button" className="btn btn-sm pr-error__retry" onClick={() => void submit()}>{t("actions.retry", "Retry")}</button> : null} {lastGhError?.retryable ? <button type="button" className="btn btn-sm pr-error__retry" onClick={() => void submit()}>{t("actions.retry", "Retry")}</button> : null}
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => { setLastGhError(null); setSubmitError(null); }} aria-label={t("pr.dismissError", "Dismiss PR error")}>×</button> <button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => { setLastGhError(null); setSubmitError(null); }} aria-label={t("pr.dismissError", "Dismiss PR error")}>×</button>
</div> </div>

View File

@@ -238,7 +238,7 @@ function PrCard({
<div>{lastGhError.message}</div> <div>{lastGhError.message}</div>
{lastGhError.hint ? <div className="pr-error__hint">{lastGhError.hint}</div> : null} {lastGhError.hint ? <div className="pr-error__hint">{lastGhError.hint}</div> : null}
<div className="pr-error__actions"> <div className="pr-error__actions">
{lastGhError.action?.kind === "shell" ? <div>Action: run <code>{lastGhError.action.command}</code></div> : null} {lastGhError.action?.kind === "shell" ? <div>{t("pr.error.actionRun", "Action: run")} <code>{lastGhError.action.command}</code></div> : null}
{lastGhError.retryable ? <button className="btn btn-sm pr-error__retry" onClick={() => void handleRefresh()}>{t("git.retryButton", "Retry")}</button> : null} {lastGhError.retryable ? <button className="btn btn-sm pr-error__retry" onClick={() => void handleRefresh()}>{t("git.retryButton", "Retry")}</button> : null}
<button className="btn btn-sm pr-error__dismiss" onClick={() => setLastGhError(null)} aria-label={t("git.dismissPrError", "Dismiss PR error")}>×</button> <button className="btn btn-sm pr-error__dismiss" onClick={() => setLastGhError(null)} aria-label={t("git.dismissPrError", "Dismiss PR error")}>×</button>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { import {
GitPullRequest, GitPullRequest,
GitMerge, GitMerge,
@@ -101,6 +102,7 @@ function ChecksIcon({ rollup }: { rollup: string }) {
} }
export function PullRequestView(props: PullRequestViewProps) { export function PullRequestView(props: PullRequestViewProps) {
const { t } = useTranslation("app");
const { detail: detailProp, pullRequestId, projectId, onAction, loadPullRequest } = props; const { detail: detailProp, pullRequestId, projectId, onAction, loadPullRequest } = props;
const [detail, setDetail] = useState<PrDetail | null>(detailProp ?? null); const [detail, setDetail] = useState<PrDetail | null>(detailProp ?? null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -166,7 +168,7 @@ export function PullRequestView(props: PullRequestViewProps) {
if (!detail) { if (!detail) {
return ( return (
<div className="pr-view pr-view--loading" data-testid="pr-view-loading"> <div className="pr-view pr-view--loading" data-testid="pr-view-loading">
Loading PR… {t("pr.view.loading", "Loading PR…")}
</div> </div>
); );
} }
@@ -179,7 +181,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-view" data-testid="pr-view" data-state="creating"> <div className="pr-view" data-testid="pr-view" data-state="creating">
<PrIdentityHeader detail={detail} /> <PrIdentityHeader detail={detail} />
<div className="pr-placeholder" data-testid="pr-creating"> <div className="pr-placeholder" data-testid="pr-creating">
<Clock size={16} /> Creating PR… <Clock size={16} /> {t("pr.view.creating", "Creating PR…")}
</div> </div>
</div> </div>
); );
@@ -192,7 +194,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<PrIdentityHeader detail={detail} /> <PrIdentityHeader detail={detail} />
<div className="pr-error-reason" data-testid="pr-failed"> <div className="pr-error-reason" data-testid="pr-failed">
<AlertTriangle size={16} className="pr-icon-failure" /> <AlertTriangle size={16} className="pr-icon-failure" />
<span>{detail.failureReason ?? "PR creation failed"}</span> <span>{detail.failureReason ?? t("pr.view.creationFailed", "PR creation failed")}</span>
</div> </div>
<div className="pr-action-bar"> <div className="pr-action-bar">
<button <button
@@ -202,7 +204,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "retry-create"} disabled={busy === "retry-create"}
onClick={() => void runAction("retry-create")} onClick={() => void runAction("retry-create")}
> >
<RotateCcw size={14} /> Retry PR creation <RotateCcw size={14} /> {t("pr.view.retryCreation", "Retry PR creation")}
</button> </button>
</div> </div>
{error && <div className="pr-inline-error">{error}</div>} {error && <div className="pr-inline-error">{error}</div>}
@@ -216,7 +218,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-view" data-testid="pr-view" data-state="unverified"> <div className="pr-view" data-testid="pr-view" data-state="unverified">
<PrIdentityHeader detail={detail} /> <PrIdentityHeader detail={detail} />
<div className="pr-notice pr-notice--unverified" data-testid="pr-unverified"> <div className="pr-notice pr-notice--unverified" data-testid="pr-unverified">
<Clock size={16} /> Verifying with GitHub… <Clock size={16} /> {t("pr.view.verifyingGithub", "Verifying with GitHub…")}
</div> </div>
<div className="pr-action-bar"> <div className="pr-action-bar">
<button <button
@@ -224,9 +226,9 @@ export function PullRequestView(props: PullRequestViewProps) {
className="pr-action" className="pr-action"
data-testid="pr-merge" data-testid="pr-merge"
disabled disabled
title="Merge is disabled until GitHub verifies this PR" title={t("pr.view.mergeDisabledUntilVerified", "Merge is disabled until GitHub verifies this PR")}
> >
<GitMerge size={14} /> Merge <GitMerge size={14} /> {t("pr.view.merge", "Merge")}
</button> </button>
</div> </div>
{/* checks/threads hidden while unverified */} {/* checks/threads hidden while unverified */}
@@ -243,8 +245,7 @@ export function PullRequestView(props: PullRequestViewProps) {
{/* responding banner */} {/* responding banner */}
{state === "responding" && ( {state === "responding" && (
<div className="pr-banner pr-banner--responding" data-testid="pr-responding"> <div className="pr-banner pr-banner--responding" data-testid="pr-responding">
<MessageSquare size={16} /> Response run in progress — {summary.pendingThreads} threads <MessageSquare size={16} /> {t("pr.view.responsePending", "Response run in progress — {{count}} threads pending", { count: summary.pendingThreads })}
pending
</div> </div>
)} )}
@@ -257,17 +258,17 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={state === "responding" || busy === "approve"} disabled={state === "responding" || busy === "approve"}
onClick={() => void runAction("approve")} onClick={() => void runAction("approve")}
> >
<ThumbsUp size={14} /> Approve <ThumbsUp size={14} /> {t("pr.view.approve", "Approve")}
</button> </button>
<button <button
type="button" type="button"
className="pr-action pr-action--retry" className="pr-action pr-action--retry"
data-testid="pr-retry" data-testid="pr-retry"
disabled={state === "responding" || busy === "retry"} disabled={state === "responding" || busy === "retry"}
title={state === "responding" ? "A response run is already in progress" : undefined} title={state === "responding" ? t("pr.view.responseAlreadyInProgress", "A response run is already in progress") : undefined}
onClick={() => void runAction("retry")} onClick={() => void runAction("retry")}
> >
<RotateCcw size={14} /> Request retry <RotateCcw size={14} /> {t("pr.view.requestRetry", "Request retry")}
</button> </button>
{!confirmingMerge ? ( {!confirmingMerge ? (
<button <button
@@ -275,10 +276,10 @@ export function PullRequestView(props: PullRequestViewProps) {
className="pr-action pr-action--merge" className="pr-action pr-action--merge"
data-testid="pr-merge" data-testid="pr-merge"
disabled={conflicting || state === "responding" || busy === "merge"} disabled={conflicting || state === "responding" || busy === "merge"}
title={conflicting ? "Resolve conflicts on GitHub before merging" : undefined} title={conflicting ? t("pr.view.resolveConflictsBeforeMerge", "Resolve conflicts on GitHub before merging") : undefined}
onClick={() => setConfirmingMerge(true)} onClick={() => setConfirmingMerge(true)}
> >
<GitMerge size={14} /> Merge <GitMerge size={14} /> {t("pr.view.merge", "Merge")}
</button> </button>
) : ( ) : (
<button <button
@@ -288,7 +289,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "merge"} disabled={busy === "merge"}
onClick={() => void runAction("merge")} onClick={() => void runAction("merge")}
> >
<GitMerge size={14} /> Confirm merge <GitMerge size={14} /> {t("pr.view.confirmMerge", "Confirm merge")}
</button> </button>
)} )}
<button <button
@@ -298,7 +299,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "close"} disabled={busy === "close"}
onClick={() => void runAction("close")} onClick={() => void runAction("close")}
> >
<XCircle size={14} /> Close <XCircle size={14} /> {t("pr.view.close", "Close")}
</button> </button>
<label className="pr-automerge-toggle" data-testid="pr-automerge"> <label className="pr-automerge-toggle" data-testid="pr-automerge">
@@ -308,7 +309,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "automerge"} disabled={busy === "automerge"}
onChange={(e) => void runAction("automerge", { enabled: e.target.checked })} onChange={(e) => void runAction("automerge", { enabled: e.target.checked })}
/> />
<span>Auto-merge</span> <span>{t("pr.view.autoMerge", "Auto-merge")}</span>
<span className="pr-automerge-gate" data-testid="pr-automerge-gate"> <span className="pr-automerge-gate" data-testid="pr-automerge-gate">
{summary.autoMergeReason} {summary.autoMergeReason}
</span> </span>
@@ -324,17 +325,17 @@ export function PullRequestView(props: PullRequestViewProps) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Resolve conflicts on GitHub <ExternalLink size={12} /> {t("pr.view.resolveConflictsOnGithub", "Resolve conflicts on GitHub")} <ExternalLink size={12} />
</a> </a>
)} )}
{/* ── merge-readiness summary ─────────────────────────────────────── */} {/* ── merge-readiness summary ─────────────────────────────────────── */}
<div className="pr-summary" data-testid="pr-summary"> <div className="pr-summary" data-testid="pr-summary">
<span className="pr-summary-item" data-testid="pr-summary-mergeable"> <span className="pr-summary-item" data-testid="pr-summary-mergeable">
Mergeable: {summary.mergeable} {t("pr.view.mergeableLabel", "Mergeable:")} {summary.mergeable}
</span> </span>
<span className="pr-summary-item" data-testid="pr-summary-review"> <span className="pr-summary-item" data-testid="pr-summary-review">
Review: {summary.reviewDecision ?? "none"} {t("pr.view.reviewLabel", "Review:")} {summary.reviewDecision ?? t("pr.view.none", "none")}
</span> </span>
<span className="pr-summary-item" data-testid="pr-summary-checks"> <span className="pr-summary-item" data-testid="pr-summary-checks">
<ChecksIcon rollup={summary.checksRollup} /> {summary.checksRollup} <ChecksIcon rollup={summary.checksRollup} /> {summary.checksRollup}
@@ -344,7 +345,7 @@ export function PullRequestView(props: PullRequestViewProps) {
{/* ── threads (agent replies nested) ───────────────────────────────── */} {/* ── threads (agent replies nested) ───────────────────────────────── */}
<div className="pr-threads" data-testid="pr-threads"> <div className="pr-threads" data-testid="pr-threads">
{detail.threads.length === 0 ? ( {detail.threads.length === 0 ? (
<div className="pr-threads-empty">No review threads.</div> <div className="pr-threads-empty">{t("pr.view.noReviewThreads", "No review threads.")}</div>
) : ( ) : (
detail.threads.map((thread) => ( detail.threads.map((thread) => (
<div <div
@@ -358,24 +359,24 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-thread-head"> <div className="pr-thread-head">
{thread.outcome === "pending" && ( {thread.outcome === "pending" && (
<span className="pr-thread-pending"> <span className="pr-thread-pending">
<Clock size={12} /> pending <Clock size={12} /> {t("pr.view.threadPending", "pending")}
</span> </span>
)} )}
{thread.outcome === "disagreed" && ( {thread.outcome === "disagreed" && (
<span className="pr-thread-disagreed"> <span className="pr-thread-disagreed">
<AlertTriangle size={12} /> agent disagreed <AlertTriangle size={12} /> {t("pr.view.agentDisagreed", "agent disagreed")}
</span> </span>
)} )}
{thread.outcome === "fixed" && ( {thread.outcome === "fixed" && (
<span className="pr-thread-fixed"> <span className="pr-thread-fixed">
<CheckCircle size={12} /> fixed <CheckCircle size={12} /> {t("pr.view.threadFixed", "fixed")}
</span> </span>
)} )}
<span className="pr-thread-id">{thread.threadId}</span> <span className="pr-thread-id">{thread.threadId}</span>
</div> </div>
{thread.fixCommitSha && ( {thread.fixCommitSha && (
<div className="pr-thread-reply" data-testid="pr-thread-reply"> <div className="pr-thread-reply" data-testid="pr-thread-reply">
Agent reply — fix {thread.fixCommitSha.slice(0, 8)} {t("pr.view.agentReplyFix", "Agent reply — fix {{sha}}", { sha: thread.fixCommitSha.slice(0, 8) })}
</div> </div>
)} )}
</div> </div>

View File

@@ -1850,10 +1850,10 @@ export function SettingsModal({
const info = await fetchBackups(projectId); const info = await fetchBackups(projectId);
setBackupInfo(info); setBackupInfo(info);
} else { } else {
addToast(result.error || "Failed to create backup", "error"); addToast(result.error || t("settings.backups.createFailed", "Failed to create backup"), "error");
} }
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to create backup", "error"); addToast(getErrorMessage(err) || t("settings.backups.createFailed", "Failed to create backup"), "error");
} finally { } finally {
setBackupLoading(false); setBackupLoading(false);
} }
@@ -1879,10 +1879,14 @@ export function SettingsModal({
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
const scopeLabel = scope === "global" ? "global" : scope === "project" ? "project" : "all"; const scopeLabel = scope === "global"
addToast(`Settings exported (${scopeLabel} scope)`, "success"); ? t("settings.importExport.scopeLabel.global", "global")
: scope === "project"
? t("settings.importExport.scopeLabel.project", "project")
: t("settings.importExport.scopeLabel.all", "all");
addToast(t("settings.importExport.exported", "Settings exported ({{scope}} scope)", { scope: scopeLabel }), "success");
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to export settings", "error"); addToast(getErrorMessage(err) || t("settings.importExport.exportFailed", "Failed to export settings"), "error");
} }
}, [addToast, activeSectionScope, projectId]); }, [addToast, activeSectionScope, projectId]);
@@ -1899,7 +1903,7 @@ export function SettingsModal({
setImportPreview(data); setImportPreview(data);
setImportDialogOpen(true); setImportDialogOpen(true);
} catch (err) { } catch (err) {
addToast(`Invalid JSON file: ${getErrorMessage(err)}`, "error"); addToast(t("settings.importExport.invalidJson", "Invalid JSON file: {{error}}", { error: getErrorMessage(err) }), "error");
setImportFile(null); setImportFile(null);
} finally { } finally {
setImportLoading(false); setImportLoading(false);
@@ -1914,10 +1918,10 @@ export function SettingsModal({
const result = await importSettings(importPreview, { scope: importScope, merge: importMerge }, projectId); const result = await importSettings(importPreview, { scope: importScope, merge: importMerge }, projectId);
if (result.success) { if (result.success) {
const parts: string[] = []; const parts: string[] = [];
if (result.globalCount > 0) parts.push(`${result.globalCount} global`); if (result.globalCount > 0) parts.push(t("settings.importExport.counts.global", "{{count}} global", { count: result.globalCount }));
if (result.projectCount > 0) parts.push(`${result.projectCount} project`); if (result.projectCount > 0) parts.push(t("settings.importExport.counts.project", "{{count}} project", { count: result.projectCount }));
if (result.workflowSettingsCount > 0) parts.push(`${result.workflowSettingsCount} workflow setting value(s)`); if (result.workflowSettingsCount > 0) parts.push(t("settings.importExport.counts.workflowSettings", "{{count}} workflow setting value", { count: result.workflowSettingsCount }));
addToast(`Imported ${parts.join(", ")} setting(s)`, "success"); addToast(t("settings.importExport.imported", "Imported {{counts}} setting(s)", { counts: parts.join(", ") }), "success");
setImportDialogOpen(false); setImportDialogOpen(false);
setImportPreview(null); setImportPreview(null);
setImportFile(null); setImportFile(null);
@@ -1925,10 +1929,10 @@ export function SettingsModal({
const refreshed = await fetchSettings(projectId); const refreshed = await fetchSettings(projectId);
setForm(refreshed); setForm(refreshed);
} else { } else {
addToast(result.error || "Import failed", "error"); addToast(result.error || t("settings.importExport.importFailed", "Import failed"), "error");
} }
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to import settings", "error"); addToast(getErrorMessage(err) || t("settings.importExport.importFailedDetailed", "Failed to import settings"), "error");
} finally { } finally {
setImportLoading(false); setImportLoading(false);
} }
@@ -2373,11 +2377,11 @@ export function SettingsModal({
const result = await installQmd(projectId); const result = await installQmd(projectId);
await refreshMemoryBackend(); await refreshMemoryBackend();
addToast( addToast(
result.qmdAvailable ? "qmd installed successfully" : "qmd install finished, but qmd is still unavailable", result.qmdAvailable ? t("settings.memory.qmdInstalled", "qmd installed successfully") : t("settings.memory.qmdInstallUnavailable", "qmd install finished, but qmd is still unavailable"),
result.qmdAvailable ? "success" : "warning", result.qmdAvailable ? "success" : "warning",
); );
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to install qmd", "error"); addToast(getErrorMessage(err) || t("settings.memory.qmdInstallFailed", "Failed to install qmd"), "error");
} finally { } finally {
setQmdInstallLoading(false); setQmdInstallLoading(false);
} }
@@ -2471,14 +2475,14 @@ export function SettingsModal({
try { try {
const result = await installCloudflared(projectId); const result = await installCloudflared(projectId);
if (!result.success) { if (!result.success) {
setCloudflaredInstallError(result.error ?? "Installation failed"); setCloudflaredInstallError(result.error ?? t("settings.remote.installationFailed", "Installation failed"));
return; return;
} }
const status = await fetchRemoteStatus(projectId); const status = await fetchRemoteStatus(projectId);
setRemoteStatus(status); setRemoteStatus(status);
addToast(t("settings.remote.cloudflaredInstalled", "cloudflared installed successfully"), "success"); addToast(t("settings.remote.cloudflaredInstalled", "cloudflared installed successfully"), "success");
} catch (err) { } catch (err) {
setCloudflaredInstallError(err instanceof Error ? err.message : "Installation failed"); setCloudflaredInstallError(err instanceof Error ? err.message : t("settings.remote.installationFailed", "Installation failed"));
} finally { } finally {
setCloudflaredInstalling(false); setCloudflaredInstalling(false);
} }
@@ -2871,15 +2875,15 @@ export function SettingsModal({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="settings-github-star-btn" className="settings-github-star-btn"
aria-label="Star Fusion on GitHub" aria-label={t("settings.header.starFusion", "Star Fusion on GitHub")}
title="Star Fusion on GitHub" title={t("settings.header.starFusion", "Star Fusion on GitHub")}
onClick={markStarClicked} onClick={markStarClicked}
data-clicked={starClicked ? "true" : "false"} data-clicked={starClicked ? "true" : "false"}
> >
<span className="settings-github-star-btn__action"> <span className="settings-github-star-btn__action">
<ProviderIcon provider="github" size="sm" /> <ProviderIcon provider="github" size="sm" />
<Star size={11} aria-hidden="true" /> <Star size={11} aria-hidden="true" />
Star {t("settings.header.star", "Star")}
</span> </span>
{gitHubStarCount !== null && ( {gitHubStarCount !== null && (
<span className="settings-github-star-btn__count" aria-label={`${gitHubStarCount.toLocaleString()} stars`}> <span className="settings-github-star-btn__count" aria-label={`${gitHubStarCount.toLocaleString()} stars`}>
@@ -2894,14 +2898,14 @@ export function SettingsModal({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="btn btn-sm settings-header-discord-btn" className="btn btn-sm settings-header-discord-btn"
aria-label="Join our Discord" aria-label={t("settings.header.joinDiscord", "Join our Discord")}
title="Join our Discord" title={t("settings.header.joinDiscord", "Join our Discord")}
> >
<DiscordIcon size={13} /> <DiscordIcon size={13} />
{t("settings.header.discord", "Discord")} {t("settings.header.discord", "Discord")}
</a> </a>
</div> </div>
<button className="modal-close" onClick={onClose} aria-label="Close"> <button className="modal-close" onClick={onClose} aria-label={t("actions.close", "Close")}>
&times; &times;
</button> </button>
</div> </div>
@@ -2971,8 +2975,8 @@ export function SettingsModal({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="btn btn-sm settings-footer-help-btn" className="btn btn-sm settings-footer-help-btn"
aria-label="Help and discussions" aria-label={t("settings.footer.helpDiscussions", "Help and discussions")}
title="Help and discussions" title={t("settings.footer.helpDiscussions", "Help and discussions")}
> >
<HelpCircle size={13} aria-hidden="true" /> <HelpCircle size={13} aria-hidden="true" />
{t("settings.footer.help", "Help")} {t("settings.footer.help", "Help")}
@@ -2986,8 +2990,8 @@ export function SettingsModal({
void handleCheckForUpdates(); void handleCheckForUpdates();
}} }}
disabled={updateCheckLoading} disabled={updateCheckLoading}
aria-label="Check for updates" aria-label={t("settings.footer.checkUpdates", "Check for updates")}
title="Check for updates" title={t("settings.footer.checkUpdates", "Check for updates")}
> >
<span className="settings-modal-version">{t("settings.footer.version", "Version {{version}}", { version: appVersion })}</span> <span className="settings-modal-version">{t("settings.footer.version", "Version {{version}}", { version: appVersion })}</span>
<RefreshCw size={12} className={updateCheckLoading ? "spinning" : undefined} /> <RefreshCw size={12} className={updateCheckLoading ? "spinning" : undefined} />
@@ -3030,7 +3034,7 @@ export function SettingsModal({
className="btn btn-sm" className="btn btn-sm"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={importLoading} disabled={importLoading}
title="Import settings from JSON file" title={t("settings.importExport.importTitleAttr", "Import settings from JSON file")}
> >
{importLoading ? t("settings.importExport.loadingFile", "Loading…") : t("settings.importExport.importBtn", "Import")} {importLoading ? t("settings.importExport.loadingFile", "Loading…") : t("settings.importExport.importBtn", "Import")}
</button> </button>
@@ -3052,18 +3056,18 @@ export function SettingsModal({
onClick={handleOverlapPathPickerOverlayClick} onClick={handleOverlapPathPickerOverlayClick}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Browse workspace path" aria-label={t("settings.scheduling.browseWorkspacePath", "Browse workspace path")}
> >
<div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}> <div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h3>{t("settings.scheduling.selectIgnoredOverlapPath", "Select ignored overlap path")}</h3> <h3>{t("settings.scheduling.selectIgnoredOverlapPath", "Select ignored overlap path")}</h3>
<button className="modal-close" onClick={closeOverlapPathPicker} aria-label="Close"> <button className="modal-close" onClick={closeOverlapPathPicker} aria-label={t("actions.close", "Close")}>
&times; &times;
</button> </button>
</div> </div>
<div className="modal-body settings-overlap-path-picker-body"> <div className="modal-body settings-overlap-path-picker-body">
<p className="settings-overlap-path-picker-note"> <p className="settings-overlap-path-picker-note">
Choose a file to ignore directly, or navigate into a folder and select the current directory. {t("settings.scheduling.overlapPickerNote", "Choose a file to ignore directly, or navigate into a folder and select the current directory.")}
</p> </p>
<FileBrowser <FileBrowser
entries={overlapPathPickerEntries} entries={overlapPathPickerEntries}
@@ -3080,7 +3084,7 @@ export function SettingsModal({
<div className="modal-actions"> <div className="modal-actions">
<div className="modal-actions-left"> <div className="modal-actions-left">
<small> <small>
Current directory: <code>{overlapPathPickerCurrentPath === "." ? "(project root)" : overlapPathPickerCurrentPath}</code> {t("settings.fileBrowser.currentDirectory", "Current directory:")} <code>{overlapPathPickerCurrentPath === "." ? t("settings.fileBrowser.projectRoot", "(project root)") : overlapPathPickerCurrentPath}</code>
</small> </small>
</div> </div>
<div className="modal-actions-right"> <div className="modal-actions-right">
@@ -3106,18 +3110,18 @@ export function SettingsModal({
onClick={handleWorktreesDirPickerOverlayClick} onClick={handleWorktreesDirPickerOverlayClick}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Browse worktrees directory" aria-label={t("settings.worktrees.browseWorktreesDirectory", "Browse worktrees directory")}
> >
<div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}> <div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h3>{t("settings.worktrees.selectWorktreesDir", "Select worktrees directory")}</h3> <h3>{t("settings.worktrees.selectWorktreesDir", "Select worktrees directory")}</h3>
<button className="modal-close" onClick={closeWorktreesDirPicker} aria-label="Close"> <button className="modal-close" onClick={closeWorktreesDirPicker} aria-label={t("actions.close", "Close")}>
&times; &times;
</button> </button>
</div> </div>
<div className="modal-body settings-overlap-path-picker-body"> <div className="modal-body settings-overlap-path-picker-body">
<p className="settings-overlap-path-picker-note"> <p className="settings-overlap-path-picker-note">
Navigate to the folder where Fusion should create task worktrees, then select the current directory. {t("settings.worktrees.worktreesPickerNote", "Navigate to the folder where Fusion should create task worktrees, then select the current directory.")}
</p> </p>
<FileBrowser <FileBrowser
entries={worktreesDirPickerEntries} entries={worktreesDirPickerEntries}
@@ -3134,7 +3138,7 @@ export function SettingsModal({
<div className="modal-actions"> <div className="modal-actions">
<div className="modal-actions-left"> <div className="modal-actions-left">
<small> <small>
Current directory: <code>{worktreesDirPickerCurrentPath === "." ? "(project root)" : worktreesDirPickerCurrentPath}</code> {t("settings.fileBrowser.currentDirectory", "Current directory:")} <code>{worktreesDirPickerCurrentPath === "." ? t("settings.fileBrowser.projectRoot", "(project root)") : worktreesDirPickerCurrentPath}</code>
</small> </small>
</div> </div>
<div className="modal-actions-right"> <div className="modal-actions-right">
@@ -3156,7 +3160,7 @@ export function SettingsModal({
<div className="modal modal-md"> <div className="modal modal-md">
<div className="modal-header"> <div className="modal-header">
<h3>{t("settings.importExport.importTitle", "Import Settings")}</h3> <h3>{t("settings.importExport.importTitle", "Import Settings")}</h3>
<button className="modal-close" onClick={() => setImportDialogOpen(false)} aria-label="Close"> <button className="modal-close" onClick={() => setImportDialogOpen(false)} aria-label={t("actions.close", "Close")}>
&times; &times;
</button> </button>
</div> </div>
@@ -3165,7 +3169,7 @@ export function SettingsModal({
{importPreview.global && Object.keys(importPreview.global).length > 0 && ( {importPreview.global && Object.keys(importPreview.global).length > 0 && (
<div className="form-group"> <div className="form-group">
<strong>Global Settings:</strong> <strong>{t("settings.importExport.globalSettings", "Global Settings:")}</strong>
<ul className="import-preview-list"> <ul className="import-preview-list">
{Object.entries(importPreview.global) {Object.entries(importPreview.global)
.filter(([, v]) => v !== undefined) .filter(([, v]) => v !== undefined)
@@ -3178,7 +3182,7 @@ export function SettingsModal({
{importPreview.project && Object.keys(importPreview.project).length > 0 && ( {importPreview.project && Object.keys(importPreview.project).length > 0 && (
<div className="form-group"> <div className="form-group">
<strong>Project Settings:</strong> <strong>{t("settings.importExport.projectSettings", "Project Settings:")}</strong>
<ul className="import-preview-list"> <ul className="import-preview-list">
{Object.entries(importPreview.project) {Object.entries(importPreview.project)
.filter(([, v]) => v !== undefined) .filter(([, v]) => v !== undefined)
@@ -3190,15 +3194,15 @@ export function SettingsModal({
)} )}
<div className="form-group"> <div className="form-group">
<label htmlFor="import-scope">Import Scope:</label> <label htmlFor="import-scope">{t("settings.importExport.importScope", "Import Scope:")}</label>
<select <select
id="import-scope" id="import-scope"
value={importScope} value={importScope}
onChange={(e) => setImportScope(e.target.value as 'global' | 'project' | 'both')} onChange={(e) => setImportScope(e.target.value as 'global' | 'project' | 'both')}
> >
<option value="both">Both global and project settings</option> <option value="both">{t("settings.importExport.scopeBoth", "Both global and project settings")}</option>
<option value="global">Global settings only</option> <option value="global">{t("settings.importExport.scopeGlobal", "Global settings only")}</option>
<option value="project">Project settings only</option> <option value="project">{t("settings.importExport.scopeProject", "Project settings only")}</option>
</select> </select>
</div> </div>
@@ -3210,9 +3214,9 @@ export function SettingsModal({
checked={importMerge} checked={importMerge}
onChange={(e) => setImportMerge(e.target.checked)} onChange={(e) => setImportMerge(e.target.checked)}
/> />
Merge with existing settings (recommended) {t("settings.importExport.mergeExisting", "Merge with existing settings (recommended)")}
</label> </label>
<small>If unchecked, existing settings will be replaced with imported values.</small> <small>{t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")}</small>
</div> </div>
</div> </div>
<div className="modal-actions"> <div className="modal-actions">

View File

@@ -152,14 +152,14 @@ export function SetupWizardModal({
{/* Header */} {/* Header */}
<div className="setup-wizard-header"> <div className="setup-wizard-header">
<div className="setup-wizard-heading"> <div className="setup-wizard-heading">
<div className="setup-wizard-brand" aria-label="Fusion"> <div className="setup-wizard-brand" aria-label={t("setup.brandName", "Fusion")}>
<svg <svg
className="setup-wizard-brand-logo" className="setup-wizard-brand-logo"
width={28} width={28}
height={28} height={28}
viewBox="0 0 128 128" viewBox="0 0 128 128"
fill="none" fill="none"
aria-label="Fusion logo" aria-label={t("setup.brandLogo", "Fusion logo")}
role="img" role="img"
> >
<circle <circle
@@ -174,12 +174,12 @@ export function SetupWizardModal({
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
<span className="setup-wizard-brand-name">Fusion</span> <span className="setup-wizard-brand-name">{t("setup.brandName", "Fusion")}</span>
</div> </div>
<h2 id="wizard-title" className="setup-wizard-title"> <h2 id="wizard-title" className="setup-wizard-title">
{state.step === "auth" && t("setup.setAuthToken", "Set Auth Token")} {state.step === "auth" && t("setup.setAuthToken", "Set Auth Token")}
{state.step === "manual" && t("setup.welcomeToFusion", "Welcome to Fusion")} {state.step === "manual" && t("setup.welcomeToFusion", "Welcome to Fusion")}
{state.step === "complete" && t("setup.setupComplete", "Setup Complete!")} {state.step === "complete" && t("setup.setupCompleteTitle", "Setup Complete!")}
</h2> </h2>
</div> </div>
{state.step !== "complete" && ( {state.step !== "complete" && (

View File

@@ -3,6 +3,8 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { ChevronDown, Loader2, Maximize2, Minimize2, Send } from "lucide-react"; import { ChevronDown, Loader2, Maximize2, Minimize2, Send } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { addSteeringComment, refineTask } from "../api"; import { addSteeringComment, refineTask } from "../api";
import { useAgentLogs } from "../hooks/useAgentLogs"; import { useAgentLogs } from "../hooks/useAgentLogs";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
@@ -58,18 +60,18 @@ function isTranscriptNearBottom(container: HTMLElement): boolean {
return container.scrollHeight - (container.scrollTop + container.clientHeight) <= BOTTOM_FOLLOW_THRESHOLD; return container.scrollHeight - (container.scrollTop + container.clientHeight) <= BOTTOM_FOLLOW_THRESHOLD;
} }
function getRoleLabel(role: AgentLogRole): string { function getRoleLabel(role: AgentLogRole, t: TFunction<"app">): string {
switch (role) { switch (role) {
case "triage": case "triage":
return "Planner"; return t("taskChat.roles.planner", "Planner");
case "executor": case "executor":
return "Executor"; return t("taskChat.roles.executor", "Executor");
case "reviewer": case "reviewer":
return "Reviewer"; return t("taskChat.roles.reviewer", "Reviewer");
case "merger": case "merger":
return "Merger"; return t("taskChat.roles.merger", "Merger");
default: default:
return "Agent"; return t("taskChat.roles.agent", "Agent");
} }
} }
@@ -137,7 +139,7 @@ function mergeUserMessages(persistedComments: readonly SteeringComment[] | undef
return messages; return messages;
} }
function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: readonly UserChatMessage[]): TaskChatTranscriptItem[] { function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: readonly UserChatMessage[], t: TFunction<"app">): TaskChatTranscriptItem[] {
const orderedItems = [ const orderedItems = [
...entries.map((entry, index) => ({ kind: "agent" as const, entry, index, timestamp: getTimestampMs(entry.timestamp) })), ...entries.map((entry, index) => ({ kind: "agent" as const, entry, index, timestamp: getTimestampMs(entry.timestamp) })),
...userMessages.map((message, index) => ({ kind: "user" as const, message, index, timestamp: getTimestampMs(message.createdAt) })), ...userMessages.map((message, index) => ({ kind: "user" as const, message, index, timestamp: getTimestampMs(message.createdAt) })),
@@ -155,7 +157,7 @@ function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: r
previousItem.entries.push(item.entry); previousItem.entries.push(item.entry);
return items; return items;
} }
items.push({ kind: "agent", role, label: getRoleLabel(role), entries: [item.entry] }); items.push({ kind: "agent", role, label: getRoleLabel(role, t), entries: [item.entry] });
return items; return items;
}, []); }, []);
} }
@@ -178,25 +180,29 @@ function isToolLikeEntry(entry: AgentLogEntry): boolean {
return entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error"; return entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error";
} }
function formatEntryLabel(entry: AgentLogEntry): string { function formatEntryLabel(entry: AgentLogEntry, t: TFunction<"app">): string {
switch (entry.type) { switch (entry.type) {
case "tool": case "tool":
return "Tool call"; return t("taskChat.toolCall", "Tool call");
case "tool_result": case "tool_result":
return "Tool result"; return t("taskChat.toolResult", "Tool result");
case "tool_error": case "tool_error":
return "Tool error"; return t("taskChat.toolError", "Tool error");
case "thinking": case "thinking":
return "Thinking"; return t("taskChat.thinking", "Thinking");
default: default:
return "Message"; return t("taskChat.message", "Message");
} }
} }
function formatCompletionLabel(entry: AgentLogEntry, t: TFunction<"app">): string {
return entry.type === "tool_error" ? t("taskChat.error", "Error") : t("taskChat.result", "Result");
}
const TOOL_NAME_SUMMARY_LIMIT = 5; const TOOL_NAME_SUMMARY_LIMIT = 5;
function formatToolCallCount(count: number): string { function formatToolCallCount(count: number, t: TFunction<"app">): string {
return count === 1 ? "1 tool call" : `${count} tool calls`; return t("taskChat.toolCallCount", "{{count}} tool call", { count });
} }
function getToolInvocationEntries(entries: AgentLogEntry[]): AgentLogEntry[] { function getToolInvocationEntries(entries: AgentLogEntry[]): AgentLogEntry[] {
@@ -270,12 +276,14 @@ function TaskChatText({ entries }: { entries: AgentLogEntry[] }) {
} }
function TaskChatToolEntry({ entry }: { entry: AgentLogEntry }) { function TaskChatToolEntry({ entry }: { entry: AgentLogEntry }) {
const { t } = useTranslation("app");
return ( return (
<article <article
className={`task-chat-tool-entry task-chat-tool-entry--${entry.type.replace("_", "-")}`} className={`task-chat-tool-entry task-chat-tool-entry--${entry.type.replace("_", "-")}`}
data-testid={`task-chat-entry-${entry.type}`} data-testid={`task-chat-entry-${entry.type}`}
> >
<div className="task-chat-entry-kicker">{formatEntryLabel(entry)}</div> <div className="task-chat-entry-kicker">{formatEntryLabel(entry, t)}</div>
<div className="task-chat-entry-text">{entry.text}</div> <div className="task-chat-entry-text">{entry.text}</div>
{entry.detail ? <pre className="task-chat-tool-detail">{linkifyFilePaths(entry.detail)}</pre> : null} {entry.detail ? <pre className="task-chat-tool-detail">{linkifyFilePaths(entry.detail)}</pre> : null}
</article> </article>
@@ -310,25 +318,26 @@ function getToolGroupRows(entries: AgentLogEntry[]): TaskChatToolGroupRow[] {
} }
function TaskChatToolInvocation({ row }: { row: Extract<TaskChatToolGroupRow, { kind: "invocation" }> }) { function TaskChatToolInvocation({ row }: { row: Extract<TaskChatToolGroupRow, { kind: "invocation" }> }) {
const { t } = useTranslation("app");
const completion = row.completion; const completion = row.completion;
const completionLabel = completion ? formatEntryLabel(completion).replace("Tool ", "") : undefined; const completionLabel = completion ? formatCompletionLabel(completion, t) : undefined;
const className = `task-chat-tool-entry task-chat-tool-invocation${completion?.type === "tool_error" ? " task-chat-tool-entry--tool-error" : ""}`; const className = `task-chat-tool-entry task-chat-tool-invocation${completion?.type === "tool_error" ? " task-chat-tool-entry--tool-error" : ""}`;
return ( return (
<article className={className} data-testid="task-chat-tool-invocation"> <article className={className} data-testid="task-chat-tool-invocation">
<div className="task-chat-entry-kicker"> <div className="task-chat-entry-kicker">
{completionLabel ? `Tool call → ${completionLabel}` : "Tool call"} {completionLabel ? t("taskChat.toolCallTo", "Tool call → {{label}}", { label: completionLabel }) : t("taskChat.toolCall", "Tool call")}
</div> </div>
<div className="task-chat-entry-text">{row.call.text}</div> <div className="task-chat-entry-text">{row.call.text}</div>
{row.call.detail ? ( {row.call.detail ? (
<div className="task-chat-tool-detail-block"> <div className="task-chat-tool-detail-block">
<div className="task-chat-tool-detail-label">Arguments</div> <div className="task-chat-tool-detail-label">{t("taskChat.arguments", "Arguments")}</div>
<pre className="task-chat-tool-detail">{linkifyFilePaths(row.call.detail)}</pre> <pre className="task-chat-tool-detail">{linkifyFilePaths(row.call.detail)}</pre>
</div> </div>
) : null} ) : null}
{completion?.detail ? ( {completion?.detail ? (
<div className="task-chat-tool-detail-block"> <div className="task-chat-tool-detail-block">
<div className="task-chat-tool-detail-label">{completion.type === "tool_error" ? "Error" : "Result"}</div> <div className="task-chat-tool-detail-label">{completion.type === "tool_error" ? t("taskChat.error", "Error") : t("taskChat.result", "Result")}</div>
<pre className="task-chat-tool-detail">{linkifyFilePaths(completion.detail)}</pre> <pre className="task-chat-tool-detail">{linkifyFilePaths(completion.detail)}</pre>
</div> </div>
) : null} ) : null}
@@ -337,6 +346,7 @@ function TaskChatToolInvocation({ row }: { row: Extract<TaskChatToolGroupRow, {
} }
function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) { function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
const { t } = useTranslation("app");
const invocationEntries = getToolInvocationEntries(entries); const invocationEntries = getToolInvocationEntries(entries);
const invocationCount = invocationEntries.length; const invocationCount = invocationEntries.length;
const errorCount = entries.filter((entry) => entry.type === "tool_error").length; const errorCount = entries.filter((entry) => entry.type === "tool_error").length;
@@ -346,16 +356,16 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
return ( return (
<details className="task-chat-tool-group" data-testid="task-chat-tool-group"> <details className="task-chat-tool-group" data-testid="task-chat-tool-group">
<summary className="task-chat-tool-group-summary"> <summary className="task-chat-tool-group-summary">
<span className="task-chat-tool-group-count">{formatToolCallCount(invocationCount)}</span> <span className="task-chat-tool-group-count">{formatToolCallCount(invocationCount, t)}</span>
{visibleNames.length > 0 ? ( {visibleNames.length > 0 ? (
<span className="task-chat-tool-group-names" aria-label="Tool names"> <span className="task-chat-tool-group-names" aria-label={t("taskChat.toolNames", "Tool names")}>
{visibleNames.join(", ")} {visibleNames.join(", ")}
{overflowCount > 0 ? <span className="task-chat-tool-group-overflow">, +{overflowCount} more</span> : null} {overflowCount > 0 ? <span className="task-chat-tool-group-overflow">{t("taskChat.moreTools", ", +{{count}} more", { count: overflowCount })}</span> : null}
</span> </span>
) : null} ) : null}
{errorCount > 0 ? ( {errorCount > 0 ? (
<span className="task-chat-tool-group-error-count"> <span className="task-chat-tool-group-error-count">
{errorCount === 1 ? "1 error" : `${errorCount} errors`} {t("taskChat.errorCount", "{{count}} error", { count: errorCount })}
</span> </span>
) : null} ) : null}
</summary> </summary>
@@ -373,11 +383,12 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
} }
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) { function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
const { t } = useTranslation("app");
const combinedThinkingText = entries.map((entry) => entry.text).join(""); const combinedThinkingText = entries.map((entry) => entry.text).join("");
return ( return (
<details className="task-chat-thinking" data-testid="task-chat-thinking" open> <details className="task-chat-thinking" data-testid="task-chat-thinking" open>
<summary className="task-chat-thinking-summary">Thinking</summary> <summary className="task-chat-thinking-summary">{t("taskChat.thinking", "Thinking")}</summary>
<div className="task-chat-thinking-body"> <div className="task-chat-thinking-body">
<div <div
className="markdown-body task-chat-markdown task-chat-thinking-markdown" className="markdown-body task-chat-markdown task-chat-thinking-markdown"
@@ -407,12 +418,13 @@ FNXC:TaskChatTimestamps 2026-06-17-15:43:
FN-6597 requires small relative timestamps on both task-chat agent group headers and user message headers, computed at render time from existing transcript timestamps without adding a live timer. FN-6597 requires small relative timestamps on both task-chat agent group headers and user message headers, computed at render time from existing transcript timestamps without adding a live timer.
*/ */
function TaskChatUserMessage({ message }: { message: UserChatMessage }) { function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
const { t } = useTranslation("app");
const relativeTime = formatRelativeTimeAgo(message.createdAt); const relativeTime = formatRelativeTimeAgo(message.createdAt);
return ( return (
<section className="task-chat-user-group" aria-label="You message"> <section className="task-chat-user-group" aria-label={t("taskChat.youMessage", "You message")}>
<div className="task-chat-user-header"> <div className="task-chat-user-header">
<div className="task-chat-role-label">You</div> <div className="task-chat-role-label">{t("taskChat.you", "You")}</div>
{relativeTime ? ( {relativeTime ? (
<span className="task-chat-timestamp" data-testid="task-chat-user-time"> <span className="task-chat-timestamp" data-testid="task-chat-user-time">
{relativeTime} {relativeTime}
@@ -431,6 +443,7 @@ function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
} }
export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded }: TaskChatTabProps) { export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded }: TaskChatTabProps) {
const { t } = useTranslation("app");
const { entries, loading, loadMore, hasMore, loadingMore } = useAgentLogs(task.id, active, projectId); const { entries, loading, loadMore, hasMore, loadingMore } = useAgentLogs(task.id, active, projectId);
const [draft, setDraft] = useState(""); const [draft, setDraft] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
@@ -452,7 +465,7 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
() => mergeUserMessages(task.steeringComments, optimisticMessages), () => mergeUserMessages(task.steeringComments, optimisticMessages),
[optimisticMessages, task.steeringComments], [optimisticMessages, task.steeringComments],
); );
const transcriptItems = useMemo(() => buildTranscriptItems(entries, userMessages), [entries, userMessages]); const transcriptItems = useMemo(() => buildTranscriptItems(entries, userMessages, t), [entries, t, userMessages]);
const transcriptItemCount = entries.length + userMessages.length; const transcriptItemCount = entries.length + userMessages.length;
const firstEntryKey = entries[0] ? getEntryKey(entries[0], 0) : null; const firstEntryKey = entries[0] ? getEntryKey(entries[0], 0) : null;
const activeSession = isActiveAgentSession(task, { sessionLive }); const activeSession = isActiveAgentSession(task, { sessionLive });
@@ -463,13 +476,13 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
* The task-detail chat must never silently accept a question when no agent session will consume it. Keep idle chats sendable, but surface that the message is saved as guidance for the next task run instead of implying a live reply. * The task-detail chat must never silently accept a question when no agent session will consume it. Keep idle chats sendable, but surface that the message is saved as guidance for the next task run instead of implying a live reply.
*/ */
const sessionHint = isDoneTask const sessionHint = isDoneTask
? "Send a message to start a refinement task for this completed task." ? t("taskChat.doneSessionHint", "Send a message to start a refinement task for this completed task.")
: activeSession : activeSession
? "Message the active agent session. Guidance is delivered to the running session in real time." ? t("taskChat.activeSessionHint", "Message the active agent session. Guidance is delivered to the running session in real time.")
: "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs."; : t("taskChat.idleSessionHint", "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.");
const composerPlaceholder = isDoneTask const composerPlaceholder = isDoneTask
? "Start a refinement task for this completed task" ? t("taskChat.donePlaceholder", "Start a refinement task for this completed task")
: "Steer the currently executing agent"; : t("taskChat.activePlaceholder", "Steer the currently executing agent");
const canSend = draft.trim().length > 0 && !sending; const canSend = draft.trim().length > 0 && !sending;
const resizeComposer = useCallback(() => { const resizeComposer = useCallback(() => {
@@ -711,7 +724,7 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
type="button" type="button"
className="btn btn-icon btn-sm task-chat-expand-toggle task-chat-expand-toggle--overlay" className="btn btn-icon btn-sm task-chat-expand-toggle task-chat-expand-toggle--overlay"
onClick={onToggleExpanded} onClick={onToggleExpanded}
aria-label={expanded ? "Collapse chat" : "Expand chat to full modal"} aria-label={expanded ? t("taskChat.collapseChat", "Collapse chat") : t("taskChat.expandChat", "Expand chat to full modal")}
aria-pressed={expanded} aria-pressed={expanded}
data-testid="task-chat-expand-toggle" data-testid="task-chat-expand-toggle"
> >
@@ -731,17 +744,17 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
{loadingMore ? ( {loadingMore ? (
<div className="task-chat-load-previous-status" role="status" data-testid="task-chat-load-previous-loading"> <div className="task-chat-load-previous-status" role="status" data-testid="task-chat-load-previous-loading">
<Loader2 className="animate-spin" aria-hidden="true" /> <Loader2 className="animate-spin" aria-hidden="true" />
<span>Loading earlier messages…</span> <span>{t("taskChat.loadingEarlierMessages", "Loading earlier messages…")}</span>
</div> </div>
) : ( ) : (
<button <button
type="button" type="button"
className="btn btn-secondary btn-sm task-chat-load-previous" className="btn btn-secondary btn-sm task-chat-load-previous"
onClick={() => { void loadPreviousMessages(); }} onClick={() => { void loadPreviousMessages(); }}
aria-label="Load previous messages" aria-label={t("taskChat.loadPreviousMessages", "Load previous messages")}
data-testid="task-chat-load-previous" data-testid="task-chat-load-previous"
> >
Load previous messages {t("taskChat.loadPreviousMessages", "Load previous messages")}
</button> </button>
)} )}
</div> </div>
@@ -749,10 +762,10 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
{loading && transcriptItemCount === 0 ? ( {loading && transcriptItemCount === 0 ? (
<div className="task-chat-empty" role="status"> <div className="task-chat-empty" role="status">
<Loader2 className="animate-spin" aria-hidden="true" /> <Loader2 className="animate-spin" aria-hidden="true" />
<span>Loading agent output…</span> <span>{t("taskChat.loadingAgentOutput", "Loading agent output…")}</span>
</div> </div>
) : transcriptItemCount === 0 ? ( ) : transcriptItemCount === 0 ? (
<div className="task-chat-empty">No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.</div> <div className="task-chat-empty">{t("taskChat.emptyAgentOutput", "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.")}</div>
) : ( ) : (
transcriptItems.map((item, itemIndex) => { transcriptItems.map((item, itemIndex) => {
if (item.kind === "user") { if (item.kind === "user") {
@@ -768,13 +781,13 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
const latestEntryTimestamp = item.entries[item.entries.length - 1]?.timestamp ?? ""; const latestEntryTimestamp = item.entries[item.entries.length - 1]?.timestamp ?? "";
const relativeTime = formatRelativeTimeAgo(latestEntryTimestamp); const relativeTime = formatRelativeTimeAgo(latestEntryTimestamp);
return ( return (
<section className="task-chat-group" key={`${item.role ?? "agent"}-${itemIndex}`} aria-label={`${item.label} messages`}> <section className="task-chat-group" key={`${item.role ?? "agent"}-${itemIndex}`} aria-label={t("taskChat.agentMessages", "{{label}} messages", { label: item.label })}>
<header className="task-chat-group-header"> <header className="task-chat-group-header">
<AgentAvatar agent={avatarAgent} className="task-chat-avatar" /> <AgentAvatar agent={avatarAgent} className="task-chat-avatar" />
<div> <div>
<div className="task-chat-role-label">{item.label}</div> <div className="task-chat-role-label">{item.label}</div>
<div className="task-chat-group-meta"> <div className="task-chat-group-meta">
<span>{item.entries.length === 1 ? "1 entry" : `${item.entries.length} entries`}</span> <span>{t("taskChat.entryCount", "{{count}} entry", { count: item.entries.length })}</span>
{relativeTime ? ( {relativeTime ? (
<span className="task-chat-timestamp" data-testid="task-chat-group-time"> <span className="task-chat-timestamp" data-testid="task-chat-group-time">
{relativeTime} {relativeTime}
@@ -798,11 +811,11 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
type="button" type="button"
className="task-chat-jump-to-bottom" className="task-chat-jump-to-bottom"
onClick={scrollTranscriptToBottom} onClick={scrollTranscriptToBottom}
aria-label="Jump to latest message" aria-label={t("taskChat.jumpToLatestMessage", "Jump to latest message")}
data-testid="task-chat-jump-to-bottom" data-testid="task-chat-jump-to-bottom"
> >
<ChevronDown aria-hidden="true" /> <ChevronDown aria-hidden="true" />
<span>Latest</span> <span>{t("taskChat.latest", "Latest")}</span>
</button> </button>
) : null} ) : null}
</div> </div>
@@ -826,15 +839,15 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
onChange={(event) => setDraft(event.target.value)} onChange={(event) => setDraft(event.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={sending} disabled={sending}
aria-label="Message active agent session" aria-label={t("taskChat.messageActiveAgentSession", "Message active agent session")}
rows={1} rows={1}
/> />
<button <button
type="submit" type="submit"
className="btn btn-primary btn-icon task-chat-send" className="btn btn-primary btn-icon task-chat-send"
disabled={!canSend} disabled={!canSend}
aria-label={sending ? "Sending" : "Send"} aria-label={sending ? t("taskChat.sending", "Sending") : t("common:actions.send", "Send")}
title={sending ? "Sending" : "Send"} title={sending ? t("taskChat.sending", "Sending") : t("common:actions.send", "Send")}
> >
{sending ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />} {sending ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
</button> </button>

View File

@@ -115,7 +115,7 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
<div className="detail-log-header comments-header-row"> <div className="detail-log-header comments-header-row">
<div className="comments-author-row"> <div className="comments-author-row">
{isAIGuidance ? ( {isAIGuidance ? (
<span className="ai-guidance-badge" data-testid="ai-guidance-badge">AI Guidance</span> <span className="ai-guidance-badge" data-testid="ai-guidance-badge">{t("comments.aiGuidance", "AI Guidance")}</span>
) : ( ) : (
<strong>{comment.author}</strong> <strong>{comment.author}</strong>
)} )}

View File

@@ -71,14 +71,20 @@ interface ModelSelection {
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]); const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/; const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/;
const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
const STRING_OBJECT_TAG = "[object String]";
function isStringValue(value: unknown): value is string {
return Object.prototype.toString.call(value) === STRING_OBJECT_TAG;
}
const markdownLinkifyComponents: Components = { const markdownLinkifyComponents: Components = {
p: ({ children, ...props }) => <p {...props}>{linkifyReactChildren(children)}</p>, p: ({ children, ...props }) => <p {...props}>{linkifyReactChildren(children)}</p>,
li: ({ children, ...props }) => <li {...props}>{linkifyReactChildren(children)}</li>, li: ({ children, ...props }) => <li {...props}>{linkifyReactChildren(children)}</li>,
code: ({ children, ...props }) => { code: ({ children, ...props }) => {
const text = typeof children === "string" ? children : React.Children.toArray(children).join(""); const text = React.Children.toArray(children).join(EMPTY_MARKDOWN_CHILD_SEPARATOR);
const linkedChildren = linkifyFilePaths(text); const linkedChildren = linkifyFilePaths(text);
if (linkedChildren.length === 1 && typeof linkedChildren[0] === "string") { if (linkedChildren.length === 1 && linkedChildren[0]?.constructor === String) {
return <code {...props}>{children}</code>; return <code {...props}>{children}</code>;
} }
return <code {...props}>{linkedChildren}</code>; return <code {...props}>{linkedChildren}</code>;
@@ -92,25 +98,25 @@ const markdownLinkifyComponents: Components = {
*/ */
function extractExecutorModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null { function extractExecutorModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null; let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) { entries.forEach((entry) => {
if (entry.agent !== "executor" || entry.type !== "text") continue; if (entry.agent !== "executor" || entry.type !== "text") return;
const match = entry.text.match(/^Executor using model: (.+?)\/(.+)$/); const match = entry.text.match(/^Executor using model: (.+?)\/(.+)$/);
if (match) { if (match) {
result = { provider: match[1], modelId: match[2] }; result = { provider: match[1], modelId: match[2] };
} }
} });
return result; return result;
} }
function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null { function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null; let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) { entries.forEach((entry) => {
if (entry.agent !== "reviewer" || entry.type !== "text") continue; if (entry.agent !== "reviewer" || entry.type !== "text") return;
const match = entry.text.match(/^Reviewer using model: (.+?)\/(.+)$/); const match = entry.text.match(/^Reviewer using model: (.+?)\/(.+)$/);
if (match) { if (match) {
result = { provider: match[1], modelId: match[2] }; result = { provider: match[1], modelId: match[2] };
} }
} });
return result; return result;
} }
@@ -129,7 +135,7 @@ function hasUsableTrackingTitle(task: { title?: string | null; description?: str
function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSelection { function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSelection {
const runtimeConfig = (agent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined; const runtimeConfig = (agent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined;
const model = typeof runtimeConfig?.model === "string" ? runtimeConfig.model.trim() : ""; const model = isStringValue(runtimeConfig?.model) ? runtimeConfig.model.trim() : "";
if (model) { if (model) {
const slashIdx = model.indexOf("/"); const slashIdx = model.indexOf("/");
if (slashIdx > 0 && slashIdx < model.length - 1) { if (slashIdx > 0 && slashIdx < model.length - 1) {
@@ -140,8 +146,8 @@ function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSele
} }
} }
const provider = typeof runtimeConfig?.modelProvider === "string" ? runtimeConfig.modelProvider.trim() : ""; const provider = isStringValue(runtimeConfig?.modelProvider) ? runtimeConfig.modelProvider.trim() : "";
const modelId = typeof runtimeConfig?.modelId === "string" ? runtimeConfig.modelId.trim() : ""; const modelId = isStringValue(runtimeConfig?.modelId) ? runtimeConfig.modelId.trim() : "";
return { return {
provider: provider || undefined, provider: provider || undefined,
modelId: modelId || undefined, modelId: modelId || undefined,
@@ -209,13 +215,13 @@ function resolveEffectiveValidator(
function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null { function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
// Iterate in chronological order; last match wins // Iterate in chronological order; last match wins
let result: { provider: string; modelId: string } | null = null; let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) { entries.forEach((entry) => {
if (entry.agent !== "triage" || entry.type !== "text") continue; if (entry.agent !== "triage" || entry.type !== "text") return;
const match = entry.text.match(/^Triage using model: (.+?)\/(.+)$/); const match = entry.text.match(/^Triage using model: (.+?)\/(.+)$/);
if (match) { if (match) {
result = { provider: match[1], modelId: match[2] }; result = { provider: match[1], modelId: match[2] };
} }
} });
return result; return result;
} }
@@ -431,7 +437,7 @@ function normalizeSourceIssueUrl(value: string): string | undefined {
} }
function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority { function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority {
return typeof priority === "string" && (TASK_PRIORITIES as readonly string[]).includes(priority) return isStringValue(priority) && (TASK_PRIORITIES as readonly string[]).includes(priority)
? (priority as TaskPriority) ? (priority as TaskPriority)
: DEFAULT_TASK_PRIORITY; : DEFAULT_TASK_PRIORITY;
} }
@@ -456,7 +462,7 @@ interface ProvenanceLabelOptions {
function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined { function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined {
const issueUrl = metadata?.issueUrl; const issueUrl = metadata?.issueUrl;
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined; return isStringValue(issueUrl) && issueUrl.length > 0 ? issueUrl : undefined;
} }
function parseGithubIssueLabel(url: string): { label: string; href: string } | null { function parseGithubIssueLabel(url: string): { label: string; href: string } | null {
@@ -474,12 +480,12 @@ function parseGithubIssueLabel(url: string): { label: string; href: string } | n
function getResearchContextInfo(metadata: Task["sourceMetadata"]): string | undefined { function getResearchContextInfo(metadata: Task["sourceMetadata"]): string | undefined {
const findingLabel = metadata?.findingLabel; const findingLabel = metadata?.findingLabel;
if (typeof findingLabel === "string" && findingLabel.length > 0) { if (isStringValue(findingLabel) && findingLabel.length > 0) {
return findingLabel; return findingLabel;
} }
const runId = metadata?.runId; const runId = metadata?.runId;
return typeof runId === "string" && runId.length > 0 ? runId : undefined; return isStringValue(runId) && runId.length > 0 ? runId : undefined;
} }
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView }))); const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
@@ -659,7 +665,7 @@ export function TaskDetailContent({
(task.stuckKillCount ?? 0) > 0 || (task.stuckKillCount ?? 0) > 0 ||
(task.recoveryRetryCount ?? 0) > 0 || (task.recoveryRetryCount ?? 0) > 0 ||
Boolean(task.nextRecoveryAt); Boolean(task.nextRecoveryAt);
const nearDuplicateOf = typeof workingTask.sourceMetadata?.nearDuplicateOf === "string" const nearDuplicateOf = isStringValue(workingTask.sourceMetadata?.nearDuplicateOf)
? workingTask.sourceMetadata.nearDuplicateOf ? workingTask.sourceMetadata.nearDuplicateOf
: null; : null;
const nearDuplicateCanonical = nearDuplicateOf const nearDuplicateCanonical = nearDuplicateOf
@@ -810,11 +816,11 @@ export function TaskDetailContent({
setCustomFieldValues(updated.customFields ?? {}); setCustomFieldValues(updated.customFields ?? {});
onTaskUpdated?.(updated); onTaskUpdated?.(updated);
} catch (err) { } catch (err) {
if (err instanceof ApiRequestError && err.details && typeof err.details.fieldId === "string") { if (err instanceof ApiRequestError && err.details && isStringValue(err.details.fieldId)) {
setCustomFieldError({ setCustomFieldError({
code: (err.details.code as CustomFieldRejection["code"]) ?? "type-mismatch", code: (err.details.code as CustomFieldRejection["code"]) ?? "type-mismatch",
fieldId: err.details.fieldId, fieldId: err.details.fieldId,
detail: typeof err.details.detail === "string" ? err.details.detail : err.message, detail: isStringValue(err.details.detail) ? err.details.detail : err.message,
}); });
return; return;
} }
@@ -909,7 +915,7 @@ export function TaskDetailContent({
tabId: `plugin-${entry.pluginId}-${index}` as TabId, tabId: `plugin-${entry.pluginId}-${index}` as TabId,
})); }));
const activePluginTab = const activePluginTab =
typeof activeTab === "string" && activeTab.startsWith("plugin-") isStringValue(activeTab) && activeTab.startsWith("plugin-")
? pluginTabs.find((tab) => tab.tabId === activeTab) ?? null ? pluginTabs.find((tab) => tab.tabId === activeTab) ?? null
: null; : null;
@@ -2981,7 +2987,7 @@ export function TaskDetailContent({
)} )}
{provenanceDisplay.parentTaskId && ( {provenanceDisplay.parentTaskId && (
<> <>
{" "}of{" "} {" "}{t("taskDetail.provenance.parentTaskOf", "of")}{" "}
<button <button
type="button" type="button"
className="detail-provenance-link" className="detail-provenance-link"
@@ -3021,7 +3027,7 @@ export function TaskDetailContent({
<div className="detail-provenance detail-pr-link-row"> <div className="detail-provenance detail-pr-link-row">
<GitBranch aria-hidden="true" /> <GitBranch aria-hidden="true" />
<span> <span>
PR{" "} {t("taskDetail.pr.label", "PR")} {" "}
{task.prInfo?.url ? ( {task.prInfo?.url ? (
<a <a
className="detail-provenance-link" className="detail-provenance-link"

View File

@@ -42,6 +42,11 @@ import type { DiscoveredSkill } from "../api";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import { useConfirm } from "../hooks/useConfirm"; import { useConfirm } from "../hooks/useConfirm";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 localizes this workflow surface through t() and authored en catalog keys so hardcoded user-facing copy does not need a lint.ignore deferral.
*/
import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { useAppSettings } from "../hooks/useAppSettings"; import { useAppSettings } from "../hooks/useAppSettings";
import { isMobileViewport, useViewportMode } from "../hooks/useViewportMode"; import { isMobileViewport, useViewportMode } from "../hooks/useViewportMode";
@@ -210,6 +215,9 @@ const NOTIFY_EVENT_OPTIONS = [
"workflow-notify", "workflow-notify",
] as const; ] as const;
const NOTIFY_CUSTOM_EVENT_VALUE = "__custom"; const NOTIFY_CUSTOM_EVENT_VALUE = "__custom";
const WORKFLOW_CLI_COMMAND_PLACEHOLDER = "npm test -- --runInBand";
const WORKFLOW_CODE_SOURCE_PLACEHOLDER = "export default async (ctx) => ({ outcome: \"success\" });";
const WORKFLOW_NOTIFY_MESSAGE_PLACEHOLDER = "Task {{taskId}} reached {{workflowName}}";
const PALETTE: Array<{ kind: WorkflowEditorNodeKind; label: string; icon: typeof MessageSquare; presetConfig?: Record<string, unknown> }> = [ const PALETTE: Array<{ kind: WorkflowEditorNodeKind; label: string; icon: typeof MessageSquare; presetConfig?: Record<string, unknown> }> = [
{ kind: "prompt", label: "Prompt", icon: MessageSquare }, { kind: "prompt", label: "Prompt", icon: MessageSquare },
@@ -1019,11 +1027,11 @@ function InnerEditor({
return isMobileMode ? null : data[0]?.id ?? null; return isMobileMode ? null : data[0]?.id ?? null;
}); });
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to load workflows", "error"); addToast(getErrorMessage(err) || t("workflows.loadFailed", "Failed to load workflows"), "error");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [projectId, addToast, isMobileMode, initialAction, initialWorkflowId]); }, [projectId, addToast, isMobileMode, initialAction, initialWorkflowId, t]);
useEffect(() => { useEffect(() => {
void loadWorkflows(); void loadWorkflows();
@@ -1752,11 +1760,11 @@ function InnerEditor({
setWorkflows((ws) => [...ws, created]); setWorkflows((ws) => [...ws, created]);
setActiveId(created.id); setActiveId(created.id);
setWorkflowListStageOpen(false); setWorkflowListStageOpen(false);
addToast(`Duplicated to "${created.name}" — editable`, "success"); addToast(t("workflows.duplicatedEditable", "Duplicated to \"{{name}}\" — editable", { name: created.name }), "success");
} catch (err) { } catch (err) {
addToast(getErrorMessage(err) || "Failed to duplicate workflow", "error"); addToast(getErrorMessage(err) || t("workflows.duplicateFailed", "Failed to duplicate workflow"), "error");
} }
}, [activeWorkflow, projectId, addToast]); }, [activeWorkflow, projectId, addToast, t]);
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (!activeWorkflow) return; if (!activeWorkflow) return;
@@ -2107,25 +2115,25 @@ function InnerEditor({
// step-review offers an optional review model picker (KTD-4). // step-review offers an optional review model picker (KTD-4).
if (selectedNode?.data.kind === "step-review" && models.length === 0) { if (selectedNode?.data.kind === "step-review" && models.length === 0) {
fetchModels().then((res) => setModels(res.models)).catch((err) => { fetchModels().then((res) => setModels(res.models)).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load models", "error"); addToast(getErrorMessage(err) || t("workflowEditor.modelsLoadFailed", "Failed to load models"), "error");
}); });
return; return;
} }
if (!selectedNode || (selectedNode.data.kind !== "prompt" && selectedNode.data.kind !== "gate")) return; if (!selectedNode || (selectedNode.data.kind !== "prompt" && selectedNode.data.kind !== "gate")) return;
if (currentExecutor === "model" && models.length === 0) { if (currentExecutor === "model" && models.length === 0) {
fetchModels().then((res) => setModels(res.models)).catch((err) => { fetchModels().then((res) => setModels(res.models)).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load models", "error"); addToast(getErrorMessage(err) || t("workflowEditor.modelsLoadFailed", "Failed to load models"), "error");
}); });
} else if (currentExecutor === "agent" && agents.length === 0) { } else if (currentExecutor === "agent" && agents.length === 0) {
// Project-scoped, matching WorkflowColumnPanel's fetchAgents(undefined, // Project-scoped, matching WorkflowColumnPanel's fetchAgents(undefined,
// projectId) — an unscoped fetch returns the wrong registry in // projectId) — an unscoped fetch returns the wrong registry in
// multi-project deployments (PR #1432 review). // multi-project deployments (PR #1432 review).
fetchAgents(undefined, projectId).then(setAgents).catch((err) => { fetchAgents(undefined, projectId).then(setAgents).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load agents", "error"); addToast(getErrorMessage(err) || t("workflowEditor.agentsLoadFailed", "Failed to load agents"), "error");
}); });
} else if (currentExecutor === "skill" && skills.length === 0) { } else if (currentExecutor === "skill" && skills.length === 0) {
fetchDiscoveredSkills(projectId).then(setSkills).catch((err) => { fetchDiscoveredSkills(projectId).then(setSkills).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load skills", "error"); addToast(getErrorMessage(err) || t("workflowEditor.skillsLoadFailed", "Failed to load skills"), "error");
}); });
} }
}, [ }, [
@@ -2137,6 +2145,7 @@ function InnerEditor({
models.length, models.length,
agents.length, agents.length,
skills.length, skills.length,
t,
]); ]);
// ── Dirty-state dismissal guard (U4, R7) ──────────────────────────────────── // ── Dirty-state dismissal guard (U4, R7) ────────────────────────────────────
@@ -2198,12 +2207,12 @@ function InnerEditor({
Promise.resolve(fetchAgents(undefined, projectId)).then((list) => { Promise.resolve(fetchAgents(undefined, projectId)).then((list) => {
if (!cancelled) setAgents(list ?? []); if (!cancelled) setAgents(list ?? []);
}).catch((err) => { }).catch((err) => {
if (!cancelled) addToast(getErrorMessage(err) || "Failed to load agents", "error"); if (!cancelled) addToast(getErrorMessage(err) || t("workflowEditor.agentsLoadFailed", "Failed to load agents"), "error");
}); });
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [overrideColumnBinding, agents.length, projectId, addToast]); }, [overrideColumnBinding, agents.length, projectId, addToast, t]);
const overlayProps = useOverlayDismiss(requestClose); const overlayProps = useOverlayDismiss(requestClose);
const promptFullscreenOverlay = const promptFullscreenOverlay =
@@ -2226,7 +2235,7 @@ function InnerEditor({
</button> </button>
</div> </div>
<label className="wf-field"> <label className="wf-field">
<span>Prompt</span> <span>{t("workflowEditor.prompt", "Prompt")}</span>
<textarea <textarea
rows={undefined} rows={undefined}
value={selectedNodePromptValue} value={selectedNodePromptValue}
@@ -2263,8 +2272,8 @@ function InnerEditor({
}} }}
> >
<header className="wf-editor-header"> <header className="wf-editor-header">
<h2>Workflows</h2> <h2>{t("workflows.title", "Workflows")}</h2>
<button className="wf-editor-close" onClick={requestClose} aria-label="Close workflow editor"> <button className="wf-editor-close" onClick={requestClose} aria-label={t("workflows.closeEditor", "Close workflow editor")}>
<X size={18} /> <X size={18} />
</button> </button>
</header> </header>
@@ -2349,10 +2358,10 @@ function InnerEditor({
) : null} ) : null}
{loading ? ( {loading ? (
<div className="wf-editor-empty"> <div className="wf-editor-empty">
<Loader2 size={16} className="wf-spin" /> Loading… <Loader2 size={16} className="wf-spin" /> {t("workflows.loading", "Loading…")}
</div> </div>
) : workflows.length === 0 ? ( ) : workflows.length === 0 ? (
<div className="wf-editor-empty">No workflows yet.</div> <div className="wf-editor-empty">{t("workflows.noneYet", "No workflows yet.")}</div>
) : ( ) : (
<ul className="wf-editor-list"> <ul className="wf-editor-list">
{workflows.map((w) => ( {workflows.map((w) => (
@@ -3236,7 +3245,7 @@ function InnerEditor({
!(compactLayoutEnabled && !isMobileMode) && ( !(compactLayoutEnabled && !isMobileMode) && (
<aside className="wf-editor-inspector" data-testid="wf-node-inspector"> <aside className="wf-editor-inspector" data-testid="wf-node-inspector">
<div className="wf-inspector-heading"> <div className="wf-inspector-heading">
<h3>Node</h3> <h3>{t("workflowNodes.nodeInspector", "Node")}</h3>
{isMobileMode && ( {isMobileMode && (
<button <button
type="button" type="button"
@@ -3258,14 +3267,14 @@ function InnerEditor({
</div> </div>
{isBuiltin && ( {isBuiltin && (
<p className="wf-inspector-note wf-inspector-note--info"> <p className="wf-inspector-note wf-inspector-note--info">
Read-only built-in — duplicate the workflow to edit nodes. {t("workflowNodes.readOnlyDuplicateToEdit", "Read-only built-in — duplicate the workflow to edit nodes.")}
</p> </p>
)} )}
<fieldset className="wf-inspector-fields" disabled={isBuiltin}> <fieldset className="wf-inspector-fields" disabled={isBuiltin}>
{/* FNXC:WorkflowEditor 2026-06-17-00:20: Start labels are structural and ignored by flowToIr, so exposing the generic Name editor would create a no-op rename. */} {/* FNXC:WorkflowEditor 2026-06-17-00:20: Start labels are structural and ignored by flowToIr, so exposing the generic Name editor would create a no-op rename. */}
{selectedNode.data.kind !== "start" && ( {selectedNode.data.kind !== "start" && (
<label className="wf-field"> <label className="wf-field">
<span>Name</span> <span>{t("common:labels.name", "Name")}</span>
<input <input
value={selectedNode.data.label} value={selectedNode.data.label}
onChange={(e) => updateSelectedData({ label: e.target.value })} onChange={(e) => updateSelectedData({ label: e.target.value })}
@@ -3307,7 +3316,7 @@ function InnerEditor({
{selectedNode.data.kind === "prompt" || selectedNode.data.kind === "gate" ? ( {selectedNode.data.kind === "prompt" || selectedNode.data.kind === "gate" ? (
<div className="wf-prompt-editor"> <div className="wf-prompt-editor">
<label className="wf-field"> <label className="wf-field">
<span>Prompt</span> <span>{t("workflowEditor.prompt", "Prompt")}</span>
<textarea <textarea
rows={5} rows={5}
value={selectedNodePromptValue} value={selectedNodePromptValue}
@@ -3334,15 +3343,15 @@ function InnerEditor({
{selectedNode.data.kind === "prompt" ? ( {selectedNode.data.kind === "prompt" ? (
<> <>
<label className="wf-field"> <label className="wf-field">
<span>Executor</span> <span>{t("workflowEditor.executor", "Executor")}</span>
<select <select
value={currentExecutor} value={currentExecutor}
onChange={(e) => updateSelectedData({ config: { executor: e.target.value } })} onChange={(e) => updateSelectedData({ config: { executor: e.target.value } })}
> >
<option value="model">Model</option> <option value="model">{t("workflowEditor.model", "Model")}</option>
<option value="agent">Agent</option> <option value="agent">{t("workflowEditor.agent", "Agent")}</option>
<option value="skill">Skill</option> <option value="skill">{t("workflowEditor.skill", "Skill")}</option>
<option value="cli">CLI / script</option> <option value="cli">{t("workflowEditor.cliScript", "CLI / script")}</option>
<option value="cli-agent">{t("workflowEditor.cliAgent.executorOption")}</option> <option value="cli-agent">{t("workflowEditor.cliAgent.executorOption")}</option>
</select> </select>
</label> </label>
@@ -3362,9 +3371,9 @@ function InnerEditor({
{currentExecutor === "model" && ( {currentExecutor === "model" && (
<label className="wf-field"> <label className="wf-field">
<span>Model</span> <span>{t("workflowEditor.model", "Model")}</span>
<CustomModelDropdown <CustomModelDropdown
label="Model" label={t("workflowEditor.model", "Model")}
models={models} models={models}
value={getModelDropdownValue( value={getModelDropdownValue(
String(selectedNode.data.config?.modelProvider ?? ""), String(selectedNode.data.config?.modelProvider ?? ""),
@@ -3386,12 +3395,12 @@ function InnerEditor({
const nodeAgentStale = nodeAgentId !== "" && !agents.some((a) => a.id === nodeAgentId); const nodeAgentStale = nodeAgentId !== "" && !agents.some((a) => a.id === nodeAgentId);
return ( return (
<label className="wf-field"> <label className="wf-field">
<span>Agent</span> <span>{t("workflowEditor.agent", "Agent")}</span>
<select <select
value={nodeAgentId} value={nodeAgentId}
onChange={(e) => updateSelectedData({ config: { agentId: e.target.value || undefined } })} onChange={(e) => updateSelectedData({ config: { agentId: e.target.value || undefined } })}
> >
<option value="">— select agent —</option> <option value="">{t("workflowEditor.selectAgent", "— select agent —")}</option>
{nodeAgentStale && ( {nodeAgentStale && (
<option value={nodeAgentId}> <option value={nodeAgentId}>
{t("workflowColumns.agentNotFound", "Agent not found — {{id}}", { id: nodeAgentId })} {t("workflowColumns.agentNotFound", "Agent not found — {{id}}", { id: nodeAgentId })}
@@ -3412,12 +3421,12 @@ function InnerEditor({
{currentExecutor === "skill" && ( {currentExecutor === "skill" && (
<label className="wf-field"> <label className="wf-field">
<span>Skill</span> <span>{t("workflowEditor.skill", "Skill")}</span>
<select <select
value={String(selectedNode.data.config?.skillName ?? "")} value={String(selectedNode.data.config?.skillName ?? "")}
onChange={(e) => updateSelectedData({ config: { skillName: e.target.value || undefined } })} onChange={(e) => updateSelectedData({ config: { skillName: e.target.value || undefined } })}
> >
<option value="">— select skill —</option> <option value="">{t("workflowEditor.selectSkill", "— select skill —")}</option>
{skills.map((s) => ( {skills.map((s) => (
<option key={s.id} value={s.name}>{s.name}</option> <option key={s.id} value={s.name}>{s.name}</option>
))} ))}
@@ -3428,26 +3437,26 @@ function InnerEditor({
{currentExecutor === "cli" && ( {currentExecutor === "cli" && (
<> <>
<label className="wf-field"> <label className="wf-field">
<span>CLI mode</span> <span>{t("workflowEditor.cliMode", "CLI mode")}</span>
<select <select
value={String(selectedNode.data.config?.cliMode ?? "command")} value={String(selectedNode.data.config?.cliMode ?? "command")}
onChange={(e) => updateSelectedData({ config: { cliMode: e.target.value } })} onChange={(e) => updateSelectedData({ config: { cliMode: e.target.value } })}
> >
<option value="command">Command</option> <option value="command">{t("workflowEditor.command", "Command")}</option>
<option value="script">Named script</option> <option value="script">{t("workflowEditor.namedScript", "Named script")}</option>
</select> </select>
</label> </label>
{(selectedNode.data.config?.cliMode ?? "command") === "command" ? ( {(selectedNode.data.config?.cliMode ?? "command") === "command" ? (
<label className="wf-field"> <label className="wf-field">
<span>Command</span> <span>{t("workflowEditor.command", "Command")}</span>
<textarea <textarea
rows={3} rows={3}
placeholder="npm test -- --runInBand" placeholder={WORKFLOW_CLI_COMMAND_PLACEHOLDER}
value={String(selectedNode.data.config?.cliCommand ?? "")} value={String(selectedNode.data.config?.cliCommand ?? "")}
onChange={(e) => updateSelectedData({ config: { cliCommand: e.target.value } })} onChange={(e) => updateSelectedData({ config: { cliCommand: e.target.value } })}
/> />
<p className="wf-inspector-note wf-inspector-note--info"> <p className="wf-inspector-note wf-inspector-note--info">
Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT. {t("workflowEditor.cliCommandNote", "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.")}
</p> </p>
<label className="wf-field wf-field--checkbox"> <label className="wf-field wf-field--checkbox">
<input <input
@@ -3455,17 +3464,17 @@ function InnerEditor({
checked={selectedNode.data.config?.cliSkipApproval === true} checked={selectedNode.data.config?.cliSkipApproval === true}
onChange={(e) => updateSelectedData({ config: { cliSkipApproval: e.target.checked } })} onChange={(e) => updateSelectedData({ config: { cliSkipApproval: e.target.checked } })}
/> />
<span>Skip first-run approval (runs without pausing)</span> <span>{t("workflowEditor.skipFirstRunApproval", "Skip first-run approval (runs without pausing)")}</span>
</label> </label>
</label> </label>
) : ( ) : (
<label className="wf-field"> <label className="wf-field">
<span>Script name</span> <span>{t("workflowEditor.scriptName", "Script name")}</span>
<input <input
value={String(selectedNode.data.config?.scriptName ?? "")} value={String(selectedNode.data.config?.scriptName ?? "")}
onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })} onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })}
/> />
<span className="wf-inspector-note">Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.</span> <span className="wf-inspector-note">{t("workflowEditor.namedScriptNote", "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.")}</span>
</label> </label>
)} )}
</> </>
@@ -3554,16 +3563,16 @@ function InnerEditor({
checked={Boolean(selectedNode.data.config?.autoApprove)} checked={Boolean(selectedNode.data.config?.autoApprove)}
onChange={(e) => updateSelectedData({ config: { autoApprove: e.target.checked } })} onChange={(e) => updateSelectedData({ config: { autoApprove: e.target.checked } })}
/> />
<span>Auto-approve requests</span> <span>{t("workflowEditor.autoApproveRequests", "Auto-approve requests")}</span>
</label> </label>
{Boolean(selectedNode.data.config?.autoApprove) && ( {Boolean(selectedNode.data.config?.autoApprove) && (
<p className="wf-inspector-note"> <p className="wf-inspector-note">
Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off. {t("workflowEditor.autoApproveRequestsNote", "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.")}
</p> </p>
)} )}
<label className="wf-field"> <label className="wf-field">
<span>Max retries</span> <span>{t("workflowEditor.maxRetries", "Max retries")}</span>
<input <input
type="number" type="number"
min={1} min={1}
@@ -3594,11 +3603,11 @@ function InnerEditor({
checked={Boolean(selectedNode.data.config?.awaitInput)} checked={Boolean(selectedNode.data.config?.awaitInput)}
onChange={(e) => updateSelectedData({ config: { awaitInput: e.target.checked } })} onChange={(e) => updateSelectedData({ config: { awaitInput: e.target.checked } })}
/> />
<span>Wait for user input</span> <span>{t("workflowEditor.waitForUserInput", "Wait for user input")}</span>
</label> </label>
{Boolean(selectedNode.data.config?.awaitInput) && ( {Boolean(selectedNode.data.config?.awaitInput) && (
<p className="wf-inspector-note wf-inspector-note--info"> <p className="wf-inspector-note wf-inspector-note--info">
This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question. {t("workflowEditor.waitForUserInputNote", "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question.")}
</p> </p>
)} )}
</> </>
@@ -3606,7 +3615,7 @@ function InnerEditor({
{selectedNode.data.kind === "script" ? ( {selectedNode.data.kind === "script" ? (
<label className="wf-field"> <label className="wf-field">
<span>Script name</span> <span>{t("workflowEditor.scriptName", "Script name")}</span>
<input <input
value={String(selectedNode.data.config?.scriptName ?? "")} value={String(selectedNode.data.config?.scriptName ?? "")}
onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })} onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })}
@@ -4086,7 +4095,7 @@ function InnerEditor({
className="wf-code-source" className="wf-code-source"
rows={8} rows={8}
spellCheck={false} spellCheck={false}
placeholder={"export default async (ctx) => ({ outcome: \"success\" });"} placeholder={WORKFLOW_CODE_SOURCE_PLACEHOLDER}
value={String(selectedNode.data.config?.source ?? "")} value={String(selectedNode.data.config?.source ?? "")}
onChange={(e) => updateSelectedData({ config: { source: e.target.value } })} onChange={(e) => updateSelectedData({ config: { source: e.target.value } })}
/> />
@@ -4176,7 +4185,7 @@ function InnerEditor({
<textarea <textarea
rows={4} rows={4}
value={String(selectedNode.data.config?.message ?? "")} value={String(selectedNode.data.config?.message ?? "")}
placeholder="Task {{taskId}} reached {{workflowName}}" placeholder={WORKFLOW_NOTIFY_MESSAGE_PLACEHOLDER}
onChange={(e) => updateSelectedData({ config: { message: e.target.value } })} onChange={(e) => updateSelectedData({ config: { message: e.target.value } })}
/> />
</label> </label>
@@ -4299,8 +4308,8 @@ function InnerEditor({
value={String(selectedEdge.data?.condition ?? "success")} value={String(selectedEdge.data?.condition ?? "success")}
onChange={(e) => updateSelectedEdge({ condition: e.target.value })} onChange={(e) => updateSelectedEdge({ condition: e.target.value })}
> >
<option value="success">success</option> <option value="success">{t("workflowNodes.conditionSuccess", "success")}</option>
<option value="failure">failure</option> <option value="failure">{t("workflowNodes.conditionFailure", "failure")}</option>
</select> </select>
</label> </label>
) : ( ) : (

View File

@@ -2,6 +2,11 @@ import "@xyflow/react/dist/style.css";
import "./WorkflowResultsTab.css"; import "./WorkflowResultsTab.css";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 requires the workflow/task/setup/PR dashboard cluster to render user-facing copy through locale catalogs so i18n:lint can scan these files again without narrow deferrals.
*/
import { Check, ChevronDown, ChevronRight, ChevronUp, Maximize2, Pencil, X } from "lucide-react"; import { Check, ChevronDown, ChevronRight, ChevronUp, Maximize2, Pencil, X } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -62,10 +67,11 @@ interface WorkflowResultsTabProps {
/** Extract the user-facing question from a workflow-input paused reason. /** Extract the user-facing question from a workflow-input paused reason.
* Strips the leading "workflow-input:<nodeId>: " prefix if present. */ * Strips the leading "workflow-input:<nodeId>: " prefix if present. */
function parseWorkflowInputQuestion(pausedReason?: string): string { function parseWorkflowInputQuestion(pausedReason: string | undefined, t: ReturnType<typeof useTranslation>["t"]): string {
if (!pausedReason) return "Reply in the comments and unpause the task to continue."; const fallback = t("app:workflow.replyInComments", "Reply in the comments and unpause the task to continue.");
if (!pausedReason) return fallback;
const match = /^workflow-input:[^:]+:\s*(.*)$/s.exec(pausedReason); const match = /^workflow-input:[^:]+:\s*(.*)$/s.exec(pausedReason);
if (match) return match[1].trim() || "Reply in the comments and unpause the task to continue."; if (match) return match[1].trim() || fallback;
return pausedReason; return pausedReason;
} }
@@ -89,15 +95,15 @@ interface WorkflowStepOption {
function getStatusLabel(status: WorkflowStepResult["status"], t: ReturnType<typeof useTranslation>["t"]): string { function getStatusLabel(status: WorkflowStepResult["status"], t: ReturnType<typeof useTranslation>["t"]): string {
switch (status) { switch (status) {
case "passed": case "passed":
return t("workflow.statusPassed", "Passed"); return t("app:workflow.statusPassed", "Passed");
case "failed": case "failed":
return t("workflow.statusFailed", "Failed"); return t("app:workflow.statusFailed", "Failed");
case "advisory_failure": case "advisory_failure":
return t("workflow.statusAdvisory", "Advisory failure"); return t("app:workflow.statusAdvisory", "Advisory failure");
case "skipped": case "skipped":
return t("workflow.statusSkipped", "Skipped"); return t("app:workflow.statusSkipped", "Skipped");
case "pending": case "pending":
return t("workflow.statusRunning", "Running…"); return t("app:workflow.statusRunning", "Running…");
default: default:
return status; return status;
} }
@@ -135,7 +141,7 @@ function phaseBadge(phase: "pre-merge" | "post-merge", id: string, prefix: strin
className={`phase-badge ${phaseClass}`} className={`phase-badge ${phaseClass}`}
data-testid={`${prefix}-${id}`} data-testid={`${prefix}-${id}`}
> >
{phase === "post-merge" ? t("workflow.postMerge", "Post-merge") : t("workflow.preMerge", "Pre-merge")} {phase === "post-merge" ? t("app:workflow.postMerge", "Post-merge") : t("app:workflow.preMerge", "Pre-merge")}
</span> </span>
); );
} }
@@ -145,9 +151,9 @@ function getWorkflowName(
workflows: WorkflowDefinition[], workflows: WorkflowDefinition[],
t: ReturnType<typeof useTranslation>["t"], t: ReturnType<typeof useTranslation>["t"],
): string { ): string {
if (!selectedWorkflowId) return t("workflow.defaultWorkflow", "Default"); if (!selectedWorkflowId) return t("app:workflow.defaultWorkflow", "Default");
const match = workflows.find((workflow) => workflow.id === selectedWorkflowId); const match = workflows.find((workflow) => workflow.id === selectedWorkflowId);
return match?.name || t("workflow.customWorkflowFallback", "Custom workflow"); return match?.name || t("app:workflow.customWorkflowFallback", "Custom workflow");
} }
function getAggregateWorkflowResult( function getAggregateWorkflowResult(
@@ -155,18 +161,18 @@ function getAggregateWorkflowResult(
t: ReturnType<typeof useTranslation>["t"], t: ReturnType<typeof useTranslation>["t"],
): { label: string; badgeClass: string; testId: string } { ): { label: string; badgeClass: string; testId: string } {
if (results.some((result) => result.status === "failed")) { if (results.some((result) => result.status === "failed")) {
return { label: t("workflow.statusFailed", "Failed"), badgeClass: "workflow-result-badge--failed", testId: "failed" }; return { label: t("app:workflow.statusFailed", "Failed"), badgeClass: "workflow-result-badge--failed", testId: "failed" };
} }
if (results.some((result) => result.status === "advisory_failure")) { if (results.some((result) => result.status === "advisory_failure")) {
return { label: t("workflow.statusAdvisory", "Advisory"), badgeClass: "workflow-result-badge--advisory_failure", testId: "advisory" }; return { label: t("app:workflow.aggregateAdvisory", "Advisory"), badgeClass: "workflow-result-badge--advisory_failure", testId: "advisory" };
} }
if (results.some((result) => result.status === "pending")) { if (results.some((result) => result.status === "pending")) {
return { label: t("workflow.aggregateInProgress", "In progress"), badgeClass: "workflow-result-badge--pending", testId: "pending" }; return { label: t("app:workflow.aggregateInProgress", "In progress"), badgeClass: "workflow-result-badge--pending", testId: "pending" };
} }
if (results.length === 0) { if (results.length === 0) {
return { label: t("workflow.aggregateNoResults", "No results"), badgeClass: "workflow-result-badge--skipped", testId: "no-results" }; return { label: t("app:workflow.aggregateNoResults", "No results"), badgeClass: "workflow-result-badge--skipped", testId: "no-results" };
} }
return { label: t("workflow.aggregateAllPassed", "All passed"), badgeClass: "workflow-result-badge--passed", testId: "passed" }; return { label: t("app:workflow.aggregateAllPassed", "All passed"), badgeClass: "workflow-result-badge--passed", testId: "passed" };
} }
function getExecutionPhase( function getExecutionPhase(
@@ -177,13 +183,13 @@ function getExecutionPhase(
t: ReturnType<typeof useTranslation>["t"], t: ReturnType<typeof useTranslation>["t"],
): { label: string; badgeClass: string; testId: string } { ): { label: string; badgeClass: string; testId: string } {
if (taskStatus === "awaiting-user-input") { if (taskStatus === "awaiting-user-input") {
return { label: t("workflow.executionAwaitingInput", "Awaiting input"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-input" }; return { label: t("app:workflow.executionAwaitingInput", "Awaiting input"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-input" };
} }
if (taskStatus === "awaiting-cli-approval") { if (taskStatus === "awaiting-cli-approval") {
return { label: t("workflow.executionAwaitingCliApproval", "Awaiting CLI approval"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-cli-approval" }; return { label: t("app:workflow.executionAwaitingCliApproval", "Awaiting CLI approval"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-cli-approval" };
} }
if (taskStatus === "paused" || taskPausedReason) { if (taskStatus === "paused" || taskPausedReason) {
return { label: t("workflow.executionPaused", "Paused"), badgeClass: "workflow-result-badge--pending", testId: "paused" }; return { label: t("app:workflow.executionPaused", "Paused"), badgeClass: "workflow-result-badge--pending", testId: "paused" };
} }
const pendingResult = results.find((result) => result.status === "pending"); const pendingResult = results.find((result) => result.status === "pending");
@@ -191,8 +197,8 @@ function getExecutionPhase(
const isPostMerge = (pendingResult.phase || "pre-merge") === "post-merge"; const isPostMerge = (pendingResult.phase || "pre-merge") === "post-merge";
return { return {
label: isPostMerge label: isPostMerge
? t("workflow.executionPostMerge", "Post-merge steps running") ? t("app:workflow.executionPostMerge", "Post-merge steps running")
: t("workflow.executionPreMerge", "Pre-merge steps running"), : t("app:workflow.executionPreMerge", "Pre-merge steps running"),
badgeClass: "workflow-result-badge--pending", badgeClass: "workflow-result-badge--pending",
testId: isPostMerge ? "post-merge" : "pre-merge", testId: isPostMerge ? "post-merge" : "pre-merge",
}; };
@@ -200,14 +206,14 @@ function getExecutionPhase(
const hasTerminalResults = results.length > 0 && results.every((result) => ["passed", "failed", "advisory_failure", "skipped"].includes(result.status)); const hasTerminalResults = results.length > 0 && results.every((result) => ["passed", "failed", "advisory_failure", "skipped"].includes(result.status));
if (hasTerminalResults || taskStatus === "done" || task?.column === "done" || task?.column === "in-review") { if (hasTerminalResults || taskStatus === "done" || task?.column === "done" || task?.column === "in-review") {
return { label: t("workflow.executionCompleted", "Completed"), badgeClass: "workflow-result-badge--passed", testId: "completed" }; return { label: t("app:workflow.executionCompleted", "Completed"), badgeClass: "workflow-result-badge--passed", testId: "completed" };
} }
return { label: t("workflow.executionNotStarted", "Not started"), badgeClass: "workflow-result-badge--pending", testId: "not-started" }; return { label: t("app:workflow.executionNotStarted", "Not started"), badgeClass: "workflow-result-badge--pending", testId: "not-started" };
} }
function formatModelValue(selection: { provider?: string; modelId?: string } | undefined): string { function formatModelValue(selection: { provider?: string; modelId?: string } | undefined, t: ReturnType<typeof useTranslation>["t"]): string {
if (!selection?.provider || !selection.modelId) return "Default"; if (!selection?.provider || !selection.modelId) return t("app:workflow.modelDefault", "Default");
return `${selection.provider}/${selection.modelId}`; return `${selection.provider}/${selection.modelId}`;
} }
@@ -245,7 +251,7 @@ function LiveAgentLogOutput({
if (stepEntries.length === 0) { if (stepEntries.length === 0) {
return ( return (
<div className="workflow-live-log" data-testid={`workflow-live-log-${stepId}`}> <div className="workflow-live-log" data-testid={`workflow-live-log-${stepId}`}>
<div className="workflow-live-log-empty">{t("workflow.waitingForOutput", "Waiting for agent output…")}</div> <div className="workflow-live-log-empty">{t("app:workflow.waitingForOutput", "Waiting for agent output…")}</div>
</div> </div>
); );
} }
@@ -570,7 +576,7 @@ export function WorkflowResultsTab({
return { return {
id: stepId, id: stepId,
name: stepInfo?.name || stepId, name: stepInfo?.name || stepId,
description: stepInfo?.description || t("workflow.stepDefinitionNotFound", "Step definition not found."), description: stepInfo?.description || t("app:workflow.stepDefinitionNotFound", "Step definition not found."),
phase: stepInfo?.phase || "pre-merge", phase: stepInfo?.phase || "pre-merge",
} as WorkflowStepOption; } as WorkflowStepOption;
}); });
@@ -595,7 +601,7 @@ export function WorkflowResultsTab({
<div className="workflow-results-editor" data-testid="workflow-steps-editor"> <div className="workflow-results-editor" data-testid="workflow-steps-editor">
<div className="workflow-steps-section"> <div className="workflow-steps-section">
<small className="workflow-steps-description"> <small className="workflow-steps-description">
{t("workflow.selectStepsDescription", "Select steps to run after task implementation completes")} {t("app:workflow.selectStepsDescription", "Select steps to run after task implementation completes")}
</small> </small>
<div className="workflow-steps-list"> <div className="workflow-steps-list">
{workflowStepOptions.map((step) => ( {workflowStepOptions.map((step) => (
@@ -625,7 +631,7 @@ export function WorkflowResultsTab({
{selectedWorkflowSteps.length > 1 && ( {selectedWorkflowSteps.length > 1 && (
<div className="workflow-step-order" data-testid="workflow-step-order"> <div className="workflow-step-order" data-testid="workflow-step-order">
<small className="workflow-step-order-label">{t("workflow.executionOrder", "Execution order:")}</small> <small className="workflow-step-order-label">{t("app:workflow.executionOrder", "Execution order:")}</small>
{selectedWorkflowSteps.map((stepId, index) => { {selectedWorkflowSteps.map((stepId, index) => {
const stepInfo = workflowStepLookup.get(stepId); const stepInfo = workflowStepLookup.get(stepId);
return ( return (
@@ -639,7 +645,7 @@ export function WorkflowResultsTab({
onClick={() => moveWorkflowStepUp(index)} onClick={() => moveWorkflowStepUp(index)}
disabled={index === 0} disabled={index === 0}
data-testid={`workflow-step-move-up-${stepId}`} data-testid={`workflow-step-move-up-${stepId}`}
title={t("workflow.moveUp", "Move up")} title={t("app:workflow.moveUp", "Move up")}
> >
<ChevronUp /> <ChevronUp />
</button> </button>
@@ -649,7 +655,7 @@ export function WorkflowResultsTab({
onClick={() => moveWorkflowStepDown(index)} onClick={() => moveWorkflowStepDown(index)}
disabled={index === selectedWorkflowSteps.length - 1} disabled={index === selectedWorkflowSteps.length - 1}
data-testid={`workflow-step-move-down-${stepId}`} data-testid={`workflow-step-move-down-${stepId}`}
title={t("workflow.moveDown", "Move down")} title={t("app:workflow.moveDown", "Move down")}
> >
<ChevronDown /> <ChevronDown />
</button> </button>
@@ -658,7 +664,7 @@ export function WorkflowResultsTab({
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm"
onClick={() => removeWorkflowStep(stepId)} onClick={() => removeWorkflowStep(stepId)}
data-testid={`workflow-step-remove-${stepId}`} data-testid={`workflow-step-remove-${stepId}`}
title={t("workflow.remove", "Remove")} title={t("app:workflow.remove", "Remove")}
> >
<X /> <X />
</button> </button>
@@ -677,7 +683,7 @@ export function WorkflowResultsTab({
return ( return (
<div className="workflow-results-loading" data-testid="workflow-results-loading"> <div className="workflow-results-loading" data-testid="workflow-results-loading">
<div className="workflow-results-spinner" /> <div className="workflow-results-spinner" />
<span>{t("workflow.loadingResults", "Loading workflow results…")}</span> <span>{t("app:workflow.loadingResults", "Loading workflow results…")}</span>
</div> </div>
); );
} }
@@ -685,9 +691,9 @@ export function WorkflowResultsTab({
if (!hasResults) { if (!hasResults) {
return ( return (
<div className="workflow-results-empty" data-testid="workflow-results-empty"> <div className="workflow-results-empty" data-testid="workflow-results-empty">
<p>{t("workflow.noStepsConfigured", "No workflow steps configured for this task.")}</p> <p>{t("app:workflow.noStepsConfigured", "No workflow steps configured for this task.")}</p>
<p className="workflow-results-empty-hint"> <p className="workflow-results-empty-hint">
{t("workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")} {t("app:workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
</p> </p>
</div> </div>
); );
@@ -699,26 +705,26 @@ export function WorkflowResultsTab({
const skipped = results.filter((r) => r.status === "skipped").length; const skipped = results.filter((r) => r.status === "skipped").length;
const pending = results.filter((r) => r.status === "pending").length; const pending = results.filter((r) => r.status === "pending").length;
const summaryParts: string[] = [t("workflow.summaryStepCount", { count: results.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })]; const summaryParts: string[] = [t("app:workflow.summaryStepCount", { count: results.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })];
if (passed > 0) summaryParts.push(t("workflow.summaryPassed", "{{count}} passed", { count: passed })); if (passed > 0) summaryParts.push(t("app:workflow.summaryPassed", "{{count}} passed", { count: passed }));
if (failed > 0) summaryParts.push(t("workflow.summaryFailed", "{{count}} failed", { count: failed })); if (failed > 0) summaryParts.push(t("app:workflow.summaryFailed", "{{count}} failed", { count: failed }));
if (advisoryFailures.length > 0) summaryParts.push(t("workflow.summaryAdvisory", "{{count}} advisory", { count: advisoryFailures.length })); if (advisoryFailures.length > 0) summaryParts.push(t("app:workflow.summaryAdvisory", "{{count}} advisory", { count: advisoryFailures.length }));
if (skipped > 0) summaryParts.push(t("workflow.summarySkipped", "{{count}} skipped", { count: skipped })); if (skipped > 0) summaryParts.push(t("app:workflow.summarySkipped", "{{count}} skipped", { count: skipped }));
if (pending > 0) summaryParts.push(t("workflow.summaryRunning", "{{count}} running", { count: pending })); if (pending > 0) summaryParts.push(t("app:workflow.summaryRunning", "{{count}} running", { count: pending }));
return ( return (
<div className="workflow-results-list" data-testid="workflow-results-list"> <div className="workflow-results-list" data-testid="workflow-results-list">
<div className="workflow-results-summary-bar" data-testid="workflow-results-summary"> <div className="workflow-results-summary-bar" data-testid="workflow-results-summary">
{summaryParts.join(t("workflow.summarySeparator", " · "))} {summaryParts.join(t("app:workflow.summarySeparator", " · "))}
</div> </div>
{advisoryFailures.length > 0 && ( {advisoryFailures.length > 0 && (
<div className="workflow-polish-notes" data-testid="workflow-polish-notes"> <div className="workflow-polish-notes" data-testid="workflow-polish-notes">
<h4>{t("workflow.polishNotes", "Polish notes")}</h4> <h4>{t("app:workflow.polishNotes", "Polish notes")}</h4>
<p>{t("workflow.advisoryExplanation", "Advisory workflow steps flagged non-blocking improvements:")}</p> <p>{t("app:workflow.advisoryExplanation", "Advisory workflow steps flagged non-blocking improvements:")}</p>
<ul> <ul>
{advisoryFailures.map((result, index) => ( {advisoryFailures.map((result, index) => (
<li key={`advisory-${result.workflowStepId}-${index}`}> <li key={`advisory-${result.workflowStepId}-${index}`}>
<strong>{result.workflowStepName}:</strong> {result.output || t("workflow.needsReview", "Needs follow-up review.")} <strong>{result.workflowStepName}:</strong> {result.output || t("app:workflow.needsReview", "Needs follow-up review.")}
</li> </li>
))} ))}
</ul> </ul>
@@ -758,7 +764,7 @@ export function WorkflowResultsTab({
{result.notes && result.status !== "pending" && ( {result.notes && result.status !== "pending" && (
<div className="workflow-result-notes" data-testid={`workflow-result-notes-${result.workflowStepId}`}> <div className="workflow-result-notes" data-testid={`workflow-result-notes-${result.workflowStepId}`}>
<span className="workflow-result-notes-label">{t("workflow.notes", "Notes:")} </span> <span className="workflow-result-notes-label">{t("app:workflow.notes", "Notes:")} </span>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}> <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{result.notes} {result.notes}
</ReactMarkdown> </ReactMarkdown>
@@ -767,7 +773,7 @@ export function WorkflowResultsTab({
<div className="workflow-result-meta"> <div className="workflow-result-meta">
{result.startedAt && ( {result.startedAt && (
<span className="workflow-result-timestamp">{t("workflow.started", "Started:")} {formatTimestamp(result.startedAt)}</span> <span className="workflow-result-timestamp">{t("app:workflow.started", "Started:")} {formatTimestamp(result.startedAt)}</span>
)} )}
{result.completedAt && ( {result.completedAt && (
<span className="workflow-result-duration">{formatDuration(result.startedAt, result.completedAt)}</span> <span className="workflow-result-duration">{formatDuration(result.startedAt, result.completedAt)}</span>
@@ -785,14 +791,14 @@ export function WorkflowResultsTab({
) : result.output ? ( ) : result.output ? (
<div className="workflow-result-output-section"> <div className="workflow-result-output-section">
<div className="workflow-result-output-header"> <div className="workflow-result-output-header">
<span className="workflow-result-output-label">{t("workflow.output", "Output:")} </span> <span className="workflow-result-output-label">{t("app:workflow.output", "Output:")} </span>
<button <button
type="button" type="button"
className="btn btn-sm workflow-result-toggle" className="btn btn-sm workflow-result-toggle"
onClick={() => toggleOutput(result.workflowStepId)} onClick={() => toggleOutput(result.workflowStepId)}
data-testid={`workflow-result-toggle-${result.workflowStepId}`} data-testid={`workflow-result-toggle-${result.workflowStepId}`}
> >
{isExpanded ? t("workflow.hideOutput", "Hide output") : t("workflow.showOutput", "Show output")} {isExpanded ? t("app:workflow.hideOutput", "Hide output") : t("app:workflow.showOutput", "Show output")}
</button> </button>
{!isExpanded && ( {!isExpanded && (
<span <span
@@ -809,16 +815,16 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-result-mode-toggle" className="btn btn-sm workflow-result-mode-toggle"
onClick={() => toggleRenderMode(result.workflowStepId)} onClick={() => toggleRenderMode(result.workflowStepId)}
data-testid={`workflow-result-mode-toggle-${result.workflowStepId}`} data-testid={`workflow-result-mode-toggle-${result.workflowStepId}`}
title={(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("workflow.switchToPlain", "Switch to plain text") : t("workflow.switchToMarkdown", "Switch to markdown")} title={(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("app:workflow.switchToPlain", "Switch to plain text") : t("app:workflow.switchToMarkdown", "Switch to markdown")}
> >
{(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("workflow.markdown", "Markdown") : t("workflow.plain", "Plain")} {(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("app:workflow.markdown", "Markdown") : t("app:workflow.plain", "Plain")}
</button> </button>
<button <button
type="button" type="button"
className="btn btn-icon btn-sm workflow-result-expand-toggle" className="btn btn-icon btn-sm workflow-result-expand-toggle"
onClick={() => openExpandedView(result.workflowStepId)} onClick={() => openExpandedView(result.workflowStepId)}
data-testid={`workflow-result-expand-${result.workflowStepId}`} data-testid={`workflow-result-expand-${result.workflowStepId}`}
title={t("workflow.expandOutput", "Expand output")} title={t("app:workflow.expandOutput", "Expand output")}
> >
<Maximize2 size={12} /> <Maximize2 size={12} />
</button> </button>
@@ -858,18 +864,18 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-results-edit-toggle" className="btn btn-sm workflow-results-edit-toggle"
onClick={() => setIsEditing((prev) => !prev)} onClick={() => setIsEditing((prev) => !prev)}
data-testid="workflow-steps-edit-toggle" data-testid="workflow-steps-edit-toggle"
aria-label={isEditing ? t("workflow.doneEditingAriaLabel", "Done editing workflow steps") : t("workflow.editAriaLabel", "Edit workflow steps")} aria-label={isEditing ? t("app:workflow.doneEditingAriaLabel", "Done editing workflow steps") : t("app:workflow.editAriaLabel", "Edit workflow steps")}
title={isEditing ? t("workflow.done", "Done") : t("workflow.edit", "Edit")} title={isEditing ? t("app:workflow.done", "Done") : t("app:workflow.edit", "Edit")}
> >
{isEditing ? ( {isEditing ? (
<> <>
<Check size={14} /> <Check size={14} />
{t("workflow.done", "Done")} {t("app:workflow.done", "Done")}
</> </>
) : ( ) : (
<> <>
<Pencil size={14} /> <Pencil size={14} />
{t("workflow.edit", "Edit")} {t("app:workflow.edit", "Edit")}
</> </>
)} )}
</button> </button>
@@ -891,7 +897,7 @@ export function WorkflowResultsTab({
setInputText(""); setInputText("");
setSubmitted(true); setSubmitted(true);
} catch (err) { } catch (err) {
setResumeError(getErrorMessage(err) || "Failed to resume task"); setResumeError(getErrorMessage(err) || t("app:workflow.resumeTaskError", "Failed to resume task"));
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@@ -905,7 +911,7 @@ export function WorkflowResultsTab({
await approveTaskWorkflowCli(taskId, projectId); await approveTaskWorkflowCli(taskId, projectId);
setSubmitted(true); setSubmitted(true);
} catch (err) { } catch (err) {
setResumeError(getErrorMessage(err) || "Failed to approve command"); setResumeError(getErrorMessage(err) || t("app:workflow.approveCommandError", "Failed to approve command"));
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@@ -915,16 +921,16 @@ export function WorkflowResultsTab({
<div className="workflow-results-tab" data-task-id={taskId}> <div className="workflow-results-tab" data-task-id={taskId}>
{isAwaitingInput && ( {isAwaitingInput && (
<div className="workflow-input-banner" role="alert"> <div className="workflow-input-banner" role="alert">
<strong>Waiting for your input</strong> <strong>{t("app:workflow.awaitingInputTitle", "Waiting for your input")}</strong>
<span>{parseWorkflowInputQuestion(taskPausedReason)}</span> <span>{parseWorkflowInputQuestion(taskPausedReason, t)}</span>
{submitted ? ( {submitted ? (
<span className="workflow-input-resuming">Resuming…</span> <span className="workflow-input-resuming">{t("app:workflow.resuming", "Resuming…")}</span>
) : ( ) : (
<div className="workflow-input-actions"> <div className="workflow-input-actions">
<textarea <textarea
className="workflow-input-textarea" className="workflow-input-textarea"
rows={3} rows={3}
placeholder="Type your reply…" placeholder={t("app:workflow.inputPlaceholder", "Type your reply…")}
value={inputText} value={inputText}
onChange={(e) => setInputText(e.target.value)} onChange={(e) => setInputText(e.target.value)}
disabled={submitting} disabled={submitting}
@@ -935,7 +941,7 @@ export function WorkflowResultsTab({
onClick={handleSubmitInput} onClick={handleSubmitInput}
disabled={submitting || !inputText.trim()} disabled={submitting || !inputText.trim()}
> >
{submitting ? "Submitting…" : "Submit & resume"} {submitting ? t("app:workflow.submitting", "Submitting…") : t("app:workflow.submitAndResume", "Submit & resume")}
</button> </button>
{resumeError && ( {resumeError && (
<span className="workflow-input-error" role="alert">{resumeError}</span> <span className="workflow-input-error" role="alert">{resumeError}</span>
@@ -946,13 +952,13 @@ export function WorkflowResultsTab({
)} )}
{isAwaitingCliApproval && ( {isAwaitingCliApproval && (
<div className="workflow-input-banner workflow-input-banner--approval" role="alert"> <div className="workflow-input-banner workflow-input-banner--approval" role="alert">
<strong>Approve CLI command?</strong> <strong>{t("app:workflow.cliApprovalTitle", "Approve CLI command?")}</strong>
<span className="workflow-input-approval-warning"> <span className="workflow-input-approval-warning">
This command will run in the task worktree. Approving trusts this exact command for future runs. {t("app:workflow.cliApprovalWarning", "This command will run in the task worktree. Approving trusts this exact command for future runs.")}
</span> </span>
<pre className="workflow-input-approval-command"><code>{parseCliApprovalCommand(taskPausedReason)}</code></pre> <pre className="workflow-input-approval-command"><code>{parseCliApprovalCommand(taskPausedReason)}</code></pre>
{submitted ? ( {submitted ? (
<span className="workflow-input-resuming">Resuming…</span> <span className="workflow-input-resuming">{t("app:workflow.resuming", "Resuming…")}</span>
) : ( ) : (
<div className="workflow-input-actions"> <div className="workflow-input-actions">
<button <button
@@ -961,9 +967,9 @@ export function WorkflowResultsTab({
onClick={handleApproveCli} onClick={handleApproveCli}
disabled={submitting} disabled={submitting}
> >
{submitting ? "Approving…" : "Approve & run"} {submitting ? t("app:workflow.approving", "Approving…") : t("app:workflow.approveAndRun", "Approve & run")}
</button> </button>
<span className="workflow-input-keep-paused">To reject, keep the task paused and do not approve.</span> <span className="workflow-input-keep-paused">{t("app:workflow.cliApprovalRejectHint", "To reject, keep the task paused and do not approve.")}</span>
{resumeError && ( {resumeError && (
<span className="workflow-input-error" role="alert">{resumeError}</span> <span className="workflow-input-error" role="alert">{resumeError}</span>
)} )}
@@ -973,28 +979,28 @@ export function WorkflowResultsTab({
)} )}
<section className="card workflow-state-summary" data-testid="workflow-state-summary"> <section className="card workflow-state-summary" data-testid="workflow-state-summary">
<div className="workflow-state-summary__header"> <div className="workflow-state-summary__header">
<h4>{t("workflow.overview", "Workflow overview")}</h4> <h4>{t("app:workflow.overview", "Workflow overview")}</h4>
</div> </div>
<div className="workflow-state-summary__grid"> <div className="workflow-state-summary__grid">
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-name"> <div className="workflow-state-summary__item" data-testid="workflow-state-summary-name">
<span className="workflow-state-summary__label">{t("workflow.workflowName", "Workflow")}</span> <span className="workflow-state-summary__label">{t("app:workflow.workflowName", "Workflow")}</span>
<span className="workflow-state-summary__value">{workflowName}</span> <span className="workflow-state-summary__value">{workflowName}</span>
</div> </div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-phase"> <div className="workflow-state-summary__item" data-testid="workflow-state-summary-phase">
<span className="workflow-state-summary__label">{t("workflow.executionPhase", "Execution phase")}</span> <span className="workflow-state-summary__label">{t("app:workflow.executionPhase", "Execution phase")}</span>
<span className={`workflow-result-badge ${executionPhase.badgeClass}`} data-testid={`workflow-phase-badge-${executionPhase.testId}`}> <span className={`workflow-result-badge ${executionPhase.badgeClass}`} data-testid={`workflow-phase-badge-${executionPhase.testId}`}>
{executionPhase.label} {executionPhase.label}
</span> </span>
</div> </div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-aggregate"> <div className="workflow-state-summary__item" data-testid="workflow-state-summary-aggregate">
<span className="workflow-state-summary__label">{t("workflow.aggregateResult", "Aggregate result")}</span> <span className="workflow-state-summary__label">{t("app:workflow.aggregateResult", "Aggregate result")}</span>
<span className={`workflow-result-badge ${aggregateResult.badgeClass}`} data-testid={`workflow-aggregate-badge-${aggregateResult.testId}`}> <span className={`workflow-result-badge ${aggregateResult.badgeClass}`} data-testid={`workflow-aggregate-badge-${aggregateResult.testId}`}>
{aggregateResult.label} {aggregateResult.label}
</span> </span>
</div> </div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-count"> <div className="workflow-state-summary__item" data-testid="workflow-state-summary-count">
<span className="workflow-state-summary__label">{t("workflow.stepProgress", "Step count")}</span> <span className="workflow-state-summary__label">{t("app:workflow.stepProgress", "Step count")}</span>
<span className="workflow-state-summary__value">{t("workflow.stepProgressValue", "{{completed}} of {{total}} steps completed", { completed: completedStepCount, total: results.length })}</span> <span className="workflow-state-summary__value">{t("app:workflow.stepProgressValue", "{{completed}} of {{total}} steps completed", { completed: completedStepCount, total: results.length })}</span>
</div> </div>
</div> </div>
</section> </section>
@@ -1008,19 +1014,19 @@ export function WorkflowResultsTab({
> >
<span className="workflow-disclosure__title"> <span className="workflow-disclosure__title">
{graphExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />} {graphExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />}
{t("workflow.graph", "Workflow graph")} {t("app:workflow.graph", "Workflow graph")}
</span> </span>
</button> </button>
{graphExpanded && ( {graphExpanded && (
<div className="workflow-disclosure__content"> <div className="workflow-disclosure__content">
{!selectedWorkflowId ? ( {!selectedWorkflowId ? (
<p className="workflow-disclosure__empty" data-testid="workflow-graph-empty"> <p className="workflow-disclosure__empty" data-testid="workflow-graph-empty">
{t("workflow.noWorkflowAssigned", "No workflow assigned")} {t("app:workflow.noWorkflowAssigned", "No workflow assigned")}
</p> </p>
) : workflowGraphLoading && !graphWorkflow ? ( ) : workflowGraphLoading && !graphWorkflow ? (
<div className="workflow-results-loading" data-testid="workflow-graph-loading"> <div className="workflow-results-loading" data-testid="workflow-graph-loading">
<div className="workflow-results-spinner" /> <div className="workflow-results-spinner" />
<span>{t("workflow.loadingGraph", "Loading workflow graph…")}</span> <span>{t("app:workflow.loadingGraph", "Loading workflow graph…")}</span>
</div> </div>
) : graphFlow ? ( ) : graphFlow ? (
<div className="workflow-graph-preview" data-testid="workflow-graph-preview"> <div className="workflow-graph-preview" data-testid="workflow-graph-preview">
@@ -1042,7 +1048,7 @@ export function WorkflowResultsTab({
</div> </div>
) : ( ) : (
<p className="workflow-disclosure__empty" data-testid="workflow-graph-unavailable"> <p className="workflow-disclosure__empty" data-testid="workflow-graph-unavailable">
{t("workflow.graphUnavailable", "Workflow graph unavailable")} {t("app:workflow.graphUnavailable", "Workflow graph unavailable")}
</p> </p>
)} )}
</div> </div>
@@ -1051,7 +1057,7 @@ export function WorkflowResultsTab({
<section className="card workflow-management" data-testid="workflow-management-section"> <section className="card workflow-management" data-testid="workflow-management-section">
<div className="workflow-management__header"> <div className="workflow-management__header">
<h4>{t("workflow.workflowName", "Workflow")}</h4> <h4>{t("app:workflow.workflowName", "Workflow")}</h4>
{canEdit && selectedWorkflowId && onEditWorkflow && ( {canEdit && selectedWorkflowId && onEditWorkflow && (
<button <button
type="button" type="button"
@@ -1060,7 +1066,7 @@ export function WorkflowResultsTab({
data-testid="workflow-edit-button" data-testid="workflow-edit-button"
> >
<Pencil aria-hidden /> <Pencil aria-hidden />
{t("workflow.editWorkflow", "Edit workflow")} {t("app:workflow.editWorkflow", "Edit workflow")}
</button> </button>
)} )}
</div> </div>
@@ -1068,7 +1074,7 @@ export function WorkflowResultsTab({
value={selectedWorkflowId} value={selectedWorkflowId}
onChange={handleWorkflowSelect} onChange={handleWorkflowSelect}
projectId={projectId} projectId={projectId}
label="Custom workflow" label={t("app:workflow.customWorkflowLabel", "Custom workflow")}
disabled={!canEdit} disabled={!canEdit}
/> />
</section> </section>
@@ -1082,16 +1088,16 @@ export function WorkflowResultsTab({
> >
<span className="workflow-disclosure__title"> <span className="workflow-disclosure__title">
{modelSettingsExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />} {modelSettingsExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />}
{t("workflow.modelSettings", "Model settings")} {t("app:workflow.modelSettings", "Model settings")}
</span> </span>
</button> </button>
{modelSettingsExpanded && ( {modelSettingsExpanded && (
<div className="workflow-disclosure__content workflow-state-summary__grid" data-testid="workflow-model-settings-content"> <div className="workflow-disclosure__content workflow-state-summary__grid" data-testid="workflow-model-settings-content">
{[ {[
{ key: "executor", label: t("models.targetLabels.executor", "Executor"), value: formatModelValue(effectiveExecutor), provider: effectiveExecutor?.provider }, { key: "executor", label: t("models.targetLabels.executor", "Executor"), value: formatModelValue(effectiveExecutor, t), provider: effectiveExecutor?.provider },
{ key: "reviewer", label: t("models.targetLabels.validator", "Reviewer"), value: formatModelValue(effectiveValidator), provider: effectiveValidator?.provider }, { key: "reviewer", label: t("models.targetLabels.validator", "Reviewer"), value: formatModelValue(effectiveValidator, t), provider: effectiveValidator?.provider },
{ key: "planning", label: t("models.targetLabels.planning", "Planning"), value: formatModelValue(effectivePlanning), provider: effectivePlanning?.provider }, { key: "planning", label: t("models.targetLabels.planning", "Planning"), value: formatModelValue(effectivePlanning, t), provider: effectivePlanning?.provider },
{ key: "thinking", label: t("workflow.thinkingLevel", "Thinking level"), value: task?.thinkingLevel || "Default" }, { key: "thinking", label: t("app:workflow.thinkingLevel", "Thinking level"), value: task?.thinkingLevel || "Default" },
].map((item) => ( ].map((item) => (
<div className="workflow-state-summary__item" key={item.key} data-testid={`workflow-model-setting-${item.key}`}> <div className="workflow-state-summary__item" key={item.key} data-testid={`workflow-model-setting-${item.key}`}>
<span className="workflow-state-summary__label">{item.label}</span> <span className="workflow-state-summary__label">{item.label}</span>
@@ -1108,9 +1114,9 @@ export function WorkflowResultsTab({
<div className="workflow-configured-steps" data-testid="workflow-configured-steps"> <div className="workflow-configured-steps" data-testid="workflow-configured-steps">
<div className="workflow-configured-header" data-testid="workflow-configured-header"> <div className="workflow-configured-header" data-testid="workflow-configured-header">
<div className="workflow-configured-title-row"> <div className="workflow-configured-title-row">
<h4>{t("workflow.configuredSteps", "Configured Workflow Steps")}</h4> <h4>{t("app:workflow.configuredSteps", "Configured Workflow Steps")}</h4>
<span className="workflow-configured-count" data-testid="workflow-configured-count"> <span className="workflow-configured-count" data-testid="workflow-configured-count">
{t("workflow.stepCount", { count: configuredSteps.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })} {t("app:workflow.stepCount", { count: configuredSteps.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })}
</span> </span>
</div> </div>
{editButton} {editButton}
@@ -1133,7 +1139,7 @@ export function WorkflowResultsTab({
</div> </div>
<p className="workflow-results-empty-hint"> <p className="workflow-results-empty-hint">
{t("workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")} {t("app:workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
</p> </p>
{renderEditor()} {renderEditor()}
@@ -1142,7 +1148,7 @@ export function WorkflowResultsTab({
<> <>
{showEditHeaderForResults && ( {showEditHeaderForResults && (
<div className="workflow-results-edit-header" data-testid="workflow-results-edit-header"> <div className="workflow-results-edit-header" data-testid="workflow-results-edit-header">
<h4>{t("workflow.steps", "Workflow Steps")}</h4> <h4>{t("app:workflow.steps", "Workflow Steps")}</h4>
{editButton} {editButton}
</div> </div>
)} )}
@@ -1179,9 +1185,9 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-result-mode-toggle" className="btn btn-sm workflow-result-mode-toggle"
onClick={() => toggleRenderMode(result.workflowStepId)} onClick={() => toggleRenderMode(result.workflowStepId)}
data-testid="workflow-output-modal-mode-toggle" data-testid="workflow-output-modal-mode-toggle"
title={renderMode === "markdown" ? t("workflow.switchToPlain", "Switch to plain text") : t("workflow.switchToMarkdown", "Switch to markdown")} title={renderMode === "markdown" ? t("app:workflow.switchToPlain", "Switch to plain text") : t("app:workflow.switchToMarkdown", "Switch to markdown")}
> >
{renderMode === "markdown" ? t("workflow.markdown", "Markdown") : t("workflow.plain", "Plain")} {renderMode === "markdown" ? t("app:workflow.markdown", "Markdown") : t("app:workflow.plain", "Plain")}
</button> </button>
<button <button
type="button" type="button"

View File

@@ -8,6 +8,11 @@ import { fetchWorkflow, fetchWorkflows, fetchProjectDefaultWorkflow, setProjectD
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { useConfirm } from "../hooks/useConfirm"; import { useConfirm } from "../hooks/useConfirm";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 localizes this workflow surface through t() and authored en catalog keys so hardcoded user-facing copy does not need a lint.ignore deferral.
*/
interface WorkflowSelectorProps { interface WorkflowSelectorProps {
/** Currently selected workflow id, or null for none. */ /** Currently selected workflow id, or null for none. */
value: string | null; value: string | null;
@@ -63,7 +68,7 @@ export function WorkflowSelector({
}) })
.catch((err) => { .catch((err) => {
if (!cancelled) setWorkflows([]); if (!cancelled) setWorkflows([]);
addToast?.(getErrorMessage(err) || "Failed to load workflows", "error"); addToast?.(getErrorMessage(err) || t("workflowSelector.loadFailed", "Failed to load workflows"), "error");
}) })
.finally(() => { .finally(() => {
if (!cancelled) setLoading(false); if (!cancelled) setLoading(false);
@@ -93,7 +98,7 @@ export function WorkflowSelector({
try { try {
await onChange(workflowId); await onChange(workflowId);
} catch (err) { } catch (err) {
addToast?.(getErrorMessage(err) || "Failed to apply workflow", "error"); addToast?.(getErrorMessage(err) || t("workflowSelector.applyFailed", "Failed to apply workflow"), "error");
} finally { } finally {
setApplying(false); setApplying(false);
} }
@@ -115,7 +120,7 @@ export function WorkflowSelector({
disabled={disabled || loading || applying} disabled={disabled || loading || applying}
onChange={(e) => void handleChange(e.target.value)} onChange={(e) => void handleChange(e.target.value)}
> >
<option value="">None</option> <option value="">{t("workflowSelector.none", "None")}</option>
{workflows.map((w) => ( {workflows.map((w) => (
<option key={w.id} value={w.id}> <option key={w.id} value={w.id}>
{w.name} {w.name}
@@ -125,7 +130,7 @@ export function WorkflowSelector({
</div> </div>
{onManage && ( {onManage && (
<button type="button" className="workflow-selector-manage" onClick={onManage}> <button type="button" className="workflow-selector-manage" onClick={onManage}>
Manage… {t("workflowSelector.manage", "Manage…")}
</button> </button>
)} )}
</div> </div>
@@ -140,6 +145,7 @@ interface ProjectDefaultWorkflowFieldProps {
/** Self-contained project-default workflow picker for the settings modal. */ /** Self-contained project-default workflow picker for the settings modal. */
export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: ProjectDefaultWorkflowFieldProps) { export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: ProjectDefaultWorkflowFieldProps) {
const { t } = useTranslation("app");
const [value, setValue] = useState<string | null>(null); const [value, setValue] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -162,9 +168,9 @@ export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: P
async (workflowId: string | null) => { async (workflowId: string | null) => {
const res = await setProjectDefaultWorkflow(workflowId, projectId); const res = await setProjectDefaultWorkflow(workflowId, projectId);
setValue(res.workflowId); setValue(res.workflowId);
addToast?.(workflowId ? "Default workflow set" : "Default workflow cleared", "success"); addToast?.(workflowId ? t("workflowSelector.defaultSet", "Default workflow set") : t("workflowSelector.defaultCleared", "Default workflow cleared"), "success");
}, },
[projectId, addToast], [projectId, addToast, t],
); );
return ( return (
@@ -173,7 +179,7 @@ export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: P
onChange={handleChange} onChange={handleChange}
projectId={projectId} projectId={projectId}
addToast={addToast} addToast={addToast}
label="Default workflow for new tasks" label={t("workflowSelector.defaultWorkflowLabel", "Default workflow for new tasks")}
onManage={onManage} onManage={onManage}
/> />
); );

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead." "workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
}, },
"todo": "To Do", "todo": "To Do",
"triage": "Triage" "triage": "Triage",
"workflow": {
"edit": "Edit workflows",
"new": "New workflow"
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "Abandon group", "abandonGroup": "Abandon group",
@@ -1810,7 +1814,8 @@
"heading": "Comments", "heading": "Comments",
"placeholder": "Add a comment", "placeholder": "Add a comment",
"postingButton": "Posting…", "postingButton": "Posting…",
"updatedSuccess": "Comment updated" "updatedSuccess": "Comment updated",
"aiGuidance": "AI Guidance"
}, },
"commit": { "commit": {
"filesChanged_one": "Files Changed ({{count}})", "filesChanged_one": "Files Changed ({{count}})",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Re-run preflight", "rerunPreflight": "Re-run preflight",
"revertToAi": "Revert to AI version", "revertToAi": "Revert to AI version",
"titleLabel": "Title", "titleLabel": "Title",
"usingTemplate": "Using <code>.github/pull_request_template.md</code>" "usingTemplate": "Using <code>.github/pull_request_template.md</code>",
"assignees": "Assignees",
"dismissConflictResolutionError": "Dismiss conflict resolution error",
"dismissPushBranchError": "Dismiss push branch error",
"error": {
"actionOpen": "Action: open",
"actionRun": "Action: run",
"docs": "docs"
},
"generatingBody": "Generating AI body…",
"generatingTitle": "Generating AI title…",
"labels": "Labels",
"loadingOptions": "Loading PR options…",
"loadingPreflight": "Loading pre-flight checks…",
"noChangedFiles": "No changed files detected.",
"noCommits": "No commits found.",
"pushBranch": {
"button": "Push branch to remote",
"message": "Fusion will push this task's branch to origin so the PR can be created.",
"title": "Push branch to remote"
},
"resolveConflicts": {
"button": "Resolve conflicts with AI",
"message": "Fusion will use AI to resolve conflicts on this branch and push it.",
"title": "Resolve conflicts with AI"
},
"reviewers": "Reviewers",
"view": {
"agentDisagreed": "agent disagreed",
"agentReplyFix": "Agent reply — fix {{sha}}",
"approve": "Approve",
"autoMerge": "Auto-merge",
"close": "Close",
"confirmMerge": "Confirm merge",
"creating": "Creating PR…",
"creationFailed": "PR creation failed",
"loading": "Loading PR…",
"merge": "Merge",
"mergeableLabel": "Mergeable:",
"mergeDisabledUntilVerified": "Merge is disabled until GitHub verifies this PR",
"none": "none",
"noReviewThreads": "No review threads.",
"requestRetry": "Request retry",
"resolveConflictsBeforeMerge": "Resolve conflicts on GitHub before merging",
"resolveConflictsOnGithub": "Resolve conflicts on GitHub",
"responseAlreadyInProgress": "A response run is already in progress",
"responsePending_one": "Response run in progress — {{count}} threads pending",
"responsePending_other": "Response run in progress — {{count}} threads pending",
"retryCreation": "Retry PR creation",
"reviewLabel": "Review:",
"threadFixed": "fixed",
"threadPending": "pending",
"verifyingGithub": "Verifying with GitHub…"
}
}, },
"preview": { "preview": {
"blockedDescription": "You can view the preview in a separate browser tab.", "blockedDescription": "You can view the preview in a separate browser tab.",
@@ -5553,7 +5611,8 @@
"totalSize": "total size", "totalSize": "total size",
"view": "View ", "view": "View ",
"whenEnabledProjectAndAgentMemoryFilesAre": "When enabled, project and agent memory files are backed up automatically on a schedule.", "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" "whenEnabledTheDatabaseIsBackedUpAutomatically": "When enabled, the database is backed up automatically on a schedule",
"createFailed": "Failed to create backup"
}, },
"clearToDefault": "Reset to default", "clearToDefault": "Reset to default",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "Help", "help": "Help",
"version": "Version {{version}}" "version": "Version {{version}}",
"checkUpdates": "Check for updates",
"helpDiscussions": "Help and discussions"
}, },
"general": { "general": {
"25": "25", "25": "25",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local " "whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "Join our Discord",
"star": "Star",
"starFusion": "Star Fusion on GitHub"
}, },
"importExport": { "importExport": {
"confirmImport": "Confirm Import", "confirmImport": "Confirm Import",
@@ -5756,7 +5820,35 @@
"importing": "Importing…", "importing": "Importing…",
"importTitle": "Import Settings", "importTitle": "Import Settings",
"loadingFile": "Loading…", "loadingFile": "Loading…",
"reviewPrompt": "Review the settings to be imported:" "reviewPrompt": "Review the settings to be imported:",
"counts": {
"global_one": "{{count}} global",
"global_other": "{{count}} global",
"project_one": "{{count}} project",
"project_other": "{{count}} project",
"workflowSettings_one": "{{count}} workflow setting value",
"workflowSettings_other": "{{count}} workflow setting value"
},
"exported": "Settings exported ({{scope}} scope)",
"exportFailed": "Failed to export settings",
"globalSettings": "Global Settings:",
"imported": "Imported {{counts}} setting(s)",
"importFailed": "Import failed",
"importFailedDetailed": "Failed to import settings",
"importScope": "Import Scope:",
"importTitleAttr": "Import settings from JSON file",
"invalidJson": "Invalid JSON file: {{error}}",
"mergeExisting": "Merge with existing settings (recommended)",
"projectSettings": "Project Settings:",
"replaceWarning": "If unchecked, existing settings will be replaced with imported values.",
"scopeBoth": "Both global and project settings",
"scopeGlobal": "Global settings only",
"scopeLabel": {
"all": "all",
"global": "global",
"project": "project"
},
"scopeProject": "Project settings only"
}, },
"jsonPlaceholder": "Enter JSON value...", "jsonPlaceholder": "Enter JSON value...",
"keepLocal": "Keep Local", "keepLocal": "Keep Local",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "Search memory with qmd", "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." "turnsDailyNotesIntoDREAMSMdAndPromotes": "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md.",
"qmdInstalled": "qmd installed successfully",
"qmdInstallFailed": "Failed to install qmd",
"qmdInstallUnavailable": "qmd install finished, but qmd is still unavailable"
}, },
"merge": { "merge": {
"abort": "Abort", "abort": "Abort",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ", "uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.", "uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
"useExisting": "Use Existing", "useExisting": "Use Existing",
"usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed." "usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed.",
"installationFailed": "Installation failed"
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "Advanced — external search providers", "advancedExternalSearchProviders": "Advanced — external search providers",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "Timeout in minutes for detecting stuck tasks. When a task&apos;s agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.", "timeoutInMinutesForDetectingStuckTasksWhen": "Timeout in minutes for detecting stuck tasks. When a task&apos;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", "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", "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." "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.",
"browseWorkspacePath": "Browse workspace path",
"overlapPickerNote": "Choose a file to ignore directly, or navigate into a folder and select the current directory."
}, },
"scope": { "scope": {
"globalBanner": "These settings are shared across all your Fusion projects.", "globalBanner": "These settings are shared across all your Fusion projects.",
@@ -6335,7 +6433,12 @@
"worktrunk": " worktrunk ", "worktrunk": " worktrunk ",
"worktrunkBinaryPath": "Worktrunk binary path", "worktrunkBinaryPath": "Worktrunk binary path",
"worktrunkFailureBehavior": "Worktrunk failure behavior", "worktrunkFailureBehavior": "Worktrunk failure behavior",
"worktrunkIntegration": "Worktrunk integration" "worktrunkIntegration": "Worktrunk integration",
"worktreesPickerNote": "Navigate to the folder where Fusion should create task worktrees, then select the current directory."
},
"fileBrowser": {
"currentDirectory": "Current directory:",
"projectRoot": "(project root)"
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Create tasks manually", "withoutGitHub1": "Create tasks manually",
"withoutGitHub2": "Describe work for AI agents", "withoutGitHub2": "Describe work for AI agents",
"withoutGitHub3": "Track progress on the board", "withoutGitHub3": "Track progress on the board",
"withoutGitHubHeading": "Without GitHub (available now):" "withoutGitHubHeading": "Without GitHub (available now):",
"brandLogo": "Fusion logo",
"brandName": "Fusion",
"setupCompleteTitle": "Setup Complete!"
}, },
"shell": { "shell": {
"activePill": "Active", "activePill": "Active",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Merging fixes…", "mergingFixes": "Merging fixes…",
"mergingPr": "Merging PR…", "mergingPr": "Merging PR…",
"startPrReview": "Start PR Review", "startPrReview": "Start PR Review",
"statusRefreshed": "PR status refreshed" "statusRefreshed": "PR status refreshed",
"label": "PR"
}, },
"priority": { "priority": {
"ariaLabel": "Task priority", "ariaLabel": "Task priority",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "Created by", "createdBy": "Created by",
"createdVia": "Created via" "createdVia": "Created via",
"parentTaskOf": "of"
}, },
"recoveryState": "Recovery state", "recoveryState": "Recovery state",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Switch to markdown", "switchToMarkdown": "Switch to markdown",
"switchToPlain": "Switch to plain text", "switchToPlain": "Switch to plain text",
"thinkingLevel": "Thinking level", "thinkingLevel": "Thinking level",
"workflowName": "Workflow" "workflowName": "Workflow",
"aggregateAdvisory": "Advisory",
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"approveAndRun": "Approve & run",
"approveCommandError": "Failed to approve command",
"approving": "Approving…",
"awaitingInputTitle": "Waiting for your input",
"cliApprovalRejectHint": "To reject, keep the task paused and do not approve.",
"cliApprovalTitle": "Approve CLI command?",
"cliApprovalWarning": "This command will run in the task worktree. Approving trusts this exact command for future runs.",
"customWorkflowFallback": "Custom workflow",
"customWorkflowLabel": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"inputPlaceholder": "Type your reply…",
"modelDefault": "Default",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"replyInComments": "Reply in the comments and unpause the task to continue.",
"resumeTaskError": "Failed to resume task",
"resuming": "Resuming…",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"submitAndResume": "Submit & resume",
"submitting": "Submitting…",
"waitingForOutput": "Waiting for agent output…"
}, },
"workflowColumns": { "workflowColumns": {
"add": "Add column", "add": "Add column",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "Collapse prompt editor", "collapsePrompt": "Collapse prompt editor",
"editingPrompt": "Editing Prompt", "editingPrompt": "Editing Prompt",
"expandPrompt": "Expand prompt editor" "expandPrompt": "Expand prompt editor",
"agent": "Agent",
"agentsLoadFailed": "Failed to load agents",
"autoApproveRequests": "Auto-approve requests",
"autoApproveRequestsNote": "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.",
"cliCommandNote": "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.",
"cliMode": "CLI mode",
"cliScript": "CLI / script",
"command": "Command",
"executor": "Executor",
"maxRetries": "Max retries",
"model": "Model",
"modelsLoadFailed": "Failed to load models",
"namedScript": "Named script",
"namedScriptNote": "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.",
"prompt": "Prompt",
"scriptName": "Script name",
"selectAgent": "— select agent —",
"selectSkill": "— select skill —",
"skill": "Skill",
"skillsLoadFailed": "Failed to load skills",
"skipFirstRunApproval": "Skip first-run approval (runs without pausing)",
"waitForUserInput": "Wait for user input",
"waitForUserInputNote": "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question."
}, },
"workflowFields": { "workflowFields": {
"add": "Add field", "add": "Add field",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Plugin steps", "templatesPluginSteps": "Plugin steps",
"templatesSection": "Templates", "templatesSection": "Templates",
"timeoutMs": "{{timeout}}ms", "timeoutMs": "{{timeout}}ms",
"trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out." "trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out.",
"conditionFailure": "failure",
"conditionSuccess": "success",
"nodeInspector": "Node",
"readOnlyDuplicateToEdit": "Read-only built-in — duplicate the workflow to edit nodes."
}, },
"workflows": { "workflows": {
"aiEdit": "Design with AI", "aiEdit": "Design with AI",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nodes", "templateNodeCount_other": "{{count}} nodes",
"templatePickerLabel": "Start from", "templatePickerLabel": "Start from",
"templateSectionBuiltin": "Built-in workflows", "templateSectionBuiltin": "Built-in workflows",
"templateSectionYours": "Your workflows" "templateSectionYours": "Your workflows",
"closeEditor": "Close workflow editor",
"duplicatedEditable": "Duplicated to \"{{name}}\" — editable",
"duplicateFailed": "Failed to duplicate workflow",
"loadFailed": "Failed to load workflows",
"loading": "Loading…",
"noneYet": "No workflows yet.",
"title": "Workflows"
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?", "switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
"switchActiveTitle": "Switch workflow?", "switchActiveTitle": "Switch workflow?",
"switchCancel": "Cancel", "switchCancel": "Cancel",
"switchConfirm": "Switch and abort" "switchConfirm": "Switch and abort",
"applyFailed": "Failed to apply workflow",
"defaultCleared": "Default workflow cleared",
"defaultSet": "Default workflow set",
"defaultWorkflowLabel": "Default workflow for new tasks",
"loadFailed": "Failed to load workflows",
"manage": "Manage…",
"none": "None"
}, },
"workflowSettings": { "workflowSettings": {
"add": "Add setting", "add": "Add setting",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk install request", "installRequestTitle": "Worktrunk install request",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "Version" "version": "Version"
},
"taskChat": {
"activePlaceholder": "Steer the currently executing agent",
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
"agentMessages": "{{label}} messages",
"arguments": "Arguments",
"collapseChat": "Collapse chat",
"donePlaceholder": "Start a refinement task for this completed task",
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
"entryCount_one": "{{count}} entry",
"entryCount_other": "{{count}} entries",
"error": "Error",
"errorCount_one": "{{count}} error",
"errorCount_other": "{{count}} errors",
"expandChat": "Expand chat to full modal",
"idleSessionHint": "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.",
"jumpToLatestMessage": "Jump to latest message",
"latest": "Latest",
"loadingAgentOutput": "Loading agent output…",
"loadingEarlierMessages": "Loading earlier messages…",
"loadPreviousMessages": "Load previous messages",
"message": "Message",
"messageActiveAgentSession": "Message active agent session",
"moreTools_one": ", +{{count}} more",
"moreTools_other": ", +{{count}} more",
"result": "Result",
"roles": {
"agent": "Agent",
"executor": "Executor",
"merger": "Merger",
"planner": "Planner",
"reviewer": "Reviewer"
},
"sending": "Sending",
"thinking": "Thinking",
"toolCall": "Tool call",
"toolCallCount_one": "{{count}} tool call",
"toolCallCount_other": "{{count}} tool calls",
"toolCallTo": "Tool call → {{label}}",
"toolError": "Tool error",
"toolNames": "Tool names",
"toolResult": "Tool result",
"you": "You",
"youMessage": "You message"
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": "Send"
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "↓ Declining", "trendDeclining": "↓ Declining",
@@ -78,6 +81,9 @@
"offline": "Offline", "offline": "Offline",
"online": "Online" "online": "Online"
}, },
"labels": {
"name": "Name"
},
"merge": { "merge": {
"unknown": "Unknown" "unknown": "Unknown"
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "Initial load", "refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual" "refreshSourceManual": "Manual"
}, },
"workflow": {
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"customWorkflowFallback": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "Waits for user input", "summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript", "summaryCodeDefault": "TypeScript",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "" "workflowMismatch": ""
}, },
"todo": "Por hacer", "todo": "Por hacer",
"triage": "Triaje" "triage": "Triaje",
"workflow": {
"edit": "",
"new": ""
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "", "abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "Comentarios", "heading": "Comentarios",
"placeholder": "Añadir un comentario", "placeholder": "Añadir un comentario",
"postingButton": "Publicando…", "postingButton": "Publicando…",
"updatedSuccess": "Comentario actualizado" "updatedSuccess": "Comentario actualizado",
"aiGuidance": ""
}, },
"commit": { "commit": {
"filesChanged_one": "", "filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Volver a ejecutar la comprobación previa", "rerunPreflight": "Volver a ejecutar la comprobación previa",
"revertToAi": "Revertir a la versión de IA", "revertToAi": "Revertir a la versión de IA",
"titleLabel": "Título", "titleLabel": "Título",
"usingTemplate": "Usando <code>.github/pull_request_template.md</code>" "usingTemplate": "Usando <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
}, },
"preview": { "preview": {
"blockedDescription": "Puede ver la vista previa en una pestaña del navegador separada.", "blockedDescription": "Puede ver la vista previa en una pestaña del navegador separada.",
@@ -5553,7 +5611,8 @@
"totalSize": "", "totalSize": "",
"view": "", "view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "", "whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "" "whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
}, },
"clearToDefault": "", "clearToDefault": "",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "Ayuda", "help": "Ayuda",
"version": "Versión {{version}}" "version": "Versión {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
}, },
"general": { "general": {
"25": "", "25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": "" "whenEnabledStartupRefreshesModelsThroughTheLocal": ""
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
}, },
"importExport": { "importExport": {
"confirmImport": "Confirmar importación", "confirmImport": "Confirmar importación",
@@ -5756,7 +5820,35 @@
"importing": "Importando…", "importing": "Importando…",
"importTitle": "Importar configuración", "importTitle": "Importar configuración",
"loadingFile": "Cargando…", "loadingFile": "Cargando…",
"reviewPrompt": "Revisa la configuración a importar:" "reviewPrompt": "Revisa la configuración a importar:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
}, },
"jsonPlaceholder": "Ingresa un valor JSON...", "jsonPlaceholder": "Ingresa un valor JSON...",
"keepLocal": "Mantener local", "keepLocal": "Mantener local",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "", "searchMemoryWithQmd": "",
"testing": "Probando…", "testing": "Probando…",
"testRetrieval": "Probar recuperación", "testRetrieval": "Probar recuperación",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "" "turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
}, },
"merge": { "merge": {
"abort": "", "abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "", "uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "", "uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "Usar existente", "useExisting": "Usar existente",
"usingQuickTunnel": "" "usingQuickTunnel": "",
"installationFailed": ""
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "", "advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "", "timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "", "whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "", "whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": "" "whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
}, },
"scope": { "scope": {
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.", "globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
@@ -6335,7 +6433,12 @@
"worktrunk": "", "worktrunk": "",
"worktrunkBinaryPath": "", "worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "", "worktrunkFailureBehavior": "",
"worktrunkIntegration": "" "worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Crear tareas manualmente", "withoutGitHub1": "Crear tareas manualmente",
"withoutGitHub2": "Describir trabajo para agentes de IA", "withoutGitHub2": "Describir trabajo para agentes de IA",
"withoutGitHub3": "Seguir el progreso en el tablero", "withoutGitHub3": "Seguir el progreso en el tablero",
"withoutGitHubHeading": "Sin GitHub (disponible ahora):" "withoutGitHubHeading": "Sin GitHub (disponible ahora):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
}, },
"shell": { "shell": {
"activePill": "Activo", "activePill": "Activo",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Fusionando correcciones…", "mergingFixes": "Fusionando correcciones…",
"mergingPr": "Fusionando PR…", "mergingPr": "Fusionando PR…",
"startPrReview": "Iniciar revisión del PR", "startPrReview": "Iniciar revisión del PR",
"statusRefreshed": "Estado del PR actualizado" "statusRefreshed": "Estado del PR actualizado",
"label": ""
}, },
"priority": { "priority": {
"ariaLabel": "Prioridad de la tarea", "ariaLabel": "Prioridad de la tarea",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "Creado por", "createdBy": "Creado por",
"createdVia": "Creado mediante" "createdVia": "Creado mediante",
"parentTaskOf": ""
}, },
"recoveryState": "Estado de recuperación", "recoveryState": "Estado de recuperación",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Cambiar a Markdown", "switchToMarkdown": "Cambiar a Markdown",
"switchToPlain": "Cambiar a texto sin formato", "switchToPlain": "Cambiar a texto sin formato",
"thinkingLevel": "", "thinkingLevel": "",
"workflowName": "" "workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
}, },
"workflowColumns": { "workflowColumns": {
"add": "", "add": "",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "", "collapsePrompt": "",
"editingPrompt": "", "editingPrompt": "",
"expandPrompt": "" "expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
}, },
"workflowFields": { "workflowFields": {
"add": "Agregar campo", "add": "Agregar campo",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Pasos de plugin", "templatesPluginSteps": "Pasos de plugin",
"templatesSection": "Plantillas", "templatesSection": "Plantillas",
"timeoutMs": "", "timeoutMs": "",
"trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo." "trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
}, },
"workflows": { "workflows": {
"aiEdit": "Diseñar con IA", "aiEdit": "Diseñar con IA",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nodos", "templateNodeCount_other": "{{count}} nodos",
"templatePickerLabel": "Comenzar desde", "templatePickerLabel": "Comenzar desde",
"templateSectionBuiltin": "Flujos de trabajo integrados", "templateSectionBuiltin": "Flujos de trabajo integrados",
"templateSectionYours": "Tus flujos de trabajo" "templateSectionYours": "Tus flujos de trabajo",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "", "switchActiveMessage": "",
"switchActiveTitle": "", "switchActiveTitle": "",
"switchCancel": "", "switchCancel": "",
"switchConfirm": "" "switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
}, },
"workflowSettings": { "workflowSettings": {
"add": "", "add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Solicitud de instalación de Worktrunk", "installRequestTitle": "Solicitud de instalación de Worktrunk",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "Versión" "version": "Versión"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": ""
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "", "trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "", "offline": "",
"online": "" "online": ""
}, },
"labels": {
"name": ""
},
"merge": { "merge": {
"unknown": "" "unknown": ""
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "", "refreshSourceInitialLoad": "",
"refreshSourceManual": "" "refreshSourceManual": ""
}, },
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "", "summaryAwaitInput": "",
"summaryCodeDefault": "", "summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "" "workflowMismatch": ""
}, },
"todo": "À faire", "todo": "À faire",
"triage": "Triage" "triage": "Triage",
"workflow": {
"edit": "",
"new": ""
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "", "abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "Commentaires", "heading": "Commentaires",
"placeholder": "Ajouter un commentaire", "placeholder": "Ajouter un commentaire",
"postingButton": "Publication en cours…", "postingButton": "Publication en cours…",
"updatedSuccess": "Commentaire mis à jour" "updatedSuccess": "Commentaire mis à jour",
"aiGuidance": ""
}, },
"commit": { "commit": {
"filesChanged_one": "", "filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Relancer les vérifications de pré-vol", "rerunPreflight": "Relancer les vérifications de pré-vol",
"revertToAi": "Revenir à la version IA", "revertToAi": "Revenir à la version IA",
"titleLabel": "Titre", "titleLabel": "Titre",
"usingTemplate": "Utilisation de <code>.github/pull_request_template.md</code>" "usingTemplate": "Utilisation de <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
}, },
"preview": { "preview": {
"blockedDescription": "Vous pouvez afficher l'aperçu dans un onglet du navigateur séparé.", "blockedDescription": "Vous pouvez afficher l'aperçu dans un onglet du navigateur séparé.",
@@ -5553,7 +5611,8 @@
"totalSize": "", "totalSize": "",
"view": "", "view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "", "whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "" "whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
}, },
"clearToDefault": "", "clearToDefault": "",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "Aide", "help": "Aide",
"version": "Version {{version}}" "version": "Version {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
}, },
"general": { "general": {
"25": "", "25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": "" "whenEnabledStartupRefreshesModelsThroughTheLocal": ""
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
}, },
"importExport": { "importExport": {
"confirmImport": "Confirmer l'importation", "confirmImport": "Confirmer l'importation",
@@ -5756,7 +5820,35 @@
"importing": "Importation…", "importing": "Importation…",
"importTitle": "Importer les paramètres", "importTitle": "Importer les paramètres",
"loadingFile": "Chargement…", "loadingFile": "Chargement…",
"reviewPrompt": "Vérifiez les paramètres à importer :" "reviewPrompt": "Vérifiez les paramètres à importer :",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
}, },
"jsonPlaceholder": "Entrez une valeur JSON...", "jsonPlaceholder": "Entrez une valeur JSON...",
"keepLocal": "Garder la version locale", "keepLocal": "Garder la version locale",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "", "searchMemoryWithQmd": "",
"testing": "Test en cours…", "testing": "Test en cours…",
"testRetrieval": "Tester la récupération", "testRetrieval": "Tester la récupération",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "" "turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
}, },
"merge": { "merge": {
"abort": "", "abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "", "uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "", "uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "Utiliser l'existant", "useExisting": "Utiliser l'existant",
"usingQuickTunnel": "" "usingQuickTunnel": "",
"installationFailed": ""
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "", "advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "", "timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "", "whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "", "whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": "" "whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
}, },
"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.",
@@ -6335,7 +6433,12 @@
"worktrunk": "", "worktrunk": "",
"worktrunkBinaryPath": "", "worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "", "worktrunkFailureBehavior": "",
"worktrunkIntegration": "" "worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Créer des tâches manuellement", "withoutGitHub1": "Créer des tâches manuellement",
"withoutGitHub2": "Décrire le travail pour les agents IA", "withoutGitHub2": "Décrire le travail pour les agents IA",
"withoutGitHub3": "Suivre la progression sur le tableau", "withoutGitHub3": "Suivre la progression sur le tableau",
"withoutGitHubHeading": "Sans GitHub (disponible maintenant) :" "withoutGitHubHeading": "Sans GitHub (disponible maintenant) :",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
}, },
"shell": { "shell": {
"activePill": "Actif", "activePill": "Actif",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Fusion des corrections…", "mergingFixes": "Fusion des corrections…",
"mergingPr": "Fusion du PR…", "mergingPr": "Fusion du PR…",
"startPrReview": "Démarrer la révision du PR", "startPrReview": "Démarrer la révision du PR",
"statusRefreshed": "Statut du PR actualisé" "statusRefreshed": "Statut du PR actualisé",
"label": ""
}, },
"priority": { "priority": {
"ariaLabel": "Priorité de la tâche", "ariaLabel": "Priorité de la tâche",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "Créé par", "createdBy": "Créé par",
"createdVia": "Créé via" "createdVia": "Créé via",
"parentTaskOf": ""
}, },
"recoveryState": "État de récupération", "recoveryState": "État de récupération",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Basculer vers Markdown", "switchToMarkdown": "Basculer vers Markdown",
"switchToPlain": "Basculer vers texte brut", "switchToPlain": "Basculer vers texte brut",
"thinkingLevel": "", "thinkingLevel": "",
"workflowName": "" "workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
}, },
"workflowColumns": { "workflowColumns": {
"add": "", "add": "",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "", "collapsePrompt": "",
"editingPrompt": "", "editingPrompt": "",
"expandPrompt": "" "expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
}, },
"workflowFields": { "workflowFields": {
"add": "Ajouter un champ", "add": "Ajouter un champ",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Étapes de plugin", "templatesPluginSteps": "Étapes de plugin",
"templatesSection": "Modèles", "templatesSection": "Modèles",
"timeoutMs": "", "timeoutMs": "",
"trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer." "trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
}, },
"workflows": { "workflows": {
"aiEdit": "Concevoir avec l'IA", "aiEdit": "Concevoir avec l'IA",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nœuds", "templateNodeCount_other": "{{count}} nœuds",
"templatePickerLabel": "Partir de", "templatePickerLabel": "Partir de",
"templateSectionBuiltin": "Workflows intégrés", "templateSectionBuiltin": "Workflows intégrés",
"templateSectionYours": "Vos workflows" "templateSectionYours": "Vos workflows",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "", "switchActiveMessage": "",
"switchActiveTitle": "", "switchActiveTitle": "",
"switchCancel": "", "switchCancel": "",
"switchConfirm": "" "switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
}, },
"workflowSettings": { "workflowSettings": {
"add": "", "add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Demande d'installation de Worktrunk", "installRequestTitle": "Demande d'installation de Worktrunk",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "Version" "version": "Version"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": ""
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "", "trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "", "offline": "",
"online": "" "online": ""
}, },
"labels": {
"name": ""
},
"merge": { "merge": {
"unknown": "" "unknown": ""
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "", "refreshSourceInitialLoad": "",
"refreshSourceManual": "" "refreshSourceManual": ""
}, },
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "", "summaryAwaitInput": "",
"summaryCodeDefault": "", "summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "" "workflowMismatch": ""
}, },
"todo": "할 일", "todo": "할 일",
"triage": "트리아지" "triage": "트리아지",
"workflow": {
"edit": "",
"new": ""
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "", "abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "댓글", "heading": "댓글",
"placeholder": "댓글 추가", "placeholder": "댓글 추가",
"postingButton": "게시 중…", "postingButton": "게시 중…",
"updatedSuccess": "댓글이 업데이트되었습니다" "updatedSuccess": "댓글이 업데이트되었습니다",
"aiGuidance": ""
}, },
"commit": { "commit": {
"filesChanged_one": "", "filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "사전 확인 다시 실행", "rerunPreflight": "사전 확인 다시 실행",
"revertToAi": "AI 버전으로 되돌리기", "revertToAi": "AI 버전으로 되돌리기",
"titleLabel": "제목", "titleLabel": "제목",
"usingTemplate": "<code>.github/pull_request_template.md</code> 템플릿 사용 중" "usingTemplate": "<code>.github/pull_request_template.md</code> 템플릿 사용 중",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
}, },
"preview": { "preview": {
"blockedDescription": "별도의 브라우저 탭에서 미리보기를 볼 수 있습니다.", "blockedDescription": "별도의 브라우저 탭에서 미리보기를 볼 수 있습니다.",
@@ -5553,7 +5611,8 @@
"totalSize": "", "totalSize": "",
"view": "", "view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "", "whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "" "whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
}, },
"clearToDefault": "", "clearToDefault": "",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "도움말", "help": "도움말",
"version": "버전 {{version}}" "version": "버전 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
}, },
"general": { "general": {
"25": "", "25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": "" "whenEnabledStartupRefreshesModelsThroughTheLocal": ""
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
}, },
"importExport": { "importExport": {
"confirmImport": "가져오기 확인", "confirmImport": "가져오기 확인",
@@ -5756,7 +5820,35 @@
"importing": "가져오는 중…", "importing": "가져오는 중…",
"importTitle": "설정 가져오기", "importTitle": "설정 가져오기",
"loadingFile": "불러오는 중…", "loadingFile": "불러오는 중…",
"reviewPrompt": "가져올 설정을 검토하세요:" "reviewPrompt": "가져올 설정을 검토하세요:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
}, },
"jsonPlaceholder": "JSON 값을 입력하세요...", "jsonPlaceholder": "JSON 값을 입력하세요...",
"keepLocal": "로컬 유지", "keepLocal": "로컬 유지",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "", "searchMemoryWithQmd": "",
"testing": "테스트 중…", "testing": "테스트 중…",
"testRetrieval": "검색 테스트", "testRetrieval": "검색 테스트",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "" "turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
}, },
"merge": { "merge": {
"abort": "", "abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "", "uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "", "uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "기존 사용", "useExisting": "기존 사용",
"usingQuickTunnel": "" "usingQuickTunnel": "",
"installationFailed": ""
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "", "advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "", "timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "", "whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "", "whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": "" "whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
}, },
"scope": { "scope": {
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.", "globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
@@ -6335,7 +6433,12 @@
"worktrunk": "", "worktrunk": "",
"worktrunkBinaryPath": "", "worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "", "worktrunkFailureBehavior": "",
"worktrunkIntegration": "" "worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "작업 수동 생성", "withoutGitHub1": "작업 수동 생성",
"withoutGitHub2": "AI 에이전트를 위한 작업 설명", "withoutGitHub2": "AI 에이전트를 위한 작업 설명",
"withoutGitHub3": "보드에서 진행 상황 추적", "withoutGitHub3": "보드에서 진행 상황 추적",
"withoutGitHubHeading": "GitHub 없이 (지금 사용 가능):" "withoutGitHubHeading": "GitHub 없이 (지금 사용 가능):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
}, },
"shell": { "shell": {
"activePill": "활성", "activePill": "활성",
@@ -7201,7 +7307,8 @@
"mergingFixes": "수정 사항 병합 중…", "mergingFixes": "수정 사항 병합 중…",
"mergingPr": "PR 병합 중…", "mergingPr": "PR 병합 중…",
"startPrReview": "PR 검토 시작", "startPrReview": "PR 검토 시작",
"statusRefreshed": "PR 상태가 새로 고침되었습니다" "statusRefreshed": "PR 상태가 새로 고침되었습니다",
"label": ""
}, },
"priority": { "priority": {
"ariaLabel": "작업 우선순위", "ariaLabel": "작업 우선순위",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "작성자", "createdBy": "작성자",
"createdVia": "생성 경로" "createdVia": "생성 경로",
"parentTaskOf": ""
}, },
"recoveryState": "복구 상태", "recoveryState": "복구 상태",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Markdown으로 전환", "switchToMarkdown": "Markdown으로 전환",
"switchToPlain": "일반 텍스트로 전환", "switchToPlain": "일반 텍스트로 전환",
"thinkingLevel": "", "thinkingLevel": "",
"workflowName": "" "workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
}, },
"workflowColumns": { "workflowColumns": {
"add": "", "add": "",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "", "collapsePrompt": "",
"editingPrompt": "", "editingPrompt": "",
"expandPrompt": "" "expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
}, },
"workflowFields": { "workflowFields": {
"add": "필드 추가", "add": "필드 추가",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "플러그인 단계", "templatesPluginSteps": "플러그인 단계",
"templatesSection": "템플릿", "templatesSection": "템플릿",
"timeoutMs": "", "timeoutMs": "",
"trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요." "trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
}, },
"workflows": { "workflows": {
"aiEdit": "AI로 디자인", "aiEdit": "AI로 디자인",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "노드 {{count}}개", "templateNodeCount_other": "노드 {{count}}개",
"templatePickerLabel": "시작점", "templatePickerLabel": "시작점",
"templateSectionBuiltin": "기본 제공 워크플로", "templateSectionBuiltin": "기본 제공 워크플로",
"templateSectionYours": "내 워크플로" "templateSectionYours": "내 워크플로",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "", "switchActiveMessage": "",
"switchActiveTitle": "", "switchActiveTitle": "",
"switchCancel": "", "switchCancel": "",
"switchConfirm": "" "switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
}, },
"workflowSettings": { "workflowSettings": {
"add": "", "add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 설치 요청", "installRequestTitle": "Worktrunk 설치 요청",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "버전" "version": "버전"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": ""
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "", "trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "", "offline": "",
"online": "" "online": ""
}, },
"labels": {
"name": ""
},
"merge": { "merge": {
"unknown": "" "unknown": ""
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "", "refreshSourceInitialLoad": "",
"refreshSourceManual": "" "refreshSourceManual": ""
}, },
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "", "summaryAwaitInput": "",
"summaryCodeDefault": "", "summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "" "workflowMismatch": ""
}, },
"todo": "待办", "todo": "待办",
"triage": "分诊" "triage": "分诊",
"workflow": {
"edit": "",
"new": ""
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "", "abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "评论", "heading": "评论",
"placeholder": "添加评论", "placeholder": "添加评论",
"postingButton": "发布中…", "postingButton": "发布中…",
"updatedSuccess": "评论已更新" "updatedSuccess": "评论已更新",
"aiGuidance": ""
}, },
"commit": { "commit": {
"filesChanged_one": "", "filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "重新运行飞行前检查", "rerunPreflight": "重新运行飞行前检查",
"revertToAi": "恢复为 AI 版本", "revertToAi": "恢复为 AI 版本",
"titleLabel": "标题", "titleLabel": "标题",
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>" "usingTemplate": "使用 <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
}, },
"preview": { "preview": {
"blockedDescription": "您可以在单独的浏览器标签页中查看预览。", "blockedDescription": "您可以在单独的浏览器标签页中查看预览。",
@@ -5553,7 +5611,8 @@
"totalSize": "", "totalSize": "",
"view": "", "view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "", "whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "" "whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
}, },
"clearToDefault": "", "clearToDefault": "",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "帮助", "help": "帮助",
"version": "版本 {{version}}" "version": "版本 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
}, },
"general": { "general": {
"25": "", "25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": "" "whenEnabledStartupRefreshesModelsThroughTheLocal": ""
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
}, },
"importExport": { "importExport": {
"confirmImport": "确认导入", "confirmImport": "确认导入",
@@ -5756,7 +5820,35 @@
"importing": "导入中…", "importing": "导入中…",
"importTitle": "导入设置", "importTitle": "导入设置",
"loadingFile": "加载中…", "loadingFile": "加载中…",
"reviewPrompt": "查看要导入的设置:" "reviewPrompt": "查看要导入的设置:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
}, },
"jsonPlaceholder": "输入 JSON 值...", "jsonPlaceholder": "输入 JSON 值...",
"keepLocal": "保留本地", "keepLocal": "保留本地",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "", "searchMemoryWithQmd": "",
"testing": "测试中…", "testing": "测试中…",
"testRetrieval": "测试检索", "testRetrieval": "测试检索",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "" "turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
}, },
"merge": { "merge": {
"abort": "", "abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "", "uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "", "uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "使用现有", "useExisting": "使用现有",
"usingQuickTunnel": "" "usingQuickTunnel": "",
"installationFailed": ""
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "", "advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "", "timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "", "whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "", "whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": "" "whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
}, },
"scope": { "scope": {
"globalBanner": "这些设置在所有 Fusion 项目中共享。", "globalBanner": "这些设置在所有 Fusion 项目中共享。",
@@ -6335,7 +6433,12 @@
"worktrunk": "", "worktrunk": "",
"worktrunkBinaryPath": "", "worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "", "worktrunkFailureBehavior": "",
"worktrunkIntegration": "" "worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "手动创建任务", "withoutGitHub1": "手动创建任务",
"withoutGitHub2": "为 AI 代理描述工作", "withoutGitHub2": "为 AI 代理描述工作",
"withoutGitHub3": "在看板上跟踪进度", "withoutGitHub3": "在看板上跟踪进度",
"withoutGitHubHeading": "不使用 GitHub(现在可用):" "withoutGitHubHeading": "不使用 GitHub(现在可用):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
}, },
"shell": { "shell": {
"activePill": "活跃", "activePill": "活跃",
@@ -7201,7 +7307,8 @@
"mergingFixes": "正在合并修复…", "mergingFixes": "正在合并修复…",
"mergingPr": "正在合并 PR…", "mergingPr": "正在合并 PR…",
"startPrReview": "开始 PR 审查", "startPrReview": "开始 PR 审查",
"statusRefreshed": "PR 状态已刷新" "statusRefreshed": "PR 状态已刷新",
"label": ""
}, },
"priority": { "priority": {
"ariaLabel": "任务优先级", "ariaLabel": "任务优先级",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "创建者:", "createdBy": "创建者:",
"createdVia": "通过…创建" "createdVia": "通过…创建",
"parentTaskOf": ""
}, },
"recoveryState": "恢复状态", "recoveryState": "恢复状态",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "切换到 Markdown", "switchToMarkdown": "切换到 Markdown",
"switchToPlain": "切换到纯文本", "switchToPlain": "切换到纯文本",
"thinkingLevel": "", "thinkingLevel": "",
"workflowName": "" "workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
}, },
"workflowColumns": { "workflowColumns": {
"add": "", "add": "",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "", "collapsePrompt": "",
"editingPrompt": "", "editingPrompt": "",
"expandPrompt": "" "expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
}, },
"workflowFields": { "workflowFields": {
"add": "添加字段", "add": "添加字段",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "插件步骤", "templatesPluginSteps": "插件步骤",
"templatesSection": "模板", "templatesSection": "模板",
"timeoutMs": "", "timeoutMs": "",
"trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。" "trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
}, },
"workflows": { "workflows": {
"aiEdit": "用 AI 设计", "aiEdit": "用 AI 设计",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} 个节点", "templateNodeCount_other": "{{count}} 个节点",
"templatePickerLabel": "从…开始", "templatePickerLabel": "从…开始",
"templateSectionBuiltin": "内置工作流", "templateSectionBuiltin": "内置工作流",
"templateSectionYours": "我的工作流" "templateSectionYours": "我的工作流",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "", "switchActiveMessage": "",
"switchActiveTitle": "", "switchActiveTitle": "",
"switchCancel": "", "switchCancel": "",
"switchConfirm": "" "switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
}, },
"workflowSettings": { "workflowSettings": {
"add": "", "add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 安装请求", "installRequestTitle": "Worktrunk 安装请求",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "版本" "version": "版本"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": ""
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "", "trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "", "offline": "",
"online": "" "online": ""
}, },
"labels": {
"name": ""
},
"merge": { "merge": {
"unknown": "" "unknown": ""
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "", "refreshSourceInitialLoad": "",
"refreshSourceManual": "" "refreshSourceManual": ""
}, },
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "", "summaryAwaitInput": "",
"summaryCodeDefault": "", "summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "" "workflowMismatch": ""
}, },
"todo": "待辦", "todo": "待辦",
"triage": "分診" "triage": "分診",
"workflow": {
"edit": "",
"new": ""
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "", "abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "評論", "heading": "評論",
"placeholder": "新增評論", "placeholder": "新增評論",
"postingButton": "發佈中…", "postingButton": "發佈中…",
"updatedSuccess": "評論已更新" "updatedSuccess": "評論已更新",
"aiGuidance": ""
}, },
"commit": { "commit": {
"filesChanged_one": "", "filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "重新執行飛行前檢查", "rerunPreflight": "重新執行飛行前檢查",
"revertToAi": "恢復為 AI 版本", "revertToAi": "恢復為 AI 版本",
"titleLabel": "標題", "titleLabel": "標題",
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>" "usingTemplate": "使用 <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
}, },
"preview": { "preview": {
"blockedDescription": "您可以在單獨的瀏覽器分頁中查看預覽。", "blockedDescription": "您可以在單獨的瀏覽器分頁中查看預覽。",
@@ -5553,7 +5611,8 @@
"totalSize": "", "totalSize": "",
"view": "", "view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "", "whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "" "whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
}, },
"clearToDefault": "", "clearToDefault": "",
"cliAgents": { "cliAgents": {
@@ -5597,7 +5656,9 @@
}, },
"footer": { "footer": {
"help": "說明", "help": "說明",
"version": "版本 {{version}}" "version": "版本 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
}, },
"general": { "general": {
"25": "", "25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": "" "whenEnabledStartupRefreshesModelsThroughTheLocal": ""
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
}, },
"importExport": { "importExport": {
"confirmImport": "確認匯入", "confirmImport": "確認匯入",
@@ -5756,7 +5820,35 @@
"importing": "匯入中…", "importing": "匯入中…",
"importTitle": "匯入設定", "importTitle": "匯入設定",
"loadingFile": "載入中…", "loadingFile": "載入中…",
"reviewPrompt": "查看要匯入的設定:" "reviewPrompt": "查看要匯入的設定:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
}, },
"jsonPlaceholder": "輸入 JSON 值...", "jsonPlaceholder": "輸入 JSON 值...",
"keepLocal": "保留本機", "keepLocal": "保留本機",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "", "searchMemoryWithQmd": "",
"testing": "測試中…", "testing": "測試中…",
"testRetrieval": "測試擷取", "testRetrieval": "測試擷取",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "" "turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
}, },
"merge": { "merge": {
"abort": "", "abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "", "uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "", "uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "使用現有", "useExisting": "使用現有",
"usingQuickTunnel": "" "usingQuickTunnel": "",
"installationFailed": ""
}, },
"researchGlobal": { "researchGlobal": {
"advancedExternalSearchProviders": "", "advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "", "timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "", "whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "", "whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": "" "whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
}, },
"scope": { "scope": {
"globalBanner": "這些設定在所有 Fusion 專案中共用。", "globalBanner": "這些設定在所有 Fusion 專案中共用。",
@@ -6335,7 +6433,12 @@
"worktrunk": "", "worktrunk": "",
"worktrunkBinaryPath": "", "worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "", "worktrunkFailureBehavior": "",
"worktrunkIntegration": "" "worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
} }
}, },
"setup": { "setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "手動建立任務", "withoutGitHub1": "手動建立任務",
"withoutGitHub2": "為 AI 代理描述工作", "withoutGitHub2": "為 AI 代理描述工作",
"withoutGitHub3": "在看板上追蹤進度", "withoutGitHub3": "在看板上追蹤進度",
"withoutGitHubHeading": "不使用 GitHub(現在可用):" "withoutGitHubHeading": "不使用 GitHub(現在可用):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
}, },
"shell": { "shell": {
"activePill": "作用中", "activePill": "作用中",
@@ -7201,7 +7307,8 @@
"mergingFixes": "正在合併修復…", "mergingFixes": "正在合併修復…",
"mergingPr": "正在合併 PR…", "mergingPr": "正在合併 PR…",
"startPrReview": "開始 PR 審查", "startPrReview": "開始 PR 審查",
"statusRefreshed": "PR 狀態已刷新" "statusRefreshed": "PR 狀態已刷新",
"label": ""
}, },
"priority": { "priority": {
"ariaLabel": "任務優先級", "ariaLabel": "任務優先級",
@@ -7216,7 +7323,8 @@
}, },
"provenance": { "provenance": {
"createdBy": "創建者:", "createdBy": "創建者:",
"createdVia": "通過…建立" "createdVia": "通過…建立",
"parentTaskOf": ""
}, },
"recoveryState": "恢復狀態", "recoveryState": "恢復狀態",
"refine": { "refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "切換為 Markdown", "switchToMarkdown": "切換為 Markdown",
"switchToPlain": "切換為純文字", "switchToPlain": "切換為純文字",
"thinkingLevel": "", "thinkingLevel": "",
"workflowName": "" "workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
}, },
"workflowColumns": { "workflowColumns": {
"add": "", "add": "",
@@ -8007,7 +8151,30 @@
}, },
"collapsePrompt": "", "collapsePrompt": "",
"editingPrompt": "", "editingPrompt": "",
"expandPrompt": "" "expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
}, },
"workflowFields": { "workflowFields": {
"add": "新增欄位", "add": "新增欄位",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "外掛步驟", "templatesPluginSteps": "外掛步驟",
"templatesSection": "範本", "templatesSection": "範本",
"timeoutMs": "", "timeoutMs": "",
"trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。" "trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
}, },
"workflows": { "workflows": {
"aiEdit": "使用 AI 設計", "aiEdit": "使用 AI 設計",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} 個節點", "templateNodeCount_other": "{{count}} 個節點",
"templatePickerLabel": "起始來源", "templatePickerLabel": "起始來源",
"templateSectionBuiltin": "內建工作流程", "templateSectionBuiltin": "內建工作流程",
"templateSectionYours": "你的工作流程" "templateSectionYours": "你的工作流程",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
}, },
"workflowSelector": { "workflowSelector": {
"switchActiveMessage": "", "switchActiveMessage": "",
"switchActiveTitle": "", "switchActiveTitle": "",
"switchCancel": "", "switchCancel": "",
"switchConfirm": "" "switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
}, },
"workflowSettings": { "workflowSettings": {
"add": "", "add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 安裝請求", "installRequestTitle": "Worktrunk 安裝請求",
"sha256": "SHA-256", "sha256": "SHA-256",
"version": "版本" "version": "版本"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"actions": {
"send": ""
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "", "trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "", "offline": "",
"online": "" "online": ""
}, },
"labels": {
"name": ""
},
"merge": { "merge": {
"unknown": "" "unknown": ""
}, },
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "", "refreshSourceInitialLoad": "",
"refreshSourceManual": "" "refreshSourceManual": ""
}, },
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "", "summaryAwaitInput": "",
"summaryCodeDefault": "", "summaryCodeDefault": "",

View File

@@ -1183,7 +1183,11 @@ export default interface Resources {
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead." "workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
}, },
"todo": "To Do", "todo": "To Do",
"triage": "Triage" "triage": "Triage",
"workflow": {
"edit": "Edit workflows",
"new": "New workflow"
}
}, },
"branchGroup": { "branchGroup": {
"abandonGroup": "Abandon group", "abandonGroup": "Abandon group",
@@ -1805,6 +1809,7 @@ export default interface Resources {
"comments": { "comments": {
"addButton": "Add Comment", "addButton": "Add Comment",
"addedSuccess": "Comment added", "addedSuccess": "Comment added",
"aiGuidance": "AI Guidance",
"deletedSuccess": "Comment deleted", "deletedSuccess": "Comment deleted",
"deletingButton": "Deleting…", "deletingButton": "Deleting…",
"editedSuffix": "(edited)", "editedSuffix": "(edited)",
@@ -4661,6 +4666,7 @@ export default interface Resources {
"viewUnavailable": "Plugin view unavailable" "viewUnavailable": "Plugin view unavailable"
}, },
"pr": { "pr": {
"assignees": "Assignees",
"authFail": "Run gh auth login and try again.", "authFail": "Run gh auth login and try again.",
"authOk": "GitHub CLI auth is available.", "authOk": "GitHub CLI auth is available.",
"baseBranch": "Base branch", "baseBranch": "Base branch",
@@ -4680,15 +4686,67 @@ export default interface Resources {
"createDraftPr": "Create draft PR", "createDraftPr": "Create draft PR",
"createPr": "Create PR", "createPr": "Create PR",
"createTitle": "Create Pull Request", "createTitle": "Create Pull Request",
"dismissConflictResolutionError": "Dismiss conflict resolution error",
"dismissError": "Dismiss PR error", "dismissError": "Dismiss PR error",
"dismissPushBranchError": "Dismiss push branch error",
"error": {
"actionOpen": "Action: open",
"actionRun": "Action: run",
"docs": "docs"
},
"generatingBody": "Generating AI body…",
"generatingTitle": "Generating AI title…",
"labels": "Labels",
"loadingOptions": "Loading PR options…",
"loadingPreflight": "Loading pre-flight checks…",
"noChangedFiles": "No changed files detected.",
"noCommits": "No commits found.",
"noConflicts": "No merge conflicts detected.", "noConflicts": "No merge conflicts detected.",
"preflightChecks": "Pre-flight checks", "preflightChecks": "Pre-flight checks",
"previewTitle": "Diff & commit preview", "previewTitle": "Diff & commit preview",
"pushBranch": {
"button": "Push branch to remote",
"message": "Fusion will push this task's branch to origin so the PR can be created.",
"title": "Push branch to remote"
},
"regenerate": "Regenerate", "regenerate": "Regenerate",
"rerunPreflight": "Re-run preflight", "rerunPreflight": "Re-run preflight",
"resolveConflicts": {
"button": "Resolve conflicts with AI",
"message": "Fusion will use AI to resolve conflicts on this branch and push it.",
"title": "Resolve conflicts with AI"
},
"revertToAi": "Revert to AI version", "revertToAi": "Revert to AI version",
"reviewers": "Reviewers",
"titleLabel": "Title", "titleLabel": "Title",
"usingTemplate": "Using <code>.github/pull_request_template.md</code>" "usingTemplate": "Using <code>.github/pull_request_template.md</code>",
"view": {
"agentDisagreed": "agent disagreed",
"agentReplyFix": "Agent reply — fix {{sha}}",
"approve": "Approve",
"autoMerge": "Auto-merge",
"close": "Close",
"confirmMerge": "Confirm merge",
"creating": "Creating PR…",
"creationFailed": "PR creation failed",
"loading": "Loading PR…",
"merge": "Merge",
"mergeDisabledUntilVerified": "Merge is disabled until GitHub verifies this PR",
"mergeableLabel": "Mergeable:",
"noReviewThreads": "No review threads.",
"none": "none",
"requestRetry": "Request retry",
"resolveConflictsBeforeMerge": "Resolve conflicts on GitHub before merging",
"resolveConflictsOnGithub": "Resolve conflicts on GitHub",
"responseAlreadyInProgress": "A response run is already in progress",
"responsePending_one": "Response run in progress — {{count}} threads pending",
"responsePending_other": "Response run in progress — {{count}} threads pending",
"retryCreation": "Retry PR creation",
"reviewLabel": "Review:",
"threadFixed": "fixed",
"threadPending": "pending",
"verifyingGithub": "Verifying with GitHub…"
}
}, },
"preview": { "preview": {
"blockedDescription": "You can view the preview in a separate browser tab.", "blockedDescription": "You can view the preview in a separate browser tab.",
@@ -5527,6 +5585,7 @@ export default interface Resources {
"backupS": " backup(s)", "backupS": " backup(s)",
"backupScheduleCron": "Backup Schedule (Cron)", "backupScheduleCron": "Backup Schedule (Cron)",
"backups": "backups", "backups": "backups",
"createFailed": "Failed to create backup",
"creating": "Creating…", "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) ", "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).", "cronExpressionForMemoryBackupTimingDefault0": "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).",
@@ -5597,8 +5656,14 @@ export default interface Resources {
"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. ", "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" "featureFlags": "Feature Flags"
}, },
"fileBrowser": {
"currentDirectory": "Current directory:",
"projectRoot": "(project root)"
},
"footer": { "footer": {
"checkUpdates": "Check for updates",
"help": "Help", "help": "Help",
"helpDiscussions": "Help and discussions",
"version": "Version {{version}}" "version": "Version {{version}}"
}, },
"general": { "general": {
@@ -5748,17 +5813,48 @@ export default interface Resources {
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local " "whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
}, },
"header": { "header": {
"discord": "Discord" "discord": "Discord",
"joinDiscord": "Join our Discord",
"star": "Star",
"starFusion": "Star Fusion on GitHub"
}, },
"importExport": { "importExport": {
"confirmImport": "Confirm Import", "confirmImport": "Confirm Import",
"counts": {
"global_one": "{{count}} global",
"global_other": "{{count}} global",
"project_one": "{{count}} project",
"project_other": "{{count}} project",
"workflowSettings_one": "{{count}} workflow setting value",
"workflowSettings_other": "{{count}} workflow setting value"
},
"exportBtn": "Export", "exportBtn": "Export",
"exportFailed": "Failed to export settings",
"exportTitle": "Export settings to JSON file", "exportTitle": "Export settings to JSON file",
"exported": "Settings exported ({{scope}} scope)",
"globalSettings": "Global Settings:",
"importBtn": "Import", "importBtn": "Import",
"importFailed": "Import failed",
"importFailedDetailed": "Failed to import settings",
"importScope": "Import Scope:",
"importTitle": "Import Settings", "importTitle": "Import Settings",
"importTitleAttr": "Import settings from JSON file",
"imported": "Imported {{counts}} setting(s)",
"importing": "Importing…", "importing": "Importing…",
"invalidJson": "Invalid JSON file: {{error}}",
"loadingFile": "Loading…", "loadingFile": "Loading…",
"reviewPrompt": "Review the settings to be imported:" "mergeExisting": "Merge with existing settings (recommended)",
"projectSettings": "Project Settings:",
"replaceWarning": "If unchecked, existing settings will be replaced with imported values.",
"reviewPrompt": "Review the settings to be imported:",
"scopeBoth": "Both global and project settings",
"scopeGlobal": "Global settings only",
"scopeLabel": {
"all": "all",
"global": "global",
"project": "project"
},
"scopeProject": "Project settings only"
}, },
"jsonPlaceholder": "Enter JSON value...", "jsonPlaceholder": "Enter JSON value...",
"keepLocal": "Keep Local", "keepLocal": "Keep Local",
@@ -5800,6 +5896,9 @@ export default interface Resources {
"noMatchingMemoryFound": "No matching memory found.", "noMatchingMemoryFound": "No matching memory found.",
"processDreamsFromDailyMemory": " Process dreams from daily memory ", "processDreamsFromDailyMemory": " Process dreams from daily memory ",
"qmd": " qmd ", "qmd": " qmd ",
"qmdInstallFailed": "Failed to install qmd",
"qmdInstallUnavailable": "qmd install finished, but qmd is still unavailable",
"qmdInstalled": "qmd installed successfully",
"qmdIsNotInstalledSearchWillUseLocal": " qmd is not installed. Search will use local files. Install indexed retrieval: ", "qmdIsNotInstalledSearchWillUseLocal": " qmd is not installed. Search will use local files. Install indexed retrieval: ",
"result": " result", "result": " result",
"runsTheSameQmdBackedMemorySearchPath": "Runs the same qmd-backed memory_search path agents use.", "runsTheSameQmdBackedMemorySearchPath": "Runs the same qmd-backed memory_search path agents use.",
@@ -6115,6 +6214,7 @@ export default interface Resources {
"ifHomebrewIsUnavailable": "If Homebrew is unavailable: ", "ifHomebrewIsUnavailable": "If Homebrew is unavailable: ",
"ingressURL": "Ingress URL", "ingressURL": "Ingress URL",
"installCloudflared": "Install cloudflared", "installCloudflared": "Install cloudflared",
"installationFailed": "Installation failed",
"installing": "Installing…", "installing": "Installing…",
"lastShortLivedTokenExpiresAt": "Last short-lived token expires at ", "lastShortLivedTokenExpiresAt": "Last short-lived token expires at ",
"manualInstall": "Manual install: ", "manualInstall": "Manual install: ",
@@ -6235,6 +6335,7 @@ export default interface Resources {
"archiveCompletedTasksAfterDays": "Archive Completed Tasks After (days)", "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.", "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 ", "browse": " Browse ",
"browseWorkspacePath": "Browse workspace path",
"closeParenPeriod": ").", "closeParenPeriod": ").",
"compactModeKeepsArchiveSizeLowWhilePreserving": "Compact mode keeps archive size low while preserving recent agent activity for context.", "compactModeKeepsArchiveSizeLowWhilePreserving": "Compact mode keeps archive size low while preserving recent agent activity for context.",
"compactSummaryAndRecentEntries": "Compact summary and recent entries", "compactSummaryAndRecentEntries": "Compact summary and recent entries",
@@ -6261,6 +6362,7 @@ export default interface Resources {
"off": "Off", "off": "Off",
"optionalFileOrDirectoryPathsToIgnoreWhen": " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ", "optionalFileOrDirectoryPathsToIgnoreWhen": " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ",
"or": " or ", "or": " or ",
"overlapPickerNote": "Choose a file to ignore directly, or navigate into a folder and select the current directory.",
"pollIntervalMs": "Poll Interval (ms)", "pollIntervalMs": "Poll Interval (ms)",
"preserveStepProgressOnStuckTaskRequeue": " Preserve step progress on stuck-task requeue ", "preserveStepProgressOnStuckTaskRequeue": " Preserve step progress on stuck-task requeue ",
"remove": " Remove ", "remove": " Remove ",
@@ -6334,6 +6436,7 @@ export default interface Resources {
"worktreeNamingStyle": "Worktree Naming Style", "worktreeNamingStyle": "Worktree Naming Style",
"worktrees": "Worktrees", "worktrees": "Worktrees",
"worktreesDirectory": "Worktrees Directory", "worktreesDirectory": "Worktrees Directory",
"worktreesPickerNote": "Navigate to the folder where Fusion should create task worktrees, then select the current directory.",
"worktrunk": " worktrunk ", "worktrunk": " worktrunk ",
"worktrunkBinaryPath": "Worktrunk binary path", "worktrunkBinaryPath": "Worktrunk binary path",
"worktrunkFailureBehavior": "Worktrunk failure behavior", "worktrunkFailureBehavior": "Worktrunk failure behavior",
@@ -6362,6 +6465,8 @@ export default interface Resources {
"authToken": "Auth Token", "authToken": "Auth Token",
"authTokenOptional": "Auth token (optional)", "authTokenOptional": "Auth token (optional)",
"back": "← Back", "back": "← Back",
"brandLogo": "Fusion logo",
"brandName": "Fusion",
"browserAuthToken": "Browser Auth Token", "browserAuthToken": "Browser Auth Token",
"cancelLogin": "Cancel", "cancelLogin": "Cancel",
"childProcess": "Child-Process", "childProcess": "Child-Process",
@@ -6602,6 +6707,7 @@ export default interface Resources {
"setUpAi": "Set Up AI", "setUpAi": "Set Up AI",
"setUpProject": "Set Up Project", "setUpProject": "Set Up Project",
"setupComplete": "Setup complete! Head to the board to create your first task, or explore the dashboard to see what's available.", "setupComplete": "Setup complete! Head to the board to create your first task, or explore the dashboard to see what's available.",
"setupCompleteTitle": "Setup Complete!",
"setupMode": "Setup Mode", "setupMode": "Setup Mode",
"setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.", "setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.",
"skip": "Skip", "skip": "Skip",
@@ -6925,6 +7031,51 @@ export default interface Resources {
"toggleWordWrap": "Toggle word wrap", "toggleWordWrap": "Toggle word wrap",
"unavailable": "Detailed file changes unavailable." "unavailable": "Detailed file changes unavailable."
}, },
"taskChat": {
"activePlaceholder": "Steer the currently executing agent",
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
"agentMessages": "{{label}} messages",
"arguments": "Arguments",
"collapseChat": "Collapse chat",
"donePlaceholder": "Start a refinement task for this completed task",
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
"entryCount_one": "{{count}} entry",
"entryCount_other": "{{count}} entries",
"error": "Error",
"errorCount_one": "{{count}} error",
"errorCount_other": "{{count}} errors",
"expandChat": "Expand chat to full modal",
"idleSessionHint": "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.",
"jumpToLatestMessage": "Jump to latest message",
"latest": "Latest",
"loadPreviousMessages": "Load previous messages",
"loadingAgentOutput": "Loading agent output…",
"loadingEarlierMessages": "Loading earlier messages…",
"message": "Message",
"messageActiveAgentSession": "Message active agent session",
"moreTools_one": ", +{{count}} more",
"moreTools_other": ", +{{count}} more",
"result": "Result",
"roles": {
"agent": "Agent",
"executor": "Executor",
"merger": "Merger",
"planner": "Planner",
"reviewer": "Reviewer"
},
"sending": "Sending",
"thinking": "Thinking",
"toolCall": "Tool call",
"toolCallCount_one": "{{count}} tool call",
"toolCallCount_other": "{{count}} tool calls",
"toolCallTo": "Tool call → {{label}}",
"toolError": "Tool error",
"toolNames": "Tool names",
"toolResult": "Tool result",
"you": "You",
"youMessage": "You message"
},
"taskDetail": { "taskDetail": {
"actions": { "actions": {
"menuBtn": "Actions" "menuBtn": "Actions"
@@ -7199,6 +7350,7 @@ export default interface Resources {
"checkPrStatus": "Check PR Status", "checkPrStatus": "Check PR Status",
"creatingPr": "Creating PR…", "creatingPr": "Creating PR…",
"finishAndClose": "Finish & Close", "finishAndClose": "Finish & Close",
"label": "PR",
"mergeAndClose": "Merge & Close", "mergeAndClose": "Merge & Close",
"mergingFixes": "Merging fixes…", "mergingFixes": "Merging fixes…",
"mergingPr": "Merging PR…", "mergingPr": "Merging PR…",
@@ -7218,7 +7370,8 @@ export default interface Resources {
}, },
"provenance": { "provenance": {
"createdBy": "Created by", "createdBy": "Created by",
"createdVia": "Created via" "createdVia": "Created via",
"parentTaskOf": "of"
}, },
"recoveryState": "Recovery state", "recoveryState": "Recovery state",
"refine": { "refine": {
@@ -7903,22 +8056,45 @@ export default interface Resources {
}, },
"workflow": { "workflow": {
"advisoryExplanation": "Advisory workflow steps flagged non-blocking improvements:", "advisoryExplanation": "Advisory workflow steps flagged non-blocking improvements:",
"aggregateAdvisory": "Advisory",
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"aggregateResult": "Aggregate result", "aggregateResult": "Aggregate result",
"approveAndRun": "Approve & run",
"approveCommandError": "Failed to approve command",
"approving": "Approving…",
"awaitingInputTitle": "Waiting for your input",
"cliApprovalRejectHint": "To reject, keep the task paused and do not approve.",
"cliApprovalTitle": "Approve CLI command?",
"cliApprovalWarning": "This command will run in the task worktree. Approving trusts this exact command for future runs.",
"configuredSteps": "Configured Workflow Steps", "configuredSteps": "Configured Workflow Steps",
"customWorkflowFallback": "Custom workflow",
"customWorkflowLabel": "Custom workflow",
"defaultWorkflow": "Default",
"done": "Done", "done": "Done",
"doneEditingAriaLabel": "Done editing workflow steps", "doneEditingAriaLabel": "Done editing workflow steps",
"edit": "Edit", "edit": "Edit",
"editAriaLabel": "Edit workflow steps", "editAriaLabel": "Edit workflow steps",
"editWorkflow": "Edit workflow", "editWorkflow": "Edit workflow",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionOrder": "Execution order:", "executionOrder": "Execution order:",
"executionPaused": "Paused",
"executionPhase": "Execution phase", "executionPhase": "Execution phase",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"expandOutput": "Expand output", "expandOutput": "Expand output",
"graph": "Workflow graph", "graph": "Workflow graph",
"graphUnavailable": "Workflow graph unavailable", "graphUnavailable": "Workflow graph unavailable",
"hideOutput": "Hide output", "hideOutput": "Hide output",
"inputPlaceholder": "Type your reply…",
"loadingGraph": "Loading workflow graph…", "loadingGraph": "Loading workflow graph…",
"loadingResults": "Loading workflow results…", "loadingResults": "Loading workflow results…",
"markdown": "Markdown", "markdown": "Markdown",
"modelDefault": "Default",
"modelSettings": "Model settings", "modelSettings": "Model settings",
"moveDown": "Move down", "moveDown": "Move down",
"moveUp": "Move up", "moveUp": "Move up",
@@ -7930,10 +8106,20 @@ export default interface Resources {
"overview": "Workflow overview", "overview": "Workflow overview",
"plain": "Plain", "plain": "Plain",
"polishNotes": "Polish notes", "polishNotes": "Polish notes",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"remove": "Remove", "remove": "Remove",
"replyInComments": "Reply in the comments and unpause the task to continue.",
"resumeTaskError": "Failed to resume task",
"resuming": "Resuming…",
"selectStepsDescription": "Select steps to run after task implementation completes", "selectStepsDescription": "Select steps to run after task implementation completes",
"showOutput": "Show output", "showOutput": "Show output",
"started": "Started:", "started": "Started:",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"stepCount_one": "{{count}} step", "stepCount_one": "{{count}} step",
"stepCount_other": "{{count}} steps", "stepCount_other": "{{count}} steps",
"stepDefinitionNotFound": "Step definition not found.", "stepDefinitionNotFound": "Step definition not found.",
@@ -7941,6 +8127,8 @@ export default interface Resources {
"stepProgressValue": "{{completed}} of {{total}} steps completed", "stepProgressValue": "{{completed}} of {{total}} steps completed",
"steps": "Workflow Steps", "steps": "Workflow Steps",
"stepsExplanation": "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.", "stepsExplanation": "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.",
"submitAndResume": "Submit & resume",
"submitting": "Submitting…",
"summaryAdvisory_one": "{{count}} advisory", "summaryAdvisory_one": "{{count}} advisory",
"summaryAdvisory_other": "{{count}} advisory", "summaryAdvisory_other": "{{count}} advisory",
"summaryFailed_one": "{{count}} failed", "summaryFailed_one": "{{count}} failed",
@@ -7957,6 +8145,7 @@ export default interface Resources {
"switchToMarkdown": "Switch to markdown", "switchToMarkdown": "Switch to markdown",
"switchToPlain": "Switch to plain text", "switchToPlain": "Switch to plain text",
"thinkingLevel": "Thinking level", "thinkingLevel": "Thinking level",
"waitingForOutput": "Waiting for agent output…",
"workflowName": "Workflow" "workflowName": "Workflow"
}, },
"workflowColumns": { "workflowColumns": {
@@ -7993,6 +8182,10 @@ export default interface Resources {
"unplacedCount_other": "{{count}} nodes not placed in a column" "unplacedCount_other": "{{count}} nodes not placed in a column"
}, },
"workflowEditor": { "workflowEditor": {
"agent": "Agent",
"agentsLoadFailed": "Failed to load agents",
"autoApproveRequests": "Auto-approve requests",
"autoApproveRequestsNote": "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.",
"cliAgent": { "cliAgent": {
"adapterLabel": "CLI adapter", "adapterLabel": "CLI adapter",
"adapterNote": "Drives a CLI coding agent in an engine-owned terminal for this step.", "adapterNote": "Drives a CLI coding agent in an engine-owned terminal for this step.",
@@ -8007,9 +8200,28 @@ export default interface Resources {
"notifyLabel": "Waiting-on-input notification", "notifyLabel": "Waiting-on-input notification",
"notifyNote": "How you are alerted when the agent pauses waiting for input on this step." "notifyNote": "How you are alerted when the agent pauses waiting for input on this step."
}, },
"cliCommandNote": "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.",
"cliMode": "CLI mode",
"cliScript": "CLI / script",
"collapsePrompt": "Collapse prompt editor", "collapsePrompt": "Collapse prompt editor",
"command": "Command",
"editingPrompt": "Editing Prompt", "editingPrompt": "Editing Prompt",
"expandPrompt": "Expand prompt editor" "executor": "Executor",
"expandPrompt": "Expand prompt editor",
"maxRetries": "Max retries",
"model": "Model",
"modelsLoadFailed": "Failed to load models",
"namedScript": "Named script",
"namedScriptNote": "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.",
"prompt": "Prompt",
"scriptName": "Script name",
"selectAgent": "— select agent —",
"selectSkill": "— select skill —",
"skill": "Skill",
"skillsLoadFailed": "Failed to load skills",
"skipFirstRunApproval": "Skip first-run approval (runs without pausing)",
"waitForUserInput": "Wait for user input",
"waitForUserInputNote": "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question."
}, },
"workflowFields": { "workflowFields": {
"add": "Add field", "add": "Add field",
@@ -8052,6 +8264,8 @@ export default interface Resources {
"codeSource": "Source (TypeScript)", "codeSource": "Source (TypeScript)",
"codeTimeout": "Timeout (ms)", "codeTimeout": "Timeout (ms)",
"collapseInspector": "Collapse", "collapseInspector": "Collapse",
"conditionFailure": "failure",
"conditionSuccess": "success",
"cycleBlocked": "That connection would create a cycle — only rework edges inside a for-each template may loop back", "cycleBlocked": "That connection would create a cycle — only rework edges inside a for-each template may loop back",
"deleteEdge": "Delete edge", "deleteEdge": "Delete edge",
"deleteNode": "Delete node", "deleteNode": "Delete node",
@@ -8108,6 +8322,7 @@ export default interface Resources {
"mobileMoveDown": "Move down", "mobileMoveDown": "Move down",
"mobileMoveUp": "Move up", "mobileMoveUp": "Move up",
"mobileNodeKinds": "Node types", "mobileNodeKinds": "Node types",
"nodeInspector": "Node",
"notifyCustom": "Custom", "notifyCustom": "Custom",
"notifyCustomEvent": "Custom event", "notifyCustomEvent": "Custom event",
"notifyEvent": "Event type", "notifyEvent": "Event type",
@@ -8117,6 +8332,7 @@ export default interface Resources {
"parseArtifact": "Artifact", "parseArtifact": "Artifact",
"parseParser": "Parser", "parseParser": "Parser",
"quorumN": "Quorum count (n)", "quorumN": "Quorum count (n)",
"readOnlyDuplicateToEdit": "Read-only built-in — duplicate the workflow to edit nodes.",
"releaseCapacity": "Downstream capacity", "releaseCapacity": "Downstream capacity",
"releaseCondition": "Release condition", "releaseCondition": "Release condition",
"releaseDependency": "Dependency complete", "releaseDependency": "Dependency complete",
@@ -8146,6 +8362,13 @@ export default interface Resources {
"trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out." "trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out."
}, },
"workflowSelector": { "workflowSelector": {
"applyFailed": "Failed to apply workflow",
"defaultCleared": "Default workflow cleared",
"defaultSet": "Default workflow set",
"defaultWorkflowLabel": "Default workflow for new tasks",
"loadFailed": "Failed to load workflows",
"manage": "Manage…",
"none": "None",
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?", "switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
"switchActiveTitle": "Switch workflow?", "switchActiveTitle": "Switch workflow?",
"switchCancel": "Cancel", "switchCancel": "Cancel",
@@ -8226,6 +8449,7 @@ export default interface Resources {
"backToWorkflowList": "Back to workflows", "backToWorkflowList": "Back to workflows",
"clickToEditDescription": "Click to edit description", "clickToEditDescription": "Click to edit description",
"clickToRename": "Click to rename", "clickToRename": "Click to rename",
"closeEditor": "Close workflow editor",
"createDescription": "Description (optional)", "createDescription": "Description (optional)",
"createFailed": "Failed to create workflow", "createFailed": "Failed to create workflow",
"createName": "Name", "createName": "Name",
@@ -8242,7 +8466,9 @@ export default interface Resources {
"discardConfirm": "Discard", "discardConfirm": "Discard",
"discardMessage": "You have unsaved changes to this workflow. Discard them?", "discardMessage": "You have unsaved changes to this workflow. Discard them?",
"discardTitle": "Discard unsaved changes?", "discardTitle": "Discard unsaved changes?",
"duplicateFailed": "Failed to duplicate workflow",
"duplicateToCustomize": "Duplicate to customize", "duplicateToCustomize": "Duplicate to customize",
"duplicatedEditable": "Duplicated to \"{{name}}\" — editable",
"emptyDescription": "Workflows orchestrate the steps and gates that run around task execution. Create one to start arranging that flow.", "emptyDescription": "Workflows orchestrate the steps and gates that run around task execution. Create one to start arranging that flow.",
"emptyTitle": "No workflow selected", "emptyTitle": "No workflow selected",
"export": "Export", "export": "Export",
@@ -8255,11 +8481,14 @@ export default interface Resources {
"importStripped": "Auto-approval flags were removed from imported nodes", "importStripped": "Auto-approval flags were removed from imported nodes",
"importTooltip": "Import a workflow from a JSON file", "importTooltip": "Import a workflow from a JSON file",
"imported": "Imported workflow \"{{name}}\"", "imported": "Imported workflow \"{{name}}\"",
"loadFailed": "Failed to load workflows",
"loading": "Loading…",
"migrationNotice": "Your legacy workflow steps were converted — find them as templates in the palette and as the \"Migrated steps\" workflow.", "migrationNotice": "Your legacy workflow steps were converted — find them as templates in the palette and as the \"Migrated steps\" workflow.",
"mobileEditorNav": "Workflow editor sections", "mobileEditorNav": "Workflow editor sections",
"mobileSelectNote": "Select a workflow to edit.", "mobileSelectNote": "Select a workflow to edit.",
"nameLabel": "Workflow name", "nameLabel": "Workflow name",
"newWorkflow": "New workflow", "newWorkflow": "New workflow",
"noneYet": "No workflows yet.",
"readOnlyBuiltin": "Read-only built-in workflow", "readOnlyBuiltin": "Read-only built-in workflow",
"saveFailed": "Failed to save workflow", "saveFailed": "Failed to save workflow",
"saved": "Workflow saved", "saved": "Workflow saved",
@@ -8273,7 +8502,8 @@ export default interface Resources {
"templateNodeCount_other": "{{count}} nodes", "templateNodeCount_other": "{{count}} nodes",
"templatePickerLabel": "Start from", "templatePickerLabel": "Start from",
"templateSectionBuiltin": "Built-in workflows", "templateSectionBuiltin": "Built-in workflows",
"templateSectionYours": "Your workflows" "templateSectionYours": "Your workflows",
"title": "Workflows"
}, },
"workspace": { "workspace": {
"projectRoot": "Project Root", "projectRoot": "Project Root",
@@ -8519,6 +8749,9 @@ export default interface Resources {
} }
}, },
"common": { "common": {
"actions": {
"send": "Send"
},
"agents": { "agents": {
"ratings": { "ratings": {
"trendDeclining": "↓ Declining", "trendDeclining": "↓ Declining",
@@ -8598,6 +8831,9 @@ export default interface Resources {
"offline": "Offline", "offline": "Offline",
"online": "Online" "online": "Online"
}, },
"labels": {
"name": "Name"
},
"merge": { "merge": {
"unknown": "Unknown" "unknown": "Unknown"
}, },
@@ -8734,28 +8970,6 @@ export default interface Resources {
"refreshSourceInitialLoad": "Initial load", "refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual" "refreshSourceManual": "Manual"
}, },
"workflow": {
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"customWorkflowFallback": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": { "workflowNodes": {
"summaryAwaitInput": "Waits for user input", "summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript", "summaryCodeDefault": "TypeScript",