From 518fc71cece899a853a4234269016fa2e17d895f Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:57:23 -0500 Subject: [PATCH] [codex] Add work timeline page (#8938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board UI is where operators inspect company activity, agent work, and issue progress. > - Existing board views show individual issue and run details, but they do not give operators a compact time-based picture of work across agents. > - A work timeline helps operators scan when agents worked, how handoffs happened, and where overlapping work occurred. > - This pull request adds a company-scoped Work Timeline page backed by the existing API surface and renders the timeline as a custom SVG Gantt-style view. > - The benefit is faster operator understanding of multi-agent execution without opening each issue thread individually. ## Linked Issues or Issue Description No public GitHub issue exists for this change. Subsystem affected: - ui/ — React + Vite board UI Problem or motivation: - Operators can inspect individual issues and runs, but there is no compact time-based view of company work across agents. - This makes it harder to scan overlaps, handoffs, retries, and activity windows without opening many issue threads. Proposed solution: - Add a company-scoped Work Timeline page in the board UI. - Render agent and system run spans as a custom SVG Gantt-style chart with packed overlap lanes. - Show issue color identity, kickoff attribution, hover-revealed delegation connectors, retry styling, zoom controls, a sticky actor gutter, and a minimap brush. - Keep human activity lightweight by showing human kickoff chips without plotting standalone human event rows. Alternatives considered: - Add the same information to existing issue-list or run-list views. That would preserve simpler UI, but it would not show temporal overlap or handoff paths clearly. - Build this as a plugin-only surface. That keeps core smaller, but the board already has the company-scoped route, navigation, and API client patterns needed for this operator workflow. Roadmap alignment: - `ROADMAP.md` does not list an existing duplicate work-timeline milestone. This supports the broader operator visibility direction around artifacts, enforced outcomes, and higher-autonomy execution. Additional context: - Storybook includes `Pages/Work Timeline` stories for hour/day zoom and a human-activity sample so reviewers can inspect the component without a live backend. ## What Changed - Added the Work Timeline page, route, sidebar entry, API client, query key, and company-prefixed route helper coverage. - Added a pure timeline layout transform for row packing, issue colors, kickoff attribution, connector calculation, tick selection, and duration formatting. - Added the custom SVG timeline chart with sticky actor gutter, hover-revealed connectors, zoom controls, minimap brushing, visible-range feedback, and issue navigation. - Added Storybook coverage plus sample fixtures for the work timeline. - Added and corrected focused UI tests covering layout, chart behavior, routing, sidebar behavior, and collapsed-sidebar expectations. - Addressed Greptile feedback for kickoff fallback ordering, minimap range math, document drag listener cleanup, and stable default `now` handling. ## Verification - `pnpm --filter @paperclipai/ui exec vitest run src/lib/timeline/layout.test.ts src/components/timeline/WorkTimelineChart.test.tsx src/pages/Timeline.test.tsx src/lib/company-routes.test.ts src/components/Sidebar.test.tsx src/components/RequestCollapsedSidebar.test.tsx` - `pnpm --filter @paperclipai/ui typecheck` - `git merge-tree $(git merge-base HEAD origin/master) HEAD origin/master | rg -n "<<<<<<<|changed in both|CONFLICT"` returned no conflicts. - Attempted Storybook screenshot capture with Playwright; Storybook ran locally, but Chromium could not launch in this container because native browser libraries such as `libatk-1.0.so.0` are unavailable and `npx playwright install-deps chromium` requires interactive sudo. ## Risks - Medium UI risk: this adds a substantial visual surface with custom SVG interaction logic, so browser-level review is still useful for responsive behavior and usability. - Low backend risk: this PR only adds a UI client/page around the existing timeline API contract and does not change database schema or server routes. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI GPT-5 via Codex coding agent, with repository file access, shell command execution, GitHub connector access, and focused test execution. Exact context-window details are not exposed in this runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .../src/packaged-artifacts.test.ts | 2 +- ui/src/App.tsx | 2 + ui/src/api/workTimeline.ts | 31 + .../RequestCollapsedSidebar.test.tsx | 41 +- ui/src/components/Sidebar.test.tsx | 18 + ui/src/components/Sidebar.tsx | 2 + .../timeline/WorkTimelineChart.test.tsx | 454 ++++ .../components/timeline/WorkTimelineChart.tsx | 902 ++++++++ ui/src/lib/company-routes.test.ts | 7 + ui/src/lib/company-routes.ts | 1 + ui/src/lib/queryKeys.ts | 1 + ui/src/lib/timeline/layout.test.ts | 216 ++ ui/src/lib/timeline/layout.ts | 365 ++++ ui/src/pages/Timeline.test.tsx | 138 ++ ui/src/pages/Timeline.tsx | 318 +++ .../fixtures/workTimeline.human.sample.json | 1824 +++++++++++++++++ .../fixtures/workTimeline.sample.json | 1 + .../stories/work-timeline.stories.tsx | 129 ++ 18 files changed, 4435 insertions(+), 17 deletions(-) create mode 100644 ui/src/api/workTimeline.ts create mode 100644 ui/src/components/timeline/WorkTimelineChart.test.tsx create mode 100644 ui/src/components/timeline/WorkTimelineChart.tsx create mode 100644 ui/src/lib/timeline/layout.test.ts create mode 100644 ui/src/lib/timeline/layout.ts create mode 100644 ui/src/pages/Timeline.test.tsx create mode 100644 ui/src/pages/Timeline.tsx create mode 100644 ui/storybook/fixtures/workTimeline.human.sample.json create mode 100644 ui/storybook/fixtures/workTimeline.sample.json create mode 100644 ui/storybook/stories/work-timeline.stories.tsx diff --git a/packages/skills-catalog/src/packaged-artifacts.test.ts b/packages/skills-catalog/src/packaged-artifacts.test.ts index e8fb872dd0..e8331290ec 100644 --- a/packages/skills-catalog/src/packaged-artifacts.test.ts +++ b/packages/skills-catalog/src/packaged-artifacts.test.ts @@ -52,5 +52,5 @@ describe("skills catalog package artifacts", () => { expect(paths).toContain("catalog/bundled/software-development/github-pr-workflow/SKILL.md"); expect(paths).toContain("catalog/optional/browser/agent-browser/SKILL.md"); expect(paths).toContain("package.json"); - }, 30_000); + }, 120_000); }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f11df9b259..943b2ecd54 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -8,6 +8,7 @@ import { OnboardingWizardVariant } from "./components/OnboardingWizardVariant"; import { CloudAccessGate } from "./components/CloudAccessGate"; import { Dashboard } from "./pages/Dashboard"; import { DashboardLive } from "./pages/DashboardLive"; +import { Timeline } from "./pages/Timeline"; import { Companies } from "./pages/Companies"; import { Agents } from "./pages/Agents"; import { AgentDetail } from "./pages/AgentDetail"; @@ -80,6 +81,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/api/workTimeline.ts b/ui/src/api/workTimeline.ts new file mode 100644 index 0000000000..42a22b8ebf --- /dev/null +++ b/ui/src/api/workTimeline.ts @@ -0,0 +1,31 @@ +import type { WorkTimelineResult } from "@paperclipai/shared"; +import { api } from "./client"; + +export interface WorkTimelineParams { + from?: string; + to?: string; + /** lens: work kicked off / touched by this user. */ + userId?: string; + goalId?: string; + projectId?: string; + issueId?: string; + limit?: number; +} + +function query(params: WorkTimelineParams): string { + const search = new URLSearchParams(); + if (params.from) search.set("from", params.from); + if (params.to) search.set("to", params.to); + if (params.userId) search.set("userId", params.userId); + if (params.goalId) search.set("goalId", params.goalId); + if (params.projectId) search.set("projectId", params.projectId); + if (params.issueId) search.set("issueId", params.issueId); + if (params.limit) search.set("limit", String(params.limit)); + const qs = search.toString(); + return qs ? `?${qs}` : ""; +} + +export const workTimelineApi = { + get: (companyId: string, params: WorkTimelineParams = {}) => + api.get(`/companies/${companyId}/timeline${query(params)}`), +}; diff --git a/ui/src/components/RequestCollapsedSidebar.test.tsx b/ui/src/components/RequestCollapsedSidebar.test.tsx index a7657e5f3d..0e1d0339e3 100644 --- a/ui/src/components/RequestCollapsedSidebar.test.tsx +++ b/ui/src/components/RequestCollapsedSidebar.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { act } from "react"; +import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SidebarProvider, useSidebar } from "../context/SidebarContext"; @@ -30,11 +30,18 @@ function Harness({ onRoute }: { onRoute: boolean }) { ); } -function render(onRoute: boolean): { root: Root; host: HTMLDivElement } { +async function flushReact() { + await Promise.resolve(); + await new Promise((resolve) => window.setTimeout(resolve, 0)); + flushSync(() => {}); +} + +async function render(onRoute: boolean): Promise<{ root: Root; host: HTMLDivElement }> { const host = document.createElement("div"); document.body.appendChild(host); const root = createRoot(host); - act(() => root.render()); + flushSync(() => root.render()); + await flushReact(); return { root, host }; } @@ -68,45 +75,47 @@ describe("RequestCollapsedSidebar", () => { afterEach(() => { if (active) { - act(() => active!.root.unmount()); + flushSync(() => active!.root.unmount()); active.host.remove(); active = null; } localStorage.clear(); }); - it("requests collapsed while mounted when there is no user pin", () => { - active = render(true); + it("requests collapsed while mounted when there is no user pin", async () => { + active = await render(true); expect(capturedValue?.routeRequestsCollapsed).toBe(true); expect(capturedValue?.collapsed).toBe(true); }); - it("lets an explicit user pin override the route request", () => { - active = render(true); + it("lets an explicit user pin override the route request", async () => { + active = await render(true); expect(capturedValue?.collapsed).toBe(true); // User explicitly pins expanded — must win over the route's request. - act(() => capturedValue?.setCollapsed(false)); + flushSync(() => capturedValue?.setCollapsed(false)); expect(capturedValue?.routeRequestsCollapsed).toBe(true); expect(capturedValue?.collapsed).toBe(false); }); - it("clears the request on unmount, restoring the global default", () => { - active = render(true); + it("clears the request on unmount, restoring the global default", async () => { + active = await render(true); expect(capturedValue?.collapsed).toBe(true); // Navigate away: the route (and its ) unmounts. - act(() => active!.root.render()); + flushSync(() => active!.root.render()); + await flushReact(); expect(capturedValue?.routeRequestsCollapsed).toBe(false); expect(capturedValue?.collapsed).toBe(false); }); - it("keeps a user pin after navigating away (pin persists, request cleared)", () => { - active = render(true); - act(() => capturedValue?.setCollapsed(true)); + it("keeps a user pin after navigating away (pin persists, request cleared)", async () => { + active = await render(true); + flushSync(() => capturedValue?.setCollapsed(true)); expect(localStorage.getItem(COLLAPSED_STORAGE_KEY)).toBe("1"); - act(() => active!.root.render()); + flushSync(() => active!.root.render()); + await flushReact(); // Route request gone, but the explicit collapsed pin still applies. expect(capturedValue?.routeRequestsCollapsed).toBe(false); expect(capturedValue?.collapsed).toBe(true); diff --git a/ui/src/components/Sidebar.test.tsx b/ui/src/components/Sidebar.test.tsx index 6f04696336..dfbdf9677b 100644 --- a/ui/src/components/Sidebar.test.tsx +++ b/ui/src/components/Sidebar.test.tsx @@ -310,6 +310,24 @@ describe("Sidebar", () => { }); }); + it("places Timeline in the Company section", async () => { + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false }); + const root = await renderSidebar(); + + const sections = [...container.querySelectorAll("nav > div")]; + const workSection = sections.find((section) => section.textContent?.startsWith("Work")); + const companySection = sections.find((section) => section.textContent?.startsWith("Company")); + expect(workSection?.textContent).not.toContain("Timeline"); + expect(companySection?.textContent).toContain("Timeline"); + + const timelineLink = [...container.querySelectorAll("a")].find((anchor) => anchor.textContent === "Timeline"); + expect(timelineLink?.getAttribute("href")).toBe("/timeline"); + + flushSync(() => { + root.unmount(); + }); + }); + it("shows the Conference Room nav item when conference room chat is enabled (PAP-137)", async () => { mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false, diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 78cd9b882f..fc5bac9d76 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -18,6 +18,7 @@ import { PanelLeftOpen, Pin, MessagesSquare, + GanttChartSquare, } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { NavLink } from "@/lib/router"; @@ -210,6 +211,7 @@ export function Sidebar() { + diff --git a/ui/src/components/timeline/WorkTimelineChart.test.tsx b/ui/src/components/timeline/WorkTimelineChart.test.tsx new file mode 100644 index 0000000000..73daea7f2d --- /dev/null +++ b/ui/src/components/timeline/WorkTimelineChart.test.tsx @@ -0,0 +1,454 @@ +// @vitest-environment jsdom + +import { flushSync } from "react-dom"; +import type { ComponentProps } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import type { WorkTimelineResult } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { WorkTimelineChart } from "./WorkTimelineChart"; +import { computeLayout } from "@/lib/timeline/layout"; + +vi.mock("@/lib/router", () => ({ + useLocation: () => ({ pathname: "/PAP/timeline" }), +})); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + flushSync(() => root.unmount()); + container.remove(); + vi.restoreAllMocks(); +}); + +function renderChart( + data: WorkTimelineResult, + props: Partial> = {}, +) { + flushSync(() => { + root.render( + , + ); + }); +} + +function timelineSample(): WorkTimelineResult { + return { + actors: [ + { id: "agent:codex", type: "agent", name: "CodexCoder" }, + { id: "agent:qa", type: "agent", name: "QA" }, + ], + spans: [ + { + actorId: "agent:codex", + laneHint: null, + runId: "run-1", + issueId: "issue-1", + issueIdentifier: "PAP-12443", + issueTitle: "Work Timeline sticky gutter", + start: "2026-07-02T09:00:00.000Z", + end: "2026-07-02T10:00:00.000Z", + status: "completed", + retryOfRunId: null, + }, + { + actorId: "agent:qa", + laneHint: null, + runId: "run-2", + issueId: "issue-2", + issueIdentifier: "PAP-12426", + issueTitle: "QA validation", + start: "2026-07-02T11:00:00.000Z", + end: "2026-07-02T11:30:00.000Z", + status: "completed", + retryOfRunId: null, + }, + ], + events: [], + edges: [], + pagination: { limit: 200, offset: 0, totalIssues: 2, hasMore: false }, + window: { + from: "2026-07-02T00:00:00.000Z", + to: "2026-07-03T00:00:00.000Z", + capped: false, + }, + }; +} + +describe("WorkTimelineChart", () => { + it("renders date-aware AM/PM labels on the header axis", () => { + renderChart(timelineSample()); + + const timeAxis = container.querySelector("[data-testid='work-timeline-time-axis']"); + + expect(timeAxis?.textContent).toContain("Jul 2"); + expect(timeAxis?.textContent).toContain("AM"); + expect(timeAxis?.textContent).not.toContain("09:00"); + }); + + it("freezes the time axis over vertical scrolling while preserving horizontal alignment", async () => { + renderChart(timelineSample()); + + const scroller = container.querySelector("[data-testid='work-timeline-scroll']")!; + const timeAxis = container.querySelector("[data-testid='work-timeline-time-axis']")!; + const axisSvg = timeAxis.querySelector("svg")!; + + expect(timeAxis.getAttribute("class")).toContain("absolute"); + expect(timeAxis.getAttribute("class")).toContain("top-0"); + expect(timeAxis.style.height).toBe("32px"); + expect(axisSvg.style.transform).toBe("translateX(0px)"); + + flushSync(() => { + Object.defineProperty(scroller, "scrollTop", { configurable: true, value: 400 }); + Object.defineProperty(scroller, "scrollLeft", { configurable: true, value: 240 }); + scroller.dispatchEvent(new Event("scroll", { bubbles: true })); + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(timeAxis.textContent).toContain("Jul 2"); + expect(axisSvg.style.transform).toBe("translateX(-240px)"); + }); + + it("renders actor labels in a sticky gutter outside the horizontally scrolling SVG", () => { + renderChart(timelineSample()); + + const scroller = container.querySelector("[data-testid='work-timeline-scroll']"); + const gutter = container.querySelector("[data-testid='work-timeline-actor-gutter']"); + const chartSvg = container.querySelector("svg.absolute"); + + expect(scroller).not.toBeNull(); + expect(gutter).not.toBeNull(); + expect(chartSvg).not.toBeNull(); + expect(gutter?.getAttribute("class")).toContain("sticky"); + expect(gutter?.getAttribute("class")).toContain("left-0"); + expect(gutter?.getAttribute("class")).not.toContain("top-0"); + expect(gutter?.getAttribute("width")).toBe("176"); + expect(chartSvg?.getAttribute("width")).not.toBe(gutter?.getAttribute("width")); + expect(gutter?.textContent).toContain("CodexCoder"); + + flushSync(() => { + scroller!.scrollLeft = 10_000; + scroller!.dispatchEvent(new Event("scroll", { bubbles: true })); + }); + + expect(container.querySelector("[data-testid='work-timeline-actor-gutter']")?.textContent).toContain("CodexCoder"); + }); + + it("does not render created diamonds or comment bubbles from instant events", () => { + const data = timelineSample(); + data.actors.push({ id: "user:dotta", type: "user", name: "Dotta" }); + data.events = [ + { actorId: "user:dotta", kind: "created", issueId: "issue-1", at: "2026-07-02T08:30:00.000Z" }, + { actorId: "user:dotta", kind: "commented", issueId: "issue-2", at: "2026-07-02T09:15:00.000Z" }, + { actorId: "user:dotta", kind: "approved", issueId: "issue-1", at: "2026-07-02T10:05:00.000Z" }, + ]; + renderChart(data); + + const gutter = container.querySelector("[data-testid='work-timeline-actor-gutter']"); + expect(gutter?.textContent).not.toContain("Dotta"); + expect(container.querySelectorAll("[data-testid='timeline-event-marker']")).toHaveLength(0); + expect(container.querySelectorAll("[data-testid='timeline-comment-marker']")).toHaveLength(0); + }); + + it("keeps connectors hidden until hover, renders them orthogonally, and highlights the connected graph", async () => { + const data = timelineSample(); + data.actors.push({ id: "agent:cto", type: "agent", name: "CTO" }); + data.spans.push( + { + actorId: "agent:cto", + laneHint: null, + runId: "run-3", + issueId: "issue-3", + issueIdentifier: "PAP-12427", + issueTitle: "Follow-up validation", + start: "2026-07-02T11:45:00.000Z", + end: "2026-07-02T12:00:00.000Z", + status: "completed", + retryOfRunId: null, + }, + { + actorId: "agent:codex", + laneHint: null, + runId: "run-4", + issueId: "issue-4", + issueIdentifier: "PAP-12428", + issueTitle: "Unrelated work", + start: "2026-07-02T13:00:00.000Z", + end: "2026-07-02T14:00:00.000Z", + status: "completed", + retryOfRunId: null, + }, + ); + data.edges = [ + { + fromActorId: "agent:codex", + toActorId: "agent:qa", + issueId: "issue-2", + at: "2026-07-02T10:45:00.000Z", + kind: "delegation", + }, + { + fromActorId: "agent:qa", + toActorId: "agent:cto", + issueId: "issue-3", + at: "2026-07-02T11:35:00.000Z", + kind: "delegation", + }, + ]; + renderChart(data); + + expect(container.querySelectorAll("[data-testid='timeline-connector']")).toHaveLength(0); + + const hovered = container.querySelector("[data-run-id='run-2']")!; + flushSync(() => { + hovered.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, clientX: 100, clientY: 100 })); + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(container.querySelectorAll("[data-testid='timeline-connector']")).toHaveLength(2); + const connectorStrokePaths = Array.from( + container.querySelectorAll("[data-testid='timeline-connector'] path[fill='none']"), + ); + expect(connectorStrokePaths.map((path) => path.getAttribute("d"))).toEqual( + expect.arrayContaining([expect.stringMatching(/ V.+ H/)]), + ); + expect(container.querySelector("[data-run-id='run-1']")?.getAttribute("data-connected-state")).toBe("connected"); + expect(container.querySelector("[data-run-id='run-2']")?.getAttribute("data-connected-state")).toBe("connected"); + expect(container.querySelector("[data-run-id='run-3']")?.getAttribute("data-connected-state")).toBe("connected"); + expect(container.querySelector("[data-run-id='run-4']")?.getAttribute("data-connected-state")).toBe("faded"); + + const layout = computeLayout(data, { + gutter: 176, + rowH: 34, + barH: 15, + laneGap: 4, + pxPerMinute: 8, + nowMs: new Date("2026-07-02T12:00:00.000Z").getTime(), + }); + expect(layout.connectors).toMatchObject([ + { sourceRunId: "run-1", targetRunId: "run-2", dashed: false }, + { sourceRunId: "run-2", targetRunId: "run-3", dashed: false }, + ]); + const bars = new Map(layout.rows.flatMap((row) => row.bars.map((bar) => [bar.span.runId, bar]))); + expect(layout.connectors[0].x1).toBe(bars.get("run-1")?.x2); + expect(layout.connectors[0].x2).toBe(bars.get("run-2")?.x1); + }); + + it("renders kickoff chips for human users but not delegating agents", () => { + const data = timelineSample(); + data.actors.push({ id: "user:dotta", type: "user", name: "Dotta" }); + data.edges = [ + { + fromActorId: "user:dotta", + toActorId: "agent:codex", + issueId: "issue-1", + at: "2026-07-02T08:45:00.000Z", + kind: "delegation", + }, + { + fromActorId: "agent:codex", + toActorId: "agent:qa", + issueId: "issue-2", + at: "2026-07-02T10:45:00.000Z", + kind: "delegation", + }, + ]; + + renderChart(data); + + const kickoffChips = container.querySelectorAll("[data-testid='timeline-kickoff-chip']"); + expect(kickoffChips).toHaveLength(1); + expect(kickoffChips[0].textContent).toContain("DO"); + }); + + it("reserves normal wheel input for panning and uses modifier-wheel for continuous zoom", () => { + const onZoomScaleChange = vi.fn(); + renderChart(timelineSample(), { onZoomScaleChange }); + + const scroller = container.querySelector("[data-testid='work-timeline-scroll']")!; + flushSync(() => { + scroller.dispatchEvent(new WheelEvent("wheel", { deltaY: 80, bubbles: true, cancelable: true })); + }); + expect(onZoomScaleChange).not.toHaveBeenCalled(); + + flushSync(() => { + scroller.dispatchEvent(new WheelEvent("wheel", { deltaY: 80, ctrlKey: true, bubbles: true, cancelable: true })); + }); + expect(onZoomScaleChange).toHaveBeenCalledTimes(1); + }); + + it("opens task bars in a new company-prefixed window", () => { + const open = vi.spyOn(window, "open").mockImplementation(() => null); + renderChart(timelineSample()); + + const bar = container.querySelector("[data-run-id='run-1']")!; + flushSync(() => { + bar.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(open).toHaveBeenCalledWith("/PAP/issues/issue-1", "_blank", "noopener,noreferrer"); + }); + + it("lets minimap edge handles resize the visible range and update zoom", () => { + const onZoomScaleChange = vi.fn(); + renderChart(timelineSample(), { onZoomScaleChange }); + + const rightHandle = container.querySelector("[data-testid='timeline-minimap-right-handle']")!; + const minimap = rightHandle.ownerSVGElement!; + vi.spyOn(minimap, "getBoundingClientRect").mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 900, + bottom: 54, + width: 900, + height: 54, + toJSON: () => ({}), + }); + + flushSync(() => { + rightHandle.dispatchEvent(new MouseEvent("mousedown", { clientX: 300, bubbles: true, cancelable: true })); + document.dispatchEvent(new MouseEvent("mousemove", { clientX: 520, bubbles: true, cancelable: true })); + document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })); + }); + + expect(onZoomScaleChange).toHaveBeenCalled(); + }); + + it("cleans up chart drag listeners when unmounted mid-drag", () => { + const add = vi.spyOn(document, "addEventListener"); + const remove = vi.spyOn(document, "removeEventListener"); + renderChart(timelineSample(), { onZoomScaleChange: vi.fn() }); + + const chartSvg = container.querySelector("svg.absolute")!; + vi.spyOn(chartSvg, "getBoundingClientRect").mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 1000, + bottom: 400, + width: 1000, + height: 400, + toJSON: () => ({}), + }); + + flushSync(() => { + chartSvg.dispatchEvent(new MouseEvent("mousedown", { clientX: 260, bubbles: true, cancelable: true })); + }); + + expect(add).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(add).toHaveBeenCalledWith("mouseup", expect.any(Function)); + + flushSync(() => root.unmount()); + root = createRoot(container); + + expect(remove).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(remove).toHaveBeenCalledWith("mouseup", expect.any(Function)); + }); + + it("cleans up minimap drag listeners when unmounted mid-drag", () => { + const add = vi.spyOn(document, "addEventListener"); + const remove = vi.spyOn(document, "removeEventListener"); + renderChart(timelineSample(), { onZoomScaleChange: vi.fn() }); + + const rightHandle = container.querySelector("[data-testid='timeline-minimap-right-handle']")!; + const minimap = rightHandle.ownerSVGElement!; + vi.spyOn(minimap, "getBoundingClientRect").mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 900, + bottom: 54, + width: 900, + height: 54, + toJSON: () => ({}), + }); + + flushSync(() => { + rightHandle.dispatchEvent(new MouseEvent("mousedown", { clientX: 300, bubbles: true, cancelable: true })); + }); + + expect(add).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(add).toHaveBeenCalledWith("mouseup", expect.any(Function)); + + flushSync(() => root.unmount()); + root = createRoot(container); + + expect(remove).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(remove).toHaveBeenCalledWith("mouseup", expect.any(Function)); + }); + + it("keeps the default now timestamp stable across rerenders", () => { + const now = new Date("2026-07-02T12:00:00.000Z").getTime(); + const later = new Date("2026-07-02T13:00:00.000Z").getTime(); + let currentNow = now; + vi.spyOn(Date, "now").mockImplementation(() => currentNow); + const data = timelineSample(); + data.spans[0] = { + ...data.spans[0], + end: null, + status: "running", + }; + + renderChart(data, { nowMs: undefined }); + const initialWidth = container + .querySelector("[data-run-id='run-1'] rect") + ?.getAttribute("width"); + + currentNow = later; + renderChart(data, { nowMs: undefined }); + + expect(container.querySelector("[data-run-id='run-1'] rect")?.getAttribute("width")).toBe(initialWidth); + }); + + it("lets dragging the chart grid select a time range to zoom into", () => { + const onZoomScaleChange = vi.fn(); + renderChart(timelineSample(), { onZoomScaleChange }); + + const chartSvg = container.querySelector("svg.absolute")!; + const width = Number(chartSvg.getAttribute("width") ?? "1000"); + const height = Number(chartSvg.getAttribute("height") ?? "400"); + vi.spyOn(chartSvg, "getBoundingClientRect").mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: width, + bottom: height, + width, + height, + toJSON: () => ({}), + }); + + flushSync(() => { + chartSvg.dispatchEvent(new MouseEvent("mousedown", { clientX: 260, bubbles: true, cancelable: true })); + document.dispatchEvent(new MouseEvent("mousemove", { clientX: 520, bubbles: true, cancelable: true })); + }); + + expect(container.querySelector("[data-testid='timeline-drag-selection']")).not.toBeNull(); + + flushSync(() => { + document.dispatchEvent(new MouseEvent("mouseup", { clientX: 520, bubbles: true, cancelable: true })); + }); + + expect(onZoomScaleChange).toHaveBeenCalled(); + expect(container.querySelector("[data-testid='timeline-drag-selection']")).toBeNull(); + }); +}); diff --git a/ui/src/components/timeline/WorkTimelineChart.tsx b/ui/src/components/timeline/WorkTimelineChart.tsx new file mode 100644 index 0000000000..689f6ead78 --- /dev/null +++ b/ui/src/components/timeline/WorkTimelineChart.tsx @@ -0,0 +1,902 @@ +/** + * Work Timeline — custom-SVG Gantt (board-locked Direction C, PAP-12422). + * + * Renders actor rows with concurrency sub-lanes, run bars (no issue IDs on the + * bar — identity is the thin left colour tab; truncated title shows on hover), + * human kickoff chips at each bar's leading edge, straight + * hover-revealed agent→agent delegation connectors (dashed for retries), an + * in-progress fade to "now", a hover tooltip, and a full-window mini-map with a + * draggable brush. + */ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useLocation } from "@/lib/router"; +import type { WorkTimelineActor, WorkTimelineResult } from "@paperclipai/shared"; +import { applyCompanyPrefix, extractCompanyPrefixFromPath } from "@/lib/company-routes"; +import { + AXIS_H, + actorType, + barColor, + chooseTickStepMs, + computeLayout, + formatDuration, + isCancelledStatus, + shortLabel, + TIMELINE_COLORS, + type LayoutOptions, + type PositionedBar, +} from "@/lib/timeline/layout"; + +export type ZoomLevel = "hour" | "day" | "week"; + +const ZOOM_DURATION_MIN: Record = { + hour: 60, + day: 24 * 60, + week: 7 * 24 * 60, +}; +const MIN_PX_PER_MIN = 0.08; +const MAX_PX_PER_MIN = 12; +const DEFAULT_VIEWPORT_W = 960; +const MIN_MINIMAP_SELECTION_MS = 15 * 60 * 1000; + +function plotViewportWidth(viewportWidth: number): number { + return Math.max(240, viewportWidth - GEOM.gutter - 24); +} + +export function zoomScaleForLevel(level: ZoomLevel, viewportWidth = DEFAULT_VIEWPORT_W): number { + return clampZoomScale(plotViewportWidth(viewportWidth) / ZOOM_DURATION_MIN[level]); +} + +export function nearestZoomForScale(pxPerMinute: number, viewportWidth = DEFAULT_VIEWPORT_W): ZoomLevel { + return (Object.entries(ZOOM_DURATION_MIN) as [ZoomLevel, number][]).reduce((best, [level]) => ( + Math.abs(zoomScaleForLevel(level, viewportWidth) - pxPerMinute) + < Math.abs(zoomScaleForLevel(best, viewportWidth) - pxPerMinute) + ? level + : best + ), "day"); +} + +export function clampZoomScale(pxPerMinute: number): number { + return Math.min(MAX_PX_PER_MIN, Math.max(MIN_PX_PER_MIN, pxPerMinute)); +} + +/** Pick an initial zoom whose plotted width comfortably fills a typical viewport. */ +export function defaultZoomForWindow(fromMs: number, toMs: number): ZoomLevel { + const hours = (toMs - fromMs) / 3_600_000; + if (hours <= 4) return "hour"; + if (hours <= 48) return "day"; + return "week"; +} + +const GEOM: Omit = { + gutter: 176, + rowH: 34, + barH: 15, + laneGap: 4, +}; +const AVATAR_R = 11; +const CHIP_R = 9; + +interface TooltipState { + x: number; + y: number; + bar: PositionedBar; + connectorHint: string | null; +} + +interface DragSelectionState { + anchorX: number; + currentX: number; +} + +function fmtClock(ms: number): string { + const d = new Date(ms); + const hasMinutes = d.getMinutes() !== 0; + return d.toLocaleTimeString("en-US", { + hour: "numeric", + minute: hasMinutes ? "2-digit" : undefined, + hour12: true, + }); +} + +function fmtTick(ms: number, stepMs: number): string { + const d = new Date(ms); + const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + if (stepMs >= 24 * 60 * 60 * 1000) { + return date; + } + return `${date}, ${fmtClock(ms)}`; +} + +export function formatVisibleDurationMinutes(minutes: number): string { + const rounded = Math.max(1, Math.round(minutes)); + if (rounded >= 7 * 24 * 60 && rounded % (7 * 24 * 60) === 0) { + const weeks = rounded / (7 * 24 * 60); + return `${weeks} week${weeks === 1 ? "" : "s"} visible`; + } + if (rounded >= 24 * 60 && rounded % (24 * 60) === 0) { + const days = rounded / (24 * 60); + return `${days} day${days === 1 ? "" : "s"} visible`; + } + if (rounded >= 24 * 60) { + const days = Math.floor(rounded / (24 * 60)); + const hours = Math.round((rounded % (24 * 60)) / 60); + return `${days}d${hours > 0 ? ` ${hours}h` : ""} visible`; + } + if (rounded >= 60 && rounded % 60 === 0) { + const hours = rounded / 60; + return `${hours} hour${hours === 1 ? "" : "s"} visible`; + } + if (rounded >= 60) return `${Math.floor(rounded / 60)}h ${rounded % 60}m visible`; + return `${rounded} minutes visible`; +} + +function truncate(text: string, n = 42): string { + return text.length > n ? `${text.slice(0, n - 1)}…` : text; +} + +/** An SVG avatar glyph: square for humans, dashed circle for system, circle for agents. */ +function AvatarGlyph({ + cx, + cy, + r, + label, + type, +}: { + cx: number; + cy: number; + r: number; + label: string; + type: string; +}) { + const stroke = "var(--color-foreground)"; + const fill = type === "system" ? "var(--color-muted)" : "var(--color-card)"; + return ( + + {type === "user" ? ( + + ) : ( + + )} + 10 ? 9 : 8} textAnchor="middle" fill={stroke}> + {label} + + + ); +} + +export interface WorkTimelineChartProps { + data: WorkTimelineResult; + zoom: ZoomLevel; + zoomScale?: number; + onZoomScaleChange?: (nextScale: number, nextZoom: ZoomLevel) => void; + onVisibleRangeLabelChange?: (label: string) => void; + /** override "now" (tests / stories); defaults to Date.now(). */ + nowMs?: number; +} + +export function WorkTimelineChart({ + data, + zoom, + zoomScale, + onZoomScaleChange, + onVisibleRangeLabelChange, + nowMs, +}: WorkTimelineChartProps) { + const location = useLocation(); + const scrollRef = useRef(null); + const initialWindowKeyRef = useRef(null); + const centerMsRef = useRef(null); + const defaultNowRef = useRef(null); + const documentDragCleanupRef = useRef<(() => void) | null>(null); + const [tooltip, setTooltip] = useState(null); + const [hoveredRunId, setHoveredRunId] = useState(null); + const [scrollLeft, setScrollLeft] = useState(0); + const [viewportW, setViewportW] = useState(0); + const [dragSelection, setDragSelection] = useState(null); + + const clearDocumentDrag = () => { + documentDragCleanupRef.current?.(); + documentDragCleanupRef.current = null; + }; + + const setDocumentDrag = (move: (event: MouseEvent) => void, up: (event: MouseEvent) => void) => { + clearDocumentDrag(); + const handleUp = (event: MouseEvent) => { + clearDocumentDrag(); + up(event); + }; + document.addEventListener("mousemove", move); + document.addEventListener("mouseup", handleUp); + documentDragCleanupRef.current = () => { + document.removeEventListener("mousemove", move); + document.removeEventListener("mouseup", handleUp); + }; + }; + + useEffect(() => () => clearDocumentDrag(), []); + + if (defaultNowRef.current == null) defaultNowRef.current = Date.now(); + const now = nowMs ?? defaultNowRef.current; + const pxPerMinute = zoomScale ?? zoomScaleForLevel(zoom, viewportW || DEFAULT_VIEWPORT_W); + const layout = useMemo( + () => computeLayout(data, { ...GEOM, pxPerMinute, nowMs: now }), + [data, pxPerMinute, now], + ); + const connectedRunIds = useMemo(() => { + if (!hoveredRunId) return null; + const connected = new Set([hoveredRunId]); + let changed = true; + while (changed) { + changed = false; + for (const c of layout.connectors) { + if (connected.has(c.sourceRunId) && !connected.has(c.targetRunId)) { + connected.add(c.targetRunId); + changed = true; + } + if (connected.has(c.targetRunId) && !connected.has(c.sourceRunId)) { + connected.add(c.sourceRunId); + changed = true; + } + } + } + return connected; + }, [hoveredRunId, layout.connectors]); + const visibleConnectors = useMemo( + () => + connectedRunIds + ? layout.connectors.filter((c) => connectedRunIds.has(c.sourceRunId) && connectedRunIds.has(c.targetRunId)) + : [], + [connectedRunIds, layout.connectors], + ); + const companyPrefix = extractCompanyPrefixFromPath(location.pathname); + + const timeToScrollLeft = (ms: number, viewportWidth: number) => { + const x = layout.gutter + ((ms - layout.fromMs) / 60000) * layout.pxPerMinute; + return Math.max(0, Math.min(layout.width - viewportWidth, x - viewportWidth / 2)); + }; + + const scrollCenterMs = (el: HTMLDivElement) => { + const centerX = el.scrollLeft + el.clientWidth / 2; + return layout.fromMs + ((centerX - layout.gutter) / layout.pxPerMinute) * 60000; + }; + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + const nextViewportW = el.clientWidth; + if (nextViewportW > 0 && nextViewportW !== viewportW) setViewportW(nextViewportW); + + const windowKey = `${data.window.from}:${data.window.to}`; + if (initialWindowKeyRef.current !== windowKey) { + initialWindowKeyRef.current = windowKey; + const latest = Math.max(0, layout.width - nextViewportW); + el.scrollLeft = latest; + setScrollLeft(latest); + centerMsRef.current = scrollCenterMs(el); + return; + } + + if (centerMsRef.current != null) { + const next = timeToScrollLeft(centerMsRef.current, nextViewportW); + el.scrollLeft = next; + setScrollLeft(next); + } + }, [data.window.from, data.window.to, layout.fromMs, layout.gutter, layout.pxPerMinute, layout.toMs, layout.width, viewportW]); + + useEffect(() => { + if (!onVisibleRangeLabelChange) return; + const effectiveViewportW = viewportW || DEFAULT_VIEWPORT_W; + const minutes = plotViewportWidth(effectiveViewportW) / layout.pxPerMinute; + onVisibleRangeLabelChange(formatVisibleDurationMinutes(minutes)); + }, [layout.pxPerMinute, onVisibleRangeLabelChange, viewportW]); + + const stepMs = chooseTickStepMs(layout.pxPerMinute); + const ticks: number[] = []; + const startTick = Math.ceil(layout.fromMs / stepMs) * stepMs; + for (let ms = startTick; ms <= layout.toMs; ms += stepMs) ticks.push(ms); + + const openIssue = (issueId: string) => { + const href = applyCompanyPrefix(`/issues/${encodeURIComponent(issueId)}`, companyPrefix); + window.open(href, "_blank", "noopener,noreferrer"); + }; + + const updateVisibleRange = (fromMs: number, toMs: number) => { + if (!onZoomScaleChange) return; + const el = scrollRef.current; + const boundedFrom = Math.max(layout.fromMs, Math.min(layout.toMs, fromMs)); + const boundedTo = Math.max(layout.fromMs, Math.min(layout.toMs, toMs)); + const startMs = Math.min(boundedFrom, boundedTo); + const endMs = Math.max(boundedFrom, boundedTo); + const durationMs = Math.max(MIN_MINIMAP_SELECTION_MS, endMs - startMs); + const centerMs = startMs + durationMs / 2; + const effectiveViewportW = el?.clientWidth || viewportW || DEFAULT_VIEWPORT_W; + const nextScale = clampZoomScale(plotViewportWidth(effectiveViewportW) / (durationMs / 60000)); + centerMsRef.current = centerMs; + onZoomScaleChange(nextScale, nearestZoomForScale(nextScale, effectiveViewportW)); + }; + + const svgXFromClientX = (clientX: number, el: SVGSVGElement) => { + const rect = el.getBoundingClientRect(); + if (rect.width <= 0) return layout.gutter; + const x = ((clientX - rect.left) / rect.width) * layout.width; + return Math.max(layout.gutter, Math.min(layout.width - 40, x)); + }; + + const msFromSvgX = (x: number) => ( + layout.fromMs + ((x - layout.gutter) / layout.pxPerMinute) * 60000 + ); + + const handlePlotMouseDown = (event: React.MouseEvent) => { + if (!onZoomScaleChange || event.button !== 0) return; + const el = event.currentTarget; + const startX = svgXFromClientX(event.clientX, el); + event.preventDefault(); + setTooltip(null); + setHoveredRunId(null); + setDragSelection({ anchorX: startX, currentX: startX }); + + const move = (moveEvent: MouseEvent) => { + setDragSelection((prev) => prev && { + ...prev, + currentX: svgXFromClientX(moveEvent.clientX, el), + }); + }; + const up = (upEvent: MouseEvent) => { + const endX = svgXFromClientX(upEvent.clientX, el); + setDragSelection(null); + if (Math.abs(endX - startX) < 8) return; + const fromMs = Math.min(msFromSvgX(startX), msFromSvgX(endX)); + const toMs = Math.max(msFromSvgX(startX), msFromSvgX(endX)); + updateVisibleRange(fromMs, toMs); + }; + setDocumentDrag(move, up); + }; + + const connectorHintForBar = (bar: PositionedBar): string | null => { + const related = layout.connectors.filter((c) => c.sourceRunId === bar.span.runId || c.targetRunId === bar.span.runId); + if (related.length === 0) return null; + return related.some((c) => c.dashed) + ? "dashed handoff: retry or changes requested" + : "solid handoff: delegation or assignment"; + }; + + const showTooltip = (evt: React.MouseEvent, bar: PositionedBar) => { + setHoveredRunId(bar.span.runId); + setTooltip({ x: evt.clientX, y: evt.clientY, bar, connectorHint: connectorHintForBar(bar) }); + }; + + const handleWheel = (evt: React.WheelEvent) => { + if (!onZoomScaleChange || !(evt.ctrlKey || evt.metaKey || evt.altKey)) return; + evt.preventDefault(); + const el = scrollRef.current; + if (el) { + centerMsRef.current = scrollCenterMs(el); + } + const nextScale = clampZoomScale(layout.pxPerMinute * Math.exp(-evt.deltaY * 0.001)); + onZoomScaleChange(nextScale, nearestZoomForScale(nextScale, el?.clientWidth ?? viewportW)); + }; + + return ( +
+
{ + setScrollLeft(e.currentTarget.scrollLeft); + centerMsRef.current = scrollCenterMs(e.currentTarget); + }} + onWheel={handleWheel} + > +
+ + + { + if (el && viewportW === 0 && scrollRef.current) setViewportW(scrollRef.current.clientWidth); + }} + > + + + + + + + + {/* row backgrounds */} + {layout.rows.map((row, i) => ( + + ))} + + {/* vertical gridlines */} + {ticks.map((ms) => { + const gx = layout.gutter + ((ms - layout.fromMs) / 60000) * layout.pxPerMinute; + return ( + + + + ); + })} + + {/* now line — teal "Signal" present marker */} + {now >= layout.fromMs && now <= layout.toMs && ( + + )} + + {/* gutter divider + axis baseline */} + + + + {/* connectors (behind bars): hover reveals the connected handoff graph. */} + {visibleConnectors.map((c, i) => { + const y1 = c.y1 + AXIS_H; + const y2 = c.y2 + AXIS_H; + const arrow = + c.x2 >= c.x1 + ? `M${c.x2},${y2} l-10,-5 l0,10 z` + : `M${c.x2},${y2} l10,-5 l0,10 z`; + return ( + + + + + + ); + })} + + {/* rows: gutter avatar/label, lane baselines, bars, human kickoff chips */} + {layout.rows.map((row) => { + const cy = row.y + AXIS_H + row.h / 2; + return ( + + + + {truncate(row.actor.name, 18)} + + + {row.actor.type} + + + {Array.from({ length: row.laneCount }).map((_, ln) => { + const ly = row.y + AXIS_H + 6 + ln * (GEOM.barH + GEOM.laneGap) + GEOM.barH / 2; + return ( + + ); + })} + + {row.bars.map((bar) => { + const yTop = bar.yTop + AXIS_H; + const w = bar.x2 - bar.x1; + const cancelled = isCancelledStatus(bar.span.status); + const color = barColor(bar); + const connectedState = + connectedRunIds == null ? "idle" : connectedRunIds.has(bar.span.runId) ? "connected" : "faded"; + const barOpacity = + connectedState === "idle" + ? 0.88 + : connectedState === "connected" + ? 1 + : 0.22; + return ( + + showTooltip(e, bar)} + onMouseOver={(e) => showTooltip(e, bar)} + onMouseMove={(e) => showTooltip(e, bar)} + onMouseLeave={() => { + setTooltip(null); + setHoveredRunId(null); + }} + onMouseDown={(e) => e.stopPropagation()} + onClick={() => openIssue(bar.span.issueId)} + > + {/* "Signal" encoding: fill = how the run started (delegated / + automation); cancelled runs drop the fill and read as a + hollow dashed bar. */} + + {/* in-progress fade to "now" */} + {bar.running && !cancelled && w > 8 && ( + + )} + + {bar.kickoff && actorType(bar.kickoff) === "user" && ( + + + + )} + + ); + })} + + + ); + })} + {dragSelection && ( + + )} + +
+
+ + + + + + {tooltip && } +
+ ); +} + +function ActorGutter({ rows, height }: { rows: ReturnType["rows"]; height: number }) { + return ( + + ); +} + +function TimeAxisOverlay({ + layout, + ticks, + stepMs, + scrollLeft, +}: { + layout: ReturnType; + ticks: number[]; + stepMs: number; + scrollLeft: number; +}) { + return ( + + ); +} + +function Tooltip({ tooltip, now }: { tooltip: TooltipState; now: number }) { + const { bar } = tooltip; + const startMs = new Date(bar.span.start).getTime(); + const endMs = bar.span.end ? new Date(bar.span.end).getTime() : now; + const title = bar.span.issueTitle ?? bar.span.issueIdentifier ?? "run"; + const left = Math.min(tooltip.x + 14, (typeof window !== "undefined" ? window.innerWidth : 1200) - 300); + return ( +
+
{truncate(title)}
+
+ {fmtClock(startMs)}–{bar.span.end ? fmtClock(endMs) : "now"} · {formatDuration(startMs, endMs)} ·{" "} + {bar.span.status} +
+ {bar.kickoff && ( +
+ kicked off by: {(bar.kickoff as WorkTimelineActor).name} + {bar.span.retryOfRunId ? " · retry" : ""} +
+ )} + {tooltip.connectorHint && ( +
{tooltip.connectorHint}
+ )} +
+ ); +} + +function MiniMap({ + layout, + scrollRef, + viewportW, + scrollLeft, + onVisibleRangeChange, +}: { + layout: ReturnType; + scrollRef: React.RefObject; + viewportW: number; + scrollLeft: number; + onVisibleRangeChange: (fromMs: number, toMs: number) => void; +}) { + const documentDragCleanupRef = useRef<(() => void) | null>(null); + const W = Math.max(320, viewportW || 900); + const H = 54; + const pad = 8; + const spanMs = layout.toMs - layout.fromMs || 1; + const mx = (ms: number) => pad + ((ms - layout.fromMs) / spanMs) * (W - 2 * pad); + + // one thin tick per run, stacked by row order + const rowIndex = new Map(layout.rows.map((r, i) => [r.actor.id, i])); + const laneH = (H - 2 * pad) / Math.max(1, layout.rows.length); + + const timeAtX = (x: number) => { + const ms = layout.fromMs + ((x - layout.gutter) / layout.pxPerMinute) * 60000; + return Math.max(layout.fromMs, Math.min(layout.toMs, ms)); + }; + const visibleStartMs = timeAtX(scrollLeft + layout.gutter); + const visibleEndMs = timeAtX(scrollLeft + layout.gutter + (viewportW || W)); + const brushX = mx(visibleStartMs); + const brushW = Math.max(24, mx(visibleEndMs) - brushX); + + const clearDocumentDrag = () => { + documentDragCleanupRef.current?.(); + documentDragCleanupRef.current = null; + }; + + const setDocumentDrag = (move: (event: MouseEvent) => void, up: (event: MouseEvent) => void) => { + clearDocumentDrag(); + const handleUp = (event: MouseEvent) => { + clearDocumentDrag(); + up(event); + }; + document.addEventListener("mousemove", move); + document.addEventListener("mouseup", handleUp); + documentDragCleanupRef.current = () => { + document.removeEventListener("mousemove", move); + document.removeEventListener("mouseup", handleUp); + }; + }; + + useEffect(() => () => clearDocumentDrag(), []); + + const msAtClientX = (clientX: number, el: SVGSVGElement) => { + const rect = el.getBoundingClientRect(); + const f = Math.min(1, Math.max(0, (clientX - rect.left - pad) / (W - 2 * pad))); + return layout.fromMs + f * spanMs; + }; + + const seek = (clientX: number, el: SVGSVGElement) => { + const centerMs = msAtClientX(clientX, el); + if (scrollRef.current) { + scrollRef.current.scrollLeft = layout.gutter + ((centerMs - layout.fromMs) / 60000) * layout.pxPerMinute - scrollRef.current.clientWidth / 2; + } + }; + + const startRangeDrag = (mode: "left" | "right" | "move", event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const el = event.currentTarget.ownerSVGElement; + if (!el) return; + const startLeftMs = visibleStartMs; + const startRightMs = visibleEndMs; + const durationMs = Math.max(MIN_MINIMAP_SELECTION_MS, startRightMs - startLeftMs); + + const move = (ev: MouseEvent) => { + const hitMs = msAtClientX(ev.clientX, el); + if (mode === "left") { + onVisibleRangeChange(Math.min(hitMs, startRightMs - MIN_MINIMAP_SELECTION_MS), startRightMs); + } else if (mode === "right") { + onVisibleRangeChange(startLeftMs, Math.max(hitMs, startLeftMs + MIN_MINIMAP_SELECTION_MS)); + } else { + const nextFrom = Math.max(layout.fromMs, Math.min(layout.toMs - durationMs, hitMs - durationMs / 2)); + onVisibleRangeChange(nextFrom, nextFrom + durationMs); + } + }; + setDocumentDrag(move, () => {}); + }; + + return ( +
+ { + const el = e.currentTarget; + seek(e.clientX, el); + const move = (ev: MouseEvent) => seek(ev.clientX, el); + setDocumentDrag(move, () => {}); + }} + > + + {layout.rows.flatMap((row) => + row.bars.map((bar) => { + const startMs = new Date(bar.span.start).getTime(); + const endMs = bar.span.end ? new Date(bar.span.end).getTime() : layout.toMs; + const yy = pad + (rowIndex.get(row.actor.id) ?? 0) * laneH; + return ( + + ); + }), + )} + startRangeDrag("move", e)} + /> + startRangeDrag("left", e)} + /> + startRangeDrag("right", e)} + /> + +
+ ); +} diff --git a/ui/src/lib/company-routes.test.ts b/ui/src/lib/company-routes.test.ts index 6278224db8..9a86268ab6 100644 --- a/ui/src/lib/company-routes.test.ts +++ b/ui/src/lib/company-routes.test.ts @@ -72,6 +72,13 @@ describe("company routes", () => { expect(toCompanyRelativePath("/PAP/artifacts")).toBe("/artifacts"); }); + it("treats /timeline as a board route that needs a company prefix", () => { + expect(isBoardPathWithoutPrefix("/timeline")).toBe(true); + expect(extractCompanyPrefixFromPath("/timeline")).toBeNull(); + expect(applyCompanyPrefix("/timeline", "PAP")).toBe("/PAP/timeline"); + expect(toCompanyRelativePath("/PAP/timeline")).toBe("/timeline"); + }); + it("preserves artifact deep-link anchors when applying the company prefix", () => { expect(applyCompanyPrefix("/issues/PAP-10205#work-product-wp-1", "PAP")).toBe( "/PAP/issues/PAP-10205#work-product-wp-1", diff --git a/ui/src/lib/company-routes.ts b/ui/src/lib/company-routes.ts index aaea26ad73..05a2c6a0b7 100644 --- a/ui/src/lib/company-routes.ts +++ b/ui/src/lib/company-routes.ts @@ -24,6 +24,7 @@ const BOARD_ROUTE_ROOTS = new Set([ "design-guide", "search", "settings", + "timeline", ]); const GLOBAL_ROUTE_ROOTS = new Set(["auth", "invite", "board-claim", "cli-auth", "docs", "instance"]); diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index f78b478999..c53aee2417 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -255,6 +255,7 @@ export const queryKeys = { ["company-search", companyId, q, scope, limit, offset] as const, }, dashboard: (companyId: string) => ["dashboard", companyId] as const, + workTimeline: (companyId: string, lens?: string) => ["work-timeline", companyId, lens ?? "all"] as const, userProfile: (companyId: string, userSlug: string) => ["user-profile", companyId, userSlug] as const, sidebarBadges: (companyId: string) => ["sidebar-badges", companyId] as const, diff --git a/ui/src/lib/timeline/layout.test.ts b/ui/src/lib/timeline/layout.test.ts new file mode 100644 index 0000000000..279de2696b --- /dev/null +++ b/ui/src/lib/timeline/layout.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, it } from "vitest"; +import type { WorkTimelineResult } from "@paperclipai/shared"; +import { chooseTickStepMs, computeLayout, issueColor, shortLabel, type LayoutOptions } from "./layout"; + +const DAY = "2026-07-02"; +const t = (hhmm: string) => `${DAY}T${hhmm}:00.000Z`; + +// A trimmed version of the board's locked-design sample (PAP-12422), expressed +// in the real endpoint contract: actor→actor edges rather than run→run. +function sample(): WorkTimelineResult { + const actors = [ + { id: "user:dotta", type: "user" as const, name: "dotta" }, + { id: "agent:ceo", type: "agent" as const, name: "CEO" }, + { id: "agent:cto", type: "agent" as const, name: "CTO Architect" }, + { id: "agent:ux", type: "agent" as const, name: "UXDesigner" }, + { id: "agent:senior", type: "agent" as const, name: "SeniorEngineer" }, + { id: "agent:codex", type: "agent" as const, name: "CodexCoder" }, + { id: "agent:qa", type: "agent" as const, name: "QA" }, + { id: "system:routine", type: "system" as const, name: "Circleback" }, + ]; + const span = ( + runId: string, + actorId: string, + issueId: string, + identifier: string, + start: string, + end: string | null, + status: string, + retryOfRunId: string | null = null, + ): WorkTimelineResult["spans"][number] => ({ + actorId, + laneHint: null, + runId, + issueId, + issueIdentifier: identifier, + issueTitle: `${identifier} title`, + start: t(start), + end: end ? t(end) : null, + status, + retryOfRunId, + }); + const spans = [ + span("r1", "agent:ceo", "i-405", "PAP-12405", "09:02", "09:10", "completed"), + span("r2", "agent:cto", "i-405", "PAP-12405", "09:14", "09:52", "completed"), + span("r3", "agent:senior", "i-075", "PAP-12075", "09:58", "10:20", "completed"), + span("r4", "agent:ux", "i-422", "PAP-12422", "10:10", "11:34", "running"), + span("r5", "agent:codex", "i-423", "PAP-12423", "10:10", "10:56", "completed"), + span("r6", "agent:codex", "i-075", "PAP-12075", "10:22", "11:08", "completed"), // overlaps r5 → sub-lane + span("r7", "agent:qa", "i-423", "PAP-12423", "11:40", "12:12", "completed"), + span("r8", "agent:codex", "i-423", "PAP-12423", "12:20", "12:38", "completed", "r5"), // retry + span("r9", "system:routine", "i-286", "PAP-12286", "20:00", "20:04", "completed"), + ]; + const edge = (from: string, to: string, issueId: string, at: string): WorkTimelineResult["edges"][number] => ({ + fromActorId: from, + toActorId: to, + issueId, + at: t(at), + kind: "delegation", + }); + const edges = [ + edge("user:dotta", "agent:ceo", "i-405", "09:01"), // human kickoff → chip only, no line + edge("agent:ceo", "agent:cto", "i-405", "09:13"), + edge("agent:cto", "agent:ux", "i-422", "10:09"), + edge("agent:cto", "agent:codex", "i-423", "10:09"), + edge("agent:senior", "agent:codex", "i-075", "10:21"), + edge("agent:codex", "agent:qa", "i-423", "11:39"), + edge("agent:qa", "agent:codex", "i-423", "12:19"), // → retry r8 (dashed) + ]; + return { + actors, + spans, + events: [], + edges, + pagination: { limit: 200, offset: 0, totalIssues: 6, hasMore: false }, + window: { from: t("09:00"), to: t("20:15"), capped: false }, + }; +} + +const OPTS: LayoutOptions = { + pxPerMinute: 1.7, + gutter: 172, + rowH: 34, + barH: 15, + laneGap: 4, + nowMs: new Date(t("13:00")).getTime(), +}; + +describe("computeLayout", () => { + it("excludes humans from rows but keeps agents and system actors", () => { + const layout = computeLayout(sample(), OPTS); + const rowActorTypes = layout.rows.map((r) => r.actor.type); + expect(rowActorTypes).not.toContain("user"); + expect(layout.rows.map((r) => r.actor.id)).toContain("system:routine"); + expect(layout.rows).toHaveLength(7); // 6 agents + 1 system + }); + + it("orders rows by first activity", () => { + const layout = computeLayout(sample(), OPTS); + expect(layout.rows[0].actor.id).toBe("agent:ceo"); + expect(layout.rows[layout.rows.length - 1].actor.id).toBe("system:routine"); + }); + + it("packs overlapping runs into concurrency sub-lanes", () => { + const layout = computeLayout(sample(), OPTS); + const codex = layout.rows.find((r) => r.actor.id === "agent:codex")!; + expect(codex.laneCount).toBe(2); // r5 and r6 overlap + const lanesTop = new Set(codex.bars.map((b) => b.yTop)); + expect(lanesTop.size).toBeGreaterThan(1); + }); + + it("derives the kickoff actor for a run (incl. human) from edges", () => { + const layout = computeLayout(sample(), OPTS); + const codexApi = layout.rows + .find((r) => r.actor.id === "agent:codex")! + .bars.find((b) => b.span.runId === "r5")!; + expect(codexApi.kickoff?.id).toBe("agent:cto"); + + const ceoRun = layout.rows.find((r) => r.actor.id === "agent:ceo")!.bars[0]; + expect(ceoRun.kickoff?.id).toBe("user:dotta"); // human kickoff shown as chip + }); + + it("prefers the nearest post-start kickoff edge when no prior edge exists", () => { + const data = sample(); + data.spans.push({ + actorId: "agent:ux", + laneHint: null, + runId: "late-edge-run", + issueId: "i-late", + issueIdentifier: "PAP-99999", + issueTitle: "Late kickoff fallback", + start: t("14:00"), + end: t("14:10"), + status: "completed", + retryOfRunId: null, + }); + data.edges.push( + { fromActorId: "agent:qa", toActorId: "agent:ux", issueId: "i-late", at: t("14:30"), kind: "delegation" }, + { fromActorId: "agent:cto", toActorId: "agent:ux", issueId: "i-late", at: t("14:02"), kind: "delegation" }, + ); + + const layout = computeLayout(data, OPTS); + const lateRun = layout.rows + .find((r) => r.actor.id === "agent:ux")! + .bars.find((b) => b.span.runId === "late-edge-run")!; + expect(lateRun.kickoff?.id).toBe("agent:cto"); + }); + + it("draws agent→agent connectors only, dashing retries, never from a human", () => { + const layout = computeLayout(sample(), OPTS); + // 6 agent→agent edges resolve to bars; the dotta→ceo human edge draws no line. + expect(layout.connectors.length).toBe(6); + const dashed = layout.connectors.filter((c) => c.dashed); + expect(dashed).toHaveLength(1); // only the QA→codex retry hop + // every connector is left-to-right (source trailing edge → target leading edge) + for (const c of layout.connectors) expect(c.x2).toBeGreaterThanOrEqual(c.x1 - 1); + }); + + it("extends in-progress runs to now and clamps sub-minute bars", () => { + const layout = computeLayout(sample(), OPTS); + const running = layout.rows + .find((r) => r.actor.id === "agent:ux")! + .bars.find((b) => b.span.runId === "r4")!; + expect(running.running).toBe(true); + expect(running.x2).toBeGreaterThan(running.x1 + 3); + }); + + it("produces a deterministic issue hue + legend", () => { + const layout = computeLayout(sample(), OPTS); + expect(layout.issues.length).toBe(5); // i-405, i-075, i-422, i-423, i-286 + expect(issueColor("i-405")).toBe(issueColor("i-405")); + }); +}); + +describe("human activity markers", () => { + const withUserEvents = (): WorkTimelineResult => ({ + ...sample(), + events: [ + { actorId: "user:dotta", kind: "created", issueId: "i-405", at: t("09:00") }, + { actorId: "user:dotta", kind: "commented", issueId: "i-422", at: t("11:00") }, + { actorId: "user:dotta", kind: "approved", issueId: "i-423", at: t("12:15") }, + ], + }); + + it("does not create a marker-only human row from instant events", () => { + const layout = computeLayout(withUserEvents(), OPTS); + const dotta = layout.rows.find((r) => r.actor.id === "user:dotta"); + expect(dotta).toBeUndefined(); + }); + + it("does not plot instant markers on run rows", () => { + const layout = computeLayout(withUserEvents(), OPTS); + expect(layout.rows.flatMap((r) => r.markers)).toHaveLength(0); + }); + + it("keeps human events visual-only as kickoff chips, not connector targets", () => { + const layout = computeLayout(withUserEvents(), OPTS); + // dotta→ceo human kickoff still draws no line; agent→agent count unchanged. + expect(layout.connectors.length).toBe(6); + }); + + it("still excludes a human with no in-window events", () => { + const layout = computeLayout(sample(), OPTS); // events: [] + expect(layout.rows.map((r) => r.actor.type)).not.toContain("user"); + }); +}); + +describe("helpers", () => { + it("shortLabel builds 2-char initials", () => { + expect(shortLabel("CodexCoder")).toBe("CO"); + expect(shortLabel("CTO Architect")).toBe("CA"); + }); + + it("chooseTickStepMs grows the step as the view zooms out", () => { + expect(chooseTickStepMs(6)).toBeLessThan(chooseTickStepMs(0.4)); + }); +}); diff --git a/ui/src/lib/timeline/layout.ts b/ui/src/lib/timeline/layout.ts new file mode 100644 index 0000000000..68b1d55398 --- /dev/null +++ b/ui/src/lib/timeline/layout.ts @@ -0,0 +1,365 @@ +/** + * Work Timeline layout — pure transform from the Phase B endpoint contract + * (`WorkTimelineResult`) into a renderable view model for the custom-SVG Gantt. + * + * Ports the board-locked "Direction C" logic (PAP-12422): agent/system rows only + * (humans never get a row), overlapping runs packed into concurrency sub-lanes, + * a kickoff actor derived per run (shown as an avatar chip — may be a human), + * and straight agent→agent delegation connectors from a source bar's trailing + * edge to a target bar's leading edge (dashed for retries / changes-requested). + * + * Everything here is deterministic given (result, options) so it can be unit + * tested without a DOM. + */ +import type { + WorkTimelineActor, + WorkTimelineEdge, + WorkTimelineEvent, + WorkTimelineResult, + WorkTimelineSpan, +} from "@paperclipai/shared"; + +export interface LayoutOptions { + /** px per minute along the x axis (set by the zoom level). */ + pxPerMinute: number; + /** width of the left actor gutter in px. */ + gutter: number; + /** row height in px. */ + rowH: number; + /** bar height in px. */ + barH: number; + /** vertical gap between concurrency sub-lanes in px. */ + laneGap: number; + /** wall-clock "now" in ms, used to close in-progress runs. */ + nowMs: number; +} + +export interface PositionedBar { + span: WorkTimelineSpan; + /** leading (start) x in px. */ + x1: number; + /** trailing (end) x in px. */ + x2: number; + /** vertical center of the bar in px. */ + yc: number; + /** top of the bar in px. */ + yTop: number; + height: number; + running: boolean; + /** the actor who kicked this run off, if resolvable (may be a human/user). */ + kickoff: WorkTimelineActor | null; +} + +/** + * Instant human/actor actions are deliberately not plotted in the main chart. + * The shape remains in the layout model so call sites do not need special cases, + * but rows now stay focused on actors with actual run participation. + */ +export interface PositionedMarker { + event: WorkTimelineEvent; + /** x position (px) of the marker centre = x(event.at). */ + x: number; + /** vertical centre of the marker in px (row-relative, excludes axis offset). */ + yc: number; +} + +export interface ActorRow { + actor: WorkTimelineActor; + /** top of the row (excluding axis offset) in px. */ + y: number; + /** row height in px. */ + h: number; + laneCount: number; + bars: PositionedBar[]; + /** reserved for instant event markers; currently empty by design. */ + markers: PositionedMarker[]; + /** number of runs plotted on this row (the "Signal" rail count). */ + runCount: number; + /** total active run time on this row in ms (the "Signal" rail active-time). */ + activeMs: number; +} + +export interface Connector { + x1: number; + y1: number; + x2: number; + y2: number; + sourceRunId: string; + targetRunId: string; + /** dashed = a return / retry (changes-requested) hop. */ + dashed: boolean; +} + +export interface TimelineLayout { + rows: ActorRow[]; + connectors: Connector[]; + /** full inner width of the chart (gutter + plotted time + pad). */ + width: number; + /** full height of the chart including the axis strip. */ + height: number; + /** domain start (ms). */ + fromMs: number; + /** domain end (ms). */ + toMs: number; + gutter: number; + pxPerMinute: number; + /** ordered list of distinct issue keys present for the task hue map. */ + issues: { key: string; label: string; color: string }[]; +} + +export const AXIS_H = 32; +const RUNNING_STATUSES = new Set(["running", "in_progress", "queued", "pending"]); +const CANCELLED_STATUSES = new Set(["cancelled", "canceled", "aborted", "skipped"]); + +export function isRunningStatus(status: string): boolean { + return RUNNING_STATUSES.has(status); +} + +export function isCancelledStatus(status: string): boolean { + return CANCELLED_STATUSES.has(status); +} + +/** + * "Signal" encoding (PAP-12694, board-picked): colour spends on ONE meaning — + * how the run started — instead of a per-issue hash rainbow. Delegated runs + * (kicked off by another actor) read blue; automation/self-started runs read + * amber. The blue/amber pair is colour-blind-safe and holds contrast on both + * light and dark backgrounds so the chart screenshots cleanly. Cancelled runs + * drop their fill entirely (rendered as a hollow dashed bar) and a teal "now" + * line marks the present. + */ +export const TIMELINE_COLORS = { + delegated: "#5b9bf6", + automation: "#f4b740", + /** stroke/ink for a hollow, cancelled bar. */ + cancelled: "#9aa3ad", + now: "#2dd4bf", +} as const; + +export type RunSourceKind = "delegated" | "automation"; + +/** How a run was started: delegated (has a kickoff actor) vs. automation/self. */ +export function barSourceKind(bar: PositionedBar): RunSourceKind { + return bar.kickoff ? "delegated" : "automation"; +} + +/** Source colour for a bar under the "Signal" encoding. */ +export function barColor(bar: PositionedBar): string { + return TIMELINE_COLORS[barSourceKind(bar)]; +} + +export function actorType(actor: WorkTimelineActor | undefined): string { + return actor?.type ?? "system"; +} + +/** Deterministic, stable hue per issue that reads on both light and dark. */ +export function issueColor(key: string): string { + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = (hash * 31 + key.charCodeAt(i)) & 0xffffffff; + } + const hue = Math.abs(hash) % 360; + return `hsl(${hue} 62% 52%)`; +} + +/** 2-char initials for an SVG avatar chip. */ +export function shortLabel(name: string): string { + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return (name.slice(0, 2) || "?").toUpperCase(); +} + +function spanStartMs(s: WorkTimelineSpan): number { + return new Date(s.start).getTime(); +} + +function spanEndMs(s: WorkTimelineSpan, nowMs: number): number { + const raw = s.end ? new Date(s.end).getTime() : nowMs; + return raw; +} + +/** + * Resolve the kickoff actor for a run: the source of the delegation/assignment + * edge that points at this run's actor on this run's issue, closest at-or-before + * the run start (falling back to the nearest such edge). Mirrors the design's + * "avatar chip at the leading edge = who kicked it off". + */ +function resolveKickoff( + span: WorkTimelineSpan, + edges: WorkTimelineEdge[], + actorById: Map, +): WorkTimelineActor | null { + const start = spanStartMs(span); + let best: { edge: WorkTimelineEdge; delta: number } | null = null; + for (const e of edges) { + if (e.toActorId !== span.actorId || e.issueId !== span.issueId) continue; + if (e.fromActorId === span.actorId) continue; // self-kickoff is not a delegation + const at = new Date(e.at).getTime(); + // Prefer edges at-or-before the run start; otherwise smallest absolute gap. + const delta = at <= start ? start - at : (at - start) + 1e12; + if (!best || delta < best.delta) best = { edge: e, delta }; + } + if (!best) return null; + return actorById.get(best.edge.fromActorId) ?? null; +} + +export function computeLayout(result: WorkTimelineResult, opts: LayoutOptions): TimelineLayout { + const { pxPerMinute, gutter, rowH, barH, laneGap, nowMs } = opts; + const fromMs = new Date(result.window.from).getTime(); + const toMs = new Date(result.window.to).getTime(); + const actorById = new Map(result.actors.map((a) => [a.id, a])); + + const x = (ms: number) => gutter + ((ms - fromMs) / 60000) * pxPerMinute; + + // Rows: any actor with in-window runs gets a row. Event-only comments/creates + // do not create marker-only rows because they make the chart noisy. + const firstActivity = new Map(); + const noteActivity = (actorId: string, t: number) => { + const cur = firstActivity.get(actorId); + if (cur === undefined || t < cur) firstActivity.set(actorId, t); + }; + for (const s of result.spans) noteActivity(s.actorId, spanStartMs(s)); + + const rowActors = result.actors + .filter((a) => firstActivity.has(a.id)) // drop actors with no run in-window + .sort((a, b) => (firstActivity.get(a.id)! - firstActivity.get(b.id)!)); + + // Issue hue map (ordered by first appearance) for the task color map. + const issueOrder: string[] = []; + const issueLabel = new Map(); + for (const s of result.spans) { + const key = s.issueId; + if (!issueLabel.has(key)) { + issueOrder.push(key); + issueLabel.set(key, s.issueIdentifier ?? s.issueTitle ?? "issue"); + } + } + const issues = issueOrder.map((key) => ({ key, label: issueLabel.get(key)!, color: issueColor(key) })); + + const barIndex = new Map(); // runId -> bar + const rows: ActorRow[] = []; + let y = 0; + for (const actor of rowActors) { + const runs = result.spans + .filter((s) => s.actorId === actor.id) + .sort((p, q) => spanStartMs(p) - spanStartMs(q)); + + // Greedy pack overlapping runs into sub-lanes. + const laneEnds: number[] = []; + const laneOf = new Map(); + for (const r of runs) { + const rs = spanStartMs(r); + const re = spanEndMs(r, nowMs); + let placed = -1; + for (let ln = 0; ln < laneEnds.length; ln++) { + if (laneEnds[ln] <= rs) { + placed = ln; + break; + } + } + if (placed === -1) { + placed = laneEnds.length; + laneEnds.push(re); + } else { + laneEnds[placed] = re; + } + laneOf.set(r.runId, placed); + } + const laneCount = Math.max(1, laneEnds.length); + const h = Math.max(rowH, laneCount * (barH + laneGap) + 8); + + const bars: PositionedBar[] = runs.map((r) => { + const lane = laneOf.get(r.runId) ?? 0; + const laneTop = y + 6 + lane * (barH + laneGap); + const x1 = x(spanStartMs(r)); + const x2raw = x(spanEndMs(r, nowMs)); + const x2 = Math.max(x1 + 3, x2raw); // clamp sub-minute runs to a visible min width + const bar: PositionedBar = { + span: r, + x1, + x2, + yTop: laneTop, + yc: laneTop + barH / 2, + height: barH, + running: isRunningStatus(r.status), + kickoff: resolveKickoff(r, result.edges, actorById), + }; + barIndex.set(r.runId, bar); + return bar; + }); + + const markers: PositionedMarker[] = []; + + const activeMs = runs.reduce((sum, r) => sum + Math.max(0, spanEndMs(r, nowMs) - spanStartMs(r)), 0); + + rows.push({ actor, y, h, laneCount, bars, markers, runCount: runs.length, activeMs }); + y += h; + } + + // Connectors: straight agent→agent lines, connected at both ends. For each bar + // with an agent/system kickoff, connect from the kickoff actor's nearest + // preceding bar (same issue preferred) to this bar's leading edge. + const connectors: Connector[] = []; + for (const row of rows) { + for (const bar of row.bars) { + const k = bar.kickoff; + if (!k || k.type === "user") continue; // agent→agent only; humans stay as chips + const source = nearestSourceBar(k.id, bar, barIndex, nowMs); + if (!source) continue; + connectors.push({ + x1: source.x2, + y1: source.yc, + x2: bar.x1, + y2: bar.yc, + sourceRunId: source.span.runId, + targetRunId: bar.span.runId, + dashed: Boolean(bar.span.retryOfRunId), + }); + } + } + + const width = gutter + ((toMs - fromMs) / 60000) * pxPerMinute + 40; + const height = y + AXIS_H; + return { rows, connectors, width, height, fromMs, toMs, gutter, pxPerMinute, issues }; +} + +/** The kickoff actor's bar that best precedes `target` (same issue preferred). */ +function nearestSourceBar( + kickoffActorId: string, + target: PositionedBar, + barIndex: Map, + nowMs: number, +): PositionedBar | null { + const targetStart = spanStartMs(target.span); + let sameIssue: PositionedBar | null = null; + let anyIssue: PositionedBar | null = null; + for (const bar of barIndex.values()) { + if (bar.span.actorId !== kickoffActorId) continue; + if (bar === target) continue; + const end = spanEndMs(bar.span, nowMs); + if (end > targetStart + 1) continue; // must precede (small tolerance) + if (bar.span.issueId === target.span.issueId) { + if (!sameIssue || spanEndMs(sameIssue.span, nowMs) < end) sameIssue = bar; + } + if (!anyIssue || spanEndMs(anyIssue.span, nowMs) < end) anyIssue = bar; + } + return sameIssue ?? anyIssue; +} + +/** Choose a "nice" gridline step (ms) targeting ~120px between labels. */ +export function chooseTickStepMs(pxPerMinute: number): number { + const targetPx = 120; + const minutesPerTarget = targetPx / pxPerMinute; + const steps = [15, 30, 60, 120, 180, 360, 720, 1440, 2880, 10080]; // minutes + for (const m of steps) { + if (m >= minutesPerTarget) return m * 60000; + } + return steps[steps.length - 1] * 60000; +} + +export function formatDuration(startMs: number, endMs: number): string { + const mins = Math.max(0, Math.round((endMs - startMs) / 60000)); + if (mins >= 1440) return `${Math.floor(mins / 1440)}d ${Math.floor((mins % 1440) / 60)}h`; + if (mins >= 60) return `${Math.floor(mins / 60)}h ${mins % 60}m`; + return `${mins}m`; +} diff --git a/ui/src/pages/Timeline.test.tsx b/ui/src/pages/Timeline.test.tsx new file mode 100644 index 0000000000..cfe8943338 --- /dev/null +++ b/ui/src/pages/Timeline.test.tsx @@ -0,0 +1,138 @@ +// @vitest-environment jsdom + +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { WorkTimelineResult } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Timeline } from "./Timeline"; + +const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); +const mockWorkTimelineApi = vi.hoisted(() => ({ + get: vi.fn(), +})); + +vi.mock("@/context/CompanyContext", () => ({ + useCompany: () => ({ selectedCompanyId: "company-1" }), +})); + +vi.mock("@/context/BreadcrumbContext", () => ({ + useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs }), +})); + +vi.mock("@/api/workTimeline", () => ({ + workTimelineApi: mockWorkTimelineApi, +})); + +vi.mock("@/components/RequestCollapsedSidebar", () => ({ + RequestCollapsedSidebar: () =>
, +})); + +const emptyTimeline: WorkTimelineResult = { + actors: [], + spans: [], + events: [], + edges: [], + pagination: { + limit: 100, + offset: 0, + totalIssues: 0, + hasMore: false, + }, + window: { + from: "2026-07-01T00:00:00.000Z", + to: "2026-07-07T23:59:59.999Z", + capped: false, + }, +}; + +async function flushReact() { + for (let index = 0; index < 3; index += 1) { + await Promise.resolve(); + await new Promise((resolve) => window.setTimeout(resolve, 0)); + } + flushSync(() => {}); +} + +describe("Timeline", () => { + let container: HTMLDivElement; + let root: ReturnType | null; + let queryClient: QueryClient; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = null; + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + mockWorkTimelineApi.get.mockResolvedValue(emptyTimeline); + }); + + afterEach(() => { + if (root) { + flushSync(() => root?.unmount()); + } + container.remove(); + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + it("requests the collapsed app sidebar by default", async () => { + root = createRoot(container); + + flushSync(() => { + root?.render( + + + , + ); + }); + await flushReact(); + + expect(container.querySelector('[data-testid="request-collapsed-sidebar"]')).not.toBeNull(); + }); + + it("renders range controls plus icon zoom controls without the user lens selector or visible-duration readout", async () => { + root = createRoot(container); + + flushSync(() => { + root?.render( + + + , + ); + }); + await flushReact(); + + expect(container.textContent).toContain("Range"); + expect(container.querySelector('[aria-label="Zoom out"]')).not.toBeNull(); + expect(container.querySelector('[aria-label="Zoom in"]')).not.toBeNull(); + expect(container.querySelector('[aria-label="Reset zoom"]')).not.toBeNull(); + expect(container.textContent).not.toContain("Everyone"); + expect(container.textContent).not.toContain("work kicked off"); + expect(container.textContent).not.toContain("visible"); + }); + + it("requests the company timeline without a user lens parameter", async () => { + root = createRoot(container); + + flushSync(() => { + root?.render( + + + , + ); + }); + await flushReact(); + + expect(mockWorkTimelineApi.get).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ + from: expect.any(String), + to: expect.any(String), + }), + ); + expect(mockWorkTimelineApi.get.mock.calls[0]?.[1]).not.toHaveProperty("userId"); + }); +}); diff --git a/ui/src/pages/Timeline.tsx b/ui/src/pages/Timeline.tsx new file mode 100644 index 0000000000..f244bedc09 --- /dev/null +++ b/ui/src/pages/Timeline.tsx @@ -0,0 +1,318 @@ +/** + * Work Timeline page (PAP-12424 / Phase C of PAP-12405). + * + * A Gantt-style view of company actor activity built on the Phase B endpoint + * (`GET /companies/:companyId/timeline`). Rendering is the board-locked + * Direction C (PAP-12422): dense rows, mini-map brush, custom inline SVG. + */ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { GanttChartSquare, Minus, Plus, RotateCcw } from "lucide-react"; +import { workTimelineApi, type WorkTimelineParams } from "@/api/workTimeline"; +import { queryKeys } from "@/lib/queryKeys"; +import { useCompany } from "@/context/CompanyContext"; +import { useBreadcrumbs } from "@/context/BreadcrumbContext"; +import { EmptyState } from "@/components/EmptyState"; +import { PageSkeleton } from "@/components/PageSkeleton"; +import { RequestCollapsedSidebar } from "@/components/RequestCollapsedSidebar"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + WorkTimelineChart, + clampZoomScale, + defaultZoomForWindow, + nearestZoomForScale, + type ZoomLevel, + zoomScaleForLevel, +} from "@/components/timeline/WorkTimelineChart"; +import { TIMELINE_COLORS } from "@/lib/timeline/layout"; +import { cn } from "@/lib/utils"; + +type RangePreset = "today" | "7d" | "30d" | "custom"; +interface DateRangeState { + fromDate: string; + toDate: string; +} + +function dateInputValue(date: Date): string { + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const dd = String(date.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; +} + +function presetRange(preset: Exclude, now = new Date()): DateRangeState { + const from = new Date(now); + const to = new Date(now); + if (preset === "today") { + return { fromDate: dateInputValue(from), toDate: dateInputValue(to) }; + } else { + from.setDate(from.getDate() - (preset === "7d" ? 6 : 29)); + } + return { fromDate: dateInputValue(from), toDate: dateInputValue(to) }; +} + +function rangeWindow(range: DateRangeState): Pick | null { + if (!range.fromDate || !range.toDate) return null; + const from = new Date(`${range.fromDate}T00:00:00`); + const to = new Date(`${range.toDate}T23:59:59.999`); + if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from > to) return null; + return { from: from.toISOString(), to: to.toISOString() }; +} + +function rangeError(range: DateRangeState): string | null { + if (!range.fromDate || !range.toDate) return "Choose a start and end date."; + if (!rangeWindow(range)) return "Start date must be before end date."; + return null; +} + +function Segmented({ + value, + options, + onChange, +}: { + value: T; + options: { value: T; label: string }[]; + onChange: (v: T) => void; +}) { + return ( +
+ {options.map((opt, i) => ( + + ))} +
+ ); +} + +/** Encoding key for the "Signal" timeline: colour = how each run started. */ +function TimelineLegend() { + return ( +
+ + + Delegated + + + + Automation + + + + Cancelled + + + + Now + +
+ ); +} + +export function Timeline() { + const { selectedCompanyId } = useCompany(); + const { setBreadcrumbs } = useBreadcrumbs(); + const [zoom, setZoom] = useState("day"); + const [zoomScale, setZoomScale] = useState(undefined); + const zoomTouched = useRef(false); + const [rangePreset, setRangePreset] = useState("7d"); + const [dateRange, setDateRange] = useState(() => presetRange("7d")); + + useEffect(() => { + setBreadcrumbs([{ label: "Timeline" }]); + }, [setBreadcrumbs]); + + const dateRangeError = rangeError(dateRange); + const params: WorkTimelineParams | null = useMemo(() => { + const window = rangeWindow(dateRange); + if (!window) return null; + return window; + }, [dateRange]); + + const { data, isLoading, error } = useQuery({ + queryKey: [...queryKeys.workTimeline(selectedCompanyId ?? ""), dateRange.fromDate, dateRange.toDate], + queryFn: () => workTimelineApi.get(selectedCompanyId!, params!), + enabled: !!selectedCompanyId && !!params, + }); + + useEffect(() => { + if (!data || zoomTouched.current) return; + const defaultZoom = defaultZoomForWindow(new Date(data.window.from).getTime(), new Date(data.window.to).getTime()); + setZoom(defaultZoom); + setZoomScale(undefined); + }, [data]); + + if (!selectedCompanyId) { + return ( + <> + + + + ); + } + + const header = ( +
+ +

Work Timeline

+
+ ); + + const adjustZoom = (factor: number) => { + zoomTouched.current = true; + const nextScale = clampZoomScale((zoomScale ?? zoomScaleForLevel(zoom)) * factor); + setZoomScale(nextScale); + setZoom(nearestZoomForScale(nextScale)); + }; + + const resetZoom = () => { + zoomTouched.current = true; + if (data) { + setZoom(defaultZoomForWindow(new Date(data.window.from).getTime(), new Date(data.window.to).getTime())); + } else { + setZoom("day"); + } + setZoomScale(undefined); + }; + + const toolbar = ( +
+ +
+ + + +
+
+ ); + + return ( +
+ + {header} + {toolbar} + + {isLoading && } + + {dateRangeError && ( + + )} + + {error && ( + + )} + + {data && !isLoading && !dateRangeError && ( + data.spans.length === 0 ? ( + + ) : ( +
+
+ + { + zoomTouched.current = true; + setZoomScale(nextScale); + setZoom(nextZoom); + }} + /> +
+

+ {data.spans.length} run{data.spans.length === 1 ? "" : "s"} ·{" "} + {new Date(data.window.from).toLocaleString()} → {new Date(data.window.to).toLocaleString()} + {data.window.capped ? " · window capped" : ""} +

+
+ ) + )} +
+ ); +} diff --git a/ui/storybook/fixtures/workTimeline.human.sample.json b/ui/storybook/fixtures/workTimeline.human.sample.json new file mode 100644 index 0000000000..abbedf4227 --- /dev/null +++ b/ui/storybook/fixtures/workTimeline.human.sample.json @@ -0,0 +1,1824 @@ +{ + "actors": [ + { + "id": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "type": "agent", + "name": "QA", + "avatar": "bug" + }, + { + "id": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "type": "agent", + "name": "CTO", + "avatar": "file-code" + }, + { + "id": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "type": "agent", + "name": "Senior Engineer Fable", + "avatar": "code" + }, + { + "id": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "type": "agent", + "name": "UXDesigner", + "avatar": "gem" + }, + { + "id": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "type": "user", + "name": "Dotta", + "avatar": "/api/assets/3b1e4f9b-6136-4315-8484-fd9f441aae54/content" + }, + { + "id": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "type": "agent", + "name": "CodexCoder", + "avatar": "code" + }, + { + "id": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "type": "agent", + "name": "CloudOpsEngineer", + "avatar": "cog" + }, + { + "id": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "type": "agent", + "name": "CEO", + "avatar": "crown" + }, + { + "id": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "type": "agent", + "name": "Strategic Ops", + "avatar": "target" + }, + { + "id": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "type": "agent", + "name": "Senior Planning Engineer Pro", + "avatar": "code" + }, + { + "id": "system:system", + "type": "system", + "name": "System", + "avatar": null + }, + { + "id": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", + "type": "agent", + "name": "DevRel", + "avatar": "globe" + }, + { + "id": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "type": "agent", + "name": "SecurityEngineer", + "avatar": "shield" + }, + { + "id": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "type": "agent", + "name": "ClaudeCoder", + "avatar": "code" + }, + { + "id": "agent:db1c09f1-422c-48c8-9711-4054d3657aac", + "type": "agent", + "name": "ConnectionCoder", + "avatar": "code" + } + ], + "spans": [ + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "571d1495-fe7e-4cf8-a79f-e4d8d779cecf", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "issueIdentifier": "PAP-12298", + "issueTitle": "blog page redesign", + "start": "2026-07-02T14:42:05.689Z", + "end": "2026-07-02T14:45:37.694Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "assignment", + "runId": "c7a242bb-adf1-46e2-a322-474ed791c7ff", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "issueIdentifier": "PAP-12420", + "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", + "start": "2026-07-02T14:44:59.649Z", + "end": "2026-07-02T14:48:23.151Z", + "status": "cancelled", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "automation", + "runId": "c214c9f0-b46b-47de-ba86-2520e1a8aa75", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "issueIdentifier": "PAP-12420", + "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", + "start": "2026-07-02T14:48:23.194Z", + "end": "2026-07-02T14:52:29.939Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "automation", + "runId": "6b0a55a9-7c1a-4d48-bdfb-159370e9c03d", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T14:50:35.277Z", + "end": "2026-07-02T14:55:43.520Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "laneHint": "assignment", + "runId": "e573c5dc-94b9-4b63-bdb3-c48327a85cea", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "issueIdentifier": "PAP-12421", + "issueTitle": "CTO/admin: attach corrected pages.paperclip.ing S3 policy to paperclip-page-uploader", + "start": "2026-07-02T14:52:04.426Z", + "end": "2026-07-02T14:56:08.638Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "laneHint": "assignment", + "runId": "8a716a87-61e2-4667-9cc4-3641fe42c6f0", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "issueIdentifier": "PAP-12422", + "issueTitle": "Work Timeline — Phase A: wireframe exploration (2–3 directions, board picks one)", + "start": "2026-07-02T14:52:21.788Z", + "end": "2026-07-02T15:07:44.596Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "laneHint": "assignment", + "runId": "4b085459-77b0-41e4-a62f-a76e1ba9b521", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "issueIdentifier": "PAP-12423", + "issueTitle": "Work Timeline — Phase B: timeline aggregation endpoint (GET /companies/:id/timeline)", + "start": "2026-07-02T14:52:21.929Z", + "end": "2026-07-02T15:03:54.376Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "laneHint": "assignment", + "runId": "972b6b5b-5fdd-419d-aea0-5641b4634448", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "issueIdentifier": "PAP-12427", + "issueTitle": "CEO/admin: unblock IAM mutation for paperclip-page-uploader pages policy", + "start": "2026-07-02T14:54:41.918Z", + "end": "2026-07-02T14:57:27.722Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "laneHint": "assignment", + "runId": "d14a8901-4569-4f05-b8c2-5e49d4216a6d", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "issueIdentifier": "PAP-12428", + "issueTitle": "just change the terminology to “Responsible”", + "start": "2026-07-02T14:55:00.330Z", + "end": "2026-07-02T15:01:10.197Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "laneHint": "automation", + "runId": "77ffd755-a755-41e6-b2cb-ee0817f11152", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T14:55:43.595Z", + "end": "2026-07-02T14:57:12.235Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "laneHint": "automation", + "runId": "960639c7-f6cc-45ad-b746-22ca507c237d", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T14:57:12.287Z", + "end": "2026-07-02T15:12:26.352Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "assignment", + "runId": "a61b657c-d297-43b7-948c-2be7bd161e3b", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "issueIdentifier": "PAP-12427", + "issueTitle": "CEO/admin: unblock IAM mutation for paperclip-page-uploader pages policy", + "start": "2026-07-02T14:57:27.831Z", + "end": "2026-07-02T15:01:08.081Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "laneHint": "assignment", + "runId": "67f65027-b802-4e88-91a6-bebb5c94f16b", + "issueId": "0dc1021f-2326-4977-a5b2-7eb4ab66e6df", + "issueIdentifier": "PAP-12429", + "issueTitle": "give the AWS_ keys that ClaudeCoder has to CodexCoder and CloudOpsEngineer and UXDesigner", + "start": "2026-07-02T14:57:50.711Z", + "end": "2026-07-02T15:01:18.973Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "laneHint": "assignment", + "runId": "e9ff0a09-6607-4793-90f3-1b3ab1741ef0", + "issueId": "16901797-89cc-4b85-8213-7a27d70f9d16", + "issueIdentifier": "PAP-12430", + "issueTitle": "merged Create a single PR from this branch PAP-12404-inbox-blocked-but-descendent-live-icon", + "start": "2026-07-02T14:59:42.783Z", + "end": "2026-07-02T15:17:12.936Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", + "laneHint": "assignment", + "runId": "91f67e3b-18ae-4230-8fae-18b414ffcd14", + "issueId": "1533f78d-d677-449f-afd9-4cf653168438", + "issueIdentifier": "PAP-12431", + "issueTitle": "2026-07-02 create a standup message i can copy to slack about everything i've been working on for the last 36 hours", + "start": "2026-07-02T15:00:19.704Z", + "end": "2026-07-02T15:05:16.672Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "laneHint": "assignment", + "runId": "e08e32ae-9106-46e0-9c2b-97ab74520fc4", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "issueIdentifier": "PAP-12432", + "issueTitle": "CEO/admin: apply paperclip-page-uploader pages.paperclip.ing inline policy", + "start": "2026-07-02T15:00:46.697Z", + "end": "2026-07-02T15:06:05.586Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "laneHint": "automation", + "runId": "12223c0b-0bad-4b95-be56-ceb1ce955b0f", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "issueIdentifier": "PAP-12425", + "issueTitle": "Work Timeline — security review: timeline endpoint ACLs and data exposure", + "start": "2026-07-02T15:01:28.563Z", + "end": "2026-07-02T15:05:43.985Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "laneHint": "assignment", + "runId": "33181a90-d2af-4081-9deb-253d996d81f1", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "issueIdentifier": "PAP-12433", + "issueTitle": "project star’ing", + "start": "2026-07-02T15:03:27.392Z", + "end": "2026-07-02T15:52:11.373Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "laneHint": "assignment", + "runId": "0961f6e8-a1cd-4493-9979-99775285c748", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "issueIdentifier": "PAP-12434", + "issueTitle": "Strategic Ops: get AWS admin to attach pages.paperclip.ing uploader policy", + "start": "2026-07-02T15:05:38.870Z", + "end": "2026-07-02T15:08:15.576Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "assignment", + "runId": "0ecdb797-e6b4-4f12-a5f0-b7bce348138f", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "issueIdentifier": "PAP-12435", + "issueTitle": "CloudOps: attach pages.paperclip.ing uploader IAM policy", + "start": "2026-07-02T15:07:45.699Z", + "end": "2026-07-02T15:11:13.138Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "assignment", + "runId": "bcfaef62-ca9a-4a96-b279-bf2314108dbf", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "issueIdentifier": "PAP-12403", + "issueTitle": "github and environment variables", + "start": "2026-07-02T15:09:15.958Z", + "end": "2026-07-02T15:20:22.053Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "automation", + "runId": "e4e854bc-9cce-4c5f-bfb9-a2f3c0094075", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "issueIdentifier": "PAP-12420", + "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", + "start": "2026-07-02T15:10:25.014Z", + "end": "2026-07-02T15:15:34.966Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "laneHint": "assignment", + "runId": "b83d9b78-f63c-465c-808c-c1e1175a18c5", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "issueIdentifier": "PAP-12436", + "issueTitle": "CTO/admin: grant IAM write path for paperclip-page-uploader pages policy", + "start": "2026-07-02T15:10:51.773Z", + "end": "2026-07-02T15:14:59.703Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "laneHint": "automation", + "runId": "90cd6835-be7c-4dd8-bdf7-017d40df9c38", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:12:26.440Z", + "end": "2026-07-02T15:14:56.976Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "laneHint": "assignment", + "runId": "f4d1d8a6-c74d-4d59-b831-b788eeaf93f2", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "issueIdentifier": "PAP-12437", + "issueTitle": "for parent task, cleanup the effecitvely merged ones", + "start": "2026-07-02T15:14:30.602Z", + "end": "2026-07-02T15:20:22.033Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "laneHint": "automation", + "runId": "cd8cf33f-5a1c-4b3a-905e-aabc0341edd1", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:14:57.094Z", + "end": "2026-07-02T15:16:53.870Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "laneHint": "assignment", + "runId": "a9ba6322-e0e3-43bb-bc0b-2af53bec9610", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "issueIdentifier": "PAP-12436", + "issueTitle": "CTO/admin: grant IAM write path for paperclip-page-uploader pages policy", + "start": "2026-07-02T15:14:59.782Z", + "end": "2026-07-02T15:20:10.000Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "df0d6959-ef5f-4a75-b710-492cfd6de658", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:16:53.952Z", + "end": "2026-07-02T15:19:28.111Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "assignment", + "runId": "4cec085d-bf1f-48a2-aef1-62cce353a09f", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "issueIdentifier": "PAP-12438", + "issueTitle": "environment variables editor re-design", + "start": "2026-07-02T15:18:55.386Z", + "end": "2026-07-02T15:26:14.263Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "automation", + "runId": "17e168c3-c2f4-4cef-9492-c4e680bebcf5", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:19:28.163Z", + "end": "2026-07-02T15:21:38.254Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "laneHint": "automation", + "runId": "8e58708e-88df-4681-84c5-f6a676d1dd6a", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "issueIdentifier": "PAP-10273", + "issueTitle": "Defect (Minor): preview emits noisy \"skill not present in package\" warnings for catalog-skill references", + "start": "2026-07-02T15:20:20.734Z", + "end": "2026-07-02T15:25:39.352Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "laneHint": "automation", + "runId": "accd3b96-89a2-47f4-a3db-65f94ec6be3a", + "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", + "issueIdentifier": "PAP-10274", + "issueTitle": "Defect (Minor UX): \"Partial state is not rolled back\" message is incorrect — install transaction rolled back cleanly", + "start": "2026-07-02T15:20:20.784Z", + "end": "2026-07-02T15:28:39.720Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "laneHint": "automation", + "runId": "ebc8e142-c416-4b85-86ff-be0fed98f41f", + "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", + "issueIdentifier": "PAP-10275", + "issueTitle": "Defect (Minor): CLI install lacks --adapter-override and --secret-value flags", + "start": "2026-07-02T15:20:20.854Z", + "end": "2026-07-02T15:22:56.590Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "85339e14-6ac3-459c-99a2-811e0e6adbba", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:21:38.307Z", + "end": "2026-07-02T15:22:48.667Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "automation", + "runId": "671705a1-718d-4be6-86b4-390295a17a74", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "issueIdentifier": "PAP-12405", + "issueTitle": "design a ‘gantt chart’ representation of my paperclip work", + "start": "2026-07-02T15:22:48.720Z", + "end": "2026-07-02T15:24:16.925Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "laneHint": "automation", + "runId": "cbce714c-a056-477d-971b-3ceb02a4441b", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "issueIdentifier": "PAP-12435", + "issueTitle": "CloudOps: attach pages.paperclip.ing uploader IAM policy", + "start": "2026-07-02T15:23:13.966Z", + "end": "2026-07-02T15:23:32.715Z", + "status": "cancelled", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "88e7b7a9-4ea6-40bc-b402-ffd731a3dfe2", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "issueIdentifier": "PAP-12298", + "issueTitle": "blog page redesign", + "start": "2026-07-02T15:23:28.308Z", + "end": "2026-07-02T15:29:09.108Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "laneHint": "assignment", + "runId": "1b734867-943b-4ef3-a10e-3f2c1edcad93", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "issueIdentifier": "PAP-12439", + "issueTitle": "Wireframes: Environment Variables Editor redesign", + "start": "2026-07-02T15:25:07.982Z", + "end": "2026-07-02T15:32:24.457Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "laneHint": "automation", + "runId": "2bd1ec14-ecbb-42d8-8412-106a251780b8", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "issueIdentifier": "PAP-12438", + "issueTitle": "environment variables editor re-design", + "start": "2026-07-02T15:26:14.310Z", + "end": "2026-07-02T15:34:59.661Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "laneHint": "assignment", + "runId": "9eb8e7ba-53b5-4d22-8f8d-cbba6a5c5bb5", + "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", + "issueIdentifier": "PAP-12440", + "issueTitle": "Defect (Minor): teams-catalog install is not atomic — mid-import failure can orphan skills/agents", + "start": "2026-07-02T15:27:39.339Z", + "end": "2026-07-02T15:37:04.236Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "assignment" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "laneHint": "automation", + "runId": "339694a1-6a7d-4de0-8466-43574a7c6b4d", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "issueIdentifier": "PAP-12422", + "issueTitle": "Work Timeline — Phase A: wireframe exploration (2–3 directions, board picks one)", + "start": "2026-07-02T15:28:13.886Z", + "end": "2026-07-02T15:42:27.258Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "laneHint": "automation", + "runId": "040ce2ef-acd4-47f2-bcf1-5a1194f5a450", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "issueIdentifier": "PAP-12438", + "issueTitle": "environment variables editor re-design", + "start": "2026-07-02T15:34:59.718Z", + "end": "2026-07-02T15:38:04.415Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "20f1e461-b4b6-4d5f-b897-6f60656429fb", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "issueIdentifier": "PAP-12298", + "issueTitle": "blog page redesign", + "start": "2026-07-02T15:35:56.578Z", + "end": "2026-07-02T15:38:40.543Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "laneHint": "automation", + "runId": "063a9727-b191-49e2-9802-b6b63ec65b97", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "issueIdentifier": "PAP-12424", + "issueTitle": "Work Timeline — Phase C: frontend Gantt component + Timeline page", + "start": "2026-07-02T15:41:30.586Z", + "end": "2026-07-02T16:06:58.629Z", + "status": "succeeded", + "retryOfRunId": null, + "continuationAttempt": 0, + "invocationSource": "automation" + } + ], + "events": [ + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "created", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "at": "2026-06-03T19:39:46.628Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "created", + "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", + "at": "2026-06-03T19:40:14.945Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "created", + "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", + "at": "2026-06-03T19:40:14.974Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "created", + "issueId": "5e955520-f5ea-4ad4-aef6-860c95b1797e", + "at": "2026-06-03T20:23:11.278Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "bc7a42bb-91bb-40b3-b3a8-6f9ebf98bc94", + "at": "2026-06-30T22:22:13.060Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "6ff91230-ab9d-4b95-9590-1471ae37297e", + "at": "2026-07-01T16:31:55.423Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-01T18:23:43.255Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "48264b4d-0004-4015-9ca0-08c867eed064", + "at": "2026-07-02T00:13:22.977Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "2e375459-115c-4f0c-a1c8-59e359b5c3f6", + "at": "2026-07-02T02:21:56.069Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "df84e69c-35e1-4323-a95f-e10d27b3227c", + "at": "2026-07-02T02:39:19.891Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "ad7f2c54-6e50-421f-a805-4c743948421a", + "at": "2026-07-02T03:29:27.493Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "44c2c3d2-7dc0-44df-beb6-ad2417b81503", + "at": "2026-07-02T04:45:14.251Z" + }, + { + "actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "kind": "delegated", + "issueId": "44c2c3d2-7dc0-44df-beb6-ad2417b81503", + "at": "2026-07-02T04:45:14.251Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "at": "2026-07-02T04:52:34.737Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "db6e4af1-0669-42d0-a6ef-e12dcfd7112d", + "at": "2026-07-02T04:56:04.034Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T05:07:46.810Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "created", + "issueId": "abe00e5c-bbcd-4782-ba9e-13532a09184f", + "at": "2026-07-02T05:08:53.619Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "approved", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T14:42:05.663Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "created", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:44:59.609Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "delegated", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:44:59.609Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T14:45:19.329Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "commented", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:48:06.921Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:48:23.164Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "approved", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T14:50:35.254Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "created", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:52:04.388Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "delegated", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:52:04.388Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T14:52:21.683Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T14:52:21.683Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:52:21.751Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "at": "2026-07-02T14:52:21.778Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "at": "2026-07-02T14:52:21.778Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "at": "2026-07-02T14:52:53.705Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "at": "2026-07-02T14:52:53.705Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "at": "2026-07-02T14:53:41.388Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "at": "2026-07-02T14:53:41.388Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "a466f1d9-cfb3-4fa1-a354-57d8bee78ce1", + "at": "2026-07-02T14:53:55.809Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "a466f1d9-cfb3-4fa1-a354-57d8bee78ce1", + "at": "2026-07-02T14:53:55.809Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "created", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T14:54:41.875Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "delegated", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T14:54:41.875Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T14:54:44.297Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "commented", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:54:55.966Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "at": "2026-07-02T14:55:00.287Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "delegated", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "at": "2026-07-02T14:55:00.287Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "commented", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:55:40.913Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T14:57:12.257Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T14:57:14.910Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "0dc1021f-2326-4977-a5b2-7eb4ab66e6df", + "at": "2026-07-02T14:57:50.676Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "16901797-89cc-4b85-8213-7a27d70f9d16", + "at": "2026-07-02T14:59:42.755Z" + }, + { + "actorId": "system:system", + "kind": "created", + "issueId": "1533f78d-d677-449f-afd9-4cf653168438", + "at": "2026-07-02T15:00:19.659Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "created", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:00:46.643Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "delegated", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:00:46.643Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T15:01:00.247Z" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "kind": "commented", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "at": "2026-07-02T15:01:05.759Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "0dc1021f-2326-4977-a5b2-7eb4ab66e6df", + "at": "2026-07-02T15:01:14.833Z" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "kind": "commented", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "at": "2026-07-02T15:01:28.524Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:02:00.701Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "at": "2026-07-02T15:03:27.354Z" + }, + { + "actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "kind": "commented", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "at": "2026-07-02T15:04:40.445Z" + }, + { + "actorId": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", + "kind": "commented", + "issueId": "1533f78d-d677-449f-afd9-4cf653168438", + "at": "2026-07-02T15:05:03.153Z" + }, + { + "actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "kind": "commented", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "at": "2026-07-02T15:05:38.298Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "created", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "at": "2026-07-02T15:05:38.834Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "delegated", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "at": "2026-07-02T15:05:38.834Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:05:52.785Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "approved", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T15:06:51.894Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T15:07:21.753Z" + }, + { + "actorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "kind": "created", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:07:45.644Z" + }, + { + "actorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "kind": "delegated", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:07:45.644Z" + }, + { + "actorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "kind": "commented", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "at": "2026-07-02T15:07:56.186Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "commented", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "at": "2026-07-02T15:09:15.910Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "commented", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T15:10:24.986Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "created", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:10:51.721Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "delegated", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:10:51.721Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:11:04.865Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T15:11:14.738Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:12:26.380Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "at": "2026-07-02T15:14:30.565Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "at": "2026-07-02T15:14:30.565Z" + }, + { + "actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:14:43.831Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "commented", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:14:44.435Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T15:15:02.505Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:15:56.982Z" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:16:35.238Z" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "kind": "commented", + "issueId": "16901797-89cc-4b85-8213-7a27d70f9d16", + "at": "2026-07-02T15:17:07.032Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "created", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:18:55.353Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "approved", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "at": "2026-07-02T15:19:05.154Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:19:11.188Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "at": "2026-07-02T15:19:25.678Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "approved", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:19:39.277Z" + }, + { + "actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "kind": "commented", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:19:55.477Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:20:10.329Z" + }, + { + "actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "kind": "commented", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "at": "2026-07-02T15:20:17.139Z" + }, + { + "actorId": "system:system", + "kind": "commented", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "at": "2026-07-02T15:20:20.709Z" + }, + { + "actorId": "system:system", + "kind": "commented", + "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", + "at": "2026-07-02T15:20:20.751Z" + }, + { + "actorId": "system:system", + "kind": "commented", + "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", + "at": "2026-07-02T15:20:20.803Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:21:26.500Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:22:40.012Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "commented", + "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", + "at": "2026-07-02T15:22:40.511Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T15:24:07.505Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "commented", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "at": "2026-07-02T15:24:36.094Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "created", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "at": "2026-07-02T15:25:07.938Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "delegated", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "at": "2026-07-02T15:25:07.938Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:25:26.349Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "commented", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "at": "2026-07-02T15:25:28.318Z" + }, + { + "actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "kind": "commented", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:25:56.773Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "created", + "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", + "at": "2026-07-02T15:27:39.300Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T15:28:01.540Z" + }, + { + "actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "kind": "commented", + "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", + "at": "2026-07-02T15:28:02.346Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "commented", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T15:28:13.852Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "commented", + "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", + "at": "2026-07-02T15:28:52.824Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "at": "2026-07-02T15:30:55.654Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:34:59.686Z" + }, + { + "actorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "kind": "approved", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T15:35:56.554Z" + }, + { + "actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "kind": "commented", + "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", + "at": "2026-07-02T15:36:48.753Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "approved", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:37:11.074Z" + }, + { + "actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "kind": "commented", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:37:24.105Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "approved", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T15:38:21.704Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-02T15:38:31.695Z" + }, + { + "actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "kind": "commented", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T15:41:22.766Z" + }, + { + "actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "kind": "approved", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "at": "2026-07-02T15:43:04.428Z" + }, + { + "actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "kind": "commented", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "at": "2026-07-02T15:47:28.798Z" + }, + { + "actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "kind": "commented", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "at": "2026-07-02T15:48:25.833Z" + } + ], + "edges": [ + { + "fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", + "at": "2026-06-03T19:39:46.628Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", + "at": "2026-06-03T19:40:14.945Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", + "at": "2026-06-03T19:40:14.974Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "5e955520-f5ea-4ad4-aef6-860c95b1797e", + "at": "2026-06-03T20:23:11.278Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "issueId": "bc7a42bb-91bb-40b3-b3a8-6f9ebf98bc94", + "at": "2026-06-30T22:22:13.060Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:db1c09f1-422c-48c8-9711-4054d3657aac", + "issueId": "6ff91230-ab9d-4b95-9590-1471ae37297e", + "at": "2026-07-01T16:31:55.423Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", + "at": "2026-07-01T18:23:43.255Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "48264b4d-0004-4015-9ca0-08c867eed064", + "at": "2026-07-02T00:13:22.977Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "2e375459-115c-4f0c-a1c8-59e359b5c3f6", + "at": "2026-07-02T02:21:56.069Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "df84e69c-35e1-4323-a95f-e10d27b3227c", + "at": "2026-07-02T02:39:19.891Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "ad7f2c54-6e50-421f-a805-4c743948421a", + "at": "2026-07-02T03:29:27.493Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "44c2c3d2-7dc0-44df-beb6-ad2417b81503", + "at": "2026-07-02T04:45:14.251Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "44c2c3d2-7dc0-44df-beb6-ad2417b81503", + "at": "2026-07-02T04:45:14.251Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", + "at": "2026-07-02T04:52:34.737Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "db6e4af1-0669-42d0-a6ef-e12dcfd7112d", + "at": "2026-07-02T04:56:04.034Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "828497e6-087d-4336-b627-331e51380ad4", + "at": "2026-07-02T05:07:46.810Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "abe00e5c-bbcd-4782-ba9e-13532a09184f", + "at": "2026-07-02T05:08:53.619Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:44:59.609Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", + "at": "2026-07-02T14:44:59.609Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:52:04.388Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", + "at": "2026-07-02T14:52:04.388Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T14:52:21.683Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", + "at": "2026-07-02T14:52:21.683Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "at": "2026-07-02T14:52:21.778Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", + "at": "2026-07-02T14:52:21.778Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "at": "2026-07-02T14:52:53.705Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", + "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", + "at": "2026-07-02T14:52:53.705Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "at": "2026-07-02T14:53:41.388Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", + "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", + "at": "2026-07-02T14:53:41.388Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "issueId": "a466f1d9-cfb3-4fa1-a354-57d8bee78ce1", + "at": "2026-07-02T14:53:55.809Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "issueId": "a466f1d9-cfb3-4fa1-a354-57d8bee78ce1", + "at": "2026-07-02T14:53:55.809Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T14:54:41.875Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", + "at": "2026-07-02T14:54:41.875Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "at": "2026-07-02T14:55:00.287Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", + "at": "2026-07-02T14:55:00.287Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "0dc1021f-2326-4977-a5b2-7eb4ab66e6df", + "at": "2026-07-02T14:57:50.676Z", + "kind": "assignment" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "16901797-89cc-4b85-8213-7a27d70f9d16", + "at": "2026-07-02T14:59:42.755Z", + "kind": "assignment" + }, + { + "fromActorId": "system:system", + "toActorId": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", + "issueId": "1533f78d-d677-449f-afd9-4cf653168438", + "at": "2026-07-02T15:00:19.659Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:00:46.643Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "0facd02e-425f-4822-875c-2f97988de98a", + "at": "2026-07-02T15:00:46.643Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", + "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", + "at": "2026-07-02T15:03:27.354Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "toActorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "at": "2026-07-02T15:05:38.834Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "toActorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", + "at": "2026-07-02T15:05:38.834Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:07:45.644Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", + "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", + "at": "2026-07-02T15:07:45.644Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:10:51.721Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", + "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", + "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", + "at": "2026-07-02T15:10:51.721Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "at": "2026-07-02T15:14:30.565Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", + "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", + "at": "2026-07-02T15:14:30.565Z", + "kind": "delegation" + }, + { + "fromActorId": "user:1gQsX9Jbj5jk1Wu51YU1G0atxCRwj1Om", + "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "issueId": "e0189c44-1245-4526-9423-1128862d7c95", + "at": "2026-07-02T15:18:55.353Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "at": "2026-07-02T15:25:07.938Z", + "kind": "assignment" + }, + { + "fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", + "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", + "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", + "at": "2026-07-02T15:25:07.938Z", + "kind": "delegation" + }, + { + "fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", + "toActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", + "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", + "at": "2026-07-02T15:27:39.300Z", + "kind": "assignment" + } + ], + "pagination": { + "limit": 500, + "offset": 0, + "totalIssues": 37, + "hasMore": false + }, + "window": { + "from": "2026-07-02T14:00:00.000Z", + "to": "2026-07-02T16:00:00.000Z", + "capped": false + } +} \ No newline at end of file diff --git a/ui/storybook/fixtures/workTimeline.sample.json b/ui/storybook/fixtures/workTimeline.sample.json new file mode 100644 index 0000000000..29a8ada09d --- /dev/null +++ b/ui/storybook/fixtures/workTimeline.sample.json @@ -0,0 +1 @@ +{"actors": [{"id": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "type": "agent", "name": "CEO", "avatar": "crown"}, {"id": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "type": "agent", "name": "ClaudeCoder", "avatar": "code"}, {"id": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "type": "agent", "name": "CloudOpsEngineer", "avatar": "cog"}, {"id": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "type": "agent", "name": "UXDesigner", "avatar": "gem"}, {"id": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", "type": "agent", "name": "CTO", "avatar": "file-code"}, {"id": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "type": "agent", "name": "Senior Engineer Fable", "avatar": "code"}, {"id": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", "type": "agent", "name": "Senior Planning Engineer Pro", "avatar": "code"}, {"id": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", "type": "agent", "name": "DevRel", "avatar": "globe"}, {"id": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "type": "agent", "name": "CodexCoder", "avatar": "code"}, {"id": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "type": "agent", "name": "QA", "avatar": "bug"}, {"id": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", "type": "agent", "name": "SecurityEngineer", "avatar": "shield"}, {"id": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", "type": "agent", "name": "Strategic Ops", "avatar": "target"}], "spans": [{"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "063a9727-b191-49e2-9802-b6b63ec65b97", "issueId": "3c1b19ae-9064-4d78-8bf4-c3983a7730d6", "issueIdentifier": "PAP-12424", "issueTitle": "Work Timeline \u2014 Phase C: frontend Gantt component + Timeline page", "start": "2026-07-02T15:41:30.586Z", "end": null, "status": "running", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "20f1e461-b4b6-4d5f-b897-6f60656429fb", "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", "issueIdentifier": "PAP-12298", "issueTitle": "blog page redesign", "start": "2026-07-02T15:35:56.578Z", "end": "2026-07-02T15:38:40.543Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "automation", "runId": "040ce2ef-acd4-47f2-bcf1-5a1194f5a450", "issueId": "e0189c44-1245-4526-9423-1128862d7c95", "issueIdentifier": "PAP-12438", "issueTitle": "environment variables editor re-design", "start": "2026-07-02T15:34:59.718Z", "end": "2026-07-02T15:38:04.415Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "laneHint": "automation", "runId": "339694a1-6a7d-4de0-8466-43574a7c6b4d", "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", "issueIdentifier": "PAP-12422", "issueTitle": "Work Timeline \u2014 Phase A: wireframe exploration (2\u20133 directions, board picks one)", "start": "2026-07-02T15:28:13.886Z", "end": "2026-07-02T15:42:27.258Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", "laneHint": "assignment", "runId": "9eb8e7ba-53b5-4d22-8f8d-cbba6a5c5bb5", "issueId": "9ae0377a-8040-4221-9376-4ae64270e3b4", "issueIdentifier": "PAP-12440", "issueTitle": "Defect (Minor): teams-catalog install is not atomic \u2014 mid-import failure can orphan skills/agents", "start": "2026-07-02T15:27:39.339Z", "end": "2026-07-02T15:37:04.236Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "laneHint": "automation", "runId": "2bd1ec14-ecbb-42d8-8412-106a251780b8", "issueId": "e0189c44-1245-4526-9423-1128862d7c95", "issueIdentifier": "PAP-12438", "issueTitle": "environment variables editor re-design", "start": "2026-07-02T15:26:14.310Z", "end": "2026-07-02T15:34:59.661Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "laneHint": "assignment", "runId": "1b734867-943b-4ef3-a10e-3f2c1edcad93", "issueId": "f5b13e28-cbe4-40e5-bd29-d88eb8ea0ee3", "issueIdentifier": "PAP-12439", "issueTitle": "Wireframes: Environment Variables Editor redesign", "start": "2026-07-02T15:25:07.982Z", "end": "2026-07-02T15:32:24.457Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "88e7b7a9-4ea6-40bc-b402-ffd731a3dfe2", "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", "issueIdentifier": "PAP-12298", "issueTitle": "blog page redesign", "start": "2026-07-02T15:23:28.308Z", "end": "2026-07-02T15:29:09.108Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "automation", "runId": "cbce714c-a056-477d-971b-3ceb02a4441b", "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", "issueIdentifier": "PAP-12435", "issueTitle": "CloudOps: attach pages.paperclip.ing uploader IAM policy", "start": "2026-07-02T15:23:13.966Z", "end": "2026-07-02T15:23:32.715Z", "status": "cancelled", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "automation", "runId": "671705a1-718d-4be6-86b4-390295a17a74", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:22:48.720Z", "end": "2026-07-02T15:24:16.925Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "85339e14-6ac3-459c-99a2-811e0e6adbba", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:21:38.307Z", "end": "2026-07-02T15:22:48.667Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "laneHint": "automation", "runId": "ebc8e142-c416-4b85-86ff-be0fed98f41f", "issueId": "153322a9-301d-4868-b15e-a24063ceeca2", "issueIdentifier": "PAP-10275", "issueTitle": "Defect (Minor): CLI install lacks --adapter-override and --secret-value flags", "start": "2026-07-02T15:20:20.854Z", "end": "2026-07-02T15:22:56.590Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "laneHint": "automation", "runId": "accd3b96-89a2-47f4-a3db-65f94ec6be3a", "issueId": "6f7ac707-363a-4e77-b3d0-7479165352db", "issueIdentifier": "PAP-10274", "issueTitle": "Defect (Minor UX): \"Partial state is not rolled back\" message is incorrect \u2014 install transaction rolled back cleanly", "start": "2026-07-02T15:20:20.784Z", "end": "2026-07-02T15:28:39.720Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "laneHint": "automation", "runId": "8e58708e-88df-4681-84c5-f6a676d1dd6a", "issueId": "e3afbdcf-98b8-4971-a094-9a92d78736d1", "issueIdentifier": "PAP-10273", "issueTitle": "Defect (Minor): preview emits noisy \"skill not present in package\" warnings for catalog-skill references", "start": "2026-07-02T15:20:20.734Z", "end": "2026-07-02T15:25:39.352Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "automation", "runId": "17e168c3-c2f4-4cef-9492-c4e680bebcf5", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:19:28.163Z", "end": "2026-07-02T15:21:38.254Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "assignment", "runId": "4cec085d-bf1f-48a2-aef1-62cce353a09f", "issueId": "e0189c44-1245-4526-9423-1128862d7c95", "issueIdentifier": "PAP-12438", "issueTitle": "environment variables editor re-design", "start": "2026-07-02T15:18:55.386Z", "end": "2026-07-02T15:26:14.263Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "df0d6959-ef5f-4a75-b710-492cfd6de658", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:16:53.952Z", "end": "2026-07-02T15:19:28.111Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "laneHint": "assignment", "runId": "a9ba6322-e0e3-43bb-bc0b-2af53bec9610", "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", "issueIdentifier": "PAP-12436", "issueTitle": "CTO/admin: grant IAM write path for paperclip-page-uploader pages policy", "start": "2026-07-02T15:14:59.782Z", "end": "2026-07-02T15:20:10.000Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "laneHint": "automation", "runId": "cd8cf33f-5a1c-4b3a-905e-aabc0341edd1", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:14:57.094Z", "end": "2026-07-02T15:16:53.870Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "laneHint": "assignment", "runId": "f4d1d8a6-c74d-4d59-b831-b788eeaf93f2", "issueId": "57a438c1-876a-40ce-9b2b-5c8a8b47ec5b", "issueIdentifier": "PAP-12437", "issueTitle": "for parent task, cleanup the effecitvely merged ones", "start": "2026-07-02T15:14:30.602Z", "end": "2026-07-02T15:20:22.033Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", "laneHint": "automation", "runId": "90cd6835-be7c-4dd8-bdf7-017d40df9c38", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T15:12:26.440Z", "end": "2026-07-02T15:14:56.976Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", "laneHint": "assignment", "runId": "b83d9b78-f63c-465c-808c-c1e1175a18c5", "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", "issueIdentifier": "PAP-12436", "issueTitle": "CTO/admin: grant IAM write path for paperclip-page-uploader pages policy", "start": "2026-07-02T15:10:51.773Z", "end": "2026-07-02T15:14:59.703Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "automation", "runId": "e4e854bc-9cce-4c5f-bfb9-a2f3c0094075", "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", "issueIdentifier": "PAP-12420", "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", "start": "2026-07-02T15:10:25.014Z", "end": "2026-07-02T15:15:34.966Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "assignment", "runId": "bcfaef62-ca9a-4a96-b279-bf2314108dbf", "issueId": "40490e19-4e4a-4fa3-8300-719a82d3633a", "issueIdentifier": "PAP-12403", "issueTitle": "github and environment variables", "start": "2026-07-02T15:09:15.958Z", "end": "2026-07-02T15:20:22.053Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "assignment", "runId": "0ecdb797-e6b4-4f12-a5f0-b7bce348138f", "issueId": "804fff38-a46b-4391-9c44-cd76fd889753", "issueIdentifier": "PAP-12435", "issueTitle": "CloudOps: attach pages.paperclip.ing uploader IAM policy", "start": "2026-07-02T15:07:45.699Z", "end": "2026-07-02T15:11:13.138Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:ef2d1ee5-5c42-42fc-8197-5e2386ac34ed", "laneHint": "assignment", "runId": "0961f6e8-a1cd-4493-9979-99775285c748", "issueId": "7751d27f-c016-4be5-a332-b29b888e8b48", "issueIdentifier": "PAP-12434", "issueTitle": "Strategic Ops: get AWS admin to attach pages.paperclip.ing uploader policy", "start": "2026-07-02T15:05:38.870Z", "end": "2026-07-02T15:08:15.576Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:b8bc4df6-599c-4228-9969-6b041e7c88f8", "laneHint": "assignment", "runId": "33181a90-d2af-4081-9deb-253d996d81f1", "issueId": "09b1e135-e25c-4fb5-a812-48dfd15edd55", "issueIdentifier": "PAP-12433", "issueTitle": "project star\u2019ing", "start": "2026-07-02T15:03:27.392Z", "end": "2026-07-02T15:52:11.373Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", "laneHint": "automation", "runId": "12223c0b-0bad-4b95-be56-ceb1ce955b0f", "issueId": "d6327ba4-5bed-4bad-b860-213256cf127a", "issueIdentifier": "PAP-12425", "issueTitle": "Work Timeline \u2014 security review: timeline endpoint ACLs and data exposure", "start": "2026-07-02T15:01:28.563Z", "end": "2026-07-02T15:05:43.985Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "laneHint": "assignment", "runId": "e08e32ae-9106-46e0-9c2b-97ab74520fc4", "issueId": "0facd02e-425f-4822-875c-2f97988de98a", "issueIdentifier": "PAP-12432", "issueTitle": "CEO/admin: apply paperclip-page-uploader pages.paperclip.ing inline policy", "start": "2026-07-02T15:00:46.697Z", "end": "2026-07-02T15:06:05.586Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:c9dcefd1-b052-489a-a1f3-4cd1783bcb5f", "laneHint": "assignment", "runId": "91f67e3b-18ae-4230-8fae-18b414ffcd14", "issueId": "1533f78d-d677-449f-afd9-4cf653168438", "issueIdentifier": "PAP-12431", "issueTitle": "2026-07-02 create a standup message i can copy to slack about everything i've been working on for the last 36 hours", "start": "2026-07-02T15:00:19.704Z", "end": "2026-07-02T15:05:16.672Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "laneHint": "assignment", "runId": "e9ff0a09-6607-4793-90f3-1b3ab1741ef0", "issueId": "16901797-89cc-4b85-8213-7a27d70f9d16", "issueIdentifier": "PAP-12430", "issueTitle": "merged Create a single PR from this branch PAP-12404-inbox-blocked-but-descendent-live-icon", "start": "2026-07-02T14:59:42.783Z", "end": "2026-07-02T15:17:12.936Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "laneHint": "assignment", "runId": "67f65027-b802-4e88-91a6-bebb5c94f16b", "issueId": "0dc1021f-2326-4977-a5b2-7eb4ab66e6df", "issueIdentifier": "PAP-12429", "issueTitle": "give the AWS_ keys that ClaudeCoder has to CodexCoder and CloudOpsEngineer and UXDesigner", "start": "2026-07-02T14:57:50.711Z", "end": "2026-07-02T15:01:18.973Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "assignment", "runId": "a61b657c-d297-43b7-948c-2be7bd161e3b", "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", "issueIdentifier": "PAP-12427", "issueTitle": "CEO/admin: unblock IAM mutation for paperclip-page-uploader pages policy", "start": "2026-07-02T14:57:27.831Z", "end": "2026-07-02T15:01:08.081Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "laneHint": "automation", "runId": "960639c7-f6cc-45ad-b746-22ca507c237d", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T14:57:12.287Z", "end": "2026-07-02T15:12:26.352Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "laneHint": "automation", "runId": "77ffd755-a755-41e6-b2cb-ee0817f11152", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T14:55:43.595Z", "end": "2026-07-02T14:57:12.235Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "laneHint": "assignment", "runId": "d14a8901-4569-4f05-b8c2-5e49d4216a6d", "issueId": "6d12933d-1393-484c-aeb8-17d98e61b8c0", "issueIdentifier": "PAP-12428", "issueTitle": "just change the terminology to \u201cResponsible\u201d", "start": "2026-07-02T14:55:00.330Z", "end": "2026-07-02T15:01:10.197Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "laneHint": "assignment", "runId": "972b6b5b-5fdd-419d-aea0-5641b4634448", "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", "issueIdentifier": "PAP-12427", "issueTitle": "CEO/admin: unblock IAM mutation for paperclip-page-uploader pages policy", "start": "2026-07-02T14:54:41.918Z", "end": "2026-07-02T14:57:27.722Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "laneHint": "assignment", "runId": "4b085459-77b0-41e4-a62f-a76e1ba9b521", "issueId": "e64a9b35-54e8-430c-9244-18f60c70f87a", "issueIdentifier": "PAP-12423", "issueTitle": "Work Timeline \u2014 Phase B: timeline aggregation endpoint (GET /companies/:id/timeline)", "start": "2026-07-02T14:52:21.929Z", "end": "2026-07-02T15:03:54.376Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "laneHint": "assignment", "runId": "8a716a87-61e2-4667-9cc4-3641fe42c6f0", "issueId": "b868097a-4b8a-447d-a667-a96ebd24e385", "issueIdentifier": "PAP-12422", "issueTitle": "Work Timeline \u2014 Phase A: wireframe exploration (2\u20133 directions, board picks one)", "start": "2026-07-02T14:52:21.788Z", "end": "2026-07-02T15:07:44.596Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", "laneHint": "assignment", "runId": "e573c5dc-94b9-4b63-bdb3-c48327a85cea", "issueId": "9c5003e8-104d-46ae-9389-c62cb6fc8889", "issueIdentifier": "PAP-12421", "issueTitle": "CTO/admin: attach corrected pages.paperclip.ing S3 policy to paperclip-page-uploader", "start": "2026-07-02T14:52:04.426Z", "end": "2026-07-02T14:56:08.638Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "laneHint": "automation", "runId": "6b0a55a9-7c1a-4d48-bdfb-159370e9c03d", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "issueIdentifier": "PAP-12405", "issueTitle": "design a \u2018gantt chart\u2019 representation of my paperclip work", "start": "2026-07-02T14:50:35.277Z", "end": "2026-07-02T14:55:43.520Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "automation", "runId": "c214c9f0-b46b-47de-ba86-2520e1a8aa75", "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", "issueIdentifier": "PAP-12420", "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", "start": "2026-07-02T14:48:23.194Z", "end": "2026-07-02T14:52:29.939Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}, {"actorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "laneHint": "assignment", "runId": "c7a242bb-adf1-46e2-a322-474ed791c7ff", "issueId": "b5a59535-38f5-4f0d-a0bf-2bf755a4be15", "issueIdentifier": "PAP-12420", "issueTitle": "Attach S3 policy to paperclip-page-uploader IAM user for pages.paperclip.ing", "start": "2026-07-02T14:44:59.649Z", "end": "2026-07-02T14:48:23.151Z", "status": "cancelled", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "assignment"}, {"actorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "laneHint": "automation", "runId": "571d1495-fe7e-4cf8-a79f-e4d8d779cecf", "issueId": "1ae0d728-2f4e-4ec9-ac95-d006a7f19741", "issueIdentifier": "PAP-12298", "issueTitle": "blog page redesign", "start": "2026-07-02T14:42:05.689Z", "end": "2026-07-02T14:45:37.694Z", "status": "succeeded", "retryOfRunId": null, "continuationAttempt": 0, "invocationSource": "automation"}], "events": [], "edges": [{"fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "issueId": "e0189c44-1245-4526-9423-1128862d7c95", "at": "2026-07-02T15:26:14.310Z", "kind": "delegation"}, {"fromActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "issueId": "e0189c44-1245-4526-9423-1128862d7c95", "at": "2026-07-02T15:34:59.718Z", "kind": "delegation"}, {"fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "toActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T14:55:43.595Z", "kind": "delegation"}, {"fromActorId": "agent:eae27ade-c807-4723-97e8-23808ad4fa14", "toActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T14:57:12.287Z", "kind": "delegation"}, {"fromActorId": "agent:6670e11b-91d3-4429-82e0-436b88b51808", "toActorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:12:26.440Z", "kind": "delegation"}, {"fromActorId": "agent:ece64c0e-767e-4d37-a15e-193694daa1d8", "toActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:14:57.094Z", "kind": "delegation"}, {"fromActorId": "agent:eab6f1c7-5950-410e-8eed-7d074426165d", "toActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:16:53.952Z", "kind": "delegation"}, {"fromActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:19:28.163Z", "kind": "delegation"}, {"fromActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "toActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:21:38.307Z", "kind": "delegation"}, {"fromActorId": "agent:3108ef8e-5ed0-41d9-b561-6b41c41b8545", "toActorId": "agent:a9f9a532-690a-45c2-8da7-64789e7cbb06", "issueId": "828497e6-087d-4336-b627-331e51380ad4", "at": "2026-07-02T15:22:48.720Z", "kind": "delegation"}, {"fromActorId": "agent:66b3c071-6cb8-4424-b833-9d9b6318de0b", "toActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "issueId": "91292c04-bbfe-4a94-9779-baceb3941857", "at": "2026-07-02T15:14:59.782Z", "kind": "delegation"}, {"fromActorId": "agent:244c0c2c-8416-43b6-84c9-ec183c074cc1", "toActorId": "agent:352aa64b-2a22-4d00-928f-71f5fb884f66", "issueId": "5a3cf258-ae30-44ba-9b57-494eaf6c0238", "at": "2026-07-02T14:57:27.831Z", "kind": "delegation"}], "pagination": {"limit": 200, "offset": 0, "totalIssues": 26, "hasMore": false}, "window": {"from": "2026-07-02T14:00:00.000Z", "to": "2026-07-02T15:50:00.000Z", "capped": false}} \ No newline at end of file diff --git a/ui/storybook/stories/work-timeline.stories.tsx b/ui/storybook/stories/work-timeline.stories.tsx new file mode 100644 index 0000000000..f717f39d6f --- /dev/null +++ b/ui/storybook/stories/work-timeline.stories.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { WorkTimelineResult } from "@paperclipai/shared"; +import { Minus, Plus, RotateCcw } from "lucide-react"; +import { + WorkTimelineChart, + clampZoomScale, + nearestZoomForScale, + type ZoomLevel, + zoomScaleForLevel, +} from "@/components/timeline/WorkTimelineChart"; +import { Button } from "@/components/ui/button"; +import sampleJson from "../fixtures/workTimeline.sample.json"; +import humanSampleJson from "../fixtures/workTimeline.human.sample.json"; + +const sample = sampleJson as unknown as WorkTimelineResult; +// A second real slice (2026-07-02 14:00–16:00Z) captured straight from the live +// `/timeline` endpoint that DOES carry human events — Dotta's created / commented / +// approved / delegated actions provide human participation and kickoff context. +const humanSample = humanSampleJson as unknown as WorkTimelineResult; +// The fixture is a real slice of PAP company activity (2026-07-02 14:00–15:50Z); +// pin "now" to the window end so in-progress runs fade correctly. +const NOW = new Date("2026-07-02T15:45:00.000Z").getTime(); + +function TimelineHarness({ + initialZoom = "day" as ZoomLevel, + data = sample, + now = NOW, +}: { + initialZoom?: ZoomLevel; + data?: WorkTimelineResult; + now?: number; +}) { + const [zoom, setZoom] = useState(initialZoom); + const [zoomScale, setZoomScale] = useState(undefined); + + const adjustZoom = (factor: number) => { + const nextScale = clampZoomScale((zoomScale ?? zoomScaleForLevel(zoom)) * factor); + setZoomScale(nextScale); + setZoom(nearestZoomForScale(nextScale)); + }; + + const resetZoom = () => { + setZoom(initialZoom); + setZoomScale(undefined); + }; + + return ( +
+
+
+

Work Timeline

+
+ +
+ + + +
+ +
+
+ { + setZoomScale(nextScale); + setZoom(nextZoom); + }} + /> +
+

+ {data.spans.length} runs · {data.actors.length} actors · {data.events.length} human/instant events · real + company data +

+
+
+
+ ); +} + +const meta: Meta = { + title: "Pages/Work Timeline", + component: TimelineHarness, + parameters: { layout: "fullscreen" }, +}; +export default meta; + +type Story = StoryObj; + +export const HourZoom: Story = { args: { initialZoom: "hour" } }; +export const DayZoom: Story = { args: { initialZoom: "day" } }; +// Live slice that carries human-originated activity and delegation context. +export const WithHumanActivity: Story = { + args: { + initialZoom: "hour", + data: humanSample, + now: new Date("2026-07-02T16:00:00.000Z").getTime(), + }, +};