Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
c3f99f53
Commit
c3f99f53
authored
May 14, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): add chat message delete action
parent
286f1972
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
294 additions
and
142 deletions
+294
-142
App.tsx
apps/ui/src/App.tsx
+27
-1
AppIcons.tsx
apps/ui/src/components/icons/AppIcons.tsx
+11
-0
ConversationWorkspaceView.tsx
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
+6
-0
MessageList.tsx
apps/ui/src/features/chat/MessageList.tsx
+139
-109
useMessageTraces.ts
apps/ui/src/features/chat/useMessageTraces.ts
+12
-1
chat.css
apps/ui/src/styles/chat.css
+7
-0
components.css
apps/ui/src/styles/components.css
+66
-12
theme-openclaw.css
apps/ui/src/styles/theme-openclaw.css
+21
-17
tailwind.css
apps/ui/src/tailwind.css
+5
-2
No files found.
apps/ui/src/App.tsx
View file @
c3f99f53
...
...
@@ -31,6 +31,7 @@ import {
RedBookIcon
,
RefreshIcon
,
ThumbIcon
,
TrashIcon
,
getIntentSuggestionIcon
,
renderExpertIcon
}
from
"./components/icons/AppIcons"
;
...
...
@@ -404,7 +405,8 @@ export default function App() {
renameMessageTrace
,
initializeMessageTrace
,
setMessageTraceExpanded
,
collapseMessageTrace
collapseMessageTrace
,
removeMessageTrace
}
=
useMessageTraces
();
const
{
saveConfig
,
...
...
@@ -1024,6 +1026,28 @@ export default function App() {
}
}
function
deleteMessage
(
messageId
:
string
)
{
if
(
!
visibleSessionId
)
{
return
;
}
updateSessionMessages
(
visibleSessionId
,
(
current
)
=>
{
const
nextMessages
=
current
.
filter
((
message
)
=>
message
.
id
!==
messageId
);
return
nextMessages
.
length
===
current
.
length
?
current
:
nextMessages
;
});
removeMessageTrace
(
messageId
);
setMessageReactions
((
current
)
=>
{
if
(
!
(
messageId
in
current
))
{
return
current
;
}
const
{
[
messageId
]:
_removed
,
...
rest
}
=
current
;
return
rest
;
});
if
(
copiedToken
===
`message:
${
messageId
}
`
)
{
setCopiedToken
(
""
);
}
}
async
function
regenerateAssistantMessage
(
messageId
:
string
)
{
if
(
sending
||
!
visibleSessionId
||
!
sessionScopeProjectId
)
{
return
;
...
...
@@ -1219,6 +1243,7 @@ export default function App() {
},
copyIcon
:
<
CopyIcon
/>,
copiedIcon
:
<
CheckIcon
/>,
deleteIcon
:
<
TrashIcon
/>,
regenerateIcon
:
<
RefreshIcon
/>,
renderThumbIcon
:
(
direction
)
=>
<
ThumbIcon
direction=
{
direction
}
/>,
renderMarkdownContent
,
...
...
@@ -1226,6 +1251,7 @@ export default function App() {
formatMessageTimestamp
,
onMessageListScroll
:
handleMessageListScroll
,
onCopyText
:
handleCopyText
,
onDeleteMessage
:
deleteMessage
,
onTraceExpandedChange
:
setMessageTraceExpanded
,
onRegenerateAssistantMessage
:
regenerateAssistantMessage
,
onToggleMessageReaction
:
toggleMessageReaction
,
...
...
apps/ui/src/components/icons/AppIcons.tsx
View file @
c3f99f53
...
...
@@ -309,6 +309,17 @@ export function CheckIcon() {
);
}
export
function
TrashIcon
()
{
return
(
<
svg
viewBox=
"0 0 24 24"
fill=
"none"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5 7h14"
stroke=
"currentColor"
strokeWidth=
"1.8"
strokeLinecap=
"round"
/>
<
path
d=
"M10 11v6M14 11v6"
stroke=
"currentColor"
strokeWidth=
"1.7"
strokeLinecap=
"round"
/>
<
path
d=
"M8 7l.6 12.2A2 2 0 0 0 10.6 21h2.8a2 2 0 0 0 2-1.8L16 7"
stroke=
"currentColor"
strokeWidth=
"1.7"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
<
path
d=
"M9 7V5.6A1.6 1.6 0 0 1 10.6 4h2.8A1.6 1.6 0 0 1 15 5.6V7"
stroke=
"currentColor"
strokeWidth=
"1.7"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
</
svg
>
);
}
export
function
ArrowUpIcon
()
{
return
(
<
svg
viewBox=
"0 0 24 24"
fill=
"none"
aria
-
hidden=
"true"
focusable=
"false"
>
...
...
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
View file @
c3f99f53
...
...
@@ -87,6 +87,7 @@ interface ConversationWorkspaceViewProps {
}
copyIcon
:
ReactNode
copiedIcon
:
ReactNode
deleteIcon
:
ReactNode
regenerateIcon
:
ReactNode
renderThumbIcon
:
(
direction
:
MessageReaction
)
=>
ReactNode
renderMarkdownContent
:
(
...
...
@@ -105,6 +106,7 @@ interface ConversationWorkspaceViewProps {
formatMessageTimestamp
:
(
value
:
string
)
=>
string
onMessageListScroll
:
(
event
:
ReactUIEvent
<
HTMLDivElement
>
)
=>
void
onCopyText
:
(
token
:
string
,
text
:
string
)
=>
void
|
Promise
<
void
>
onDeleteMessage
:
(
messageId
:
string
)
=>
void
onTraceExpandedChange
:
(
messageId
:
string
,
expanded
:
boolean
)
=>
void
onRegenerateAssistantMessage
:
(
messageId
:
string
)
=>
void
|
Promise
<
void
>
onToggleMessageReaction
:
(
messageId
:
string
,
reaction
:
MessageReaction
)
=>
void
...
...
@@ -187,6 +189,7 @@ export function ConversationWorkspaceView({
messageLabels
,
copyIcon
,
copiedIcon
,
deleteIcon
,
regenerateIcon
,
renderThumbIcon
,
renderMarkdownContent
,
...
...
@@ -194,6 +197,7 @@ export function ConversationWorkspaceView({
formatMessageTimestamp
,
onMessageListScroll
,
onCopyText
,
onDeleteMessage
,
onTraceExpandedChange
,
onRegenerateAssistantMessage
,
onToggleMessageReaction
,
...
...
@@ -342,6 +346,7 @@ export function ConversationWorkspaceView({
labels=
{
messageLabels
}
copyIcon=
{
copyIcon
}
copiedIcon=
{
copiedIcon
}
deleteIcon=
{
deleteIcon
}
regenerateIcon=
{
regenerateIcon
}
renderThumbIcon=
{
renderThumbIcon
}
renderMarkdownContent=
{
renderMarkdownContent
}
...
...
@@ -349,6 +354,7 @@ export function ConversationWorkspaceView({
formatMessageTimestamp=
{
formatMessageTimestamp
}
onScroll=
{
onMessageListScroll
}
onCopyText=
{
onCopyText
}
onDeleteMessage=
{
onDeleteMessage
}
onTraceExpandedChange=
{
onTraceExpandedChange
}
onRegenerateAssistantMessage=
{
onRegenerateAssistantMessage
}
onToggleMessageReaction=
{
onToggleMessageReaction
}
...
...
apps/ui/src/features/chat/MessageList.tsx
View file @
c3f99f53
...
...
@@ -41,6 +41,7 @@ interface MessageListProps {
labels
:
MessageListLabels
copyIcon
:
ReactNode
copiedIcon
:
ReactNode
deleteIcon
:
ReactNode
regenerateIcon
:
ReactNode
renderThumbIcon
:
(
direction
:
MessageReaction
)
=>
ReactNode
renderMarkdownContent
:
(
...
...
@@ -55,6 +56,7 @@ interface MessageListProps {
formatMessageTimestamp
:
(
value
:
string
)
=>
string
onScroll
:
(
event
:
UIEvent
<
HTMLDivElement
>
)
=>
void
onCopyText
:
(
token
:
string
,
text
:
string
)
=>
void
|
Promise
<
void
>
onDeleteMessage
:
(
messageId
:
string
)
=>
void
onTraceExpandedChange
:
(
messageId
:
string
,
expanded
:
boolean
)
=>
void
onRegenerateAssistantMessage
:
(
messageId
:
string
)
=>
void
|
Promise
<
void
>
onToggleMessageReaction
:
(
messageId
:
string
,
reaction
:
MessageReaction
)
=>
void
...
...
@@ -189,6 +191,7 @@ export function MessageList({
labels
,
copyIcon
,
copiedIcon
,
deleteIcon
,
regenerateIcon
,
renderThumbIcon
,
renderMarkdownContent
,
...
...
@@ -196,6 +199,7 @@ export function MessageList({
formatMessageTimestamp
,
onScroll
,
onCopyText
,
onDeleteMessage
,
onTraceExpandedChange
,
onRegenerateAssistantMessage
,
onToggleMessageReaction
...
...
@@ -220,12 +224,73 @@ export function MessageList({
const
traceDisplayLines
=
hasTrace
?
getTraceDisplayLines
(
messageTrace
?.
items
??
[])
:
[]
const
traceTitle
=
getTraceStripTitle
(
messageTrace
?.
items
??
[],
labels
.
thinking
,
message
.
statusLabel
,
message
.
statusDetail
)
const
canCopyMessage
=
Boolean
(
message
.
content
.
trim
())
const
hasVisibleAttachments
=
message
.
role
===
"user"
&&
Array
.
isArray
(
message
.
attachments
)
&&
message
.
attachments
.
some
((
attachment
)
=>
attachment
.
localPath
&&
attachment
.
name
)
const
canDeleteMessage
=
message
.
streamState
!==
"streaming"
&&
(
canCopyMessage
||
hasVisibleAttachments
)
const
copyToken
=
`message:${message.id}`
const
reaction
=
messageReactions
[
message
.
id
]
void
reaction
const
messageActions
=
(
canCopyMessage
||
canDeleteMessage
)
?
(
<
div
className=
"message-card-actions"
>
{
canCopyMessage
?
(
<
button
type=
"button"
className=
{
"message-action-icon"
+
(
copiedToken
===
copyToken
?
" copied"
:
""
)
}
onClick=
{
()
=>
void
onCopyText
(
copyToken
,
message
.
content
)
}
aria
-
label=
"复制消息"
title=
"复制消息"
>
{
copiedToken
===
copyToken
?
copiedIcon
:
copyIcon
}
</
button
>
)
:
null
}
{
canDeleteMessage
?
(
<
button
type=
"button"
className=
"message-action-icon message-action-delete"
onClick=
{
()
=>
onDeleteMessage
(
message
.
id
)
}
aria
-
label=
"删除消息"
title=
"删除消息"
>
{
deleteIcon
}
</
button
>
)
:
null
}
{
message
.
role
===
"assistant"
?
(
<>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
void
onRegenerateAssistantMessage
(
message
.
id
)
}
disabled=
{
sending
}
aria
-
label=
"重新生成"
title=
"重新生成"
>
{
regenerateIcon
}
</
button
>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
onToggleMessageReaction
(
message
.
id
,
"up"
)
}
aria
-
label=
"赞"
title=
"赞"
>
{
renderThumbIcon
(
"up"
)
}
</
button
>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
onToggleMessageReaction
(
message
.
id
,
"down"
)
}
aria
-
label=
"踩"
title=
"踩"
>
{
renderThumbIcon
(
"down"
)
}
</
button
>
</>
)
:
null
}
</
div
>
)
:
null
return
(
<
article
key=
{
message
.
id
}
className=
{
"message-card group "
+
message
.
role
+
(
message
.
streamState
?
" "
+
message
.
streamState
:
""
)
}
>
<
div
className=
{
"message-card-body message-card-body-"
+
message
.
role
}
>
<
div
className=
{
"message-bubble"
+
(
message
.
role
===
"assistant"
?
" message-bubble-assistant"
:
" message-bubble-user"
)
}
>
{
showReasoningStrip
?
(
<
div
className=
{
"message-trace"
+
(
message
.
streamState
===
"streaming"
?
" streaming"
:
""
)
}
aria
-
live=
{
message
.
streamState
===
"streaming"
?
"polite"
:
undefined
}
>
...
...
@@ -289,60 +354,25 @@ export function MessageList({
{
message
.
streamState
===
"streaming"
?
<
span
className=
"message-cursor"
aria
-
hidden=
"true"
/>
:
null
}
</
div
>
)
:
(
<>
<
p
className=
"message-plain-text"
>
{
message
.
content
}
{
message
.
streamState
===
"streaming"
?
<
span
className=
"message-cursor"
aria
-
hidden=
"true"
/>
:
null
}
</
p
>
)
)
:
null
}
{
message
.
role
===
"user"
?
<
MessageAttachmentStrip
attachments=
{
message
.
attachments
}
/>
:
null
}
</
div
>
<
span
className=
"message-timestamp"
aria
-
hidden=
"true"
>
{
formatMessageTimestamp
(
message
.
createdAt
)
}
</
span
>
{
message
.
role
===
"assistant"
&&
canCopyMessage
?
(
<
div
className=
"message-card-actions"
>
<
button
type=
"button"
className=
{
"message-action-icon"
+
(
copiedToken
===
copyToken
?
" copied"
:
""
)
}
onClick=
{
()
=>
void
onCopyText
(
copyToken
,
message
.
content
)
}
aria
-
label=
"复制消息"
title=
"复制消息"
>
{
copiedToken
===
copyToken
?
copiedIcon
:
copyIcon
}
</
button
>
{
message
.
role
===
"assistant"
?
(
</>
)
)
:
message
.
role
===
"user"
?
(
<>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
void
onRegenerateAssistantMessage
(
message
.
id
)
}
disabled=
{
sending
}
aria
-
label=
"重新生成"
title=
"重新生成"
>
{
regenerateIcon
}
</
button
>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
onToggleMessageReaction
(
message
.
id
,
"up"
)
}
aria
-
label=
"赞"
title=
"赞"
>
{
renderThumbIcon
(
"up"
)
}
</
button
>
<
button
type=
"button"
className=
"hidden"
onClick=
{
()
=>
onToggleMessageReaction
(
message
.
id
,
"down"
)
}
aria
-
label=
"踩"
title=
"踩"
>
{
renderThumbIcon
(
"down"
)
}
</
button
>
<
MessageAttachmentStrip
attachments=
{
message
.
attachments
}
/>
</>
)
:
null
}
</
div
>
)
:
null
}
<
div
className=
"message-card-meta"
>
<
span
className=
"message-timestamp"
aria
-
hidden=
"true"
>
{
formatMessageTimestamp
(
message
.
createdAt
)
}
</
span
>
{
messageActions
}
</
div
>
</
div
>
</
article
>
)
})
}
...
...
apps/ui/src/features/chat/useMessageTraces.ts
View file @
c3f99f53
...
...
@@ -96,12 +96,23 @@ export function useMessageTraces() {
setMessageTraceExpanded
(
messageId
,
false
)
},
[
setMessageTraceExpanded
])
const
removeMessageTrace
=
useCallback
((
messageId
:
string
)
=>
{
setMessageTraces
((
current
)
=>
{
if
(
!
(
messageId
in
current
))
{
return
current
}
const
{
[
messageId
]:
_removed
,
...
rest
}
=
current
return
rest
})
},
[])
return
{
messageTraces
,
renameMessageTrace
,
initializeMessageTrace
,
appendTrace
,
setMessageTraceExpanded
,
collapseMessageTrace
collapseMessageTrace
,
removeMessageTrace
}
}
apps/ui/src/styles/chat.css
View file @
c3f99f53
...
...
@@ -747,10 +747,17 @@
}
.conversation-shell
.message-bubble
,
.conversation-shell
.message-card-body
,
.conversation-shell
.message-bubble-assistant
,
.conversation-shell
.message-card.assistant
.message-bubble
{
max-width
:
100%
;
}
.conversation-shell
.message-card-meta
{
opacity
:
0.76
;
pointer-events
:
auto
;
transform
:
translateY
(
0
);
}
}
@media
(
max-width
:
720px
)
{
...
...
apps/ui/src/styles/components.css
View file @
c3f99f53
...
...
@@ -270,6 +270,33 @@
margin-top
:
6px
;
}
.message-card-body
{
display
:
grid
;
gap
:
4px
;
max-width
:
100%
;
}
.message-card-body-user
{
justify-self
:
end
;
}
.message-card-body-assistant
{
justify-self
:
start
;
}
.message-card-meta
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
min-height
:
28px
;
padding
:
0
2px
;
opacity
:
0
;
pointer-events
:
none
;
transform
:
translateY
(
-2px
);
transition
:
opacity
150ms
ease
,
transform
150ms
ease
;
}
.thinking-spinner
{
width
:
16px
;
height
:
16px
;
...
...
@@ -819,30 +846,57 @@
.message-card-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
margin-top
:
12px
;
justify-content
:
flex-start
;
opacity
:
0
;
transition
:
opacity
150ms
ease
;
gap
:
1px
;
width
:
max-content
;
margin-top
:
0
;
padding
:
2px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.68
);
border-radius
:
999px
;
background
:
rgba
(
248
,
250
,
252
,
0.9
);
box-shadow
:
0
6px
18px
rgba
(
15
,
23
,
42
,
0.06
);
backdrop-filter
:
blur
(
12px
);
justify-content
:
flex-end
;
pointer-events
:
auto
;
opacity
:
1
;
}
.message-card
:hover
.message-card-actions
{
.message-card
:hover
.message-card-meta
,
.message-card
:focus-within
.message-card-meta
{
pointer-events
:
auto
;
opacity
:
1
;
transform
:
translateY
(
0
);
}
.message-action-delete
:hover
{
background
:
#fef2f2
;
color
:
#dc2626
;
}
.message-action-icon
{
width
:
32
px
;
height
:
32
px
;
width
:
26
px
;
height
:
26
px
;
padding
:
0
;
border
:
0
;
border-radius
:
999px
;
background
:
#ffffff
;
color
:
#5f7773
;
box-shadow
:
0
10px
24px
rgba
(
17
,
24
,
39
,
0.08
);
background
:
transparent
;
color
:
#738196
;
cursor
:
pointer
;
box-shadow
:
none
;
transition
:
background-color
150ms
ease
,
color
150ms
ease
,
transform
120ms
ease
;
}
.message-action-icon
:hover
{
background
:
#f4fbfa
;
background
:
#eef6ff
;
color
:
#0f67de
;
}
.message-action-icon
:active
{
transform
:
scale
(
0.94
);
}
.message-action-icon
:focus-visible
{
outline
:
2px
solid
rgba
(
15
,
103
,
222
,
0.28
);
outline-offset
:
2px
;
}
.message-action-icon.copied
{
...
...
apps/ui/src/styles/theme-openclaw.css
View file @
c3f99f53
...
...
@@ -1014,6 +1014,23 @@
justify-content
:
flex-start
;
}
.conversation-shell
.message-card-body
{
width
:
auto
;
max-width
:
min
(
72%
,
760px
);
}
.conversation-shell
.message-card-body-user
{
margin-left
:
auto
;
}
.conversation-shell
.message-card-body-assistant
{
max-width
:
min
(
100%
,
880px
);
}
.conversation-shell
.message-card-body
.message-bubble
{
max-width
:
100%
;
}
.conversation-shell
.message-bubble
{
width
:
auto
;
display
:
grid
;
...
...
@@ -1095,30 +1112,17 @@
}
.conversation-shell
.message-timestamp
{
position
:
absolute
;
bottom
:
0
;
left
:
4px
;
z-index
:
1
;
position
:
static
;
color
:
#7c8da3
;
font-size
:
11px
;
line-height
:
1
;
opacity
:
0
;
pointer-events
:
none
;
transition
:
opacity
140ms
ease
;
}
.conversation-shell
.message-card.user
.message-timestamp
{
left
:
auto
;
right
:
4px
;
}
.conversation-shell
.message-card
:hover
.message-timestamp
,
.conversation-shell
.message-card
:focus-within
.message-timestamp
{
opacity
:
1
;
pointer-events
:
none
;
white-space
:
nowrap
;
}
.conversation-shell
.message-card-actions
{
margin-
top
:
8px
;
margin-
right
:
0
;
}
.conversation-shell
.composer-shell
{
...
...
apps/ui/src/tailwind.css
View file @
c3f99f53
...
...
@@ -73,6 +73,7 @@
.nav-item-icon
svg
,
.expert-chip-icon
svg
,
.message-action-icon
svg
,
.message-action-delete
svg
,
.composer-submit
svg
,
.attachment-trigger
svg
,
.markdown-code-copy
svg
{
...
...
@@ -104,11 +105,13 @@
@apply
mb-0;
}
.message-card
.assistant
.message-card-actions
{
.message-card
.message-card-meta
{
@apply
pointer-events-none;
}
.message-card.assistant
:hover
.message-card-actions
{
.message-card
:hover
.message-card-meta
,
.message-card
:focus-within
.message-card-meta
,
.message-card
.message-card-meta
:focus-within
{
@apply
pointer-events-auto;
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment