FN-008: add Activity Feed log copying

Add a localized copy action for task Activity Feed logs with clipboard feedback and coverage.

- Add the Activity Feed copy control and styling to task details.
- Localize copy, success, and failure messages across supported locales.
- Cover clipboard states and regenerate translation resource types.

Files changed:
 .changeset/fn-008-copy-task-activity-logs.md       |   7 +
 .../dashboard/app/components/TaskDetailModal.css   |  52 ++++++
 .../dashboard/app/components/TaskDetailModal.tsx   |  70 +++++--
 .../__tests__/TaskDetailModal.test-helpers.ts      |   1 +
 .../components/__tests__/TaskDetailModal.test.tsx  | 202 ++++++++++++++++++++
 packages/i18n/locales/en/app.json                  |   4 +
 packages/i18n/locales/es/app.json                  |   9 +
 packages/i18n/locales/fr/app.json                  |   9 +
 packages/i18n/locales/ko/app.json                  |   9 +
 packages/i18n/locales/pt-BR/app.json               |   9 +
 packages/i18n/locales/zh-CN/app.json               |   9 +
 packages/i18n/locales/zh-TW/app.json               |   9 +
 packages/i18n/src/resources.d.ts                   |  11 +-
 13 files changed, 385 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-008
Fusion-Task-Lineage: 41574c1e-528e-499b-bec0-9cccfdf068de
Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-18 18:54:24 +00:00
parent 6f91764f2e
commit c30d65080e
13 changed files with 385 additions and 16 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a localized action to copy displayed task Activity Feed logs.
category: feature
dev: Uses the shared clipboard fallback and preserves the bounded Feed order.

View File

