From c8ea9a5204e9f8371d73e32fbda0ff957b6340bc Mon Sep 17 00:00:00 2001
From: 07akioni <07akioni2@gmail.com>
Date: Wed, 29 Jul 2026 15:11:06 +0800
Subject: [PATCH] fix(ui-conversation): share MessageIconActions to clear jscpd
clone
User and assistant chrome both rendered copy/branch buttons; one shared
row owns the chrome and keeps clock placement / edit as props.
---
.../client/chat/AssistantMarkdown.module.css | 42 +-----------
.../src/client/chat/AssistantMarkdown.tsx | 40 ++++--------
.../client/chat/MessageIconActions.module.css | 53 +++++++++++++++
.../src/client/chat/MessageIconActions.tsx | 65 +++++++++++++++++++
.../src/client/chat/MessageItem.module.css | 43 +-----------
.../src/client/chat/MessageItem.tsx | 46 +++----------
6 files changed, 142 insertions(+), 147 deletions(-)
create mode 100644 packages/client/ui-conversation/src/client/chat/MessageIconActions.module.css
create mode 100644 packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx
diff --git a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.module.css b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.module.css
index 24bc4c0e49..d988cf52f8 100644
--- a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.module.css
+++ b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.module.css
@@ -28,55 +28,17 @@
line-height: 18px;
}
-/* Finalized footer: copy / branch / clock (figma 43:32997). */
+/* Finalized footer offset (figma 43:32997); chrome lives in MessageIconActions. */
.actions {
- display: flex;
- align-items: center;
- gap: 10px;
- height: 28px;
margin-top: 16px;
/* Optical align with 28px icon hit targets that pad 6px past the glyph. */
margin-left: -6px;
}
-/* Clock after the icon buttons; pl 12 separates it from branch. */
-.time {
- padding-left: 12px;
- font-size: 14px;
- line-height: 24px;
- color: var(--dsw-alias-label-tertiary);
- white-space: nowrap;
-}
-
-/* Hover-capable pointers: hide until the root is hovered/focused. Touch /
- hover:none keeps actions visible (opacity:0 still hit-tests). */
+/* Hover-capable pointers: reveal shared actions on root hover/focus. */
@media (hover: hover) {
- .actions {
- opacity: 0;
- transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
- }
-
.root:hover .actions,
.root:focus-within .actions {
opacity: 1;
}
}
-
-.action {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- padding: 6px;
- border: none;
- border-radius: 28px;
- background: transparent;
- color: var(--dsw-alias-label-tertiary);
- cursor: pointer;
-}
-
-.action:hover {
- background: var(--dsw-alias-interactive-bg-hover);
- color: var(--dsw-alias-label-secondary);
-}
diff --git a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
index a3a05b7af9..e5a89a9e88 100644
--- a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
+++ b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
@@ -6,14 +6,12 @@
// the turn-level loading dots live in the chat view's tail, not here.
// Finalized nodes append IconActions (copy / branch / clock) once streaming ends.
-import { memo, useCallback } from 'react'
+import { memo } from 'react'
import type { AssistantBlock } from '@deepseek-ai/dsh-client-runtime/client'
import {
- IconBranchOutline16, IconCopyOutline16, IconThinkOutline14,
- JsonBlock, MarkdownText, Tooltip,
+ IconThinkOutline14, JsonBlock, MarkdownText,
} from '@deepseek-ai/dsh-client-ui-primitives'
-import { formatMessageClock, writeClipboard } from './message-chrome.ts'
-import { useCalendarDay } from './use-calendar-day.ts'
+import { MessageIconActions } from './MessageIconActions.tsx'
import { ToolRow } from './ToolRow.tsx'
import css from './AssistantMarkdown.module.css'
@@ -55,29 +53,6 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
)
}
-/** Finalized assistant IconActions (figma 43:32997): copy live; branch stub; clock. */
-function AssistantActions({ text, time }: { text: string; time: number }) {
- const day = useCalendarDay()
- const onCopy = useCallback(() => {
- void writeClipboard(text)
- }, [text])
- return (
-
-
-
-
-
-
-
- {formatMessageClock(time, day)}
-
- )
-}
-
export const AssistantMarkdown = memo(function AssistantMarkdown({
blocks, streaming, interrupted, time,
}: AssistantMarkdownProps) {
@@ -105,7 +80,14 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
})}
{interrupted && 已停止}
- {showActions && }
+ {showActions && (
+
+ )}
)
})
diff --git a/packages/client/ui-conversation/src/client/chat/MessageIconActions.module.css b/packages/client/ui-conversation/src/client/chat/MessageIconActions.module.css
new file mode 100644
index 0000000000..30d6920609
--- /dev/null
+++ b/packages/client/ui-conversation/src/client/chat/MessageIconActions.module.css
@@ -0,0 +1,53 @@
+/* Shared message IconActions row (user + assistant). Parent modules own
+ hover-reveal selectors and layout offsets via the composed className. */
+
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ height: 28px;
+}
+
+/* Clock before icons (user figma 388:20051) / after (assistant 43:32997). */
+.timeStart {
+ padding-right: 12px;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--dsw-alias-label-tertiary);
+ white-space: nowrap;
+}
+
+.timeEnd {
+ padding-left: 12px;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--dsw-alias-label-tertiary);
+ white-space: nowrap;
+}
+
+/* Hover-capable pointers: hide until a parent hover/focus rule reveals. */
+@media (hover: hover) {
+ .actions {
+ opacity: 0;
+ transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
+ }
+}
+
+.action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ padding: 6px;
+ border: none;
+ border-radius: 28px;
+ background: transparent;
+ color: var(--dsw-alias-label-tertiary);
+ cursor: pointer;
+}
+
+.action:hover {
+ background: var(--dsw-alias-interactive-bg-hover);
+ color: var(--dsw-alias-label-secondary);
+}
diff --git a/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx
new file mode 100644
index 0000000000..7579a4c249
--- /dev/null
+++ b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx
@@ -0,0 +1,65 @@
+// Shared IconActions chrome for user and assistant messages: copy / branch
+// live (branch still a stub), date-aware clock, optional edit stub.
+
+import { useCallback } from 'react'
+import {
+ IconBranchOutline16, IconCopyOutline16, IconEditOutline16, Tooltip,
+} from '@deepseek-ai/dsh-client-ui-primitives'
+import { formatMessageClock, writeClipboard } from './message-chrome.ts'
+import { useCalendarDay } from './use-calendar-day.ts'
+import css from './MessageIconActions.module.css'
+
+export interface MessageIconActionsProps {
+ /** Plain text the copy action writes. */
+ text: string
+ /** Unix epoch ms for the clock label. */
+ time: number
+ /** Clock before icons (user) or after (assistant). */
+ clock: 'start' | 'end'
+ /** When true, append the stub edit control (user bubble). */
+ edit?: boolean | undefined
+ /** Parent layout / hover-reveal class composed onto the actions row. */
+ className?: string | undefined
+}
+
+/**
+ * Copy / branch (/ clock) IconActions row shared by user and assistant chrome.
+ * @param props - Copy text, event time, clock side, optional edit, className.
+ * @returns The actions row element.
+ */
+export function MessageIconActions({
+ text, time, clock, edit, className,
+}: MessageIconActionsProps) {
+ const day = useCalendarDay()
+ const onCopy = useCallback(() => {
+ void writeClipboard(text)
+ }, [text])
+ const clockEl = (
+
+ {formatMessageClock(time, day)}
+
+ )
+ return (
+
+ {clock === 'start' ? clockEl : null}
+
+
+
+
+
+
+ {edit === true && (
+
+
+
+ )}
+ {clock === 'end' ? clockEl : null}
+
+ )
+}
diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css
index a2c14fdc7a..260382d530 100644
--- a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css
+++ b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css
@@ -20,55 +20,14 @@
color: var(--dsw-alias-label-primary);
}
-.actions {
- display: flex;
- align-items: center;
- gap: 10px;
- height: 28px;
-}
-
-/* Clock before the icon buttons (figma 388:20051); pr 12 separates it from copy. */
-.time {
- padding-right: 12px;
- font-size: 14px;
- line-height: 24px;
- color: var(--dsw-alias-label-tertiary);
- white-space: nowrap;
-}
-
-/* Hover-capable pointers: hide until the row is hovered/focused. Touch /
- hover:none keeps actions visible (opacity:0 still hit-tests). */
+/* Hover-capable pointers: reveal shared MessageIconActions on row hover/focus. */
@media (hover: hover) {
- .actions {
- opacity: 0;
- transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
- }
-
.userRow:hover .actions,
.userRow:focus-within .actions {
opacity: 1;
}
}
-.action {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- padding: 6px;
- border: none;
- border-radius: 28px;
- background: transparent;
- color: var(--dsw-alias-label-tertiary);
- cursor: pointer;
-}
-
-.action:hover {
- background: var(--dsw-alias-interactive-bg-hover);
- color: var(--dsw-alias-label-secondary);
-}
-
.badge {
display: inline-block;
margin-bottom: 4px;
diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx
index acd2a7023f..a149d37337 100644
--- a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx
+++ b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx
@@ -4,17 +4,13 @@
// the snapshot cache; memo holds across streaming because unchanged nodes
// keep their references.
-import { memo, useCallback } from 'react'
+import { memo } from 'react'
import type { ReactNode } from 'react'
import type {
ContextMessageNode, SteeringMessageNode, UnknownSurfaceNode, UserMessageNode,
} from '@deepseek-ai/dsh-client-runtime/client'
-import {
- IconBranchOutline16, IconCopyOutline16, IconEditOutline16,
- JsonBlock, MessageText, Tooltip,
-} from '@deepseek-ai/dsh-client-ui-primitives'
-import { formatMessageClock, writeClipboard } from './message-chrome.ts'
-import { useCalendarDay } from './use-calendar-day.ts'
+import { JsonBlock, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
+import { MessageIconActions } from './MessageIconActions.tsx'
import css from './MessageItem.module.css'
export interface MessageItemProps {
@@ -64,34 +60,6 @@ function projectUserText(text: string): ReactNode {
return <>{parts}>
}
-/** User-bubble IconActions (figma 388:20051): clock + copy live; branch/edit stubs. */
-function UserActions({ text, time }: { text: string; time: number }) {
- const day = useCalendarDay()
- const onCopy = useCallback(() => {
- void writeClipboard(text)
- }, [text])
- return (
-
- {formatMessageClock(time, day)}
-
-
-
-
-
-
-
-
-
-
- )
-}
-
export const MessageItem = memo(function MessageItem({ node }: MessageItemProps) {
switch (node.kind) {
case 'user': {
@@ -102,7 +70,13 @@ export const MessageItem = memo(function MessageItem({ node }: MessageItemProps)
{projectUserText(text)}
{rest.map((block, i) => )}
-
+
)
}