mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Operators spend most of their day scanning skills, routines, inbox groups, and activity cards > - Several small UI rough edges made those surfaces harder to scan or easier to crash on real API payloads > - These fixes are grouped together because they are low-risk operator quality-of-life improvements rather than separate control-plane contracts > - This pull request polishes skills metadata, routine run-now access, grouped issue creation defaults, monitor activity rendering, and activity row identity layout > - The benefit is a smoother board workflow with fewer small interruptions while keeping the change set compact ## What Changed - Improves company skill source display and the used-by agent list. - Truncates long skill source paths and adds a copy affordance. - Adds a row-level run-now button to the routines table. - Adds grouped issue creation defaults for inbox issue groups and aligns grouped add buttons to the right. - Fixes `IssueMonitorActivityCard` when `monitorNextCheckAt` arrives as an ISO string. - Polishes activity row actor avatar/name layout by using the shared avatar primitive. ## Verification - `pnpm run preflight:workspace-links && pnpm exec vitest run ui/src/pages/Routines.test.tsx ui/src/components/IssuesList.test.tsx ui/src/lib/inbox.test.ts ui/src/components/IssueMonitorActivityCard.test.tsx` — 91 passed. - The routines test emitted the pre-existing Radix warning about missing `DialogTitle`/description in dialog content; tests still passed. - Pairwise merge checks against the other two PR branches reported no textual conflicts. ## Risks - Low: changes are UI-focused and covered by targeted component/lib tests. - Low-to-medium: activity row layout changes could affect dense feed scanability; the implementation uses the shared avatar component and keeps truncation behavior. > 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 Codex coding agent, GPT-5 model family (`gpt-5`), tool-enabled Paperclip heartbeat environment. Context window and internal reasoning mode are not exposed by the 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
235 lines
6.7 KiB
TypeScript
235 lines
6.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import type { Issue } from "@paperclipai/shared";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { IssueMonitorActivityCard } from "./IssueMonitorActivityCard";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
function createIssue(overrides: Partial<Issue> = {}): Issue {
|
|
return {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
projectId: null,
|
|
projectWorkspaceId: null,
|
|
goalId: null,
|
|
parentId: null,
|
|
title: "Watch deploy",
|
|
description: null,
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: "agent-1",
|
|
assigneeUserId: null,
|
|
checkoutRunId: null,
|
|
executionRunId: null,
|
|
executionAgentNameKey: null,
|
|
executionLockedAt: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: "local-board",
|
|
issueNumber: 1,
|
|
identifier: "PAP-1",
|
|
requestDepth: 0,
|
|
billingCode: null,
|
|
assigneeAdapterOverrides: null,
|
|
executionPolicy: {
|
|
mode: "normal",
|
|
commentRequired: true,
|
|
stages: [],
|
|
monitor: {
|
|
nextCheckAt: "2026-04-11T12:30:00.000Z",
|
|
notes: "Check deployment health",
|
|
scheduledBy: "board",
|
|
},
|
|
},
|
|
executionState: {
|
|
status: "idle",
|
|
currentStageId: null,
|
|
currentStageIndex: null,
|
|
currentStageType: null,
|
|
currentParticipant: null,
|
|
returnAssignee: null,
|
|
reviewRequest: null,
|
|
completedStageIds: [],
|
|
lastDecisionId: null,
|
|
lastDecisionOutcome: null,
|
|
monitor: {
|
|
status: "scheduled",
|
|
nextCheckAt: "2026-04-11T12:30:00.000Z",
|
|
lastTriggeredAt: null,
|
|
attemptCount: 0,
|
|
notes: "Check deployment health",
|
|
scheduledBy: "board",
|
|
clearedAt: null,
|
|
clearReason: null,
|
|
},
|
|
},
|
|
monitorNextCheckAt: new Date("2026-04-11T12:30:00.000Z"),
|
|
monitorLastTriggeredAt: null,
|
|
monitorAttemptCount: 0,
|
|
monitorNotes: "Check deployment health",
|
|
monitorScheduledBy: "board",
|
|
executionWorkspaceId: null,
|
|
executionWorkspacePreference: null,
|
|
executionWorkspaceSettings: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
cancelledAt: null,
|
|
hiddenAt: null,
|
|
createdAt: new Date("2026-04-11T10:00:00.000Z"),
|
|
updatedAt: new Date("2026-04-11T10:00:00.000Z"),
|
|
...overrides,
|
|
workMode: overrides.workMode ?? "standard",
|
|
};
|
|
}
|
|
|
|
describe("IssueMonitorActivityCard", () => {
|
|
let container: HTMLDivElement;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-04-11T12:00:00.000Z"));
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
container.remove();
|
|
});
|
|
|
|
it("renders the scheduled monitor details and check-now action", () => {
|
|
const onCheckNow = vi.fn();
|
|
const root = createRoot(container);
|
|
|
|
act(() => {
|
|
root.render(<IssueMonitorActivityCard issue={createIssue()} onCheckNow={onCheckNow} />);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Monitor scheduled");
|
|
expect(container.textContent).toContain("Next check");
|
|
expect(container.textContent).toContain("in 30m");
|
|
expect(container.textContent).toContain("Check deployment health");
|
|
|
|
const button = Array.from(container.querySelectorAll("button")).find((candidate) =>
|
|
candidate.textContent?.includes("Check now"),
|
|
);
|
|
expect(button).toBeTruthy();
|
|
|
|
act(() => {
|
|
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
|
|
expect(onCheckNow).toHaveBeenCalledTimes(1);
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it("does not render external references from monitor metadata", () => {
|
|
const root = createRoot(container);
|
|
|
|
act(() => {
|
|
root.render(
|
|
<IssueMonitorActivityCard
|
|
issue={createIssue({
|
|
executionPolicy: {
|
|
mode: "normal",
|
|
commentRequired: true,
|
|
stages: [],
|
|
monitor: {
|
|
nextCheckAt: "2026-04-11T12:30:00.000Z",
|
|
notes: "Check deployment health",
|
|
scheduledBy: "board",
|
|
serviceName: "Deploy provider",
|
|
externalRef: "https://provider.example/deploy/123?token=secret",
|
|
},
|
|
},
|
|
})}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Deploy provider");
|
|
expect(container.textContent).not.toContain("provider.example");
|
|
expect(container.textContent).not.toContain("token=secret");
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it("renders without throwing when monitorNextCheckAt arrives as an ISO string", () => {
|
|
const root = createRoot(container);
|
|
|
|
act(() => {
|
|
root.render(
|
|
<IssueMonitorActivityCard
|
|
issue={createIssue({
|
|
monitorNextCheckAt: "2026-04-11T12:30:00.000Z" as unknown as Date,
|
|
executionPolicy: {
|
|
mode: "normal",
|
|
commentRequired: true,
|
|
stages: [],
|
|
},
|
|
executionState: {
|
|
status: "idle",
|
|
currentStageId: null,
|
|
currentStageIndex: null,
|
|
currentStageType: null,
|
|
currentParticipant: null,
|
|
returnAssignee: null,
|
|
reviewRequest: null,
|
|
completedStageIds: [],
|
|
lastDecisionId: null,
|
|
lastDecisionOutcome: null,
|
|
monitor: null,
|
|
},
|
|
})}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Monitor scheduled");
|
|
expect(container.textContent).toContain("Next check");
|
|
expect(container.textContent).toContain("in 30m");
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it("renders nothing when the issue has no scheduled monitor", () => {
|
|
const root = createRoot(container);
|
|
|
|
act(() => {
|
|
root.render(
|
|
<IssueMonitorActivityCard
|
|
issue={createIssue({
|
|
executionPolicy: {
|
|
mode: "normal",
|
|
commentRequired: true,
|
|
stages: [],
|
|
},
|
|
executionState: {
|
|
status: "idle",
|
|
currentStageId: null,
|
|
currentStageIndex: null,
|
|
currentStageType: null,
|
|
currentParticipant: null,
|
|
returnAssignee: null,
|
|
reviewRequest: null,
|
|
completedStageIds: [],
|
|
lastDecisionId: null,
|
|
lastDecisionOutcome: null,
|
|
monitor: null,
|
|
},
|
|
monitorNextCheckAt: null,
|
|
monitorNotes: null,
|
|
})}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
expect(container.textContent).toBe("");
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
});
|