2026-03-28 09:46:34 -05:00
|
|
|
import type { Issue, IssueComment } from "@paperclipai/shared";
|
|
|
|
|
|
|
|
|
|
export interface IssueCommentReassignment {
|
|
|
|
|
assigneeAgentId: string | null;
|
|
|
|
|
assigneeUserId: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface OptimisticIssueComment extends IssueComment {
|
|
|
|
|
clientId: string;
|
2026-03-28 11:25:25 -05:00
|
|
|
clientStatus: "pending" | "queued";
|
|
|
|
|
queueTargetRunId?: string | null;
|
2026-03-28 09:46:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type IssueTimelineComment = IssueComment | OptimisticIssueComment;
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed 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 runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
export type LocallyQueuedIssueComment<T extends IssueComment> = T & {
|
|
|
|
|
clientStatus: "queued";
|
|
|
|
|
queueState: "queued";
|
|
|
|
|
queueTargetRunId: string;
|
|
|
|
|
};
|
2026-03-28 09:46:34 -05:00
|
|
|
|
|
|
|
|
function toTimestamp(value: Date | string) {
|
|
|
|
|
return new Date(value).getTime();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:04:46 -05:00
|
|
|
function createOptimisticCommentId() {
|
|
|
|
|
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
|
|
|
if (randomUuid) {
|
|
|
|
|
return `optimistic-${randomUuid}`;
|
|
|
|
|
}
|
|
|
|
|
return `optimistic-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 09:46:34 -05:00
|
|
|
export function sortIssueComments<T extends { createdAt: Date | string; id: string }>(comments: T[]) {
|
|
|
|
|
return [...comments].sort((a, b) => {
|
|
|
|
|
const createdAtDiff = toTimestamp(a.createdAt) - toTimestamp(b.createdAt);
|
|
|
|
|
if (createdAtDiff !== 0) return createdAtDiff;
|
|
|
|
|
return a.id.localeCompare(b.id);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 17:22:52 -05:00
|
|
|
function sortIssueCommentsDesc<T extends { createdAt: Date | string; id: string }>(comments: T[]) {
|
|
|
|
|
return sortIssueComments(comments).reverse();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 09:46:34 -05:00
|
|
|
export function createOptimisticIssueComment(params: {
|
|
|
|
|
companyId: string;
|
|
|
|
|
issueId: string;
|
|
|
|
|
body: string;
|
|
|
|
|
authorUserId: string | null;
|
2026-03-28 11:25:25 -05:00
|
|
|
clientStatus?: OptimisticIssueComment["clientStatus"];
|
|
|
|
|
queueTargetRunId?: string | null;
|
2026-03-28 09:46:34 -05:00
|
|
|
}): OptimisticIssueComment {
|
|
|
|
|
const now = new Date();
|
2026-03-28 10:04:46 -05:00
|
|
|
const clientId = createOptimisticCommentId();
|
2026-03-28 09:46:34 -05:00
|
|
|
return {
|
|
|
|
|
id: clientId,
|
|
|
|
|
clientId,
|
|
|
|
|
companyId: params.companyId,
|
|
|
|
|
issueId: params.issueId,
|
|
|
|
|
authorAgentId: null,
|
|
|
|
|
authorUserId: params.authorUserId,
|
|
|
|
|
body: params.body,
|
2026-03-28 11:25:25 -05:00
|
|
|
clientStatus: params.clientStatus ?? "pending",
|
|
|
|
|
queueTargetRunId: params.queueTargetRunId ?? null,
|
2026-03-28 09:46:34 -05:00
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 11:25:25 -05:00
|
|
|
export function isQueuedIssueComment(params: {
|
2026-04-06 08:27:53 -05:00
|
|
|
comment: Pick<IssueTimelineComment, "createdAt"> &
|
|
|
|
|
Partial<Pick<OptimisticIssueComment, "clientStatus">> & {
|
|
|
|
|
authorAgentId?: string | null;
|
|
|
|
|
};
|
2026-03-28 11:25:25 -05:00
|
|
|
activeRunStartedAt?: Date | string | null;
|
2026-04-06 08:27:53 -05:00
|
|
|
activeRunAgentId?: string | null;
|
2026-03-28 11:25:25 -05:00
|
|
|
runId?: string | null;
|
|
|
|
|
interruptedRunId?: string | null;
|
|
|
|
|
}) {
|
|
|
|
|
if (params.runId) return false;
|
|
|
|
|
if (params.interruptedRunId) return false;
|
2026-04-06 08:27:53 -05:00
|
|
|
if (params.comment.authorAgentId && params.activeRunAgentId && params.comment.authorAgentId === params.activeRunAgentId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-28 11:25:25 -05:00
|
|
|
if (params.comment.clientStatus === "queued") return true;
|
|
|
|
|
if (!params.activeRunStartedAt) return false;
|
|
|
|
|
return toTimestamp(params.comment.createdAt) >= toTimestamp(params.activeRunStartedAt);
|
|
|
|
|
}
|
|
|
|
|
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed 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 runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
export function applyLocalQueuedIssueCommentState<T extends IssueComment>(
|
|
|
|
|
comment: T,
|
|
|
|
|
params: {
|
|
|
|
|
queuedTargetRunId?: string | null;
|
|
|
|
|
hasLiveRuns: boolean;
|
|
|
|
|
runningRunId?: string | null;
|
|
|
|
|
},
|
|
|
|
|
): T | LocallyQueuedIssueComment<T> {
|
|
|
|
|
const queuedTargetRunId = params.queuedTargetRunId ?? null;
|
|
|
|
|
if (!queuedTargetRunId || !params.hasLiveRuns) return comment;
|
|
|
|
|
if (params.runningRunId && params.runningRunId !== queuedTargetRunId) return comment;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...comment,
|
|
|
|
|
clientStatus: "queued",
|
|
|
|
|
queueState: "queued",
|
|
|
|
|
queueTargetRunId: queuedTargetRunId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 09:46:34 -05:00
|
|
|
export function mergeIssueComments(
|
|
|
|
|
comments: IssueComment[] | undefined,
|
|
|
|
|
optimisticComments: OptimisticIssueComment[],
|
|
|
|
|
): IssueTimelineComment[] {
|
|
|
|
|
const merged = [...(comments ?? [])];
|
|
|
|
|
const existingIds = new Set(merged.map((comment) => comment.id));
|
|
|
|
|
for (const comment of optimisticComments) {
|
|
|
|
|
if (!existingIds.has(comment.id)) {
|
|
|
|
|
merged.push(comment);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sortIssueComments(merged);
|
|
|
|
|
}
|
|
|
|
|
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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>
2026-04-14 12:50:48 -05:00
|
|
|
export function takeOptimisticIssueComment(
|
|
|
|
|
comments: OptimisticIssueComment[],
|
|
|
|
|
clientId: string,
|
|
|
|
|
): { comments: OptimisticIssueComment[]; comment: OptimisticIssueComment | null } {
|
|
|
|
|
const index = comments.findIndex((comment) => comment.clientId === clientId);
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
return { comments, comment: null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
comments: comments.filter((comment) => comment.clientId !== clientId),
|
|
|
|
|
comment: comments[index] ?? null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 17:22:52 -05:00
|
|
|
export function flattenIssueCommentPages(
|
|
|
|
|
pages: ReadonlyArray<ReadonlyArray<IssueComment>> | undefined,
|
|
|
|
|
): IssueComment[] {
|
|
|
|
|
return sortIssueComments((pages ?? []).flatMap((page) => page));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:12:43 -05:00
|
|
|
export function getNextIssueCommentPageParam(
|
|
|
|
|
lastPage: ReadonlyArray<IssueComment> | undefined,
|
|
|
|
|
pageSize: number,
|
|
|
|
|
): string | undefined {
|
|
|
|
|
if (!lastPage || lastPage.length < pageSize) return undefined;
|
|
|
|
|
return lastPage[lastPage.length - 1]?.id;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 09:46:34 -05:00
|
|
|
export function upsertIssueComment(
|
|
|
|
|
comments: IssueComment[] | undefined,
|
|
|
|
|
nextComment: IssueComment,
|
|
|
|
|
): IssueComment[] {
|
|
|
|
|
const current = comments ?? [];
|
|
|
|
|
const existingIndex = current.findIndex((comment) => comment.id === nextComment.id);
|
|
|
|
|
if (existingIndex === -1) {
|
|
|
|
|
return sortIssueComments([...current, nextComment]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updated = [...current];
|
|
|
|
|
updated[existingIndex] = nextComment;
|
|
|
|
|
return sortIssueComments(updated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyOptimisticIssueCommentUpdate(
|
|
|
|
|
issue: Issue | undefined,
|
|
|
|
|
params: {
|
|
|
|
|
reopen?: boolean;
|
|
|
|
|
reassignment?: IssueCommentReassignment;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
if (!issue) return issue;
|
|
|
|
|
const nextIssue: Issue = { ...issue };
|
|
|
|
|
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed 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 runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
if (params.reopen === true && (issue.status === "done" || issue.status === "cancelled" || issue.status === "blocked")) {
|
2026-03-28 09:46:34 -05:00
|
|
|
nextIssue.status = "todo";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (params.reassignment) {
|
|
|
|
|
nextIssue.assigneeAgentId = params.reassignment.assigneeAgentId;
|
|
|
|
|
nextIssue.assigneeUserId = params.reassignment.assigneeUserId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nextIssue;
|
|
|
|
|
}
|
2026-04-07 18:11:41 -05:00
|
|
|
|
|
|
|
|
export function applyOptimisticIssueFieldUpdate(
|
|
|
|
|
issue: Issue | undefined,
|
|
|
|
|
data: Record<string, unknown>,
|
|
|
|
|
) {
|
|
|
|
|
if (!issue) return issue;
|
|
|
|
|
|
|
|
|
|
const nextIssue: Issue = {
|
|
|
|
|
...issue,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
};
|
|
|
|
|
const hasOwn = (key: string) => Object.prototype.hasOwnProperty.call(data, key);
|
|
|
|
|
const assign = <K extends keyof Issue>(key: K) => {
|
|
|
|
|
if (hasOwn(key)) {
|
|
|
|
|
nextIssue[key] = data[key] as Issue[K];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assign("status");
|
|
|
|
|
assign("priority");
|
|
|
|
|
assign("assigneeAgentId");
|
|
|
|
|
assign("assigneeUserId");
|
|
|
|
|
assign("projectId");
|
2026-04-10 22:26:21 -05:00
|
|
|
assign("parentId");
|
2026-04-07 18:11:41 -05:00
|
|
|
assign("projectWorkspaceId");
|
|
|
|
|
assign("executionWorkspaceId");
|
|
|
|
|
assign("executionWorkspacePreference");
|
|
|
|
|
assign("executionWorkspaceSettings");
|
|
|
|
|
assign("hiddenAt");
|
|
|
|
|
|
|
|
|
|
if (hasOwn("labelIds") && Array.isArray(data.labelIds)) {
|
|
|
|
|
const nextLabelIds = data.labelIds.filter((value): value is string => typeof value === "string");
|
|
|
|
|
nextIssue.labelIds = nextLabelIds;
|
|
|
|
|
if (issue.labels) {
|
|
|
|
|
nextIssue.labels = issue.labels.filter((label) => nextLabelIds.includes(label.id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasOwn("blockedByIssueIds") && Array.isArray(data.blockedByIssueIds) && issue.blockedBy) {
|
|
|
|
|
const nextBlockedByIds = new Set(
|
|
|
|
|
data.blockedByIssueIds.filter((value): value is string => typeof value === "string"),
|
|
|
|
|
);
|
|
|
|
|
nextIssue.blockedBy = issue.blockedBy.filter((relation) => nextBlockedByIds.has(relation.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasOwn("projectId")) {
|
|
|
|
|
nextIssue.project = issue.project?.id === nextIssue.projectId ? issue.project : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
if (hasOwn("parentId")) {
|
|
|
|
|
nextIssue.ancestors = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 18:11:41 -05:00
|
|
|
if (hasOwn("executionWorkspaceId")) {
|
|
|
|
|
nextIssue.currentExecutionWorkspace =
|
|
|
|
|
issue.currentExecutionWorkspace?.id === nextIssue.executionWorkspaceId
|
|
|
|
|
? issue.currentExecutionWorkspace
|
|
|
|
|
: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nextIssue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function matchesIssueRef(
|
|
|
|
|
issue: Pick<Issue, "id" | "identifier">,
|
|
|
|
|
refs: Iterable<string>,
|
|
|
|
|
) {
|
|
|
|
|
const refSet = refs instanceof Set ? refs : new Set(refs);
|
|
|
|
|
return refSet.has(issue.id) || (!!issue.identifier && refSet.has(issue.identifier));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyOptimisticIssueFieldUpdateToCollection(
|
|
|
|
|
issues: Issue[] | undefined,
|
|
|
|
|
refs: Iterable<string>,
|
|
|
|
|
data: Record<string, unknown>,
|
|
|
|
|
) {
|
|
|
|
|
if (!issues) return issues;
|
|
|
|
|
|
|
|
|
|
let changed = false;
|
|
|
|
|
const nextIssues = issues.map((issue) => {
|
|
|
|
|
if (!matchesIssueRef(issue, refs)) return issue;
|
|
|
|
|
changed = true;
|
|
|
|
|
return applyOptimisticIssueFieldUpdate(issue, data) ?? issue;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return changed ? nextIssues : issues;
|
|
|
|
|
}
|
2026-04-08 17:22:52 -05:00
|
|
|
|
|
|
|
|
export function upsertIssueCommentInPages(
|
|
|
|
|
pages: ReadonlyArray<ReadonlyArray<IssueComment>> | undefined,
|
|
|
|
|
nextComment: IssueComment,
|
|
|
|
|
): IssueComment[][] {
|
|
|
|
|
if (!pages || pages.length === 0) {
|
|
|
|
|
return [[nextComment]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextPages = pages.map((page) => [...page]);
|
|
|
|
|
for (let pageIndex = 0; pageIndex < nextPages.length; pageIndex += 1) {
|
|
|
|
|
const existingIndex = nextPages[pageIndex]!.findIndex((comment) => comment.id === nextComment.id);
|
|
|
|
|
if (existingIndex === -1) continue;
|
|
|
|
|
nextPages[pageIndex]![existingIndex] = nextComment;
|
|
|
|
|
nextPages[pageIndex] = sortIssueCommentsDesc(nextPages[pageIndex]!);
|
|
|
|
|
return nextPages;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextPages[0] = sortIssueCommentsDesc([...nextPages[0]!, nextComment]);
|
|
|
|
|
return nextPages;
|
|
|
|
|
}
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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>
2026-04-14 12:50:48 -05:00
|
|
|
|
|
|
|
|
export function removeIssueCommentFromPages(
|
|
|
|
|
pages: ReadonlyArray<ReadonlyArray<IssueComment>> | undefined,
|
|
|
|
|
commentId: string,
|
|
|
|
|
): IssueComment[][] {
|
|
|
|
|
if (!pages || pages.length === 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pages
|
|
|
|
|
.map((page) => page.filter((comment) => comment.id !== commentId))
|
|
|
|
|
.filter((page) => page.length > 0);
|
|
|
|
|
}
|