mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The task detail view shows the conversation as chat bubbles. The requester's own messages sit in a solid accent-colored bubble. > - The bubble container sets `text-white`, but the message body renders through `MarkdownBody`. Tailwind prose tokens (`--tw-prose-body`) win over the inherited container color. > - `prose-invert` only lightens the prose text in dark mode. In light mode the prose body stayed its default dark color, so the text read as near-black on the blue bubble and was hard to read. > - This pull request maps the human bubble's prose tokens to the inherited text color in both themes. > - The benefit is that the requester's chat text is readable white-on-blue in light mode, and dark mode stays exactly as it was. ## Linked Issues or Issue Description <!-- No public GitHub issue exists; described in-PR per the bug report template. --> **What happened?** In light mode, the text inside the user's own chat bubbles in the task detail view rendered as dark (near-black) on the solid blue accent background. This made the requester's messages hard to read. **Expected behavior** The text inside the user's accent-colored chat bubbles should be white in light mode, matching the bubble's `text-white` intent. Dark mode already rendered correctly and should not change. **Steps to reproduce** 1. Open a chat-style task detail view in light mode. 2. Post a message as the requester (human) so it renders in the solid blue accent bubble. 3. Observe the body text renders dark on blue instead of white. **Paperclip version or commit** Reproduces on `master` (branched from `814cb3367`). **Agent adapter(s) involved** Not adapter-specific (core UI bug). ## What Changed - Add the existing `paperclip-markdown-on-accent` class to the human-branch `MarkdownBody` in `TaskChatBubble.tsx`. This class (already used by `IssueChatThread` for the same accent bubble) maps prose body/heading tokens to `currentColor`, so the text follows the bubble's `text-white` in both themes. - Apply the same class to the human-branch `MarkdownBody` in `TaskChatDescriptionBubble.tsx` (the description-as-first-bubble surface) for consistency. - Add unit tests covering that the human accent bubble carries the on-accent class and the agent/neutral bubbles do not. ## Verification - `pnpm check:token-gates` → 3/3 CLEAN. - `pnpm --filter ./ui vitest run src/components/task-chat/TaskChatBubble.test.tsx` → 9/9 passing. - Manual: in light mode, the requester's chat bubble text renders white on blue; agent/neutral bubbles unchanged; dark mode unchanged. This is a visual change. Snapshot baselines are intentionally not updated, per `doc/design/DECISION-SHEET.md` → "Per-change snapshot verification demoted to dormant (Jul 13 2026)". ## Risks Low risk. The change is scoped to the human-branch `MarkdownBody` className on two chat-bubble components and only remaps prose color tokens to the inherited text color. Agent and neutral bubbles are untouched, and dark mode behavior is unchanged. ## Model Used Claude Opus 4.8 (Anthropic), model id `claude-opus-4-8`, ~200K context window, extended thinking mode, with tool use / 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 (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 - [ ] All Paperclip CI gates are green - [ ] 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: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
6.9 KiB
TypeScript
179 lines
6.9 KiB
TypeScript
import { useState } from "react";
|
|
import { Pencil } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { FoldCurtain } from "@/components/FoldCurtain";
|
|
import { InlineEditor } from "@/components/InlineEditor";
|
|
import { MarkdownBody, type MarkdownExternalReferenceMap } from "@/components/MarkdownBody";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import { AgentIcon } from "@/components/AgentIconPicker";
|
|
import type { MentionOption } from "@/components/MarkdownEditor";
|
|
import { formatTaskChatTimestamp } from "./task-chat-adapter";
|
|
|
|
/**
|
|
* Host binding for the description-as-first-bubble (PAP-375): the LIVE issue
|
|
* description plus everything the edit surface needs. Carried on the shared
|
|
* thread props (`issueBrief`); only the redesigned TaskChatThread consumes it.
|
|
*/
|
|
export interface TaskChatIssueBrief {
|
|
/** LIVE issue.description (never a frozen copy) — empty string when unset. */
|
|
description: string;
|
|
/** Originating actor: human-created → blue user bubble, agent-created → agent-side bubble. */
|
|
author: "human" | "agent";
|
|
/** Creating agent's name, shown in the agent-side avatar header. */
|
|
authorName?: string;
|
|
/** Creating agent's assigned icon (AgentIconName) for the avatar header. */
|
|
agentIcon?: string | null;
|
|
/** issue.createdAt — rendered like the other bubbles' footer timestamps. */
|
|
createdAt?: string | Date;
|
|
onSave: (description: string) => void | Promise<unknown>;
|
|
mentions?: MentionOption[];
|
|
externalReferences?: MarkdownExternalReferenceMap;
|
|
imageUploadHandler?: (file: File) => Promise<string>;
|
|
onDropFile?: (file: File) => Promise<void>;
|
|
}
|
|
|
|
interface TaskChatDescriptionBubbleProps {
|
|
brief: TaskChatIssueBrief;
|
|
}
|
|
|
|
/** ~12 text-sm lines (20px line-height) — the accepted fold default (PAP-375). */
|
|
const FOLD_COLLAPSED_HEIGHT_PX = 240;
|
|
|
|
function initialsForName(name: string) {
|
|
const parts = name.trim().split(/\s+/);
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
return name.slice(0, 2).toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* The task description as the requester's first chat bubble (PAP-375):
|
|
* human-created tasks get the blue user bubble, agent-created tasks the
|
|
* agent-side card with the avatar header — same chrome as TaskChatBubble. The
|
|
* body is the live issue.description; a hover pencil swaps the bubble for the
|
|
* same InlineEditor the pre-redesign page used (mentions, image paste/drop,
|
|
* autosave), restoring the edit surface the redesign had gated off. Long
|
|
* descriptions fold at ~12 lines behind a Show more curtain; an empty
|
|
* description renders a muted ghost row that opens the editor — never an
|
|
* empty bubble.
|
|
*/
|
|
export function TaskChatDescriptionBubble({ brief }: TaskChatDescriptionBubbleProps) {
|
|
const [editing, setEditing] = useState(false);
|
|
const isHuman = brief.author === "human";
|
|
const hasDescription = brief.description.trim().length > 0;
|
|
|
|
if (editing) {
|
|
return (
|
|
<div
|
|
className="tc-enter-bubble w-full rounded-lg border border-border bg-card px-2 py-1"
|
|
data-testid="task-chat-description-editor"
|
|
>
|
|
<InlineEditor
|
|
value={brief.description}
|
|
onSave={brief.onSave}
|
|
as="p"
|
|
className="text-sm leading-7 text-foreground"
|
|
placeholder="Add a description..."
|
|
multiline
|
|
defaultEditing
|
|
onEditingChange={(next) => {
|
|
if (!next) setEditing(false);
|
|
}}
|
|
mentions={brief.mentions}
|
|
externalReferences={brief.externalReferences}
|
|
imageUploadHandler={brief.imageUploadHandler}
|
|
onDropFile={brief.onDropFile}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!hasDescription) {
|
|
// Accepted default: no fabricated empty bubble — a muted ghost affordance.
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center gap-1.5 rounded-lg px-1 py-1.5 text-left text-sm text-muted-foreground/70 transition-colors hover:bg-accent/20 hover:text-muted-foreground"
|
|
onClick={() => setEditing(true)}
|
|
data-testid="task-chat-description-ghost"
|
|
>
|
|
<Pencil className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
Add a description...
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const timestamp = formatTaskChatTimestamp(brief.createdAt);
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"tc-enter-bubble group/brief flex w-full flex-col gap-1",
|
|
isHuman ? "items-end" : "items-start",
|
|
)}
|
|
data-testid="task-chat-description-bubble"
|
|
data-author={brief.author}
|
|
>
|
|
{!isHuman && brief.authorName ? (
|
|
<span className="flex items-center gap-2 px-1">
|
|
<Avatar size="sm" className="shrink-0" data-testid="task-chat-agent-avatar">
|
|
{brief.agentIcon ? (
|
|
<AvatarFallback>
|
|
<AgentIcon icon={brief.agentIcon} className="h-3.5 w-3.5" />
|
|
</AvatarFallback>
|
|
) : (
|
|
<AvatarFallback>{initialsForName(brief.authorName)}</AvatarFallback>
|
|
)}
|
|
</Avatar>
|
|
<span className="text-sm font-semibold text-foreground">{brief.authorName}</span>
|
|
</span>
|
|
) : null}
|
|
<div
|
|
className={cn(
|
|
"flex w-full items-start gap-1.5",
|
|
isHuman ? "flex-row-reverse" : "flex-row",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"max-w-(--pct-85) break-words px-3.5 py-2 text-sm",
|
|
isHuman
|
|
? "rounded-2xl rounded-br-sm bg-(--liveness-blue) text-white"
|
|
: "rounded-2xl rounded-bl-sm bg-(--bubble-agent) text-foreground",
|
|
)}
|
|
>
|
|
<FoldCurtain
|
|
collapsedHeight={FOLD_COLLAPSED_HEIGHT_PX}
|
|
// The curtain's fade is a mask (background-agnostic); only the
|
|
// toggle's muted colors need a lift on the solid blue bubble.
|
|
toggleClassName={isHuman ? "text-white/80 hover:text-white hover:bg-white/10" : undefined}
|
|
>
|
|
<MarkdownBody
|
|
// On the solid --liveness-blue human bubble, keep prose body text
|
|
// following the bubble's `text-white` in both themes.
|
|
className={isHuman ? "paperclip-markdown-on-accent" : undefined}
|
|
softBreaks
|
|
linkIssueReferences
|
|
externalReferences={brief.externalReferences}
|
|
>
|
|
{brief.description}
|
|
</MarkdownBody>
|
|
</FoldCurtain>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="mt-1 shrink-0 rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent/50 hover:text-foreground focus-visible:opacity-100 group-hover/brief:opacity-100"
|
|
onClick={() => setEditing(true)}
|
|
aria-label="Edit description"
|
|
data-testid="task-chat-description-edit"
|
|
>
|
|
<Pencil className="h-3.5 w-3.5" aria-hidden />
|
|
</button>
|
|
</div>
|
|
{timestamp ? (
|
|
<span className="px-1 text-(length:--text-micro) text-muted-foreground">{timestamp}</span>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|