From 946cec92cec383c9b6b5146d5757cb891bb3f051 Mon Sep 17 00:00:00 2001 From: Task Runner Tests Date: Sat, 30 May 2026 06:41:41 -0500 Subject: [PATCH 1/5] Address React Doctor findings --- apps/web/src/app.test.tsx | 133 +++++++++-------- apps/web/src/components/markdown.tsx | 11 +- apps/web/src/components/run-column.tsx | 5 +- apps/web/src/components/run-detail-drawer.tsx | 24 ++- apps/web/src/components/run-diffs-surface.tsx | 138 +++++++++--------- apps/web/src/components/run-files-surface.tsx | 24 +-- apps/web/src/components/run-note-editor.tsx | 7 +- apps/web/src/components/run-task-list.tsx | 28 ++-- apps/web/src/lib/format.ts | 19 +-- apps/web/src/lib/run-audit.ts | 2 +- apps/web/src/lib/run-chat.ts | 4 +- apps/web/src/lib/run-events.tsx | 8 +- apps/web/src/lib/run-order.ts | 4 +- apps/web/src/lib/run-timeline.ts | 2 +- apps/web/src/lib/runtime-config.ts | 4 +- apps/web/src/lib/settings.tsx | 8 +- apps/web/src/lib/use-drawer-resize.ts | 9 +- apps/web/src/lib/use-lazy-ref.ts | 9 ++ apps/web/src/routes/run-chat-panel.tsx | 45 ++---- apps/web/src/routes/runs-board-panel.tsx | 47 +++--- apps/web/src/routes/runs-dashboard.tsx | 1 + apps/web/src/routes/settings-general.tsx | 8 +- apps/web/src/routes/use-new-run-state.ts | 3 +- .../src/routes/use-runs-dashboard-state.ts | 26 ++-- apps/web/tsconfig.json | 2 +- packages/core/src/contracts/runs.ts | 4 +- packages/core/src/core/run/run-loop.ts | 5 +- packages/core/src/core/run/schedule.ts | 9 +- packages/core/src/core/run/workspace-diffs.ts | 6 +- tsconfig.base.json | 2 +- 30 files changed, 326 insertions(+), 271 deletions(-) create mode 100644 apps/web/src/lib/use-lazy-ref.ts diff --git a/apps/web/src/app.test.tsx b/apps/web/src/app.test.tsx index 7574d9a9..198166b0 100644 --- a/apps/web/src/app.test.tsx +++ b/apps/web/src/app.test.tsx @@ -157,10 +157,14 @@ vi.mock("@pierre/trees/react", () => { focusNextMatch: fileTreeTestApi.focusNextSearchMatch, focusPreviousMatch: fileTreeTestApi.focusPreviousSearchMatch, isOpen: false, - matchingPaths: fileTreeTestApi.searchMatchingPaths, open: () => {}, setValue: () => {}, - value: fileTreeTestApi.searchValue, + get matchingPaths() { + return fileTreeTestApi.searchMatchingPaths; + }, + get value() { + return fileTreeTestApi.searchValue; + }, }), useFileTreeSelection: () => [], }; @@ -1754,6 +1758,11 @@ async function findRunCard(name: string | RegExp) { ); } +async function findRunSectionTab(name: string | RegExp) { + const runSections = await screen.findByRole("tablist", { name: "Run sections" }); + return await within(runSections).findByRole("tab", { name }); +} + async function findRunRow(name: string | RegExp) { return await screen.findByRole( "button", @@ -2856,7 +2865,7 @@ describe("web app", () => { expect(hasEventSource("/api/runs/run-1/events/audit")).toBe(false); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); @@ -2879,7 +2888,7 @@ describe("web app", () => { }, }); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); expect(await screen.findByRole("region", { name: "Attempt response" })).toHaveTextContent( "Deferred response while away", ); @@ -2887,7 +2896,7 @@ describe("web app", () => { expect(fetchCallCount(fetchMock, (url) => url.endsWith("/api/runs/run-1/timeline"))).toBe(1); }); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); const auditSource = findEventSource("/api/runs/run-1/events/audit"); auditSource.emitOpen(); @@ -2916,7 +2925,7 @@ describe("web app", () => { }, }, }); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); await waitFor(() => { expect( fetchCallCount(fetchMock, (url) => /\/api\/runs\/run-1\/audit(?:\?.*)?$/.test(url)), @@ -4006,7 +4015,7 @@ describe("web app", () => { ); await renderApp("/runs/run-1"); - expect(await screen.findByText("Loading diff...")).toBeInTheDocument(); + expect(await screen.findByText("Loading diff…")).toBeInTheDocument(); await act(async () => { resolveDiff( @@ -4146,8 +4155,8 @@ describe("web app", () => { expect(await screen.findByRole("tab", { name: "Files", selected: true })).toBeInTheDocument(); const searchInput = screen.getByLabelText("Search workspace files"); expect(searchInput).toBeInTheDocument(); - expect(screen.getByText("Loading files...")).toBeInTheDocument(); - expect(screen.queryByText("Loading file...")).not.toBeInTheDocument(); + expect(screen.getByText("Loading files…")).toBeInTheDocument(); + expect(screen.queryByText("Loading file…")).not.toBeInTheDocument(); searchInput.blur(); fireEvent.keyDown(window, { key: "f" }); await waitFor(() => expect(searchInput).toHaveFocus()); @@ -5016,7 +5025,7 @@ describe("web app", () => { name: "Info", }), ); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); expect(await screen.findByRole("region", { name: "Attempt response" })).toHaveTextContent( "Streaming answer live", ); @@ -5074,32 +5083,32 @@ describe("web app", () => { if (!(scrollBottomButton instanceof HTMLButtonElement)) { throw new Error("expected Chat scroll-to-bottom button"); } - expect(scrollTopButton).toHaveAttribute("aria-hidden", "true"); - expect(scrollBottomButton).toHaveAttribute("aria-hidden", "true"); + expect(scrollTopButton).toBeDisabled(); + expect(scrollBottomButton).toBeDisabled(); defineElementMetric(list, "clientHeight", 120); defineElementMetric(list, "scrollHeight", 360); defineElementMetric(list, "scrollTop", 80); fireEvent.scroll(list); - expect(scrollTopButton).toHaveAttribute("aria-hidden", "false"); + expect(scrollTopButton).toBeEnabled(); expect(scrollTopButton).toHaveClass("chat-scroll-control--visible"); - expect(scrollBottomButton).toHaveAttribute("aria-hidden", "false"); + expect(scrollBottomButton).toBeEnabled(); expect(scrollBottomButton).toHaveClass("chat-scroll-control--visible"); await user.click(scrollBottomButton); expect(list.scrollTop).toBe(240); - expect(scrollTopButton).toHaveAttribute("aria-hidden", "false"); - expect(scrollBottomButton).toHaveAttribute("aria-hidden", "true"); + expect(scrollTopButton).toBeEnabled(); + expect(scrollBottomButton).toBeDisabled(); expect(scrollBottomButton).not.toHaveClass("chat-scroll-control--visible"); await user.click(scrollTopButton); expect(list.scrollTop).toBe(0); - expect(scrollTopButton).toHaveAttribute("aria-hidden", "true"); + expect(scrollTopButton).toBeDisabled(); expect(scrollTopButton).not.toHaveClass("chat-scroll-control--visible"); - expect(scrollBottomButton).toHaveAttribute("aria-hidden", "false"); + expect(scrollBottomButton).toBeEnabled(); }); it("keeps selected-run Chat pinned while live transcript output grows", async () => { @@ -6591,12 +6600,12 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); - expect(screen.getByRole("button", { name: "Attempts" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Attempts" })).toBeInTheDocument(); const sessionTabs = await screen.findByRole("tablist", { name: "Sessions" }); expect(within(sessionTabs).getByRole("tab", { name: "Session 1" })).toHaveTextContent("1"); expect(within(sessionTabs).getByRole("tab", { name: "Session 2" })).toHaveAttribute( @@ -6718,7 +6727,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); expect(await screen.findByRole("tab", { name: "Pending" })).toBeInTheDocument(); expect(screen.getByRole("tablist", { name: "Attempts" })).toBeInTheDocument(); @@ -6733,10 +6742,10 @@ describe("web app", () => { expect(screen.getByRole("heading", { level: 2, name: "Prepared prompt" })).toBeInTheDocument(); await user.click(screen.getByRole("tab", { name: "Response" })); - expect(screen.getByText("No response yet — this run has not started.")).toBeInTheDocument(); + expect(screen.getByText("No response yet; this run has not started.")).toBeInTheDocument(); await user.click(screen.getByRole("tab", { name: "Diagnostics" })); - expect(screen.getByText("No diagnostics yet — this run has not started.")).toBeInTheDocument(); + expect(screen.getByText("No diagnostics yet; this run has not started.")).toBeInTheDocument(); const detailSource = findEventSource("/api/runs/run-1/events/detail"); detailSource.emitOpen(); @@ -6916,7 +6925,7 @@ describe("web app", () => { expect(hasEventSource("/api/runs/run-1/events/audit")).toBe(false); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); expect(await screen.findByRole("region", { name: "Attempt response" })).toHaveTextContent( "Completed attempt", ); @@ -7061,7 +7070,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); @@ -7126,7 +7135,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); @@ -7177,7 +7186,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); @@ -7201,7 +7210,7 @@ describe("web app", () => { await user.click(await findRunCard("Build dashboard")); await user.click(await screen.findByRole("tab", { name: "Info" })); - const runSections = await screen.findByRole("navigation", { name: "Run sections" }); + const runSections = await screen.findByRole("tablist", { name: "Run sections" }); expect(runSections).toHaveClass("tabs", "tabs--scrollable"); expect(runSections.querySelectorAll(":scope > .tab")).toHaveLength(5); expect( @@ -7226,16 +7235,16 @@ describe("web app", () => { await user.click(await findRunCard("Build dashboard")); await user.click(await screen.findByRole("tab", { name: "Info" })); - const runSections = await screen.findByRole("navigation", { name: "Run sections" }); + const runSections = await screen.findByRole("tablist", { name: "Run sections" }); expect(runSections.querySelectorAll(":scope > .tab")).toHaveLength(4); expect( [...runSections.querySelectorAll(":scope > .tab")].map((tab) => tab.textContent?.replace(/\s+\S+\/\S+$/, "").trim(), ), ).toEqual(["Attachments", "Audit", "Data", "Dependencies"]); - expect(within(runSections).getByRole("button", { name: "Audit" })).toBeInTheDocument(); - expect(within(runSections).getByRole("button", { name: "Data" })).toBeInTheDocument(); - expect(within(runSections).queryByRole("button", { name: "Attempts" })).not.toBeInTheDocument(); + expect(within(runSections).getByRole("tab", { name: "Audit" })).toBeInTheDocument(); + expect(within(runSections).getByRole("tab", { name: "Data" })).toBeInTheDocument(); + expect(within(runSections).queryByRole("tab", { name: "Attempts" })).not.toBeInTheDocument(); }); it("toggles audit ordering globally and keeps audit appends at the top in newest-first mode for non-live runs", async () => { @@ -7298,7 +7307,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); const auditSource = findEventSource("/api/runs/run-1/events/audit"); auditSource.emitOpen(); @@ -7417,7 +7426,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); const auditSource = findEventSource("/api/runs/run-1/events/audit"); auditSource.emitOpen(); @@ -7531,7 +7540,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); const auditSource = findEventSource("/api/runs/run-1/events/audit"); auditSource.emitOpen(); @@ -7643,7 +7652,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: /^Audit\b/ })); + await user.click(screen.getByRole("tab", { name: /^Audit\b/ })); const auditSource = findEventSource("/api/runs/run-1/events/audit"); auditSource.emitOpen(); @@ -7683,7 +7692,7 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: "Data" })); + await user.click(screen.getByRole("tab", { name: "Data" })); const dataPanel = screen.getByLabelText("Data"); const dataTabs = within(dataPanel).getByRole("tablist", { name: "Data view" }); @@ -7776,7 +7785,7 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: "Data" })); + await user.click(screen.getByRole("tab", { name: "Data" })); const dataPanel = screen.getByLabelText("Data"); await user.click(within(dataPanel).getByRole("button", { name: "Edit run vars" })); @@ -7842,7 +7851,7 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: /^Attempts\b/ })); + await user.click(screen.getByRole("tab", { name: /^Attempts\b/ })); const attemptsPanel = screen.getByRole("region", { name: "Attempts" }); expect(within(attemptsPanel).getByRole("button", { name: "Edit run message" })).toBeEnabled(); @@ -7913,7 +7922,7 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: "Data" })); + await user.click(screen.getByRole("tab", { name: "Data" })); const dataPanel = screen.getByLabelText("Data"); await user.click(within(dataPanel).getByRole("button", { name: "Edit run vars" })); @@ -7958,11 +7967,11 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: "Data" })); + await user.click(screen.getByRole("tab", { name: "Data" })); expect(screen.queryByRole("button", { name: "Edit run vars" })).not.toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: /^Attempts\b/ })); + await user.click(screen.getByRole("tab", { name: /^Attempts\b/ })); expect(screen.queryByRole("button", { name: "Edit run message" })).not.toBeInTheDocument(); }); @@ -7980,7 +7989,7 @@ describe("web app", () => { const user = userEvent.setup(); await renderApp(); await user.click(await findRunCard("Build dashboard")); - await user.click(screen.getByRole("button", { name: "Data" })); + await user.click(screen.getByRole("tab", { name: "Data" })); const dataPanel = screen.getByLabelText("Data"); expect(within(dataPanel).getByText("No vars")).toBeInTheDocument(); @@ -8034,7 +8043,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Build dashboard")); await user.click(screen.getByRole("tab", { name: "Info" })); - await user.click(screen.getByRole("button", { name: "Attempts" })); + await user.click(screen.getByRole("tab", { name: "Attempts" })); const timelineSource = findEventSource("/api/runs/run-1/events/timeline"); timelineSource.emitOpen(); @@ -8619,11 +8628,11 @@ describe("web app", () => { expect(await screen.findByText("Ship the web UI")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /build ui/i, expanded: true })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Instructions" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Task notes" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Instructions" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Task notes" })).toBeInTheDocument(); expect(screen.queryByText("working")).not.toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: "Task notes" })); + await user.click(screen.getByRole("tab", { name: "Task notes" })); expect(await screen.findByText("working")).toBeInTheDocument(); expect(screen.queryByText("Ship the web UI")).not.toBeInTheDocument(); @@ -10386,7 +10395,7 @@ describe("web app", () => { expect(bulletOne).toBeInTheDocument(); expect(bulletOne.tagName).toBe("LI"); - await user.click(screen.getByRole("button", { name: "Task notes" })); + await user.click(screen.getByRole("tab", { name: "Task notes" })); expect(await screen.findByText("npm run check")).toBeInTheDocument(); expect(screen.getByText("npm run check").tagName).toBe("CODE"); }); @@ -10494,7 +10503,7 @@ describe("web app", () => { if (editedHeaderForNotes.getAttribute("aria-expanded") !== "true") { await user.click(editedHeaderForNotes); } - await user.click(await screen.findByRole("button", { name: "Task notes" })); + await user.click(await screen.findByRole("tab", { name: "Task notes" })); const editedArticle = editedHeaderForNotes.closest("article"); if (!editedArticle) { throw new Error("Edited task article was not rendered"); @@ -15412,7 +15421,7 @@ describe("web app", () => { expect(detailWheel.defaultPrevented).toBe(true); }); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: /^Preview notes\.md$/ })); await waitFor(() => { const previewWheel = dispatchHorizontalWheel(screen.getByLabelText("Attachment preview")); @@ -16302,7 +16311,7 @@ describe("web app", () => { await findRunCard("Second run"); await user.click(await findRunCard("Build dashboard")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: /^Preview notes\.md$/ })); expect(await screen.findByLabelText("Attachment preview")).toBeInTheDocument(); expect(await screen.findByText("Markdown preview")).toBeInTheDocument(); @@ -16443,7 +16452,7 @@ describe("web app", () => { await user.click(await findRunCard("Current run")); expect(await screen.findByLabelText("Run detail")).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: /^Dependencies\b/i })); + await user.click(await findRunSectionTab(/^Dependencies\b/i)); await user.type(screen.getByLabelText("Dependency run search"), "follow-up"); await user.click( await screen.findByRole("button", { @@ -16530,7 +16539,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Current run")); - await user.click(await screen.findByRole("button", { name: /^Dependencies\b/i })); + await user.click(await findRunSectionTab(/^Dependencies\b/i)); await user.click(screen.getByRole("tab", { name: "Run group" })); await user.type(screen.getByLabelText("Dependency run group"), "shared-group"); await user.click(screen.getByRole("button", { name: /add dependency/i })); @@ -16542,7 +16551,7 @@ describe("web app", () => { expect(screen.getByText("1/2 successful")).toBeInTheDocument(); await user.click(await findRunCard("Shared member")); - await user.click(await screen.findByRole("button", { name: /^Dependencies\b/i })); + await user.click(await findRunSectionTab(/^Dependencies\b/i)); expect(screen.getByText("via group shared-group")).toBeInTheDocument(); }); @@ -16592,8 +16601,8 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - expect(await screen.findByRole("button", { name: /^Attachments 2$/i })).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: /^Attachments 2$/i })); + expect(await findRunSectionTab(/^Attachments 2$/i)).toBeInTheDocument(); + await user.click(await findRunSectionTab(/^Attachments 2$/i)); await user.click(screen.getByRole("button", { name: /^Preview notes\.md$/ })); expect(await screen.findByRole("heading", { name: "Notes" })).toBeInTheDocument(); @@ -16677,7 +16686,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); expect(screen.queryByRole("button", { name: /^Preview report\.pdf$/ })).not.toBeInTheDocument(); @@ -16734,7 +16743,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByText("text/plain; charset=utf-8")); const preview = await screen.findByLabelText("Attachment preview content"); @@ -16803,7 +16812,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: /^Preview alpha\.txt$/ })); expect(await screen.findByText("alpha body")).toBeInTheDocument(); @@ -16944,7 +16953,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: /^Preview photo\.png$/ })); expect(await screen.findByLabelText("Attachment preview")).toBeInTheDocument(); @@ -17060,7 +17069,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: /^Preview notes\.md$/ })); expect(await screen.findByLabelText("Attachment preview loading")).toBeInTheDocument(); @@ -17346,7 +17355,7 @@ describe("web app", () => { await renderApp(); await user.click(await findRunCard("Attachment run")); - await user.click(await screen.findByRole("button", { name: /^Attachments\b/i })); + await user.click(await findRunSectionTab(/^Attachments\b/i)); await user.click(screen.getByRole("button", { name: "Open source run run-2" })); expect(await screen.findByText("Peer run")).toBeInTheDocument(); diff --git a/apps/web/src/components/markdown.tsx b/apps/web/src/components/markdown.tsx index 54803f7f..262fe65b 100644 --- a/apps/web/src/components/markdown.tsx +++ b/apps/web/src/components/markdown.tsx @@ -237,7 +237,7 @@ function MermaidDiagram({ code }: { code: string }) { aria-label="Mermaid diagram loading" className="markdown-mermaid markdown-mermaid--loading" > - Rendering Mermaid diagram... + Rendering Mermaid diagram… ); } @@ -371,8 +371,13 @@ function FrontmatterTable({ rows }: { rows: FrontmatterTableRow[] }) { } const components: Components = { - a({ node: _node, href, ...props }) { - return ; + a({ children, node: _node, href, ...props }) { + const ariaLabel = props["aria-label"] ?? (children ? undefined : (href ?? "Open link")); + return ( + + {children} + + ); }, pre({ node: _node, children, ...props }) { const mermaidCode = readMermaidBlock(children); diff --git a/apps/web/src/components/run-column.tsx b/apps/web/src/components/run-column.tsx index 48ce0da1..2e7db2b3 100644 --- a/apps/web/src/components/run-column.tsx +++ b/apps/web/src/components/run-column.tsx @@ -75,9 +75,10 @@ export function RunColumn({ className="column" data-collapsed={collapsed ? "true" : "false"} data-status={column.key} - onClick={handleColumnClick} - onKeyDown={handleColumnKeyDown} + onClick={collapsed ? handleColumnClick : undefined} + onKeyDown={collapsed ? handleColumnKeyDown : undefined} ref={columnRef} + role={collapsed ? "button" : undefined} tabIndex={collapsed ? 0 : undefined} >
diff --git a/apps/web/src/components/run-detail-drawer.tsx b/apps/web/src/components/run-detail-drawer.tsx index 3ae3899e..4ce6a5db 100644 --- a/apps/web/src/components/run-detail-drawer.tsx +++ b/apps/web/src/components/run-detail-drawer.tsx @@ -678,7 +678,7 @@ export function RunDetailDrawer({ }) { const drawerRef = useRef(null); const drawerBodyRef = useRef(null); - const sectionTabsRef = useRef(null); + const sectionTabsRef = useRef(null); const timelineContentScrollRef = useRef(null); const timelineResponseAtBottomRef = useRef(false); const latestAttemptRef = useRef(null); @@ -846,10 +846,10 @@ export function RunDetailDrawer({ }, new Map()), ([sessionIndex, attempts]) => ({ sessionIndex, - attempts: [...attempts].sort((a, b) => a.attemptNumber - b.attemptNumber), + attempts: attempts.toSorted((a, b) => a.attemptNumber - b.attemptNumber), summary: run.sessions.find((session) => session.sessionIndex === sessionIndex) ?? null, }), - ).sort((a, b) => a.sessionIndex - b.sessionIndex); + ).toSorted((a, b) => a.sessionIndex - b.sessionIndex); const pendingAttemptAvailable = (run.status === "initialized" || run.status === "ready") && run.totalAttemptCount === 0 && @@ -2293,11 +2293,17 @@ export function RunDetailDrawer({ ) : null} - + {activeSection === "attachments" ? (
@@ -3209,7 +3219,7 @@ export function RunDetailDrawer({ ) ) : timelineTab === "response" ? ( selectedPendingAttempt ? ( -

No response yet — this run has not started.

+

No response yet; this run has not started.

) : selectedAttemptResponse ? (
) ) : selectedPendingAttempt ? ( -

No diagnostics yet — this run has not started.

+

No diagnostics yet; this run has not started.

) : selectedAttemptDiagnostics ? (
(null); const rootRef = useRef(null); const codeViewRef = useRef | null>(null); + const activeDiffVersionRef = useRef(0); const mobileCollapsedPathRef = useRef(null); const searchRequestVersionRef = useRef(searchRequestVersion); const persistedSidebarWidth = viewState.diffsSidebarWidth; @@ -415,12 +416,19 @@ export function RunDiffsSurface({ }); const selectedTreePaths = useFileTreeSelection(tree.model); const treeSearch = useFileTreeSearch(tree.model); + const filePathsRef = useRef(filePaths); + const treeModelRef = useRef(tree.model); + const treeSearchRef = useRef(treeSearch); + filePathsRef.current = filePaths; + treeModelRef.current = tree.model; + treeSearchRef.current = treeSearch; const parsedDiffItems = useMemo(() => (diff ? parseDiffItems(diff) : []), [diff]); const codeViewItems = useMemo( () => parsedDiffItems.map((entry) => entry.item), [parsedDiffItems], ); const codeViewItemIds = useMemo(() => codeViewItems.map((item) => item.id), [codeViewItems]); + const codeViewItemIdSet = useMemo(() => new Set(codeViewItemIds), [codeViewItemIds]); const allDiffItemsCollapsed = codeViewItemIds.length > 0 && codeViewItemIds.every((id) => collapsedDiffItemIds.has(id)); const codeViewContentVersionKey = diff ? `${diff.displayRange}\0${diff.patch}` : ""; @@ -428,13 +436,28 @@ export function RunDiffsSurface({ () => hashCodeViewVersion(codeViewContentVersionKey), [codeViewContentVersionKey], ); - useEffect(() => { - const validIds = new Set(codeViewItemIds); - setCollapsedDiffItemIds((current) => { - const next = new Set([...current].filter((id) => validIds.has(id))); - return next.size === current.size ? current : next; - }); - }, [codeViewItemIds]); + + if (collapsedDiffItemIds.size > 0) { + const validCollapsedIds = new Set( + Array.from(collapsedDiffItemIds).filter((id) => codeViewItemIdSet.has(id)), + ); + if (validCollapsedIds.size !== collapsedDiffItemIds.size) { + setCollapsedDiffItemIds(validCollapsedIds); + } + } + + if (activeDiffVersionRef.current !== codeViewContentVersion) { + activeDiffVersionRef.current = codeViewContentVersion; + setSelectedPath( + diff + ? selectedPath && filePathSet.has(selectedPath) + ? selectedPath + : firstSelectablePath(files) + : null, + ); + setSelectedLines(null); + } + const displayedCodeViewItems = useMemo( () => codeViewItems.map( @@ -493,18 +516,6 @@ export function RunDiffsSurface({ }; }, [diff, files, parsedDiffItemById, selectedLines]); - useEffect(() => { - if (!diff) { - setSelectedPath(null); - setSelectedLines(null); - return; - } - setSelectedPath((current) => - current && filePathSet.has(current) ? current : firstSelectablePath(files), - ); - setSelectedLines(null); - }, [diff, files, filePathSet]); - useEffect(() => { tree.model.resetPaths(filePaths); tree.model.setGitStatus(gitStatus); @@ -552,59 +563,51 @@ export function RunDiffsSurface({ focusTreeSearchInput(); }, [searchRequestVersion]); - useEffect(() => { - const root = rootRef.current; - if (!root) { + function treeSearchInputFromEvent( + event: ReactKeyboardEvent, + ): HTMLInputElement | null { + const target = event.target; + return target instanceof HTMLInputElement && target.matches("[data-file-tree-search-input]") + ? target + : null; + } + + function handleTreeSearchKeyDown(event: ReactKeyboardEvent) { + if (event.defaultPrevented) { return; } - - function treeSearchInputFromEvent(event: KeyboardEvent): HTMLInputElement | null { - const target = event - .composedPath() - .find( - (element) => - element instanceof HTMLInputElement && element.matches("[data-file-tree-search-input]"), - ); - return target instanceof HTMLInputElement ? target : null; + const input = treeSearchInputFromEvent(event); + if (!input || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) { + return; } - function handleTreeSearchKeyDown(event: KeyboardEvent) { - if (event.defaultPrevented) { - return; - } - const input = treeSearchInputFromEvent(event); - if (!input || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - const hasSearch = treeSearch.value.trim().length > 0; - if (hasSearch && treeSearch.matchingPaths.length > 0) { - if (event.key === "ArrowDown") { - treeSearch.focusNextMatch(); - } else { - treeSearch.focusPreviousMatch(); - } - return; - } - + event.preventDefault(); + event.stopPropagation(); + const currentTreeSearch = treeSearchRef.current; + const hasSearch = currentTreeSearch.value.trim().length > 0; + if (hasSearch && currentTreeSearch.matchingPaths.length > 0) { if (event.key === "ArrowDown") { - const firstPath = filePaths.at(0); - if (firstPath) { - tree.model.focusPath(firstPath); - } + currentTreeSearch.focusNextMatch(); } else { - const lastPath = filePaths.at(-1); - if (lastPath) { - tree.model.focusPath(lastPath); - } + currentTreeSearch.focusPreviousMatch(); } + return; } - root.addEventListener("keydown", handleTreeSearchKeyDown, { capture: true }); - return () => root.removeEventListener("keydown", handleTreeSearchKeyDown, { capture: true }); - }, [filePaths, tree.model, treeSearch]); + const currentFilePaths = filePathsRef.current; + const currentTreeModel = treeModelRef.current; + if (event.key === "ArrowDown") { + const firstPath = currentFilePaths.at(0); + if (firstPath) { + currentTreeModel.focusPath(firstPath); + } + } else { + const lastPath = currentFilePaths.at(-1); + if (lastPath) { + currentTreeModel.focusPath(lastPath); + } + } + } function applyRange(event: FormEvent) { event.preventDefault(); @@ -793,7 +796,12 @@ export function RunDiffsSurface({ ); return ( -
+
- +
{activeTab === "body" ? ( bodyEditing ? (
diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index cd7110b6..2c3626d1 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -2,16 +2,18 @@ import type { RunSchedule, RunScheduleState } from "@kcosr/agent-runner-core/con import type { RunScheduleMode } from "@kcosr/agent-runner-core/core/run/manifest.js"; import { humanizeCronExpression } from "@kcosr/agent-runner-core/core/run/schedule.js"; +const TIMESTAMP_FORMATTER = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", +}); +const RELATIVE_TIMESTAMP_FORMATTER = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); + export function formatTimestamp(value: string | null): string { if (!value) { return "Not available"; } - const date = new Date(value); - return new Intl.DateTimeFormat(undefined, { - dateStyle: "medium", - timeStyle: "short", - }).format(date); + return TIMESTAMP_FORMATTER.format(new Date(value)); } export function formatRelativeTimestamp(value: string | null): string { @@ -21,18 +23,17 @@ export function formatRelativeTimestamp(value: string | null): string { const deltaMs = new Date(value).getTime() - Date.now(); const deltaMinutes = Math.round(deltaMs / 60_000); - const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); if (Math.abs(deltaMinutes) < 60) { - return formatter.format(deltaMinutes, "minute"); + return RELATIVE_TIMESTAMP_FORMATTER.format(deltaMinutes, "minute"); } const deltaHours = Math.round(deltaMinutes / 60); if (Math.abs(deltaHours) < 48) { - return formatter.format(deltaHours, "hour"); + return RELATIVE_TIMESTAMP_FORMATTER.format(deltaHours, "hour"); } - return formatter.format(Math.round(deltaHours / 24), "day"); + return RELATIVE_TIMESTAMP_FORMATTER.format(Math.round(deltaHours / 24), "day"); } export function formatTimestampWithRelative(value: string | null): string { diff --git a/apps/web/src/lib/run-audit.ts b/apps/web/src/lib/run-audit.ts index 17a469ae..3aa5191d 100644 --- a/apps/web/src/lib/run-audit.ts +++ b/apps/web/src/lib/run-audit.ts @@ -128,7 +128,7 @@ export function useRunAuditState({ } let merged = fetched; - for (const envelope of [...bufferRef.current].sort( + for (const envelope of bufferRef.current.toSorted( (left, right) => left.cursor - right.cursor, )) { const result = applyAuditEnvelope(merged, envelope); diff --git a/apps/web/src/lib/run-chat.ts b/apps/web/src/lib/run-chat.ts index ee4a07c4..b5bc3703 100644 --- a/apps/web/src/lib/run-chat.ts +++ b/apps/web/src/lib/run-chat.ts @@ -128,7 +128,7 @@ function deriveArtifactsByAttempt( startedAt: Date.parse(attempt.startedAt), endedAt: attempt.endedAt === null ? null : Date.parse(attempt.endedAt), })) - .sort((left, right) => left.attemptNumber - right.attemptNumber); + .toSorted((left, right) => left.attemptNumber - right.attemptNumber); for (const attachment of attachments) { const attachmentAddedAt = Date.parse(attachment.addedAt); @@ -195,7 +195,7 @@ export function deriveRunChatRows(run: RunDetail, history: RunTimelineHistory): const sessionIndexes = new Set(attemptsBySession.keys()); const rows: RunChatRow[] = []; - for (const sessionIndex of [...sessionIndexes].sort((left, right) => left - right)) { + for (const sessionIndex of Array.from(sessionIndexes).toSorted((left, right) => left - right)) { const session = sessionsByIndex.get(sessionIndex); const attempts = attemptsBySession.get(sessionIndex) ?? []; const hasBootstrapHistory = attempts.some(isBootstrapAttempt); diff --git a/apps/web/src/lib/run-events.tsx b/apps/web/src/lib/run-events.tsx index e9fb860d..8b23f6db 100644 --- a/apps/web/src/lib/run-events.tsx +++ b/apps/web/src/lib/run-events.tsx @@ -1,5 +1,5 @@ import type { AppRuntimeConfig } from "@kcosr/agent-runner-core/contracts/app-config.js"; -import { type ReactNode, createContext, useContext, useEffect, useRef, useState } from "react"; +import { type ReactNode, createContext, use, useEffect, useMemo, useRef, useState } from "react"; import { queryClient, runQueryKeys } from "./query.js"; import { removeRunFromListCache, @@ -96,9 +96,11 @@ export function RunEventsProvider({ }; }, [config, daemonToken]); - return {children}; + const contextValue = useMemo(() => ({ streamStale }), [streamStale]); + + return {children}; } export function useRunEvents(): RunEventsState { - return useContext(RunEventsContext); + return use(RunEventsContext); } diff --git a/apps/web/src/lib/run-order.ts b/apps/web/src/lib/run-order.ts index a1ab2d5b..da082ae7 100644 --- a/apps/web/src/lib/run-order.ts +++ b/apps/web/src/lib/run-order.ts @@ -29,7 +29,7 @@ function compareRunsByStartedAtDesc(left: RunSummary, right: RunSummary): number } export function sortRunsByStartedAtDesc(runs: RunSummary[]): RunSummary[] { - return [...runs].sort(compareRunsByStartedAtDesc); + return runs.toSorted(compareRunsByStartedAtDesc); } function compareRunsByStartedAt( @@ -117,5 +117,5 @@ export function sortRunsWithPinnedFirst( runs: RunSummary[], compare: (left: RunSummary, right: RunSummary) => number, ): RunSummary[] { - return [...runs].sort((left, right) => compareRunsByPinnedThen(left, right, compare)); + return runs.toSorted((left, right) => compareRunsByPinnedThen(left, right, compare)); } diff --git a/apps/web/src/lib/run-timeline.ts b/apps/web/src/lib/run-timeline.ts index c9d977b3..6bf05541 100644 --- a/apps/web/src/lib/run-timeline.ts +++ b/apps/web/src/lib/run-timeline.ts @@ -215,7 +215,7 @@ export function useRunTimelineState({ } let merged = fetched; - for (const envelope of [...bufferRef.current].sort( + for (const envelope of bufferRef.current.toSorted( (left, right) => left.cursor - right.cursor, )) { const result = applyEnvelope(merged, envelope); diff --git a/apps/web/src/lib/runtime-config.ts b/apps/web/src/lib/runtime-config.ts index cddb3da1..bd725a2b 100644 --- a/apps/web/src/lib/runtime-config.ts +++ b/apps/web/src/lib/runtime-config.ts @@ -3,7 +3,7 @@ import { appRuntimeConfigSchema, webPathPrefix, } from "@kcosr/agent-runner-core/contracts/app-config.js"; -import { createContext, useContext } from "react"; +import { createContext, use } from "react"; declare global { interface Window { @@ -62,7 +62,7 @@ export async function loadRuntimeConfig( export const RuntimeConfigContext = createContext(null); export function useRuntimeConfig(): AppRuntimeConfig { - const config = useContext(RuntimeConfigContext); + const config = use(RuntimeConfigContext); if (!config) { throw new RuntimeConfigError("Runtime config is not available"); } diff --git a/apps/web/src/lib/settings.tsx b/apps/web/src/lib/settings.tsx index b875debc..bfea52fe 100644 --- a/apps/web/src/lib/settings.tsx +++ b/apps/web/src/lib/settings.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { createContext, use, useEffect, useMemo, useRef, useState } from "react"; import { DAEMON_TOKEN_STORAGE_KEY, normalizeDaemonToken, @@ -509,7 +509,7 @@ export function DashboardSettingsProvider({ children }: { children: ReactNode }) } export function useDashboardPreferences() { - const context = useContext(DashboardPreferencesContext); + const context = use(DashboardPreferencesContext); if (!context) { throw new Error("Dashboard preferences context is unavailable"); } @@ -517,7 +517,7 @@ export function useDashboardPreferences() { } export function useDashboardViewState() { - const context = useContext(DashboardViewStateContext); + const context = use(DashboardViewStateContext); if (!context) { throw new Error("Dashboard view state context is unavailable"); } @@ -525,7 +525,7 @@ export function useDashboardViewState() { } export function useDaemonAuthToken() { - const context = useContext(DaemonAuthTokenContext); + const context = use(DaemonAuthTokenContext); if (!context) { throw new Error("Daemon auth token context is unavailable"); } diff --git a/apps/web/src/lib/use-drawer-resize.ts b/apps/web/src/lib/use-drawer-resize.ts index 52ab347b..f786a2fc 100644 --- a/apps/web/src/lib/use-drawer-resize.ts +++ b/apps/web/src/lib/use-drawer-resize.ts @@ -45,19 +45,14 @@ export function useDrawerResize(): DrawerResize { const isFullscreen = viewState.drawerFullscreen; const storedWidth = viewState.drawerWidth; - const width = isFullscreen ? maxWidth : clamp(dragWidth ?? storedWidth); + const clampedDragWidth = dragWidth === null ? null : clamp(dragWidth); + const width = isFullscreen ? maxWidth : (clampedDragWidth ?? clamp(storedWidth)); const drawerStyle = { "--drawer-width": `${width}px` } as CSSProperties; function clamp(value: number): number { return Math.min(maxWidth, Math.max(DRAWER_WIDTH_MIN, Math.round(value))); } - useEffect(() => { - if (dragWidth !== null && dragWidth > maxWidth) { - setDragWidth(maxWidth); - } - }, [dragWidth, maxWidth]); - function handleResizeStart(event: PointerEvent) { if (isFullscreen) return; event.preventDefault(); diff --git a/apps/web/src/lib/use-lazy-ref.ts b/apps/web/src/lib/use-lazy-ref.ts new file mode 100644 index 00000000..f5e673be --- /dev/null +++ b/apps/web/src/lib/use-lazy-ref.ts @@ -0,0 +1,9 @@ +import { type MutableRefObject, useRef } from "react"; + +export function useLazyRef(createValue: () => T): MutableRefObject { + const ref = useRef(null); + if (ref.current === null) { + ref.current = createValue(); + } + return ref as MutableRefObject; +} diff --git a/apps/web/src/routes/run-chat-panel.tsx b/apps/web/src/routes/run-chat-panel.tsx index ed1cb037..0d1c3a19 100644 --- a/apps/web/src/routes/run-chat-panel.tsx +++ b/apps/web/src/routes/run-chat-panel.tsx @@ -34,6 +34,13 @@ import { import type { RunTimelineState } from "../lib/run-timeline.js"; const CHAT_SCROLL_EDGE_THRESHOLD_PX = 32; +const TURN_DIVIDER_TIMESTAMP_FORMATTER = new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", +}); type DownloadAttachmentHandler = ( runId: string, @@ -73,19 +80,13 @@ function scrollElementToTop(element: HTMLElement) { } function formatTurnDividerTimestamp(value: string) { - return new Intl.DateTimeFormat(undefined, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "2-digit", - }).format(new Date(value)); + return TURN_DIVIDER_TIMESTAMP_FORMATTER.format(new Date(value)); } function assistantEmptyText(emptyState: RunChatAssistantEmptyState | undefined) { switch (emptyState) { case "waiting_live_response": - return "Waiting for live response..."; + return "Waiting for live response…"; case "no_response_recorded": return "No response recorded."; case undefined: @@ -268,7 +269,7 @@ export function RunChatView({ const [showScrollToBottom, setShowScrollToBottom] = useState(false); const listRef = useRef(null); const composerRef = useRef(null); - const resetRunIdRef = useRef(selectedRunId); + const activeRunIdRef = useRef(selectedRunId); const stickToBottomRef = useRef(true); const scrollFrameRef = useRef(null); const scrollTimeoutRef = useRef(null); @@ -296,7 +297,7 @@ export function RunChatView({ (!queueMode && selectedRun?.capabilities.canResume !== true); const composerActivityVisible = selectedRun?.isLive === true; const submitLabel = queueMode ? "Queue" : "Send"; - const submitPendingLabel = queueMode ? "Queueing..." : "Sending..."; + const submitPendingLabel = queueMode ? "Queueing…" : "Sending…"; const rememberScrollMetrics = useCallback((element: HTMLElement) => { scrollMetricsRef.current = { @@ -352,20 +353,6 @@ export function RunChatView({ [applyScrollToBottom, cancelScheduledScrollToBottom], ); - useEffect(() => { - if (resetRunIdRef.current === selectedRunId) { - return; - } - resetRunIdRef.current = selectedRunId; - setDraft(""); - setChatError(undefined); - stickToBottomRef.current = true; - scrollMetricsRef.current = null; - cancelScheduledScrollToBottom(); - setShowScrollToTop(false); - setShowScrollToBottom(false); - }, [cancelScheduledScrollToBottom, selectedRunId]); - useEffect(() => { return () => { cancelScheduledScrollToBottom(); @@ -454,13 +441,13 @@ export function RunChatView({ } else { await onSubmitResume(runId, trimmedDraft); } - if (resetRunIdRef.current !== runId) { + if (activeRunIdRef.current !== runId) { return; } setDraft(""); stickToBottomRef.current = true; } catch (error) { - if (resetRunIdRef.current !== runId) { + if (activeRunIdRef.current !== runId) { return; } setChatError( @@ -479,7 +466,7 @@ export function RunChatView({ setChatError(undefined); await onRemoveQueuedMessage(runId, messageId); } catch (error) { - if (resetRunIdRef.current !== runId) { + if (activeRunIdRef.current !== runId) { return; } setChatError(error instanceof Error ? error.message : "Remove queued message failed."); @@ -628,9 +615,9 @@ export function RunChatView({ {renderBody()}
@@ -217,7 +217,6 @@ export function NewRunRoute() {