FN-8561: add lifecycle dates to task cards
Show localized creation and completion dates directly on task cards. - Render compact, locale-aware lifecycle timestamps with accessible full-date titles. - Persist archive timestamps for completed archived-task fallbacks and add coverage. - Add translated labels, documentation, and a minor release changeset. Files changed: .changeset/fn-8561-task-card-dates.md | 7 +++ docs/dashboard-guide.md | 1 + .../__tests__/archive-entry-serialization.test.ts | 35 ++++++++++++++ packages/core/src/task-store/serialization.ts | 8 ++++ packages/core/src/types/task-core.ts | 5 ++ packages/dashboard/app/components/TaskCard.css | 27 +++++++++++ packages/dashboard/app/components/TaskCard.tsx | 55 ++++++++++++++++++++++ .../__tests__/TaskCard.host-inventory.test.tsx | 21 +++++++++ .../app/components/__tests__/TaskCard.test.tsx | 17 +++++++ .../dashboard/app/i18n/__tests__/format.test.ts | 19 +++++++- packages/dashboard/app/i18n/format.ts | 38 +++++++++++++++ packages/i18n/locales/en/app.json | 4 ++ packages/i18n/locales/es/app.json | 4 ++ packages/i18n/locales/fr/app.json | 4 ++ packages/i18n/locales/ko/app.json | 4 ++ packages/i18n/locales/zh-CN/app.json | 4 ++ packages/i18n/locales/zh-TW/app.json | 4 ++ 17 files changed, 256 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8561 Fusion-Task-Lineage: 474b949b-d8e7-469a-b564-00acae66e58a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8561-task-card-dates.md
Normal file
7
.changeset/fn-8561-task-card-dates.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Show creation and completion dates directly on task cards.
|
||||
category: feature
|
||||
dev: Archived cards retain their canonical archive timestamp for completion fallback.
|
||||
@@ -244,6 +244,7 @@ Features:
|
||||
<!-- FNXC:TaskActivity 2026-07-28-12:00: FN-8300 requires visual card activity to agree with fresh planner logs during status-null planning transitions; Board and List reuse their existing active affordances. -->
|
||||
- GitLab tracking badges on task cards for linked GitLab project issues, group issues, and merge requests; stale GitLab metadata uses a warning-colored badge while GitHub badges remain unchanged.
|
||||
- GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown in the footer with other external-source metadata
|
||||
- Task cards show their creation time for today or local calendar day otherwise; done and archived cards also show their completion date.
|
||||
- Task card header meta badges group priority and fast mode in the header; priority badges include the shared urgency glyph/color language (low blue/info, high amber/warning, urgent red/error) while agent-created provenance renders in a dedicated bottom-left row ahead of workflow identity so the ID/status/actions header does not wrap on narrow cards. Agent labels prefer `sourceMetadata.agentName` over raw agent IDs.
|
||||
- **Settings → Appearance → Show cost badges on task cards** is default off. When enabled, board cards with recorded positive token usage show a compact derived-cost badge with the card's other footer/meta chips; unpriced models display `—`, and cards with no usage render no badge shell.
|
||||
<!-- FNXC:TaskCardCostBadge 2026-07-11-12:25: The card spend badge is opt-in because card footers are dense. It must remain guess-free (unpriced `—`, no fabricated `$0`) and absent for tasks without positive token usage. -->
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { archiveEntryToTask } from "../task-store/serialization.js";
|
||||
import type { ArchivedTaskEntry } from "../types/archive-planning.js";
|
||||
|
||||
describe("archiveEntryToTask", () => {
|
||||
const entry = {
|
||||
id: "FN-8561",
|
||||
lineageId: "lineage-8561",
|
||||
description: "Archived task",
|
||||
column: "archived",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-07-20T10:00:00.000Z",
|
||||
updatedAt: "2026-07-23T10:00:00.000Z",
|
||||
columnMovedAt: "2026-07-22T10:00:00.000Z",
|
||||
executionCompletedAt: "2026-07-23T10:00:00.000Z",
|
||||
archivedAt: "2026-07-24T10:00:00.000Z",
|
||||
} as ArchivedTaskEntry;
|
||||
|
||||
it.each([false, true])("preserves distinct lifecycle timestamps in slim=%s payloads", (slim) => {
|
||||
const task = archiveEntryToTask(entry, slim);
|
||||
|
||||
expect(task.column).toBe("archived");
|
||||
expect(task.columnMovedAt).toBe("2026-07-22T10:00:00.000Z");
|
||||
expect(task.executionCompletedAt).toBe("2026-07-23T10:00:00.000Z");
|
||||
expect(task.archivedAt).toBe("2026-07-24T10:00:00.000Z");
|
||||
});
|
||||
|
||||
it("keeps legacy and active-compatible payloads readable without archivedAt", () => {
|
||||
const legacy = archiveEntryToTask({ ...entry, archivedAt: undefined } as unknown as ArchivedTaskEntry);
|
||||
expect(legacy.archivedAt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -373,6 +373,14 @@ export function archiveEntryToTask(
|
||||
planningStartedAt: entry.planningStartedAt,
|
||||
executionStartedAt: entry.executionStartedAt,
|
||||
executionCompletedAt: entry.executionCompletedAt,
|
||||
/*
|
||||
FNXC:ArchiveLifecycle 2026-07-24-11:02:
|
||||
FN-8561 needs archived TaskCard completion fallback to use the immutable
|
||||
archive transition, not the pre-archive columnMovedAt snapshot or updatedAt.
|
||||
Preserve archivedAt on every slim and full archive read without changing
|
||||
restore persistence semantics.
|
||||
*/
|
||||
archivedAt: entry.archivedAt,
|
||||
modelPresetId: entry.modelPresetId,
|
||||
modelProvider: entry.modelProvider,
|
||||
modelId: entry.modelId,
|
||||
|
||||
@@ -1149,6 +1149,11 @@ export interface Task {
|
||||
* Set once on first transition to `done`; may be cleared on reopen to
|
||||
* todo/triage when resume state is not preserved. */
|
||||
executionCompletedAt?: string;
|
||||
/**
|
||||
* Canonical archive transition timestamp. Present only on task payloads
|
||||
* hydrated from archived entries; active and legacy tasks may omit it.
|
||||
*/
|
||||
archivedAt?: string;
|
||||
deletedAt?: string;
|
||||
allowResurrection?: boolean;
|
||||
createdAt: string;
|
||||
|
||||
@@ -972,6 +972,27 @@ Task-card agent badges must retain their visible text label in narrow containers
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskCardDates 2026-07-24-11:02:
|
||||
Lifecycle dates share the card's compact metadata rhythm and wrap independently
|
||||
on narrow/mobile cards, so added terminal metadata cannot force horizontal scroll.
|
||||
*/
|
||||
.card-lifecycle-dates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-sm);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-lifecycle-dates time {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.card-footer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1941,6 +1962,12 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu
|
||||
padding: var(--space-xs) 0;
|
||||
}
|
||||
|
||||
.card-lifecycle-dates {
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-xs);
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.card-footer-row {
|
||||
margin-top: var(--space-xs);
|
||||
gap: var(--space-xs);
|
||||
|
||||
@@ -53,6 +53,7 @@ import { MAX_AUTO_MERGE_RETRIES, type BlockerFanoutEntry } from "../hooks/useBlo
|
||||
import { useRetryWarning } from "../context/RetryWarningContext";
|
||||
import { useCostBadge } from "../context/CostBadgeContext";
|
||||
import { useColumnLabel } from "../i18n/labels";
|
||||
import { formatCompactLifecycleDate, useLocaleFormat } from "../i18n/format";
|
||||
import { WorkspaceWorktreesSummary, isWorkspaceTask } from "./WorkspaceWorktreesSummary";
|
||||
import { WorkflowIcon } from "./WorkflowIcon";
|
||||
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel, type TaskContextMenuColumnFlags, type TaskContextMenuColumnMetadata, type TaskMenuActionDescriptor } from "./TaskContextMenu";
|
||||
@@ -753,6 +754,12 @@ function areCommentsEqual(previous: Task["comments"], next: Task["comments"]): b
|
||||
|
||||
// Keep this comparator aligned with the fields TaskCard renders directly and the
|
||||
// task metadata that influences child badge freshness/subscriptions.
|
||||
function millisecondsUntilNextLocalMidnight(now: Date): number {
|
||||
const next = new Date(now);
|
||||
next.setHours(24, 0, 0, 25);
|
||||
return Math.max(1, next.getTime() - now.getTime());
|
||||
}
|
||||
|
||||
function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): boolean {
|
||||
const previousTask = previous.task;
|
||||
const nextTask = next.task;
|
||||
@@ -820,6 +827,8 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
previousTask.timedExecutionMs === nextTask.timedExecutionMs &&
|
||||
previousTask.updatedAt === nextTask.updatedAt &&
|
||||
previousTask.createdAt === nextTask.createdAt &&
|
||||
previousTask.executionCompletedAt === nextTask.executionCompletedAt &&
|
||||
previousTask.archivedAt === nextTask.archivedAt &&
|
||||
previousTask.status === nextTask.status &&
|
||||
previousTask.recentAgentActivityAt === nextTask.recentAgentActivityAt &&
|
||||
previousTask.priority === nextTask.priority &&
|
||||
@@ -963,6 +972,7 @@ function TaskCardComponent({
|
||||
nearDuplicateCanonicalInactive,
|
||||
}: TaskCardProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const { locale } = useLocaleFormat();
|
||||
const columnLabel = useColumnLabel();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [fileDragOver, setFileDragOver] = useState(false);
|
||||
@@ -981,6 +991,27 @@ function TaskCardComponent({
|
||||
const [isAddressingPrFeedback, setIsAddressingPrFeedback] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [timeIndicatorNowMs, setTimeIndicatorNowMs] = useState(() => Date.now());
|
||||
const [lifecycleNowMs, setLifecycleNowMs] = useState(() => Date.now());
|
||||
|
||||
/*
|
||||
FNXC:TaskCardDates 2026-07-24-11:02:
|
||||
FN-8561 requires compact lifecycle labels to change at the viewer's local
|
||||
midnight even when memoized task props are unchanged. One boundary timer per
|
||||
mounted card avoids stale "today" time labels and is always cleaned up.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let timer: number | undefined;
|
||||
const schedule = () => {
|
||||
timer = window.setTimeout(() => {
|
||||
setLifecycleNowMs(Date.now());
|
||||
schedule();
|
||||
}, millisecondsUntilNextLocalMidnight(new Date()));
|
||||
};
|
||||
schedule();
|
||||
return () => {
|
||||
if (timer !== undefined) window.clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const descTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const touchOpenHandledRef = useRef(false);
|
||||
@@ -1622,6 +1653,16 @@ function TaskCardComponent({
|
||||
};
|
||||
}, [task.column, task.status, task.columnMovedAt, task.timedExecutionMs, task.updatedAt, task.workflowStepResults, task.log, task.firstExecutionAt, task.cumulativeActiveMs, task.cumulativePlanningMs, task.planningStartedAt, task.executionStartedAt, task.executionCompletedAt, timeIndicatorNowMs]);
|
||||
|
||||
const lifecycleDates = useMemo(() => {
|
||||
const created = formatCompactLifecycleDate(task.createdAt, locale, new Date(lifecycleNowMs));
|
||||
const completionSource = task.executionCompletedAt
|
||||
?? (task.column === "archived" ? task.archivedAt : undefined);
|
||||
const completed = (task.column === "done" || task.column === "archived")
|
||||
? formatCompactLifecycleDate(completionSource, locale, new Date(lifecycleNowMs))
|
||||
: null;
|
||||
return { created, completed };
|
||||
}, [task.createdAt, task.executionCompletedAt, task.archivedAt, task.column, locale, lifecycleNowMs]);
|
||||
|
||||
const liveBadgeData = badgeUpdates.get(`${projectId ?? "default"}:${task.id}`);
|
||||
|
||||
// Get fresh batch data if available
|
||||
@@ -3673,6 +3714,20 @@ function TaskCardComponent({
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{(lifecycleDates.created || lifecycleDates.completed) && (
|
||||
<div className="card-lifecycle-dates" data-testid="card-lifecycle-dates">
|
||||
{lifecycleDates.created && (
|
||||
<time dateTime={lifecycleDates.created.dateTime} title={t("tasks.createdAtTitle", "Created {{date}}", { date: lifecycleDates.created.full })}>
|
||||
{t("tasks.createdAt", "Created {{date}}", { date: lifecycleDates.created.compact })}
|
||||
</time>
|
||||
)}
|
||||
{lifecycleDates.completed && (
|
||||
<time dateTime={lifecycleDates.completed.dateTime} title={t("tasks.completedAtTitle", "Completed {{date}}", { date: lifecycleDates.completed.full })}>
|
||||
{t("tasks.completedAt", "Completed {{date}}", { date: lifecycleDates.completed.compact })}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(footerHasLeadingContent || (footerRightHasContent && !placeFooterRightInMeta)) && (
|
||||
<div className={`card-footer-row${chipFarRight ? " card-footer-row--chip-far-right" : ""}`}>
|
||||
{filesChangedButton}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const taskCardHosts = [
|
||||
"../Column.tsx",
|
||||
"../DockTaskList.tsx",
|
||||
"../useRightDockController.tsx",
|
||||
"../WorktreeGroup.tsx",
|
||||
"../dashboard/MainContent.tsx",
|
||||
] as const;
|
||||
|
||||
describe("TaskCard host inventory (FN-8561)", () => {
|
||||
it("keeps every shared-card host delegated to the canonical TaskCard", () => {
|
||||
for (const relativePath of taskCardHosts) {
|
||||
const source = readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
|
||||
expect(source, relativePath).toMatch(/import\s+\{\s*TaskCard\s*\}\s+from/);
|
||||
expect(source, relativePath).toMatch(/<TaskCard\b/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -243,6 +243,23 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("TaskCard", () => {
|
||||
it("renders creation on all cards and terminal completion from canonical lifecycle timestamps", () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const archivedAt = "2026-07-20T09:00:00.000Z";
|
||||
const { rerender } = render(
|
||||
<TaskCard task={makeTask({ createdAt })} onOpenDetail={noop} addToast={noop} />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("card-lifecycle-dates")).toHaveTextContent("Created");
|
||||
expect(screen.queryByText(/Completed/)).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<TaskCard task={makeTask({ column: "archived", createdAt, archivedAt, columnMovedAt: "2026-07-19T09:00:00.000Z" })} onOpenDetail={noop} addToast={noop} />,
|
||||
);
|
||||
expect(screen.getByTestId("card-lifecycle-dates")).toHaveTextContent("Completed");
|
||||
expect(screen.getByTestId("card-lifecycle-dates").querySelectorAll("time")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders GitLab tracking badges for linked and stale items without dropping GitHub badges", () => {
|
||||
const gitlabItem = {
|
||||
kind: "merge_request" as const,
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock("react-i18next", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { useLocaleFormat } = await import("../format");
|
||||
const { useLocaleFormat, formatCompactLifecycleDate } = await import("../format");
|
||||
|
||||
describe("useLocaleFormat", () => {
|
||||
it("formats numbers with the active locale's separators", () => {
|
||||
@@ -39,6 +39,23 @@ describe("useLocaleFormat", () => {
|
||||
expect(fr).toMatch(/janvier/);
|
||||
});
|
||||
|
||||
it("formats lifecycle timestamps as time today and calendar day otherwise", () => {
|
||||
const now = new Date(2026, 6, 24, 12, 0);
|
||||
const today = formatCompactLifecycleDate(new Date(2026, 6, 24, 9, 5).toISOString(), "en-US", now);
|
||||
const previousDay = formatCompactLifecycleDate(new Date(2026, 6, 23, 9, 5).toISOString(), "en-US", now);
|
||||
const priorYear = formatCompactLifecycleDate(new Date(2025, 6, 24, 9, 5).toISOString(), "en-US", now);
|
||||
|
||||
expect(today?.compact).toMatch(/9:05/);
|
||||
expect(previousDay?.compact).toMatch(/Jul/);
|
||||
expect(priorYear?.compact).toContain("2025");
|
||||
expect(today?.full).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns no lifecycle model for missing or invalid values", () => {
|
||||
expect(formatCompactLifecycleDate(undefined, "en", new Date())).toBeNull();
|
||||
expect(formatCompactLifecycleDate("not-a-date", "en", new Date())).toBeNull();
|
||||
});
|
||||
|
||||
it("exposes the resolved locale", () => {
|
||||
state.lng = "zh-CN";
|
||||
expect(renderHook(() => useLocaleFormat()).result.current.locale).toBe("zh-CN");
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface CompactLifecycleDate {
|
||||
compact: string;
|
||||
full: string;
|
||||
dateTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats lifecycle timestamps against the viewer's local calendar, rather than
|
||||
* UTC date boundaries. Invalid and missing legacy values intentionally produce
|
||||
* no model so callers never render an Invalid Date shell.
|
||||
*/
|
||||
export function formatCompactLifecycleDate(
|
||||
value: string | undefined,
|
||||
locale: string,
|
||||
now: Date = new Date(),
|
||||
): CompactLifecycleDate | null {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime()) || Number.isNaN(now.getTime())) return null;
|
||||
|
||||
const sameDay = date.getFullYear() === now.getFullYear()
|
||||
&& date.getMonth() === now.getMonth()
|
||||
&& date.getDate() === now.getDate();
|
||||
const compact = sameDay
|
||||
? new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }).format(date)
|
||||
: new Intl.DateTimeFormat(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
...(date.getFullYear() !== now.getFullYear() ? { year: "numeric" } : {}),
|
||||
}).format(date);
|
||||
|
||||
return {
|
||||
compact,
|
||||
full: new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(date),
|
||||
dateTime: date.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Locale-aware date/number formatting bound to the active i18n locale.
|
||||
*
|
||||
|
||||
@@ -8325,6 +8325,10 @@
|
||||
"closeIssue": "Close Issue",
|
||||
"collapse": "Collapse",
|
||||
"createdByAgent": "Created by agent",
|
||||
"createdAt": "Created {{date}}",
|
||||
"createdAtTitle": "Created {{date}}",
|
||||
"completedAt": "Completed {{date}}",
|
||||
"completedAtTitle": "Completed {{date}}",
|
||||
"createdByAgentNamed": "Created by agent: {{name}}",
|
||||
"createdPr": "Created PR #{{number}}",
|
||||
"createFailed": "Failed to create task",
|
||||
|
||||
@@ -8306,6 +8306,10 @@
|
||||
"closeIssue": "Cerrar issue",
|
||||
"collapse": "Contraer",
|
||||
"createdByAgent": "Creado por un agente",
|
||||
"createdAt": "Creada {{date}}",
|
||||
"createdAtTitle": "Creada {{date}}",
|
||||
"completedAt": "Completada {{date}}",
|
||||
"completedAtTitle": "Completada {{date}}",
|
||||
"createdByAgentNamed": "Creado por el agente: {{name}}",
|
||||
"createdPr": "PR #{{number}} creada",
|
||||
"createFailed": "Error al crear la tarea",
|
||||
|
||||
@@ -8306,6 +8306,10 @@
|
||||
"closeIssue": "Fermer l'issue",
|
||||
"collapse": "Réduire",
|
||||
"createdByAgent": "Créé par un agent",
|
||||
"createdAt": "Créée {{date}}",
|
||||
"createdAtTitle": "Créée {{date}}",
|
||||
"completedAt": "Terminée {{date}}",
|
||||
"completedAtTitle": "Terminée {{date}}",
|
||||
"createdByAgentNamed": "Créé par l'agent : {{name}}",
|
||||
"createdPr": "PR #{{number}} créée",
|
||||
"createFailed": "Impossible de créer la tâche",
|
||||
|
||||
@@ -8306,6 +8306,10 @@
|
||||
"closeIssue": "이슈 닫기",
|
||||
"collapse": "접기",
|
||||
"createdByAgent": "에이전트가 생성함",
|
||||
"createdAt": "생성됨 {{date}}",
|
||||
"createdAtTitle": "생성됨 {{date}}",
|
||||
"completedAt": "완료됨 {{date}}",
|
||||
"completedAtTitle": "완료됨 {{date}}",
|
||||
"createdByAgentNamed": "에이전트가 생성함: {{name}}",
|
||||
"createdPr": "PR #{{number}} 생성됨",
|
||||
"createFailed": "작업 생성에 실패했습니다",
|
||||
|
||||
@@ -8306,6 +8306,10 @@
|
||||
"closeIssue": "关闭 Issue",
|
||||
"collapse": "折叠",
|
||||
"createdByAgent": "由智能体创建",
|
||||
"createdAt": "创建于 {{date}}",
|
||||
"createdAtTitle": "创建于 {{date}}",
|
||||
"completedAt": "完成于 {{date}}",
|
||||
"completedAtTitle": "完成于 {{date}}",
|
||||
"createdByAgentNamed": "由智能体创建:{{name}}",
|
||||
"createdPr": "已创建 PR #{{number}}",
|
||||
"createFailed": "创建任务失败",
|
||||
|
||||
@@ -8306,6 +8306,10 @@
|
||||
"closeIssue": "關閉 Issue",
|
||||
"collapse": "收合",
|
||||
"createdByAgent": "由代理程式建立",
|
||||
"createdAt": "建立於 {{date}}",
|
||||
"createdAtTitle": "建立於 {{date}}",
|
||||
"completedAt": "完成於 {{date}}",
|
||||
"completedAtTitle": "完成於 {{date}}",
|
||||
"createdByAgentNamed": "由代理程式建立:{{name}}",
|
||||
"createdPr": "已建立 PR #{{number}}",
|
||||
"createFailed": "建立任務失敗",
|
||||
|
||||
Reference in New Issue
Block a user