feat(web): restore actions after steering consumption

This commit is contained in:
imccyu
2026-08-02 15:50:05 +08:00
parent dffe955ed2
commit 3a9f78c55e
15 changed files with 91 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
// Shared IconActions chrome for user and assistant messages: copy live,
// branch wired through onBranch, date-aware clock.
// Shared IconActions chrome for user, steering, and assistant messages: copy
// live, optional branch wiring, and an optional date-aware clock.
import { useCallback } from 'react'
import {
@@ -13,12 +13,14 @@ 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
/** Unix epoch ms for the clock label; omitted for transient messages. */
time?: number | undefined
/** Clock before icons (user) or after (assistant). */
clock: 'start' | 'end'
/** Fork the session at this message. */
onBranch?: (() => void) | undefined
/** Whether to render the branch action; defaults to true. */
showBranch?: boolean | undefined
/** Parent layout class composed onto the actions row. */
className?: string | undefined
/** The owning view's locale seat, passed down as a plain prop. */
@@ -31,13 +33,13 @@ export interface MessageIconActionsProps {
* @returns The actions row element.
*/
export function MessageIconActions({
text, time, clock, onBranch, className, t,
text, time, clock, onBranch, showBranch = true, className, t,
}: MessageIconActionsProps) {
const day = useCalendarDay()
const onCopy = useCallback(() => {
void writeClipboard(text)
}, [text])
const clockEl = (
const clockEl = time === undefined ? null : (
<span className={clock === 'start' ? css.timeStart : css.timeEnd}>
{formatMessageClock(time, t, day)}
</span>
@@ -50,11 +52,13 @@ export function MessageIconActions({
<IconCopyOutline16 />
</button>
</Tooltip>
<Tooltip label={t('message.branch')} side="bottom">
<button type="button" className={css.action} aria-label={t('message.branch')} onClick={onBranch}>
<IconBranchOutline16 />
</button>
</Tooltip>
{showBranch && (
<Tooltip label={t('message.branch')} side="bottom">
<button type="button" className={css.action} aria-label={t('message.branch')} onClick={onBranch}>
<IconBranchOutline16 />
</button>
</Tooltip>
)}
{clock === 'end' ? clockEl : null}
</div>
)

View File

@@ -1,6 +1,6 @@
// MessageItem: simple chat nodes — user bubble (right-aligned, with
// clock + copy / branch IconActions), steering (same bubble, no actions),
// context injection, compaction marker, retry disclosure, and
// MessageItem: simple chat nodes — user and consumed-steering bubbles
// (right-aligned, with clock + copy / branch IconActions), pending steering
// (copy only), context injection, compaction marker, retry disclosure, and
// unknown-surface JSON rows.
import { memo, useEffect, useMemo, useState } from 'react'
@@ -167,7 +167,7 @@ function projectUserText(text: string): ReactNode {
return <>{parts}</>
}
/** Right-aligned bubble shared by user and steering rows (steering has no actions). */
/** Right-aligned bubble shared by user and steering rows. */
function UserStyleBubble({
content, actions, pending = false, t,
}: {
@@ -201,7 +201,22 @@ export function PendingSteeringBubble({ content, t }: {
content: readonly unknown[]
t: ChatViewSlotProps['t']
}): ReactNode {
return <UserStyleBubble content={content} pending t={t} />
return (
<UserStyleBubble
content={content}
pending
t={t}
actions={text => (
<MessageIconActions
text={text}
clock="start"
showBranch={false}
className={css.actions}
t={t}
/>
)}
/>
)
}
export const MessageItem = memo(function MessageItem({
@@ -210,6 +225,7 @@ export const MessageItem = memo(function MessageItem({
const truncated = (total: number): string => t('json.truncated', { total })
switch (node.kind) {
case 'user':
case 'steering':
return (
<UserStyleBubble
content={node.content}
@@ -226,8 +242,6 @@ export const MessageItem = memo(function MessageItem({
)}
/>
)
case 'steering':
return <UserStyleBubble content={node.content} t={t} />
case 'context':
return (
<ContextInjectionRow content={node.content} source={node.source} t={t} />