diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json
index 0b90b2c7..fd5db6a9 100644
--- a/apps/client/public/locales/en-US/translation.json
+++ b/apps/client/public/locales/en-US/translation.json
@@ -48,6 +48,7 @@
"Create page": "Create page",
"Create space": "Create space",
"Create workspace": "Create workspace",
+ "Critical": "Critical",
"Current password": "Current password",
"Dark": "Dark",
"Date": "Date",
@@ -125,6 +126,7 @@
"Link copied": "Link copied",
"Login": "Login",
"Logout": "Logout",
+ "Major": "Major",
"Manage Group": "Manage Group",
"Manage members": "Manage members",
"member": "member",
@@ -453,6 +455,8 @@
"{{count}} Columns": "{{count}} Columns",
"{{count}} command available_one": "1 command available",
"{{count}} command available_other": "{{count}} commands available",
+ "{{count}} edits": "{{count}} edits",
+ "{{count}} major": "{{count}} major",
"{{count}} result available_one": "1 result available",
"{{count}} result available_other": "{{count}} results available",
"{{count}} result found_one": "{{count}} result found",
diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json
index 2aa13e5b..174ea79d 100644
--- a/apps/client/public/locales/ru-RU/translation.json
+++ b/apps/client/public/locales/ru-RU/translation.json
@@ -48,6 +48,7 @@
"Create page": "Создать страницу",
"Create space": "Создать пространство",
"Create workspace": "Создать рабочую область",
+ "Critical": "Критично",
"Current password": "Текущий пароль",
"Dark": "Темная",
"Date": "Дата",
@@ -127,6 +128,7 @@
"Link copied": "Ссылка скопирована",
"Login": "Войти",
"Logout": "Выйти",
+ "Major": "Существенно",
"Manage Group": "Управление группой",
"Manage members": "Управление участниками",
"member": "участник",
@@ -461,6 +463,8 @@
"{{count}} Columns": "{count, plural, one{# столбец} few{# столбца} many{# столбцов} other{# столбца}}",
"{{count}} command available_one": "Доступна 1 команда",
"{{count}} command available_other": "Доступно {{count}} команд",
+ "{{count}} edits": "{count, plural, one{# правка} few{# правки} many{# правок} other{# правки}}",
+ "{{count}} major": "{count, plural, one{# существенная} few{# существенных} many{# существенных} other{# существенных}}",
"{{count}} result available_one": "Доступен 1 результат",
"{{count}} result available_other": "Доступно {{count}} результатов",
"{{count}} result found_one": "Найден {{count}} результат",
diff --git a/apps/client/src/features/comment/components/agent-edit-card.test.tsx b/apps/client/src/features/comment/components/agent-edit-card.test.tsx
index f1722670..46849f35 100644
--- a/apps/client/src/features/comment/components/agent-edit-card.test.tsx
+++ b/apps/client/src/features/comment/components/agent-edit-card.test.tsx
@@ -1,6 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MantineProvider } from "@mantine/core";
+import { createInstance } from "i18next";
+import { initReactI18next, I18nextProvider } from "react-i18next";
import { IComment } from "@/features/comment/types/comment.types";
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
@@ -37,7 +39,7 @@ vi.mock("@/features/space/queries/space-query.ts", () => ({
useGetSpaceBySlugQuery: () => ({ data: undefined }),
}));
-import AgentEditCard from "./agent-edit-card";
+import AgentEditCard, { RunHeader } from "./agent-edit-card";
const body = (text: string) =>
JSON.stringify({
@@ -109,6 +111,18 @@ describe("AgentEditCard — suggested edit diff + Apply (#315)", () => {
expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
});
+ it("shows the Applied badge for an applied suggestion, not a pending one (F1)", () => {
+ // Pending: no Applied badge.
+ const { unmount } = renderCard(edit(), true);
+ expect(screen.queryByText("Applied")).toBeNull();
+ unmount();
+ // Applied (kept alive by replies -> resolved, #329): the badge is restored.
+ renderCard(edit({ suggestionAppliedAt: new Date() }), true);
+ expect(screen.getByText("Applied")).toBeDefined();
+ expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
+ expect(screen.queryByRole("button", { name: "Dismiss" })).toBeNull();
+ });
+
it("calls the apply mutation with {commentId, pageId} on click", () => {
applyMutateAsync.mockClear();
renderCard(edit(), true);
@@ -185,3 +199,86 @@ describe("AgentEditCard — provenance", () => {
beforeEach(() => localStorage.clear());
afterEach(() => localStorage.clear());
});
+
+// The RunHeader's "N edits · M major" is built with interpolated t() keys; the
+// default (uninitialized) react-i18next t returns the key verbatim WITHOUT
+// interpolating, so a numeric assertion needs a real, initialised i18n instance.
+// This isolated instance carries just the two count keys with escapeValue off so
+// "{{count}}" is substituted and the rendered numbers are assertable.
+const headerI18n = createInstance();
+headerI18n.use(initReactI18next).init({
+ lng: "en",
+ fallbackLng: "en",
+ resources: {
+ en: {
+ translation: {
+ "{{count}} edits": "{{count}} edits",
+ "{{count}} major": "{{count}} major",
+ },
+ },
+ },
+ interpolation: { escapeValue: false },
+});
+
+const runComment = (id: string, tag: string): IComment =>
+ edit({
+ id,
+ content: body(`${tag} some rationale`),
+ });
+
+function renderRunHeader(comments: IComment[]) {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+describe("RunHeader — agent-run series header (F3)", () => {
+ // 5 edits: 2 critical + 1 major + 1 minor + 1 unknown(verdict) => 3 "major".
+ const series = () => [
+ runComment("e1", "[Критично]"),
+ runComment("e2", "[Критично]"),
+ runComment("e3", "[Существенно]"),
+ runComment("e4", "[Незначительно]"),
+ runComment("e5", "[Неверно]"),
+ ];
+
+ it("shows the total edit count", () => {
+ renderRunHeader(series());
+ expect(screen.getByText(/5 edits/)).toBeDefined();
+ });
+
+ it("counts ONLY major+critical as major (not minor/unknown)", () => {
+ renderRunHeader(series());
+ // 2 critical + 1 major = 3; minor and the [Неверно] verdict are excluded.
+ expect(screen.getByText(/3 major/)).toBeDefined();
+ // Non-vacuous: the wrong tally (counting all 5, or 4) must NOT appear.
+ expect(screen.queryByText(/5 major/)).toBeNull();
+ expect(screen.queryByText(/4 major/)).toBeNull();
+ });
+
+ it("omits the major segment entirely when there are no significant edits", () => {
+ renderRunHeader([
+ runComment("e1", "[Незначительно]"),
+ runComment("e2", "[Неверно]"),
+ ]);
+ expect(screen.getByText(/2 edits/)).toBeDefined();
+ expect(screen.queryByText(/major/)).toBeNull();
+ });
+
+ it("renders the provenance line (agent role name)", () => {
+ renderRunHeader(series());
+ expect(screen.getAllByText("Corrector").length).toBeGreaterThan(0);
+ });
+
+ it("renders NO 'Accept all' control (rejected by product)", () => {
+ renderRunHeader(series());
+ expect(screen.queryByText(/accept all/i)).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: /accept all/i }),
+ ).toBeNull();
+ });
+});
diff --git a/apps/client/src/features/comment/components/agent-edit-card.tsx b/apps/client/src/features/comment/components/agent-edit-card.tsx
index 8aecd0b0..dc008ec8 100644
--- a/apps/client/src/features/comment/components/agent-edit-card.tsx
+++ b/apps/client/src/features/comment/components/agent-edit-card.tsx
@@ -1,5 +1,6 @@
import React, { useMemo } from "react";
import {
+ Badge,
Box,
Button,
Group,
@@ -222,6 +223,7 @@ function AgentEditCard({
const isOwnerOrAdmin =
currentUser?.user?.id === comment.creatorId || userSpaceRole === "admin";
+ const isApplied = comment.suggestionAppliedAt != null;
const showApply = canShowApply(comment, canEdit);
const showDismiss = canShowDismiss(comment, canComment, isOwnerOrAdmin);
const pending =
@@ -263,6 +265,7 @@ function AgentEditCard({
p="10px 12px"
role="button"
tabIndex={0}
+ aria-label={t("Jump to comment selection")}
onClick={() => scrollToCommentMark(comment.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
@@ -313,6 +316,21 @@ function AgentEditCard({
)}
+ {/* Applied state (#315): a suggestion that was applied but kept alive by
+ its replies (so #329 resolved instead of hard-deleting it) still
+ shows it was applied — the badge the pre-redesign card carried. A
+ childless applied suggestion is gone from the list entirely, so this
+ only renders in the Resolved tab. */}
+ {isApplied && (
+
+ {t("Applied")}
+
+ )}
{showDismiss && (