PAM · Sửa Workflow

Sửa nhỏ cho Workflow, chụp trên app thật.

Mỗi lỗi được khoanh đỏ trên ảnh chụp PAM hiện tại và khoanh xanh trên bản đã sửa, code ở ngay bên dưới. Chỉ sửa frontend, mỗi lỗi một commit, không đảo ngược quyết định nào gần đây của đồng đội.

8lỗi đã sửa
9commit, thêm 2 test mới
0thay đổi backend
5để sau: cần quyết định sản phẩm hoặc backend
Thanh tiêu đề Workflow

Không có nút nào để chạy cả Workflow

01
Vấn đề

Thanh tiêu đề có Lịch sử chạy, Phiên bản và Lưu phiên bản, nhưng không có nút chạy. Hiện tại muốn chạy hết thì phải chọn tất cả các bước rồi bấm Chạy phần đã chọn, hoặc chạy bước cuối và tự biết rằng nó kéo theo các bước phía trước.

Cách sửa

Thêm nút đen Chạy Workflow cạnh Lưu phiên bản. Nút gửi đúng yêu cầu như chọn tất cả + Chạy phần đã chọn, với mọi bước trên canvas, nên backend không đổi. Thông báo hiện "Đang chạy mọi bước".

Hiện tại: Không có nút chạy nào trên thanh tiêu đề
Hiện tạiKhông có nút chạy nào trên thanh tiêu đềBấm để xem toàn màn hình
Đã sửa: Chạy Workflow, cạnh Lưu phiên bản
Đã sửaChạy Workflow, cạnh Lưu phiên bảnBấm để xem toàn màn hình
Code · b61626b9 · 3 tệp · +29 / −1 dòngđã xoá đã thêm
src/features/workflow/components/WorkflowWorkbench.tsx
function Workbench({
variables.scope.kind === WorkflowScopeKind.Step
? t("toasts.runningStep")
- : t("toasts.runningSelection")
+ : variables.scope.nodeIds.length === graph.nodes.length
+ ? t("toasts.runningAll")
+ : t("toasts.runningSelection")
);
},
function Workbench({
runScoped({ kind: WorkflowScopeKind.Selection, nodeIds: executable });
}
+ /** Run workflow: every step on the canvas, as one run. The same request as select-all + Run
+ * selection, so the backend needs no whole-canvas scope; a one-step canvas runs that step. */
+ function runAll() {
+ const ids = graph.nodes.map((entry) => entry.id);
+ if (ids.length === 1) runNode(ids[0]!);
+ else runSelection(ids);
+ }
/** Runs a saved version forced to exactly `scope`'s nodes — the published-source counterpart
* to `runScoped`. `scope` is mandatory on every call site: there is no whole-canvas published
function Workbench({
</Button>
)}
+ {editing && graph.nodes.length > 0 && (
+ <TooltipHint label={t("header.runWorkflowHint")}>
+ <Button
+ size="sm"
+ variant="primary"
+ disabled={busy || heldNodes.size > 0}
+ loading={startDraft.isPending}
+ onClick={runAll}
+ >
+ <Play className="size-3" /> {t("header.runWorkflow")}
+ </Button>
+ </TooltipHint>
+ )}
<OverflowMenu
items={[
src/i18n/locales/en.json
…
"runningStep": "Running this step",
"runningSelection": "Running the selected steps",
+ "runningAll": "Running every step",
"runningVersion": "Running v{version}",
"archived": "Workflow archived"
…
"unsaved": "Unsaved changes",
"saveVersion": "Save version",
+ "runWorkflow": "Run workflow",
+ "runWorkflowHint": "Runs every step on the canvas, in order, as one run.",
"noChanges": "No changes since v{version}"
},
Bước Ask · bảng câu hỏi

Hỏi lại thì mất hết câu trả lời cũ

02
Vấn đề

Bấm Chạy lại từ đây ở bước Brief, bảng câu hỏi mở ra trống trơn. Muốn đổi một câu trả lời thì phải gõ lại tất cả: ở đây là sản phẩm, ưu đãi và giọng điệu.

Cách sửa

Bảng câu hỏi điền sẵn câu trả lời gần nhất của bước đó (đúng vị trí trong lượt chạy). Câu trả lời dạng chữ và dạng lựa chọn được điền sẵn; chỉ cần sửa chỗ muốn đổi rồi bấm Tiếp tục.

Hiện tại: Chạy lại từ đây: 0 / 3 đã trả lời, mọi ô đều trống
Hiện tạiChạy lại từ đây: 0 / 3 đã trả lời, mọi ô đều trốngBấm để xem toàn màn hình
Đã sửa: Cùng thao tác: câu trả lời cũ đã được điền sẵn
Đã sửaCùng thao tác: câu trả lời cũ đã được điền sẵnBấm để xem toàn màn hình
Code · 13b14fb4 · 3 tệp · +164 / −1 dòngđã xoá đã thêm
src/features/artifact/components/chat/ClarifyPanel.tsx
export function ClarifyForm({
onSubmit,
onAttach,
+ initialAnswers,
}: {
questions: { request: ClarifyRequest; context?: string }[];
export function ClarifyForm({
onSubmit: (answers: Record<string, string>) => void;
onAttach: (file: File) => Promise<string>;
+ /** Starting answers by `toolCallId`, e.g. a workflow Ask step's previous answers when it asks again. */
+ initialAnswers?: Record<string, string>;
}) {
const t = useTranslations("artifactChat.clarify");
- const [answers, setAnswers] = useState<Record<string, string>>({});
+ const [answers, setAnswers] = useState<Record<string, string>>(
+ () => initialAnswers ?? {}
+ );
const [uploads, setUploads] = useState<Record<string, boolean>>({});
const compose = useGateProps("content.write");
src/features/workflow/__tests__/ask-prefill.test.tsx
…
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { fireEvent, screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import {
+ WorkflowAttemptState,
+ WorkflowNodeType,
+ WorkflowSessionStatus,
+} from "@/shared/interfaces/generated/workflow";
+import type { WorkflowGraph } from "@/shared/interfaces/generated/workflow";
+
+import {
+ attemptOf,
+ definitionOf,
+ installBackend,
+ installDomShims,
+ renderEditor,
+ sessionOf,
+ step,
+} from "./editorHarness";
+
+vi.mock("@/libs/api", () =>
+ import("@test/helpers/api").then((m) => m.apiModule())
+);
+vi.mock("next/navigation", () =>
+ import("@test/helpers/router").then((m) => m.routerModule())
+);
+vi.mock("@/utils/toast", () =>
+ import("@test/helpers/toast").then((m) => m.toastModule())
+);
+vi.mock("../hooks/useWorkflowStream", () => ({
+ useWorkflowStream: () => ({ version: 0, connected: true }),
+}));
+vi.mock("@xyflow/react", async () => ({
+ ...(await vi.importActual<object>("@xyflow/react")),
+ Background: () => null,
+ MiniMap: () => null,
+}));
+
+beforeEach(() => {
+ installDomShims();
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+});
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+const fields = {
+ fields: [
+ { key: "product", question: "Which product?", required: true },
+ { key: "offer", question: "What is the offer?", required: true },
+ ],
+};
+const GRAPH: WorkflowGraph = {
+ nodes: [step("brief", WorkflowNodeType.Ask, "Brief", fields, 0)],
+ edges: [],
+};
+
+describe("[NO-CB] an Ask step that asks again starts from its last answers", () => {
+ // Symptom: Rerun from here on an Ask emptied every field, so changing one answer meant retyping all.
+ // Breaks if: the questions form stops reading the step's last completed attempt at the same path.
+ it("prefills the earlier answers, and Continue sends them without retyping", async () => {
+ const run = sessionOf(
+ GRAPH,
+ [
+ attemptOf("brief", {
+ id: "att-1",
+ ordinal: 1,
+ output: { offer: "50% off", product: "Green SM Taxi" },
+ }),
+ attemptOf("brief", {
+ id: "att-2",
+ ordinal: 2,
+ state: WorkflowAttemptState.AwaitingInput,
+ resolvedInput: fields,
+ finishedAt: null,
+ }),
+ ],
+ { status: WorkflowSessionStatus.AwaitingInput }
+ );
+ const backend = installBackend({
+ definition: definitionOf(GRAPH),
+ runs: [run],
+ route: (method, url) =>
+ method === "post" && url.endsWith("/actions") ? run : undefined,
+ });
+ const user = userEvent.setup({ delay: null });
+ renderEditor({ run: "run-1" });
+
+ const card = await waitFor(() => {
+ const found = document.querySelector<HTMLElement>(
+ '.react-flow__node[data-id="brief"]'
+ );
+ expect(found).not.toBeNull();
+ return found!;
+ });
+ fireEvent.click(within(card).getByText("Answer"));
+ const sheet = await screen.findByLabelText("Brief inspector");
+ const product = within(sheet).getByRole("textbox", {
+ name: "Which product?",
+ });
+ const offer = within(sheet).getByRole("textbox", {
+ name: "What is the offer?",
+ });
+ expect(product).toHaveTextContent("Green SM Taxi");
+ expect(offer).toHaveTextContent("50% off");
+
+ await user.click(
+ within(sheet).getByRole("button", { name: "Continue workflow" })
+ );
+
+ await waitFor(() =>
+ expect(
+ backend.requests.filter((r) => r.url.endsWith("/actions"))
+ ).toHaveLength(1)
+ );
+ const body = backend.requests.find((r) => r.url.endsWith("/actions"))!
+ .body as { answers: Record<string, Record<string, unknown>> };
+ expect(body.answers).toEqual({
+ brief: { product: "Green SM Taxi", offer: "50% off" },
+ });
+ });
+});
src/features/workflow/components/WorkflowReview.tsx
export function AskFieldsForm({
scrollable,
workflowSessionId,
+ previous,
onSubmit,
}: {
export function AskFieldsForm({
groups: AskGroup[];
workflowSessionId?: string;
+ /** Earlier answers by `group.key` then field key; text and choice answers prefill the form. */
+ previous?: Record<string, Record<string, unknown>>;
submitLabel: string;
pending: boolean;
export function AskFieldsForm({
submitLabel={submitLabel}
onAttach={(file) => workflowAnswerFile(brandId, file, workflowSessionId)}
+ initialAnswers={Object.fromEntries(
+ groups.flatMap((group) =>
+ group.fields.flatMap((field) => {
+ const value = previous?.[group.key]?.[field.key];
+ return typeof value === "string" && value && value !== SKIPPED
+ ? [[questionId(group, field), value]]
+ : [];
+ })
+ )
+ )}
questions={groups.flatMap((group) =>
group.fields.map((field) => ({
export function WorkflowQuestions({
<p className="text-sm text-ink/60">{t("questions.nothingWaiting")}</p>
);
+ // Asked again (Rerun from here, Ask again): start from the step's last completed answers at
+ // the same path instead of an empty form, so changing one answer is not retyping them all.
+ const previous = Object.fromEntries(
+ waiting.flatMap((attempt) => {
+ const last = run.attempts
+ .filter(
+ (entry) =>
+ entry.nodeId === attempt.nodeId &&
+ entry.path === attempt.path &&
+ entry.state === WorkflowAttemptState.Completed &&
+ entry.output &&
+ typeof entry.output === "object" &&
+ !Array.isArray(entry.output)
+ )
+ .sort((a, b) => a.ordinal - b.ordinal)
+ .at(-1);
+ return last
+ ? [[attempt.path, last.output as Record<string, unknown>]]
+ : [];
+ })
+ );
return (
<AskFieldsForm
+ previous={previous}
scrollable
brandId={brandId}
Canvas · thẻ bước Ask

Thẻ Brief liệt kê câu trả lời sai thứ tự

03
Vấn đề

Câu hỏi theo thứ tự sản phẩm, ưu đãi, giọng điệu, nhưng thẻ lại hiện Giọng điệu đầu tiên và sản phẩm cuối cùng. Cơ sở dữ liệu trả câu trả lời về với các khoá bị sắp xếp lại, và thẻ in theo thứ tự đó.

Cách sửa

Thẻ trên canvas (và bảng Kết quả) liệt kê câu trả lời theo đúng thứ tự câu hỏi của bước.

Hiện tại: Giọng điệu · ưu đãi · sản phẩm
Hiện tạiGiọng điệu · ưu đãi · sản phẩmBấm để xem toàn màn hình
Đã sửa: Sản phẩm · ưu đãi · giọng điệu
Đã sửaSản phẩm · ưu đãi · giọng điệuBấm để xem toàn màn hình
Code · 75f770c1 · 1 tệp · +10 / −4 dòngđã xoá đã thêm
src/features/workflow/graph.ts
export function askAnswerRows(
? (node.config.fields as AskField[])
: [];
- return Object.entries(output as Record<string, unknown>).map(
- ([key, value]) => {
+ // Stored answers come back with their keys reordered (jsonb sorts them), so list them in the
+ // order the questions are asked; a key the step no longer asks goes last.
+ const rank = (key: string) => {
+ const index = fields.findIndex((field) => field.key === key);
+ return index === -1 ? fields.length : index;
+ };
+ return Object.entries(output as Record<string, unknown>)
+ .sort(([a], [b]) => rank(a) - rank(b))
+ .map(([key, value]) => {
const field = fields.find((field) => field.key === key);
const refs: { ref: string; label: string }[] = [];
export function askAnswerRows(
refs,
};
- }
- );
+ });
}
Canvas · khi mở một bước

Mở một bước là thanh công cụ bị che mất một nửa

04
Vấn đề

Khi bảng của một bước đang mở, thanh công cụ phía dưới vẫn căn giữa theo cả bề rộng màn hình, nên đầu bên phải (gồm nút Nhân bản và Xoá của bước đang chọn) bị bảng che mất. Một lần refactor hồi tháng 9 đã bỏ mất thông tin bề rộng của bảng gửi xuống canvas.

Cách sửa

Gửi lại bề rộng của bảng cho canvas: thanh công cụ căn giữa trong phần còn nhìn thấy, và bước được mở được kéo vào cạnh bảng, đúng như code canvas vốn định làm. Bản đồ nhỏ tạm ẩn khi bảng mở để thanh công cụ không chạy xuống dưới nó.

Hiện tại: Thanh công cụ bị bảng che
Hiện tạiThanh công cụ bị bảng cheBấm để xem toàn màn hình
Đã sửa: Thanh công cụ nằm giữa phần canvas còn thấy
Đã sửaThanh công cụ nằm giữa phần canvas còn thấyBấm để xem toàn màn hình
Code · e27f65b9 · 2 tệp · +8 / −1 dòngđã xoá đã thêm
src/features/workflow/components/WorkflowCanvas.tsx
function FlowCanvas({
? "!bottom-6 !left-4 !m-0 !h-12 !w-20"
: "!m-4 !h-24 !w-36"
- } ${obscuredRight ? "max-lg:hidden" : "max-sm:hidden"}`}
+ } ${
+ // With a sheet open the controls bar centres in the narrower visible part and
+ // would run under the map, so the map steps aside until the sheet closes.
+ obscuredRight ? "hidden" : "max-sm:hidden"
+ }`}
/>
{!graph.nodes.length && (
src/features/workflow/components/WorkflowWorkbench.tsx
function Workbench({
)}
<WorkflowCanvas
+ // The sheets cover the canvas's right side: the toolbar centres, and steps are
+ // brought into view, in the part still visible.
+ obscuredRight={vaultRight}
activeItems={activeItems}
onActiveItem={selectItem}
Lịch sử chạy

Lịch sử chạy ghi tên các bước lộn xộn

05
Vấn đề

Một lượt chạy được ghi là "Caption, Brief, Ý tưởng, Key visual": bước được bấm trước, rồi đến thứ tự backend tình cờ trả về. Đọc không giống luồng công việc.

Cách sửa

Tên các bước đọc từ trái sang phải đúng như trên canvas: "Brief, Ý tưởng, Key visual, Caption".

Hiện tại: Caption, Brief, Ý tưởng, Key visual
Hiện tạiCaption, Brief, Ý tưởng, Key visualBấm để xem toàn màn hình
Đã sửa: Brief, Ý tưởng, Key visual, Caption
Đã sửaBrief, Ý tưởng, Key visual, CaptionBấm để xem toàn màn hình
Code · 2d619c91 · 1 tệp · +26 / −1 dòngđã xoá đã thêm
src/features/workflow/components/WorkflowWorkbench.tsx
function Workbench({
query={runsHistory}
time={time}
+ graph={graph}
/>
) : panel === "about" ? (
function RunHistoryPanel({
query,
time,
+ graph,
}: {
brandId: string;
function RunHistoryPanel({
onSelectExecution: (id: string, execution: WorkflowRunExecution) => void;
currentSessionId?: string;
+ /** The canvas, to name a press's steps left to right as they sit there. */
+ graph: WorkflowGraph;
query: UseInfiniteQueryResult<
InfiniteData<{
function RunHistoryPanel({
key={execution.id}
status={execution.status}
- title={execution.labels.join(", ")}
+ title={pressTitle(execution, graph)}
source={
execution.kind === "rerun"
function RunHistoryPanel({
* Repeat runs of the same steps share a title, so the absolute time tells them apart.
*/
+/** A press's step names in canvas order (left to right, a loop's steps at the loop), not the
+ * order the backend happened to list them in, so "Brief, Concepts, Key visual" reads like the flow. */
+function pressTitle(execution: WorkflowRunExecution, graph: WorkflowGraph) {
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
+ const place = (id: string) => {
+ const node = byId.get(id);
+ const parent = node?.parentId ? byId.get(node.parentId) : undefined;
+ const x = (parent?.position.x ?? 0) + (node?.position.x ?? Infinity);
+ const y = (parent?.position.y ?? 0) + (node?.position.y ?? 0);
+ return { x, y };
+ };
+ return execution.nodeIds
+ .map((id, index) => ({
+ label: execution.labels[index] ?? id,
+ ...place(id),
+ }))
+ .sort((a, b) => a.x - b.x || a.y - b.y)
+ .map((entry) => entry.label)
+ .join(", ");
+}
+
function HistoryRow({
status: value,
Menu ▶ của bước

Menu chạy giải thích sai điều người dùng cần biết

06
Vấn đề

"Chạy lại sẽ lặp lại lần thử của chính lượt chạy này. Chạy bước này sẽ bắt đầu một lượt chạy hoàn toàn mới." Điều người dùng cần biết là Chạy lại bỏ qua các chỉnh sửa của họ. Hơn nữa, các lượt chạy bản nháp dùng chung một phiên, nên "lượt chạy hoàn toàn mới" cũng không đúng.

Cách sửa

"Chạy lại dùng thiết lập của lượt chạy này. Chạy bước này dùng các chỉnh sửa hiện tại của bạn."

Hiện tại: Nói về lần thử và lượt chạy
Hiện tạiNói về lần thử và lượt chạyBấm để xem toàn màn hình
Đã sửa: Nói rõ mỗi lựa chọn dùng thiết lập nào
Đã sửaNói rõ mỗi lựa chọn dùng thiết lập nàoBấm để xem toàn màn hình
Code · c9f6fee5 · 3 tệp · +5 / −5 dòngđã xoá đã thêm
src/features/workflow/__tests__/wf-run-requests.test.tsx
describe("RG-WF-020 [Standard] [NEEDS-CLARIFICATION] Rerun repeats the run's own
});
- // Today's wording, recorded: the card's own run menu explains the same rule in other words.
- it("explains on the step card that Run this step starts a brand new run", async () => {
+ // The card's own run menu explains the same rule in other words: which settings each one uses.
+ it("explains on the step card that Rerun keeps the run's settings and Run this step uses edits", async () => {
await openFinishedStep();
describe("RG-WF-020 [Standard] [NEEDS-CLARIFICATION] Rerun repeats the run's own
expect(
await screen.findByText(
- "Rerun repeats this run's own attempt. Run this step starts a brand new run."
+ "Rerun uses the settings this run used. Run this step uses your current edits."
)
).toBeInTheDocument();
src/i18n/locales/en.json
…
"unsettled": "Wait for this step to finish before rerunning it.",
"cancelled": "This run was cancelled. Run this step to start a fresh run.",
- "default": "Rerun repeats this run's own attempt. Run this step starts a brand new run."
+ "default": "Rerun uses the settings this run used. Run this step uses your current edits."
},
"actions": {
Bảng Kết quả · bước Image

Hai kiểu đánh dấu khác nhau nhưng đọc như một

07
Vấn đề

Dưới ảnh có "Đánh dấu những ảnh bạn muốn giữ", rồi "Đánh dấu một kết quả để giữ trong thư viện … Chọn các kết quả được chuyển tiếp bên dưới", rồi thêm một hàng ô chọn nữa. Ba dòng cùng nói về đánh dấu, cho hai kiểu đánh dấu làm hai việc khác nhau.

Cách sửa

Một câu duy nhất: dấu tích trên ảnh là để giữ ảnh trong thư viện; các ô bên dưới là lựa chọn riêng, chọn kết quả dùng cho các bước sau.

Hiện tại: Gợi ý lặp lại, chồng chéo
Hiện tạiGợi ý lặp lại, chồng chéoBấm để xem toàn màn hình
Đã sửa: Một câu tách bạch hai lựa chọn
Đã sửaMột câu tách bạch hai lựa chọnBấm để xem toàn màn hình
Code · 2721c38c · 2 tệp · +2 / −2 dòngđã xoá đã thêm
src/i18n/locales/en.json
…
"promptSent": "Prompt sent",
"agentSaveHint": "Open an output to save it to the gallery. Nothing an agent step produces is saved automatically.",
- "mediaHint": "Tick an output to keep it in the gallery. {passed} Choose the outputs passed on below.",
+ "mediaHint": "A tick on an image keeps it in the gallery. The boxes below are separate: they pick what later steps use. {passed}",
"allPass": "Every output passes to later steps.",
"somePass": "{selected} of {total} pass to later steps.",
Thư viện · Lượt chạy gần đây

Tab Lượt chạy gần đây khi trống hướng dẫn sai

08
Vấn đề

"Hãy chạy một Workflow đã xuất bản, kết quả sẽ được liệt kê tại đây." Thực tế lượt chạy bản nháp cũng hiện ở đây, nên câu này khiến người dùng đi lưu phiên bản một cách không cần thiết.

Cách sửa

"Hãy chạy một Workflow, bản nháp hay phiên bản đã lưu, các lượt chạy sẽ được liệt kê tại đây."

Hiện tại: Nói chỉ lượt chạy đã xuất bản mới hiện
Hiện tạiNói chỉ lượt chạy đã xuất bản mới hiệnBấm để xem toàn màn hình
Đã sửa: Bản nháp hay phiên bản đã lưu đều hiện
Đã sửaBản nháp hay phiên bản đã lưu đều hiệnBấm để xem toàn màn hình
Code · 0b5a76ab · 3 tệp · +3 / −3 dòngđã xoá đã thêm
src/features/workflow/__tests__/wf-library.test.tsx
describe("CB-WF-008 [P2] the library lists, searches and explains emptiness", ()
expect(
screen.getByText(
- "Run a published workflow and its outputs will be listed here."
+ "Run any workflow, draft or saved version, and its runs will be listed here."
)
).toBeInTheDocument();
src/i18n/locales/en.json
…
"clearSearch": "Clear search",
"emptyTitle": "No runs yet",
- "emptyDescription": "Run a published workflow and its outputs will be listed here.",
+ "emptyDescription": "Run any workflow, draft or saved version, and its runs will be listed here.",
"draft": "Draft",
"version": "v{version}",

Để sau

Hiện tại = main 5d5c7dd2. Đã sửa = nhánh feat/workflow-quick-wins tại 8662907a. Cả hai được chụp trên cùng một backend local, ảnh được tạo qua model gateway UAT. Chưa push, chưa có MR. Cập nhật 23/10/2026, 16:30.