fix(ui): reveal completed thinking caret beside label (#15048)

## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The task thread shows completed agent work and its reasoning.
> - The completed work row places the disclosure caret at the far right.
> - The caret stays visible even when the reader does not inspect the
row.
> - The row needs a quiet control that stays close to the work label.
> - This pull request places the caret after that label and shows it on
hover or keyboard focus.
> - The change keeps the timestamp on the right and does not move
content on hover.

## Linked Issues or Issue Description

**What happened?**

The completed agent work row shows a disclosure caret at the far right
of the task thread. The caret stays visible when the pointer is
elsewhere.

**Expected behavior**

The caret stays near the work label. It appears on hover or keyboard
focus. The timestamp stays on the right.

**Steps to reproduce**

1. Open a task with a completed agent run.
2. Look at the completed work row without hovering over it.
3. Hover over the row and then use the keyboard to focus its button.

Related: #11772 addresses a different caret and composer layout.

## What Changed

- Move the completed work caret next to the work label.
- Keep its space reserved. Show it on hover or keyboard focus.
- Add a test for caret position, visibility classes, and the open state.

## Verification

- `node node_modules/vitest/vitest.mjs run
ui/src/components/IssueChatThread.test.tsx` passes all 100 tests.
- `node scripts/check-token-gates.mjs` passes all token gates.
- `git diff origin/master...HEAD --check` passes.
- CI passes on the latest head, including typecheck, build, tests, and
verification. The local isolated worktree could not run typecheck
because dependency installation failed while applying the existing
`codex-acp` patch.

## Risks

- Low risk. The same button still opens the work detail. Only the caret
position and visibility change.
- On a touch device, the caret is not visible without hover. The work
row stays a button that users can tap.

> This is a focused UI fix. It does not add a planned core feature from
`ROADMAP.md`.

## Model Used

- OpenAI Codex coding agent. The runner does not expose the exact model
ID or context window. The agent used reasoning, shell tools, and code
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 the details available in this
runner
- [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 linked public issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket ID
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have checked documentation impact; no documentation change is
needed for this visual fix
- [x] I have considered and documented the 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:
DottaandPaperclip authored and GitHub committed 2026-10-03 12:41:22 -05:00
1 parent 1c3abf5075
commit 2a8a99e4a5
2 files changed
+59 -13

No files matched your search

@@ -4813,6 +4813,53 @@ describe("IssueChatThread", () => {
});
});
it("keeps the completed chain-of-thought caret beside its label and reveals it on hover or focus", () => {
const root = createRoot(container);
const run = issueChatLongThreadLinkedRuns[2];
act(() => {
root.render(
<MemoryRouter>
<IssueChatThread
comments={[]}
linkedRuns={[run]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
showComposer={false}
enableLiveTranscriptPolling={false}
transcriptsByRunId={issueChatLongThreadTranscriptsByRunId}
hasOutputForRun={() => true}
/>
</MemoryRouter>,
);
});
const header = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("worked for 4 minutes"),
);
expect(header).toBeDefined();
const label = Array.from(header!.children).find((child) =>
child.textContent?.includes("worked for 4 minutes"),
);
const caret = label?.nextElementSibling;
expect(caret?.tagName.toLowerCase()).toBe("svg");
expect(header?.classList.contains("group")).toBe(true);
expect(caret?.classList.contains("opacity-0")).toBe(true);
expect(caret?.classList.contains("group-hover:opacity-100")).toBe(true);
expect(caret?.classList.contains("group-focus-visible:opacity-100")).toBe(true);
expect(caret?.nextElementSibling?.classList.contains("ml-auto")).toBe(true);
act(() => {
header!.click();
});
expect(caret?.classList.contains("rotate-180")).toBe(true);
act(() => {
root.unmount();
});
});
it("folds chain-of-thought when the same message transitions from running to complete", () => {
expect(
resolveAssistantMessageFoldedState({
+12 -13
View File
@@ -2659,19 +2659,18 @@ function IssueChatAssistantMessage({
<span className="text-xs text-muted-foreground/60">
{chainOfThoughtLabel?.toLowerCase()}
</span>
<span className="ml-auto flex items-center gap-1.5">
{message.createdAt ? (
<span className="text-(length:--text-micro) text-muted-foreground/50">
{commentDateLabel(message.createdAt)}
</span>
) : null}
<ChevronDown
className={cn(
"h-3.5 w-3.5 text-muted-foreground/40 transition-transform",
!folded && "rotate-180",
)}
/>
</span>
<ChevronDown
aria-hidden="true"
className={cn(
"h-3.5 w-3.5 shrink-0 text-muted-foreground/40 opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100",
!folded && "rotate-180",
)}
/>
{message.createdAt ? (
<span className="ml-auto text-(length:--text-micro) text-muted-foreground/50">
{commentDateLabel(message.createdAt)}
</span>
) : null}
</button>
) : (
<div className="mb-1.5 flex items-center gap-2">