fix(ui): place task message copy control left of the timestamp (#19451)

## Thinking Path

> - Paperclip is the open source app people use to manage agents at
work.
> - Task threads show a time below each message.
> - Own messages align with the right edge.
> - The copy control added in #19442 sits to the right of the time.
> - This shifts the time inward even when the control is hidden.
> - This pull request moves the control to the left of the time.
> - The time stays at the outside edge.

## Linked Issues or Issue Description

Refs #19442.

**What happened?**
The copy control occupies space to the right of the time on own
messages.

**Expected behavior**
The copy control sits to the left of the time. The time stays at the
right edge.

**Steps to reproduce**
Open a task with your own sent message. Move the pointer over its
footer.

## What Changed

- Move TaskChatBubbleActions before the time in the own-message footer.
- Keep the layout change inside TaskChatBubble. No shared contract
change is needed.

## Verification

- All 36 TaskChatBubble tests pass.
- Design-token gates pass.
- UI typecheck and production build pass.
- Browser layout checks pass on desktop and touch: copy is left of the
time, the time ends at the bubble edge, and the time does not move on
hover.
- Clipboard contents and copied feedback pass.
- Screenshots cover idle, hover, copied feedback, and touch using the
production component in the existing Storybook fixture.
- Current-head CI passed: build, typecheck, selected general tests, all
eight e2e shards, policy, security, and aggregate verification.
- Greptile and Paperclip reviews score the current commit 5/5. No open
findings or review threads remain.
- The first Greptile review requested an order assertion. The existing
copy test now checks that the control precedes the time and the time is
the last footer element. All 36 component tests pass. The review thread
is resolved.
- Initial test and Storybook commands used the wrong working directory
or argument form. Corrected commands run successfully.
- Local typecheck and Storybook initially could not resolve
mdast-util-from-markdown. A frozen dependency install restored the
missing package. Typecheck, build, and browser checks then passed. An
early browser retry reached the server before it was ready; the
ready-server run passed.

## Risks

- Low risk. This change only moves the existing control before the time.

## Model Used

- OpenAI GPT-6 Codex. Exact deployed model ID and context window are not
exposed in this runtime. The agent used reasoning, 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 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
- [x] My branch name describes the change and contains no internal
ticket id or instance details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable (existing copy
tests cover unchanged behavior; browser checks cover position)
- [x] I have updated relevant documentation to reflect my changes (the
behavior is described here)
- [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
This commit is contained in:
Dotta authored and GitHub committed 2026-11-06 07:07:04 -06:00
1 parent 2f9d3d43c3
commit 1cd1e8c598
2 files changed
+7 -4

No files matched your search

@@ -155,7 +155,10 @@ describe("TaskChatBubble attachment chips", () => {
</ThemeProvider>,
));
const toolbar = container.querySelector('[data-testid="task-chat-bubble-actions"]')!;
expect(toolbar.parentElement?.parentElement?.textContent).toContain("15m ago");
const copyWrapper = toolbar.parentElement!;
const timestamp = copyWrapper.nextElementSibling;
expect(timestamp?.textContent).toBe("15m ago");
expect(timestamp).toBe(copyWrapper.parentElement?.lastElementChild);
const button = toolbar.querySelector('button[aria-label="Copy message"]') as HTMLButtonElement;
await act(async () => button.click());
expect(copyTextToClipboard).toHaveBeenLastCalledWith(text);
@@ -434,14 +434,14 @@ function TaskChatBubbleContent({
</div>
) : isOwnMessage ? (
<div className="group/message-toolbar flex items-center gap-1">
<div className="transition-opacity motion-reduce:transition-none sm:opacity-0 sm:group-hover/message-toolbar:opacity-100 sm:group-focus-within/message-toolbar:opacity-100 pointer-coarse:opacity-100">
<TaskChatBubbleActions copyText={item.text} />
</div>
<span className="px-1 text-(length:--text-micro) text-muted-foreground">
{sentFromIMessage ? "Sent from iMessage" : null}
{sentFromIMessage && timestamp ? " · " : null}
{timestamp}
</span>
<div className="transition-opacity motion-reduce:transition-none sm:opacity-0 sm:group-hover/message-toolbar:opacity-100 sm:group-focus-within/message-toolbar:opacity-100 pointer-coarse:opacity-100">
<TaskChatBubbleActions copyText={item.text} />
</div>
</div>
) : actions ? (
streamlined ? (