mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The task thread lets an operator add guidance while an agent run is active > - A new message can wait behind that active run as a queued message > - The classic task view lets the operator interrupt the target run from that queued message > - The redesigned task view did not expose the same action > - This pull request restores the action and keeps it bound to the exact target run > - The benefit is that operators can apply urgent guidance without switching task views ## Linked Issues or Issue Description **What happened?** The redesigned task view showed `Queued` for a queued operator message, but it did not show the existing interrupt action. **Expected behavior** The queued message must show `Interrupt` next to `Queued`. The action must stop the exact run that the message is waiting behind. **Steps to reproduce** 1. Open a task in the redesigned task view while an agent run is active. 2. Send a new operator message so it enters the queued state. 3. Observe that the queued message has no interrupt action. **Paperclip version or commit** `bc0b5a1642` **Deployment mode** All deployment modes that use the redesigned task view. ## What Changed - Preserve persisted queued state and the target run ID in the redesigned thread model. - Render a token-compliant `Interrupt` action beside the queued state. - Reuse the existing exact-run interrupt callback and show a disabled `Interrupting…` state during the request. - Keep an assigned queue target immutable so an in-flight comment cannot rebind its interrupt action to a replacement run. - Add regression tests for persisted queued messages, replacement-run races, and the in-progress action state. - No documentation update was required because this restores existing behavior. ## Verification - `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx` - `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx ui/src/components/task-chat/task-chat-adapter.test.ts ui/src/pages/IssueDetail.test.tsx -t 'queued message actions|queues messages against a queued live run and interrupts that exact run|commentsToTaskChatItems'` - `pnpm exec vitest run ui/src/pages/IssueDetail.test.tsx -t 'queued message|queues messages'` - `pnpm exec vitest run ui/src/pages/IssueDetail.test.tsx` (52 passed) - `pnpm -r typecheck` - `pnpm test:run` (all server and UI groups passed; the CLI group passed after inherited static AWS credential variables were omitted) - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm exec vitest run cli/src/__tests__/secrets.test.ts` - `pnpm build` - `pnpm check:token-gates` ## Risks Low risk. The change only adds an action to queued messages that have a target run and an interrupt callback. Messages without both values keep the current rendering. > 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 with GPT-5. The runtime does not expose a more specific deployment ID or context-window size. The model used reasoning, repository tools, code execution, and test execution. ## 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 <noreply@paperclip.ing>
107 lines
4.4 KiB
TypeScript
107 lines
4.4 KiB
TypeScript
/**
|
|
* Live adapter: normalize the existing IssueChatComment stream (including
|
|
* optimistic echoes) into the redesign's TaskChatItem[] model. This is the
|
|
* seam that lets the new render layer show live task data without touching the
|
|
* comment data pipeline. Rich agent-run streaming (thinking/tool/diff) is
|
|
* demonstrated in the harness and layered onto this adapter as a later step;
|
|
* the baseline live thread renders the author-typed message history + optimistic
|
|
* echo, which is the core legibility win.
|
|
*/
|
|
import type { Agent } from "@paperclipai/shared";
|
|
import type { IssueChatComment } from "@/lib/issue-chat-messages";
|
|
import { resolveCommentAttribution } from "@/lib/comment-attribution";
|
|
import type { TaskChatAuthorKind, TaskChatItem } from "./task-chat-model";
|
|
|
|
export interface TaskChatAdapterContext {
|
|
agentMap?: Map<string, Agent>;
|
|
userLabelMap?: ReadonlyMap<string, string> | null;
|
|
currentUserId?: string | null;
|
|
/**
|
|
* Task's current assignee. Agent comments from anyone else are cross-issue
|
|
* writes and get a "for {user}" attribution chip (the open cross-task write design (attribution)).
|
|
*/
|
|
issueAssigneeAgentId?: string | null;
|
|
}
|
|
|
|
function effectiveAgentId(comment: IssueChatComment): string | null {
|
|
return comment.authorAgentId ?? comment.derivedAuthorAgentId ?? null;
|
|
}
|
|
|
|
function authorKind(comment: IssueChatComment): TaskChatAuthorKind {
|
|
// System authorship wins over any derivable run→agent linkage (PAP-443):
|
|
// recovery notices carry a derivedAuthorAgentId but must not render as
|
|
// agent bubbles.
|
|
if (comment.authorType === "system") return "system";
|
|
if (effectiveAgentId(comment)) return "agent";
|
|
if (comment.authorType === "user") return "human";
|
|
return "agent";
|
|
}
|
|
|
|
/** Shared bubble-footer time format ("2:34 PM") — also used by the description bubble (PAP-375). */
|
|
export function formatTaskChatTimestamp(value: unknown): string | undefined {
|
|
if (!value) return undefined;
|
|
const d = value instanceof Date ? value : new Date(value as string);
|
|
if (Number.isNaN(d.getTime())) return undefined;
|
|
return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
}
|
|
|
|
export function commentsToTaskChatItems(
|
|
comments: IssueChatComment[],
|
|
ctx: TaskChatAdapterContext = {},
|
|
): TaskChatItem[] {
|
|
const items: TaskChatItem[] = [];
|
|
for (const comment of comments) {
|
|
if (comment.deletedAt) continue;
|
|
const kind = authorKind(comment);
|
|
let authorName: string | undefined;
|
|
let agentIcon: string | null | undefined;
|
|
let onBehalfOfUserName: string | undefined;
|
|
if (kind === "agent") {
|
|
const agentId = effectiveAgentId(comment);
|
|
authorName = (agentId && ctx.agentMap?.get(agentId)?.name) || "Agent";
|
|
agentIcon = agentId ? ctx.agentMap?.get(agentId)?.icon : undefined;
|
|
onBehalfOfUserName = resolveCommentAttribution({
|
|
authorAgentId: agentId,
|
|
onBehalfOfUserId: comment.onBehalfOfUserId ?? null,
|
|
issueAssigneeAgentId: ctx.issueAssigneeAgentId,
|
|
resolveUserLabel: (userId) => ctx.userLabelMap?.get(userId),
|
|
})?.userName;
|
|
} else if (kind === "human") {
|
|
authorName =
|
|
(comment.authorUserId && ctx.userLabelMap?.get(comment.authorUserId)) || undefined;
|
|
}
|
|
const queued = comment.queueState === "queued" || comment.clientStatus === "queued";
|
|
const optimistic =
|
|
queued
|
|
? "queued"
|
|
: comment.clientStatus === "pending"
|
|
? "pending"
|
|
: undefined;
|
|
const createdAtIso =
|
|
comment.createdAt instanceof Date
|
|
? comment.createdAt.toISOString()
|
|
: comment.createdAt
|
|
? String(comment.createdAt)
|
|
: undefined;
|
|
items.push({
|
|
id: comment.id || comment.clientId || `${comment.createdAt}`,
|
|
kind: "message",
|
|
author: kind,
|
|
authorName,
|
|
text: comment.body,
|
|
timestamp: formatTaskChatTimestamp(comment.createdAt),
|
|
optimistic,
|
|
queueTargetRunId: queued ? comment.queueTargetRunId ?? null : null,
|
|
agentIcon,
|
|
onBehalfOfUserName,
|
|
// System notices carry their structured hints through to the render
|
|
// layer (PAP-443); other authors keep the item lean.
|
|
presentation: kind === "system" ? comment.presentation ?? null : undefined,
|
|
metadata: kind === "system" ? comment.metadata ?? null : undefined,
|
|
runAgentId: kind === "system" ? comment.runAgentId ?? null : undefined,
|
|
createdAtIso: kind === "system" ? createdAtIso : undefined,
|
|
});
|
|
}
|
|
return items;
|
|
}
|