mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 03:30:39 +09:00
[codex] Improve issue thread review flow (#4381)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Issue detail is where operators coordinate review, approvals, and follow-up work with active runs > - That thread UI needs to surface blockers, descendants, review handoffs, and reply ergonomics clearly enough for humans to guide agent work > - Several small gaps in the issue-thread flow were making review and navigation clunkier than necessary > - This pull request improves the reply composer, descendant/blocker presentation, interaction folding, and review-request handoff plumbing together as one cohesive issue-thread workflow slice > - The benefit is a cleaner operator review loop without changing the broader task model ## What Changed - restored and refined the floating reply composer behavior in the issue thread - folded expired confirmation interactions and improved post-submit thread scrolling behavior - surfaced descendant issue context and inline blocker/paused-assignee notices on the issue detail view - tightened large-board first paint behavior in `IssuesList` - added loose review-request handoffs through the issue execution-policy/update path and covered them with tests ## Verification - `pnpm vitest run ui/src/pages/IssueDetail.test.tsx` - `pnpm vitest run server/src/__tests__/issues-service.test.ts server/src/__tests__/issue-execution-policy.test.ts` - `pnpm exec vitest run --project @paperclipai/ui ui/src/components/IssueChatThread.test.tsx ui/src/components/IssueProperties.test.tsx ui/src/components/IssuesList.test.tsx ui/src/lib/issue-tree.test.ts ui/src/api/issues.test.ts` - `pnpm exec vitest run --project @paperclipai/adapter-utils packages/adapter-utils/src/server-utils.test.ts` - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/issue-comment-reopen-routes.test.ts -t "coerces executor handoff patches into workflow-controlled review wakes|wakes the return assignee with execution_changes_requested"` - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/issue-execution-policy.test.ts server/src/__tests__/issues-service.test.ts` ## Visual Evidence - UI layout changes are covered by the focused issue-thread component and issue-detail tests listed above. Browser screenshots were not attachable from this automated greploop environment, so reviewers should use the running preview for final visual confirmation. ## Risks - Moderate UI-flow risk: these changes touch the issue detail experience in multiple spots, so regressions would most likely show up as thread-layout quirks or incorrect review-handoff 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 GPT-5-based coding agent with tool use and code execution in the Codex CLI environment ## 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 or documented the visual verification path - [ ] 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>
This commit is contained in:
parent
35a9dc37b0
commit
7ad225a198
25 changed files with 1046 additions and 44 deletions
|
|
@ -22,6 +22,8 @@ const mockIssuesApi = vi.hoisted(() => ({
|
|||
listLabels: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockKanbanBoard = vi.hoisted(() => vi.fn());
|
||||
|
||||
const mockAuthApi = vi.hoisted(() => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
|
|
@ -87,7 +89,16 @@ vi.mock("./IssueRow", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("./KanbanBoard", () => ({
|
||||
KanbanBoard: () => null,
|
||||
KanbanBoard: (props: { issues: Issue[] }) => {
|
||||
mockKanbanBoard(props);
|
||||
return (
|
||||
<div data-testid="kanban-board">
|
||||
{props.issues.map((issue) => (
|
||||
<span key={issue.id}>{issue.title}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -189,6 +200,7 @@ describe("IssuesList", () => {
|
|||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
dialogState.openNewIssue.mockReset();
|
||||
mockKanbanBoard.mockReset();
|
||||
mockIssuesApi.list.mockReset();
|
||||
mockIssuesApi.listLabels.mockReset();
|
||||
mockAuthApi.getSession.mockReset();
|
||||
|
|
@ -404,6 +416,113 @@ describe("IssuesList", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("loads board issues with a separate result limit for each status column", async () => {
|
||||
localStorage.setItem(
|
||||
"paperclip:test-issues:company-1",
|
||||
JSON.stringify({ viewMode: "board" }),
|
||||
);
|
||||
|
||||
const parentIssue = createIssue({
|
||||
id: "issue-parent-total-limit",
|
||||
title: "Parent total-limited issue",
|
||||
status: "todo",
|
||||
});
|
||||
const backlogIssue = createIssue({
|
||||
id: "issue-backlog",
|
||||
title: "Backlog column issue",
|
||||
status: "backlog",
|
||||
});
|
||||
const doneIssue = createIssue({
|
||||
id: "issue-done",
|
||||
title: "Done column issue",
|
||||
status: "done",
|
||||
});
|
||||
|
||||
mockIssuesApi.list.mockImplementation((_companyId, filters) => {
|
||||
if (filters?.status === "backlog") return Promise.resolve([backlogIssue]);
|
||||
if (filters?.status === "done") return Promise.resolve([doneIssue]);
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const { root } = renderWithQueryClient(
|
||||
<IssuesList
|
||||
issues={[parentIssue]}
|
||||
agents={[]}
|
||||
projects={[]}
|
||||
viewStateKey="paperclip:test-issues"
|
||||
enableRoutineVisibilityFilter
|
||||
onUpdateIssue={() => undefined}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
status: "backlog",
|
||||
limit: 200,
|
||||
includeRoutineExecutions: true,
|
||||
}));
|
||||
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
status: "done",
|
||||
limit: 200,
|
||||
includeRoutineExecutions: true,
|
||||
}));
|
||||
expect(mockKanbanBoard).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "issue-backlog" }),
|
||||
expect.objectContaining({ id: "issue-done" }),
|
||||
]),
|
||||
}));
|
||||
expect(container.textContent).toContain("Backlog column issue");
|
||||
expect(container.textContent).toContain("Done column issue");
|
||||
expect(container.textContent).not.toContain("Parent total-limited issue");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a refinement hint when a board column hits its server cap", async () => {
|
||||
localStorage.setItem(
|
||||
"paperclip:test-issues:company-1",
|
||||
JSON.stringify({ viewMode: "board" }),
|
||||
);
|
||||
|
||||
const cappedBacklogIssues = Array.from({ length: 200 }, (_, index) =>
|
||||
createIssue({
|
||||
id: `issue-backlog-${index + 1}`,
|
||||
identifier: `PAP-${index + 1}`,
|
||||
title: `Backlog issue ${index + 1}`,
|
||||
status: "backlog",
|
||||
}),
|
||||
);
|
||||
|
||||
mockIssuesApi.list.mockImplementation((_companyId, filters) => {
|
||||
if (filters?.status === "backlog") return Promise.resolve(cappedBacklogIssues);
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const { root } = renderWithQueryClient(
|
||||
<IssuesList
|
||||
issues={[]}
|
||||
agents={[]}
|
||||
projects={[]}
|
||||
viewStateKey="paperclip:test-issues"
|
||||
onUpdateIssue={() => undefined}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(container.textContent).toContain("Some board columns are showing up to 200 issues. Refine filters or search to reveal the rest.");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("caps the first paint for large issue lists", async () => {
|
||||
const manyIssues = Array.from({ length: 220 }, (_, index) =>
|
||||
createIssue({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue