[codex] Polish operator UI work state details (#9320)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Operators use the issue list, blocked-work notices, pipeline body
documents, and issue documents to understand what agents are doing
> - Some of those surfaces had low-information details: live-work
blockers were visually easy to miss, revision history showed generic
actor labels, and selected active subtasks carried an extra accent
border
> - These details matter because Paperclip's default UI should make
agent state and work history legible without requiring users to inspect
raw logs
> - This pull request groups a small set of related operator UI polish
changes for those work-state and document-history surfaces
> - The benefit is clearer review context for blocked work, better
attribution in document revision history, and cleaner active-subtask row
styling

## Linked Issues or Issue Description

No public GitHub issue found during dedup search.

### Bug Report

**Pre-submission checklist**
- Searched existing open PRs/issues for `document revision authors` and
`blocked notice live work`; no duplicates found.
- Reproduced against the current `master` base for this PR.
- Confirmed this is core board UI behavior, not adapter/provider/local
configuration.

**What happened?**
Blocked live-work notices, document revision history, pipeline body
document revision history, and active subtask rows exposed technically
correct but low-signal UI details. Revision menus could show generic
`Board`/`Agent` labels instead of the specific author, and selected
active subtasks had an extra accent border.

**Expected behavior**
Operators should see clear blocked-live-work copy, recognizable revision
authors using available agent/user profile data, and visually consistent
selected task rows.

**Steps to reproduce**
1. Open an issue with a live-work blocker and inspect the blocked
notice.
2. Open an issue document revision menu with agent/user-authored
revisions.
3. Open a pipeline item body document revision menu with
agent/user-authored revisions.
4. Inspect a selected active subtask row in the issue list.

**Paperclip version or commit**
`8b6a06ee2` (`origin/master` at branch creation).

**Deployment mode**
Local dev / self-hosted board UI.

**Installation method**
Built from source.

**Agent adapter(s) involved**
Not adapter-specific (core board UI).

**Database mode**
Not database-related.

**Access context**
Board operator UI.

**Relevant logs or output**
Not applicable.

**Relevant config**
Not applicable.

**Additional context**
This PR is a small polish/fix contribution. `ROADMAP.md` allows tightly
scoped bugs and polish without roadmap-level coordination.

**Privacy checklist**
Reviewed the PR body for private instance references and omitted
internal ticket links.

## What Changed

- Polished the live-work blocked notice copy and tests so operators get
a clearer explanation of what is blocking progress.
- Show issue document revision authors with agent/user names and avatar
treatment instead of generic actor labels.
- Show pipeline body document revision authors with the same available
agent/user profile data.
- Removed the extra left accent border from selected active subtask
rows.
- Stabilized the document revision test helper for React runtimes where
`act` is not exported as a function.

## Verification

- `git diff --check origin/master..HEAD`
- `git diff --name-only origin/master..HEAD -- .github/workflows
pnpm-lock.yaml` produced no output.
- `pnpm check:token-gates` passed: all color literal, arbitrary bracket
value, and raw font-size gates clean.
- `pnpm exec vitest run ui/src/components/IssueBlockedNotice.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx
ui/src/components/IssueRow.test.tsx` passed: 3 test files, 32 tests.
- `pnpm --filter @paperclipai/ui typecheck` passed.
- Remote PR checks passed on head `3e18fe3f`: 22 complete, 0 pending, 0
failing.
- Greptile completed at 5/5 with no blocking issues after addressing the
pipeline revision author feedback.

## Risks

Low risk. The change is limited to board UI rendering and component
tests. The main risk is a subtle visual regression in issue/document
surfaces that are not covered by screenshots; the affected component
tests cover the intended copy, attribution, and row-style 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, tool-enabled CLI session.
Exact runtime context window is not exposed by this execution
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 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>
This commit is contained in:
Dotta 2026-07-09 17:36:53 -05:00 committed by GitHub
parent 4856558fd9
commit 9acce52aa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 317 additions and 35 deletions

View File

@ -3,6 +3,7 @@ import { ChevronDown, ChevronRight } from "lucide-react";
import { cn, relativeTime } from "../lib/utils";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@ -12,12 +13,21 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AgentIcon } from "./AgentIconPicker";
import { deriveInitials } from "./Identity";
export type DocumentFrameHeaderRevisionActor = {
kind: "agent" | "user" | "system";
name: string;
agentIcon?: string | null;
imageUrl?: string | null;
};
export type DocumentFrameHeaderRevision = {
id: string;
revisionNumber: number;
createdAt: string | Date;
actorLabel: string;
actor: DocumentFrameHeaderRevisionActor;
};
export type DocumentFrameHeaderRevisionMenu = {
@ -46,6 +56,23 @@ export interface DocumentFrameHeaderProps {
actionsSlot?: ReactNode;
}
function RevisionActorAvatar({ actor }: { actor: DocumentFrameHeaderRevisionActor }) {
return (
<Avatar size="xs" shape={actor.kind === "agent" ? "square" : "circle"} className="shrink-0">
{actor.kind === "agent" ? (
<AvatarFallback>
<AgentIcon icon={actor.agentIcon} className="h-3 w-3" />
</AvatarFallback>
) : (
<>
{actor.imageUrl ? <AvatarImage src={actor.imageUrl} alt={actor.name} /> : null}
<AvatarFallback>{deriveInitials(actor.name)}</AvatarFallback>
</>
)}
</Avatar>
);
}
export function DocumentFrameHeader({
documentKey,
documentLabel,
@ -124,9 +151,12 @@ export function DocumentFrameHeader({
</Badge>
) : null}
</div>
<span className="text-xs text-muted-foreground">
{relativeTime(revision.createdAt)} • {revision.actorLabel}
</span>
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-(length:--text-micro) text-muted-foreground">
<RevisionActorAvatar actor={revision.actor} />
<span className="truncate">
{relativeTime(revision.createdAt)} • {revision.actor.name}
</span>
</div>
</div>
</DropdownMenuRadioItem>
);

View File

@ -333,6 +333,70 @@ describe("IssueBlockedNotice", () => {
expect(stepLinks[0]).toContain("TASK-9");
expect(stepLinks[1]).toContain("TASK-10");
expect(stepLinks[2]).toContain("TASK-11");
const runningStep = node.querySelectorAll('[data-testid="issue-blocked-notice-steps"] a')[2];
if (!runningStep) throw new Error("Expected a running live-work step.");
expect(runningStep.querySelector('svg[aria-label="In Progress status"]')).not.toBeNull();
expect(node.querySelector('[data-testid="issue-blocked-notice-now-running"]')).toBeNull();
});
it("shows external now-running blockers beneath the label on a separate line", () => {
const node = render(
<IssueBlockedNotice
issueStatus="blocked"
liveIssueIds={new Set(["terminal-live"])}
blockerAttention={{
state: "covered",
reason: "active_dependency",
unresolvedBlockerCount: 1,
coveredBlockerCount: 1,
stalledBlockerCount: 0,
attentionBlockerCount: 0,
sampleBlockerIdentifier: "TASK-99",
sampleStalledBlockerIdentifier: null,
}}
blockers={[
{
id: "blocker-1",
identifier: "TASK-1",
title: "Queued dependency",
status: "todo",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
terminalBlockers: [
{
id: "terminal-live",
identifier: "TASK-99",
title: "External running task",
status: "in_progress",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
},
],
},
]}
allBlockers={[
{
id: "blocker-1",
identifier: "TASK-1",
title: "Queued dependency",
status: "todo",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
},
]}
/>,
);
const nowRunning = node.querySelector('[data-testid="issue-blocked-notice-now-running"]');
expect(nowRunning).not.toBeNull();
expect(nowRunning!.children[0]?.textContent?.trim()).toBe("Now running");
expect(nowRunning!.children[1]?.querySelector("a")?.textContent).toContain("TASK-99");
const stepText = node.querySelector('[data-testid="issue-blocked-notice-steps"]')?.textContent;
expect(stepText).not.toContain("TASK-99");
});
it("renders a recovery indicator on a blocker chip when the blocker has an active recovery action", () => {

View File

@ -22,6 +22,7 @@ import {
RECOVERY_CHIP_DEFAULT_TONE,
recoveryChipLabel,
} from "../lib/recovery-display";
import { StatusGlyph } from "./StatusGlyph";
function BlockerRecoveryIndicator({ action }: { action: IssueRecoveryAction }) {
const state = deriveActiveRecoveryDisplayState(action);
@ -133,6 +134,10 @@ const WAITING_STEP_RANK: Record<WaitingStepStatus, number> = {
queued: 2,
};
function waitingTaskStatusLabel(status: string): string {
return status.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
}
function WaitingChipLink({
blocker,
running = false,
@ -147,6 +152,11 @@ function WaitingChipLink({
to={createIssueDetailPath(issuePathId)}
className="inline-flex max-w-full items-center gap-1 rounded-md border border-blue-300/70 bg-background/80 px-2 py-1 font-mono text-xs text-blue-950 transition-colors hover:border-blue-500 hover:bg-blue-100 hover:underline dark:border-blue-500/40 dark:bg-background/40 dark:text-blue-100 dark:hover:bg-blue-500/15"
>
<StatusGlyph
status={blocker.status}
size="sm"
title={`${waitingTaskStatusLabel(blocker.status)} status`}
/>
<span>{blocker.identifier ?? blocker.id.slice(0, 8)}</span>
<span className="max-w-(--sz-18rem) truncate font-sans text-(length:--text-micro) text-blue-800 dark:text-blue-200">
{blocker.title}
@ -208,11 +218,13 @@ function WaitingOnLiveWorkNotice({
const runningCount = steps.filter((step) => step.status === "running").length;
// "Now running" replaces "Ultimately waiting on": prefer live terminal
// leaves; otherwise fall back to whichever chain blocker is live.
// leaves that are not already shown in the ordered queue list.
const stepIds = new Set(steps.map((step) => step.blocker.id));
const nowRunningSeen = new Set<string>();
const nowRunning: IssueRelationIssueSummary[] = [];
for (const blocker of [...terminalBlockers, ...chainBlockers]) {
if (!liveIds.has(blocker.id)) continue;
if (stepIds.has(blocker.id)) continue;
if (nowRunningSeen.has(blocker.id)) continue;
nowRunningSeen.add(blocker.id);
nowRunning.push(blocker);
@ -276,7 +288,7 @@ function WaitingOnLiveWorkNotice({
{steps.map(({ blocker, status }) => (
<div key={blocker.id} className="flex items-stretch gap-2">
<div className="flex w-3.5 flex-col items-center">
<span className="mt-0.5">
<span className="flex min-h-6 items-center">
<WaitingStepGlyph status={status} />
</span>
<span
@ -285,20 +297,14 @@ function WaitingOnLiveWorkNotice({
/>
</div>
<div className="min-w-0 pb-1.5">
{status === "running" ? (
<div className="rounded-md border border-blue-500/60 bg-blue-100/60 p-1 dark:border-blue-400/50 dark:bg-blue-500/15">
<WaitingChipLink blocker={blocker} running />
</div>
) : (
<WaitingChipLink blocker={blocker} />
)}
<WaitingChipLink blocker={blocker} running={status === "running"} />
</div>
</div>
))}
<div className="flex items-stretch gap-2">
<div className="flex w-3.5 flex-col items-center">
<span
className="mt-0.5 h-3 w-3 rounded-full border border-dashed border-blue-400/60 dark:border-blue-400/50"
className="mt-1.5 h-3 w-3 rounded-full border border-dashed border-blue-400/60 dark:border-blue-400/50"
aria-hidden
/>
</div>
@ -313,14 +319,16 @@ function WaitingOnLiveWorkNotice({
{nowRunning.length > 0 ? (
<div
data-testid="issue-blocked-notice-now-running"
className="flex flex-wrap items-center gap-1.5 pt-0.5"
className="space-y-1 pt-0.5"
>
<span className="text-xs font-medium text-blue-800 dark:text-blue-200">
<div className="text-xs font-medium text-blue-800 dark:text-blue-200">
Now running
</span>
{nowRunning.map((blocker) => (
<WaitingChipLink key={blocker.id} blocker={blocker} running />
))}
</div>
<div className="flex flex-wrap items-center gap-1.5">
{nowRunning.map((blocker) => (
<WaitingChipLink key={blocker.id} blocker={blocker} running />
))}
</div>
</div>
) : null}

View File

@ -1,7 +1,8 @@
// @vitest-environment jsdom
import { act } from "react";
import { act as reactAct } from "react";
import type { ComponentProps } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { DocumentRevision, Issue, IssueDocument } from "@paperclipai/shared";
@ -25,6 +26,24 @@ const markdownEditorMockState = vi.hoisted(() => ({
emitMountEmptyChange: false,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
async function act<T>(callback: () => T | Promise<T>): Promise<T> {
if (typeof reactAct === "function") {
return await (reactAct(callback) as T | Promise<T>);
}
let result: T | Promise<T> | undefined;
flushSync(() => {
result = callback();
});
const resolved = await result;
await new Promise((resolve) => setTimeout(resolve, 0));
flushSync(() => {});
return resolved as T;
}
vi.mock("../api/issues", () => ({
issuesApi: mockIssuesApi,
}));
@ -425,6 +444,86 @@ describe("IssueDocumentsSection", () => {
queryClient.clear();
});
it("shows revision authors with names and avatars in the revision history menu", async () => {
const currentDocument = createIssueDocument({
body: "Current plan body",
latestRevisionId: "revision-agent",
latestRevisionNumber: 4,
updatedByAgentId: "agent-1",
updatedByUserId: null,
updatedAt: new Date("2026-03-31T12:05:00.000Z"),
});
const issue = createIssue();
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
mockIssuesApi.listDocuments.mockResolvedValue([currentDocument]);
queryClient.setQueryData(
queryKeys.issues.documentRevisions(issue.id, "plan"),
[
createRevision({
id: "revision-agent",
revisionNumber: 4,
body: "Current plan body",
createdByAgentId: "agent-1",
createdByUserId: null,
createdAt: new Date("2026-03-31T12:05:00.000Z"),
}),
createRevision({
id: "revision-user",
revisionNumber: 3,
body: "Board-written plan body",
createdByAgentId: null,
createdByUserId: "user-1",
createdAt: new Date("2026-03-31T11:00:00.000Z"),
}),
],
);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<IssueDocumentsSection
issue={issue}
canDeleteDocuments={false}
agentMap={new Map([["agent-1", { id: "agent-1", name: "CodexCoder", icon: "code" }]])}
userProfileMap={new Map([["user-1", { label: "Dotta", image: "https://example.test/dotta.png" }]])}
/>
</QueryClientProvider>,
);
});
await flush();
await flush();
const revisionButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("rev 4"));
expect(revisionButton).toBeTruthy();
await act(async () => {
revisionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flush();
expect(document.body.textContent).toContain("CodexCoder");
expect(document.body.textContent).toContain("Dotta");
expect(document.body.textContent).not.toContain("• agent");
expect(document.body.querySelectorAll('[data-slot="avatar"]').length).toBeGreaterThanOrEqual(2);
await act(async () => {
root.unmount();
});
queryClient.clear();
});
it("shows the restored document body immediately after a revision restore", async () => {
const blankLatestDocument = createIssueDocument({
body: "",

View File

@ -34,7 +34,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Check, Copy, Diff, Download, FilePenLine, FileText, Lock, MoreHorizontal, Plus, Trash2, Unlock, X } from "lucide-react";
import { DocumentDiffModal } from "./DocumentDiffModal";
import { DocumentFrameHeader } from "./DocumentFrameHeader";
import { DocumentFrameHeader, type DocumentFrameHeaderRevisionActor } from "./DocumentFrameHeader";
import { SourceTrustBadge } from "./SourceTrustBadge";
import { Badge } from "@/components/ui/badge";
@ -116,10 +116,30 @@ function downloadDocumentFile(key: string, body: string) {
URL.revokeObjectURL(url);
}
function getRevisionActorLabel(revision: DocumentRevision) {
if (revision.createdByUserId) return "board";
if (revision.createdByAgentId) return "agent";
return "system";
function getRevisionActor(
revision: DocumentRevision,
maps: {
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
},
): DocumentFrameHeaderRevisionActor {
if (revision.createdByAgentId) {
const agent = maps.agentMap?.get(revision.createdByAgentId);
return {
kind: "agent",
name: agent?.name ?? revision.createdByAgentId.slice(0, 8),
agentIcon: agent?.icon ?? null,
};
}
if (revision.createdByUserId) {
const profile = maps.userProfileMap?.get(revision.createdByUserId);
return {
kind: "user",
name: profile?.label ?? (revision.createdByUserId === "local-board" ? "Board" : revision.createdByUserId.slice(0, 8)),
imageUrl: profile?.image ?? null,
};
}
return { kind: "system", name: "System" };
}
function documentHasUnsavedChanges(doc: IssueDocument, draft: DraftState | null) {
@ -182,7 +202,7 @@ export function IssueDocumentsSection({
options?: { allowSharing?: boolean; reason?: string },
) => Promise<void>;
extraActions?: ReactNode;
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
/**
* Seed which document annotation panels are open on first render. Mostly useful
@ -939,7 +959,7 @@ export function IssueDocumentsSection({
id: revision.id,
revisionNumber: revision.revisionNumber,
createdAt: revision.createdAt,
actorLabel: getRevisionActorLabel(revision),
actor: getRevisionActor(revision, { agentMap, userProfileMap }),
})),
selectedRevisionId,
currentRevisionId: currentRevision.id,

View File

@ -1,6 +1,7 @@
// @vitest-environment jsdom
import { act } from "react";
import { act as reactAct } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import type { Issue } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@ -28,6 +29,15 @@ vi.mock("@/lib/router", () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function act(callback: () => void) {
if (typeof reactAct === "function") {
reactAct(callback);
return;
}
flushSync(callback);
}
function createIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "issue-1",
@ -251,6 +261,31 @@ describe("IssueRow", () => {
});
});
it("marks the current checklist step without adding a left border", () => {
const root = createRoot(container);
act(() => {
root.render(
<IssueRow
issue={createIssue({ identifier: "PAP-42" })}
checklistStepNumber="2.1"
checklistCurrentStep
/>,
);
});
const link = container.querySelector("[data-inbox-issue-link]") as HTMLAnchorElement | null;
expect(link).not.toBeNull();
expect(link?.getAttribute("aria-current")).toBe("step");
expect(link?.className).toContain("bg-primary/5");
expect(link?.className).not.toContain("border-l-");
act(() => {
root.unmount();
});
});
it("does not render a planning mode marker for planning work mode issues", () => {
const root = createRoot(container);

View File

@ -143,7 +143,7 @@ export function IssueRow({
"group flex items-start gap-2 rounded-lg py-2.5 pl-2 pr-3 text-sm no-underline text-inherit sm:items-center sm:py-2 sm:pl-1",
!hideDivider && "border-b border-border last:border-b-0",
selected ? "hover:bg-transparent" : "hover:bg-accent/50",
checklistCurrentStep ? "border-l-2 border-l-primary bg-primary/5 pl-(--sz-calc-11) sm:pl-(--sz-calc-12)" : null,
checklistCurrentStep ? "bg-primary/5" : null,
className,
)}
>

View File

@ -14,7 +14,7 @@ import type { CompanyUserProfile } from "../lib/company-members";
import { queryKeys } from "../lib/queryKeys";
import { useToastActions } from "../context/ToastContext";
import { DocumentAnnotationLayer, type PendingAnchor } from "./DocumentAnnotationLayer";
import { DocumentFrameHeader } from "./DocumentFrameHeader";
import { DocumentFrameHeader, type DocumentFrameHeaderRevisionActor } from "./DocumentFrameHeader";
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "./IssueDocumentAnnotations";
import { EmptyState } from "./EmptyState";
import { FoldCurtain } from "./FoldCurtain";
@ -35,6 +35,34 @@ type CaseBodyDocument = PipelineCaseDocumentPayload["document"] & {
createdByUserId?: string | null;
};
function getPipelineRevisionActor(
revision: { createdByAgentId?: string | null; createdByUserId?: string | null },
maps: {
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
},
): DocumentFrameHeaderRevisionActor {
if (revision.createdByAgentId) {
const agent = maps.agentMap?.get(revision.createdByAgentId);
return {
kind: "agent",
name: agent?.name ?? revision.createdByAgentId.slice(0, 8),
agentIcon: agent?.icon ?? null,
};
}
if (revision.createdByUserId) {
const profile = maps.userProfileMap?.get(revision.createdByUserId);
return {
kind: "user",
name: profile?.label ?? (revision.createdByUserId === "local-board" ? "Board" : revision.createdByUserId.slice(0, 8)),
imageUrl: profile?.image ?? null,
};
}
return { kind: "system", name: "System" };
}
function isNotFound(error: unknown) {
return error instanceof ApiError && error.status === 404;
}
@ -48,7 +76,7 @@ export interface PipelineItemBodyDocumentProps {
/** Active conversation issue the body document is/should be anchored to. */
conversationIssueId: string | null;
conversationIssue: Issue | null;
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
mentions?: MentionOption[];
imageUploadHandler?: (file: File) => Promise<string>;
@ -396,7 +424,7 @@ export function PipelineItemBodyDocument({
id: revision.id,
revisionNumber: revision.revisionNumber,
createdAt: revision.createdAt,
actorLabel: revision.createdByUserId ? "board" : revision.createdByAgentId ? "agent" : "system",
actor: getPipelineRevisionActor(revision, { agentMap, userProfileMap }),
})),
selectedRevisionId,
currentRevisionId: doc?.latestRevisionId ?? null,

View File

@ -1586,8 +1586,6 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
--sz-calc-9: min(80vh,42rem); /* Extracted from ui/src/components/IssueFiltersPopover.test.tsx (max-h-[min(80vh,42rem)]). */
--sz-calc-10: min(780px,calc(100vw - 2rem)); /* Extracted from ui/src/components/IssueFiltersPopover.tsx (w-[min(780px,calc(100vw-2rem))]). */
--sz-24ch: 24ch; /* Extracted from ui/src/components/IssuePlanDecompositionsSection.tsx (max-w-[24ch]). */
--sz-calc-11: calc(0.5rem - 2px); /* Extracted from ui/src/components/IssueRow.tsx (pl-[calc(theme(spacing.2)-2px)]). */
--sz-calc-12: calc(0.25rem - 2px); /* Extracted from ui/src/components/IssueRow.tsx (pl-[calc(theme(spacing.1)-2px)]). */
--sz-calc-13: calc(0.75rem + 0.5rem); /* Extracted from ui/src/components/IssueRow.tsx (ml-[calc(theme(spacing.3)+theme(spacing.2))]). */
--sz-140px: 140px; /* Extracted from ui/src/components/JsonSchemaForm.tsx (min-h-[140px]). */
--sz-52px: 52px; /* Extracted from ui/src/components/KanbanBoard.tsx (w-[52px]). */