@@ -3369,6 +3369,31 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
padding: 0;
}
/*
FNXC:TaskActivityFeedCopy 2026-08-18-18:11:
Feed's copy and expansion actions share one tokenized overlay group so the bounded log remains scroll-owned and both controls stay reachable in modal, embedded, floating, and mobile hosts.
*/
.detail-activity-actions {
position: absolute;
top: var(--space-md);
right: var(--space-md);
z-index: 3;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.detail-activity-copy {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
white-space: nowrap;
}
.detail-activity-copy svg {
flex: 0 0 auto;
}
.activity-expand-toggle--overlay {
position: absolute;
top: var(--space-md);
@@ -3379,6 +3404,19 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
box-shadow: var(--shadow-sm);
}
.detail-activity-actions .activity-expand-toggle--overlay {
position: static;
inset: auto;
}
.detail-activity-actions ~ h4,
.detail-activity-actions ~ .detail-log-truncated,
.detail-activity-actions ~ .detail-log-loading,
.detail-activity-actions ~ .detail-log-empty,
.detail-activity-actions ~ .detail-activity-list > .detail-log-entry:first-child {
padding-inline-end: calc((var(--space-2xl) * 4) + var(--space-lg));
}
.detail-log-loading {
display: flex;
align-items: center;
@@ -3419,6 +3457,20 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
right: var(--space-sm);
}
.detail-activity-actions {
top: var(--space-sm);
right: var(--space-sm);
gap: var(--space-xs);
}
.detail-activity-actions ~ h4,
.detail-activity-actions ~ .detail-log-truncated,
.detail-activity-actions ~ .detail-log-loading,
.detail-activity-actions ~ .detail-log-empty,
.detail-activity-actions ~ .detail-activity-list > .detail-log-entry:first-child {
padding-inline-end: calc((var(--space-2xl) * 4) + var(--space-md));
}
.detail-log-loading {
min-height: calc(var(--space-2xl) + var(--space-lg));
}

View File

@@ -2,7 +2,7 @@ import "./TaskDetailModal.css";
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, Paperclip, Eye, EyeOff } from "lucide-react";
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, Paperclip, Eye, EyeOff, Copy } from "lucide-react";
import { useViewportMode } from "../hooks/useViewportMode";
import { mergeTaskSnapshot } from "../hooks/useTasks";
import { FloatingWindow } from "./FloatingWindow";
@@ -87,6 +87,7 @@ import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils
import { hasPendingAutomaticRecovery, isTaskManuallyRetryable } from "../utils/taskRecovery";
import { findInReviewStallLogEntry, IN_REVIEW_STALL_LOG_REGEX } from "../utils/findInReviewStallLogEntry";
import { getTaskLogEntryAction, getTaskLogEntryOutcome } from "../utils/taskLogEntryDisplay";
import { copyTextToClipboard } from "../utils/copyToClipboard";
import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
import { isReviewBudgetExhaustedApproval, isTaskAwaitingPlanApproval } from "../utils/reviewBudgetApproval";
import { getTaskStatusBadgeLabel, hasTaskStatusBadge, isTaskPlanningActive } from "../utils/taskStatusBadgeLabel";
@@ -127,6 +128,22 @@ type ActivityViewMenuPosition = {
maxHeight: number;
};
type TaskActivityLogEntry = NonNullable<Parameters<typeof getTaskLogEntryAction>[0]> & {
timestamp: string;
};
/*
FNXC:TaskActivityFeedCopy 2026-08-18-18:11:
Operators copy only the bounded activity already loaded into Feed, in the same newest-first order they see. Keep source timestamps, duplicate rows, and legacy action/outcome fallbacks intact, and route all clipboard attempts through the shared secure/fallback helper.
*/
function serializeTaskActivityLogs(entries: readonly TaskActivityLogEntry[]): string {
return [...entries].reverse().map((entry) => {
const action = getTaskLogEntryAction(entry);
const outcome = getTaskLogEntryOutcome(entry);
return `[${entry.timestamp}] ${action}${outcome ? `\n${outcome}` : ""}`;
}).join("\n\n");
}
function isStringValue(value: unknown): value is string {
return Object.prototype.toString.call(value) === STRING_OBJECT_TAG;
}
@@ -976,6 +993,17 @@ export function TaskDetailContent({
: task.overlapBlockedBy === undefined ? fullDetail.overlapBlockedBy : task.overlapBlockedBy,
} as TaskDetail)
: ({ ...task, prompt: "" } as TaskDetail);
const activityLog = workingTask.log ?? [];
const handleCopyActivityLogs = useCallback(async () => {
if (detailLoading || activityLog.length === 0) return;
const copied = await copyTextToClipboard(serializeTaskActivityLogs(activityLog));
addToast(
copied
? t("taskDetail.logs.copySuccess", "Displayed activity copied to clipboard")
: t("taskDetail.logs.copyFailure", "Failed to copy displayed activity"),
copied ? "success" : "error",
);
}, [activityLog, addToast, detailLoading, t]);
/*
FNXC:TaskStatusConsistency 2026-08-05-04:30:
Detail hosts consume the same reconciled snapshot as board and list cards. Show live planning as
@@ -5908,20 +5936,34 @@ export function TaskDetailContent({
</div>
) : (
<div className="detail-activity" role="tabpanel">
<button
type="button"
className="btn btn-icon btn-sm activity-expand-toggle activity-expand-toggle--overlay"
onClick={() => setActivityExpanded((value) => !value)}
aria-label={isActivityExpanded ? t("taskDetail.activity.collapse", "Collapse activity") : t("taskDetail.activity.expand", "Expand activity to full modal")}
aria-pressed={isActivityExpanded}
data-testid="task-chat-expand-toggle"
>
{isActivityExpanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
</button>
<div className="detail-activity-actions">
<button
type="button"
className="btn btn-sm detail-activity-copy"
onClick={() => void handleCopyActivityLogs()}
disabled={detailLoading || activityLog.length === 0}
aria-label={t("taskDetail.logs.copyDisplayed", "Copy displayed activity logs")}
title={t("taskDetail.logs.copyDisplayed", "Copy displayed activity logs")}
data-testid="task-activity-copy-logs"
>
<Copy aria-hidden="true" />
{t("taskDetail.logs.copy", "Copy logs")}
</button>
<button
type="button"
className="btn btn-icon btn-sm activity-expand-toggle activity-expand-toggle--overlay"
onClick={() => setActivityExpanded((value) => !value)}
aria-label={isActivityExpanded ? t("taskDetail.activity.collapse", "Collapse activity") : t("taskDetail.activity.expand", "Expand activity to full modal")}
aria-pressed={isActivityExpanded}
data-testid="task-chat-expand-toggle"
>
{isActivityExpanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
</button>
</div>
<h4>{t("taskDetail.activity.feedHeading", "Feed")}</h4>
{(workingTask as typeof workingTask & { activityLogTruncatedCount?: number }).activityLogTruncatedCount ? (
<div className="detail-log-truncated">
{t("taskDetail.logs.truncated", "Showing the most recent {{count}} activity entries.", { count: workingTask.log.length })}
{t("taskDetail.logs.truncated", "Showing the most recent {{count}} activity entries.", { count: activityLog.length })}
</div>
) : null}
{detailLoading ? (
@@ -5929,12 +5971,12 @@ export function TaskDetailContent({
<Loader2 className="animate-spin" aria-hidden="true" />
<span>{t("taskDetail.logs.loadingActivity", "Loading activity…")}</span>
</div>
) : workingTask.log && workingTask.log.length > 0 ? (
) : activityLog.length > 0 ? (
<div className="detail-activity-list" ref={activityListRef}>
{(() => {
// FNXC:TaskDetail 2026-06-14-13:43 Activity rendering must tolerate legacy `text`/`detail` log entries.
let highlightedOnce = false;
return [...workingTask.log].reverse().map((entry, i) => {
return [...activityLog].reverse().map((entry, i) => {
const action = getTaskLogEntryAction(entry);
const outcome = getTaskLogEntryOutcome(entry);
const stallMatch = action.match(IN_REVIEW_STALL_LOG_REGEX)

View File

@@ -174,6 +174,7 @@ vi.mock("lucide-react", () => ({
Paperclip: (props: any) => React.createElement("svg", { "data-testid": "paperclip-icon", ...props }),
Eye: (props: any) => React.createElement("svg", { "data-testid": "eye-icon", ...props }),
EyeOff: (props: any) => React.createElement("svg", { "data-testid": "eye-off-icon", ...props }),
Copy: (props: any) => React.createElement("svg", { "data-testid": "copy-icon", ...props }),
// FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to
// WorkflowNodeTypes.tsx (HelpCircle, DoorOpen), which WorkflowNodeEditor/WorkflowResultsTab
// import transitively behind TaskDetailModal's lazy workflow surfaces. The explicit mock list

View File

@@ -17,6 +17,7 @@ import {
mockConfirm,
mockConfirmWithCheckbox,
mockConfirmWithChoice,
readDashboardStylesSource,
} from "./TaskDetailModal.test-helpers";
import { TaskDetailContent, TaskDetailModal } from "../TaskDetailModal";
@@ -767,6 +768,7 @@ describe("TaskDetailModal Activity feed loading", () => {
function renderActivityFeedModal(
task: ReturnType<typeof makeTask> | Record<string, unknown>,
initialTab: ComponentProps<typeof TaskDetailModal>["initialTab"] = "logs",
addToast: ComponentProps<typeof TaskDetailModal>["addToast"] = noop,
) {
return render(
<TaskDetailModal
@@ -777,7 +779,7 @@ describe("TaskDetailModal Activity feed loading", () => {
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
addToast={addToast}
/>,
);
}
@@ -791,6 +793,49 @@ describe("TaskDetailModal Activity feed loading", () => {
return task;
}
let clipboardDescriptor: PropertyDescriptor | undefined;
let clipboardCaptured = false;
let execCommandDescriptor: PropertyDescriptor | undefined;
let execCommandCaptured = false;
function setClipboardApi(writeText?: ReturnType<typeof vi.fn>) {
if (!clipboardCaptured) {
clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, "clipboard");
clipboardCaptured = true;
}
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: writeText ? { writeText } : undefined,
});
}
function setExecCommand(result: boolean) {
if (!execCommandCaptured) {
execCommandDescriptor = Object.getOwnPropertyDescriptor(document, "execCommand");
execCommandCaptured = true;
}
Object.defineProperty(document, "execCommand", {
configurable: true,
writable: true,
value: vi.fn(() => result),
});
}
afterEach(() => {
if (clipboardCaptured) {
if (clipboardDescriptor) Object.defineProperty(navigator, "clipboard", clipboardDescriptor);
else delete (navigator as Navigator & { clipboard?: unknown }).clipboard;
}
if (execCommandCaptured) {
if (execCommandDescriptor) Object.defineProperty(document, "execCommand", execCommandDescriptor);
else delete (document as Document & { execCommand?: unknown }).execCommand;
}
clipboardDescriptor = undefined;
clipboardCaptured = false;
execCommandDescriptor = undefined;
execCommandCaptured = false;
});
it("shows activity loading instead of empty state while slim task detail is pending", async () => {
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
@@ -799,6 +844,7 @@ describe("TaskDetailModal Activity feed loading", () => {
renderActivityFeedModal(makeSlimTask());
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
expect(screen.getByTestId("task-activity-copy-logs")).toBeDisabled();
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
});
@@ -824,6 +870,7 @@ describe("TaskDetailModal Activity feed loading", () => {
await user.click(screen.getByRole("button", { name: "Activity" }));
await selectActivityView(user, "feed");
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
expect(screen.getByTestId("task-activity-copy-logs")).toBeDisabled();
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
});
@@ -835,9 +882,21 @@ describe("TaskDetailModal Activity feed loading", () => {
renderActivityFeedModal(makeSlimTask());
expect(await screen.findByText("(no activity)")).toBeInTheDocument();
expect(screen.getByTestId("task-activity-copy-logs")).toBeDisabled();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
it("keeps copying disabled when the loaded log is undefined", async () => {
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({ id: "FN-6040", prompt: "# Loaded", log: undefined as any }));
renderActivityFeedModal(makeSlimTask());
expect(await screen.findByText("(no activity)")).toBeInTheDocument();
expect(screen.getByTestId("task-activity-copy-logs")).toBeDisabled();
});
it("renders loaded activity entries newest first", async () => {
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
@@ -864,6 +923,147 @@ describe("TaskDetailModal Activity feed loading", () => {
TaskDetailContent feed. Entering Feed must refresh its full task snapshot because board/SSE rows
deliberately carry log=[]; otherwise a detail opened before activity exists stays empty forever.
*/
/*
FNXC:TaskActivityFeedCopy 2026-08-18-18:11:
The production modal and embedded hosts must serialize the same bounded Feed entries, while Live and Raw remain free of this Feed-only affordance.
*/
it("copies loaded Feed activity in visible newest-first order through the secure clipboard", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
const addToast = vi.fn();
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({
id: "FN-FEED-COPY",
prompt: "# Loaded",
log: [
{ timestamp: "2026-08-18T10:00:00.000Z", action: "same action" },
{ timestamp: "2026-08-18T10:01:00.000Z", text: "legacy text", detail: "legacy detail" },
{ timestamp: "2026-08-18T10:02:00.000Z", action: "same action", outcome: "completed" },
],
} as any));
setClipboardApi(writeText);
renderActivityFeedModal(makeSlimTask({ id: "FN-FEED-COPY" }), "logs", addToast);
expect(await screen.findAllByText("same action")).toHaveLength(2);
const copyButton = screen.getByTestId("task-activity-copy-logs");
expect(screen.getAllByTestId("task-activity-copy-logs")).toHaveLength(1);
expect(screen.getAllByTestId("task-chat-expand-toggle")).toHaveLength(1);
await user.click(copyButton);
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(
"[2026-08-18T10:02:00.000Z] same action\ncompleted\n\n[2026-08-18T10:01:00.000Z] legacy text\nlegacy detail\n\n[2026-08-18T10:00:00.000Z] same action",
);
expect(addToast).toHaveBeenCalledWith("Displayed activity copied to clipboard", "success");
});
await selectActivityView(user, "raw-logs");
expect(screen.queryByTestId("task-activity-copy-logs")).not.toBeInTheDocument();
await selectActivityView(user, "current");
expect(screen.queryByTestId("task-activity-copy-logs")).not.toBeInTheDocument();
});
it("exposes the same Feed copy payload in embedded task detail", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
const addToast = vi.fn();
const embeddedTask = makeTask({
id: "FN-EMBEDDED-COPY",
log: [{ timestamp: "2026-08-18T11:00:00.000Z", action: "embedded entry", outcome: "ok" }],
});
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(embeddedTask);
setClipboardApi(writeText);
render(
<TaskDetailContent
task={embeddedTask}
embedded
initialTab="chat"
onRequestClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={addToast}
/>,
);
await selectActivityView(user, "feed");
expect(await screen.findByText("embedded entry")).toBeInTheDocument();
await user.click(screen.getByTestId("task-activity-copy-logs"));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith("[2026-08-18T11:00:00.000Z] embedded entry\nok");
expect(addToast).toHaveBeenCalledWith("Displayed activity copied to clipboard", "success");
});
});
it("uses the clipboard fallback and reports failure truthfully", async () => {
const user = userEvent.setup();
const addToast = vi.fn();
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({
id: "FN-FEED-FALLBACK",
prompt: "# Loaded",
log: [{ timestamp: "2026-08-18T12:00:00.000Z", action: "fallback entry" }],
}));
setClipboardApi();
setExecCommand(true);
renderActivityFeedModal(makeSlimTask({ id: "FN-FEED-FALLBACK" }), "logs", addToast);
await screen.findByText("fallback entry");
await user.click(screen.getByTestId("task-activity-copy-logs"));
await waitFor(() => expect(addToast).toHaveBeenCalledWith("Displayed activity copied to clipboard", "success"));
addToast.mockClear();
setClipboardApi();
setExecCommand(false);
await user.click(screen.getByTestId("task-activity-copy-logs"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Failed to copy displayed activity", "error");
expect(addToast).not.toHaveBeenCalledWith("Displayed activity copied to clipboard", "success");
});
});
it("keeps the bounded truncation notice while copying only displayed entries", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
const { fetchTaskDetail } = await import("../../api");
vi.mocked(fetchTaskDetail).mockReset();
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({
id: "FN-FEED-TRUNCATED",
prompt: "# Loaded",
log: [{ timestamp: "2026-08-18T13:00:00.000Z", action: "kept entry" }],
activityLogTruncatedCount: 25,
} as any));
setClipboardApi(writeText);
renderActivityFeedModal(makeSlimTask({ id: "FN-FEED-TRUNCATED" }));
expect(await screen.findByText("Showing the most recent 1 activity entries.")).toBeInTheDocument();
await user.click(screen.getByTestId("task-activity-copy-logs"));
await waitFor(() => expect(writeText).toHaveBeenCalledWith("[2026-08-18T13:00:00.000Z] kept entry"));
expect(screen.getByText("Showing the most recent 1 activity entries.")).toBeInTheDocument();
});
it("defines tokenized desktop and mobile clearance for the Feed action group", () => {
const css = readDashboardStylesSource();
const mobileCss = css.slice(css.indexOf("@media (max-width: 768px)"));
expect(css).toContain(".detail-activity-actions");
expect(css).toContain(".detail-activity-actions ~ .detail-activity-list > .detail-log-entry:first-child");
expect(css).toContain("padding-inline-end: calc((var(--space-2xl) * 4) + var(--space-lg));");
expect(mobileCss).toContain(".detail-activity-actions");
expect(mobileCss).toContain("padding-inline-end: calc((var(--space-2xl) * 4) + var(--space-md));");
expect(css).not.toContain(".activity-toolbar");
});
it("refreshes an empty Feed when the persisted task log has gained activity", async () => {
const user = userEvent.setup();
const { fetchTaskDetail } = await import("../../api");

View File

@@ -7918,6 +7918,10 @@
"activity": "Activity",
"activityHeading": "Activity",
"agentLog": "Agent Log",
"copy": "Copy logs",
"copyDisplayed": "Copy displayed activity logs",
"copyFailure": "Failed to copy displayed activity",
"copySuccess": "Displayed activity copied to clipboard",
"loadingActivity": "Loading activity…",
"noActivity": "(no activity)",
"truncated_one": "Showing the most recent {{count}} activity entries.",

View File

@@ -6126,6 +6126,7 @@
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"modelOverrides": "",
"allow": "",
"commaSeparatedValuesSentToOpenRouterModelSync": "",
"controlsHowMuchReasoningEffortTheAIModel": "",
@@ -6550,6 +6551,10 @@
"plugins": ""
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": "",
"advancedWorkflowPolicy": "",
"aIMergeCommitSummaries": "",
@@ -7874,6 +7879,10 @@
"activity": "Actividad",
"activityHeading": "Actividad",
"agentLog": "Registro del agente",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "",
"noActivity": "(sin actividad)",
"truncated_one": "",

View File

@@ -6126,6 +6126,7 @@
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"modelOverrides": "",
"allow": "",
"commaSeparatedValuesSentToOpenRouterModelSync": "",
"controlsHowMuchReasoningEffortTheAIModel": "",
@@ -6550,6 +6551,10 @@
"plugins": ""
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": "",
"advancedWorkflowPolicy": "",
"aIMergeCommitSummaries": "",
@@ -7874,6 +7879,10 @@
"activity": "Activité",
"activityHeading": "Activité",
"agentLog": "Journal de l'agent",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "",
"noActivity": "(aucune activité)",
"truncated_one": "",

View File

@@ -6126,6 +6126,7 @@
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"modelOverrides": "",
"allow": "",
"commaSeparatedValuesSentToOpenRouterModelSync": "",
"controlsHowMuchReasoningEffortTheAIModel": "",
@@ -6550,6 +6551,10 @@
"plugins": ""
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": "",
"advancedWorkflowPolicy": "",
"aIMergeCommitSummaries": "",
@@ -7874,6 +7879,10 @@
"activity": "활동",
"activityHeading": "활동",
"agentLog": "에이전트 로그",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "",
"noActivity": "(활동 없음)",
"truncated_one": "",

View File

@@ -6468,6 +6468,10 @@
"description": "Substitui as taxas por 1 milhão de tokens usadas nas estimativas de custo do Command Center. As substituições têm prioridade sobre a linha de base integrada; modelos não listados continuam usando a linha de base. Sem padrão — não definido (sem substituições)."
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": " Adicionar predefinição ",
"advancedWorkflowPolicy": " Política avançada de fluxo de trabalho ",
"aIMergeCommitSummaries": " Resumos de commit de merge por IA ",
@@ -6537,6 +6541,7 @@
"chatDefaultReset": "Redefinir padrão do Chat"
},
"globalModels": {
"modelOverrides": "",
"allow": "allow",
"commaSeparatedValuesSentToOpenRouterModelSync": "Valores separados por vírgula enviados à sincronização de modelos do OpenRouter. Sem padrão — não definido (sem filtro).",
"controlsHowMuchReasoningEffortTheAIModel": "Controla o quanto de esforço de raciocínio o modelo de IA usa. Níveis mais altos produzem melhores resultados, mas custam mais. Sem padrão — não definido (aplica-se o esforço padrão do próprio modelo).",
@@ -7913,6 +7918,10 @@
"activity": "Atividade",
"activityHeading": "Atividade",
"agentLog": "Log do agente",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "Carregando atividade…",
"noActivity": "(sem atividade)",
"truncated_one": "Mostrando as {{count}} entradas de atividade mais recentes.",

View File

@@ -6126,6 +6126,7 @@
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"modelOverrides": "",
"allow": "",
"commaSeparatedValuesSentToOpenRouterModelSync": "",
"controlsHowMuchReasoningEffortTheAIModel": "",
@@ -6550,6 +6551,10 @@
"plugins": ""
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": "",
"advancedWorkflowPolicy": "",
"aIMergeCommitSummaries": "",
@@ -7874,6 +7879,10 @@
"activity": "活动",
"activityHeading": "活动",
"agentLog": "代理日志",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "",
"noActivity": "(无活动)",
"truncated_one": "",

View File

@@ -6126,6 +6126,7 @@
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"modelOverrides": "",
"allow": "",
"commaSeparatedValuesSentToOpenRouterModelSync": "",
"controlsHowMuchReasoningEffortTheAIModel": "",
@@ -6550,6 +6551,10 @@
"plugins": ""
},
"projectModels": {
"modelOverrides": "",
"projectLanesSubheading": "",
"summarizationPointer": "",
"workflowLanesSubheading": "",
"addPreset": "",
"advancedWorkflowPolicy": "",
"aIMergeCommitSummaries": "",
@@ -7874,6 +7879,10 @@
"activity": "活動",
"activityHeading": "活動",
"agentLog": "代理日誌",
"copy": "",
"copyDisplayed": "",
"copyFailure": "",
"copySuccess": "",
"loadingActivity": "",
"noActivity": "(無活動)",
"truncated_one": "",

View File

@@ -6193,6 +6193,7 @@ export default interface Resources {
"leaveEmptyToOmitThisHeaderDefaultFusion": "Leave empty to omit this header. No default — unset (Fusion falls back to the title \"Fusion\" when unset).",
"leaveEmptyToOmitThisHeaderDefaultHttps": "Leave empty to omit this header. No default — unset (Fusion falls back to https://runfusion.ai when unset).",
"modelLanes": "Model Lanes",
"modelOverrides": "Model Overrides",
"noFallback": "No fallback",
"openRouterAdvanced": "OpenRouter advanced",
"openRouterAllowFallbacks": "OpenRouter allow fallbacks",
@@ -6646,6 +6647,7 @@ export default interface Resources {
"loadingAvailableModels": "Loading available models…",
"loadingWorkflowModelLanes": "Loading workflow model lanes…",
"modelLanes": "Model Lanes",
"modelOverrides": "Model Overrides",
"modelPresets": "Model Presets",
"name": "Name",
"noCap": "No cap",
@@ -6660,6 +6662,7 @@ export default interface Resources {
"prDescriptionPromptInstructionsHelp": "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default — unset.",
"prTitlePromptInstructionsHelp": "Guides the AI-generated Create PR title. Leave blank to use the default PR metadata prompt. No default — unset.",
"presetEditor": "Preset editor",
"projectLanesSubheading": "Project lanes",
"reset": " Reset ",
"resetToDefaultNoCap": "Reset to default (no cap)",
"resetToInheritFromGlobal": "Reset to inherit from global",
@@ -6668,6 +6671,7 @@ export default interface Resources {
"selectChatDefaultAgent": "Select a chat default agent",
"selectChatDefaultModel": "Select a chat default model",
"selectExecutorEscalationModel": "Select an escalation model",
"summarizationPointer": "Summarization models are configured under AI Title and Git Commit Message Summarization below.",
"taskDefinitionInInputLanguage": "Write task definitions in the operator's input language",
"taskDefinitionInInputLanguageHelp": "When enabled, generated task-definition prose uses supported detectable input languages (Spanish, French, Korean, or Chinese as zh-CN). Headings, markers, and code stay English. Unsupported or undetectable input stays English. Default: disabled.",
"theseProjectOverridesApplyToTheActiveDefault": " These project overrides apply to the active default workflow. ",
@@ -6675,7 +6679,8 @@ export default interface Resources {
"useDefault": "Use default",
"useWorkflowDefault": "Use workflow default",
"whenEnabledMergeCommitMessagesIncludeAnAI": " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. Default: enabled. ",
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. "
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ",
"workflowLanesSubheading": "Workflow lanes"
},
"prompts": {
"surfaceExplanation": "Use this section for agent role system prompt templates, role assignments, and global PromptKey segment overrides. Per-workflow step prompts for prompt and gate nodes are edited in the Workflow Editor. No default — unset (built-in role prompts apply until overridden)."
@@ -7987,6 +7992,10 @@ export default interface Resources {
"activity": "Activity",
"activityHeading": "Activity",
"agentLog": "Agent Log",
"copy": "Copy logs",
"copyDisplayed": "Copy displayed activity logs",
"copyFailure": "Failed to copy displayed activity",
"copySuccess": "Displayed activity copied to clipboard",
"loadingActivity": "Loading activity…",
"noActivity": "(no activity)",
"truncated_one": "Showing the most recent {{count}} activity entries.",