Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # packages/client/runtime/README.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-conversation/tests/chat-view.spec.tsx # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api/sessions.ts
This commit is contained in:
@@ -306,6 +306,13 @@ export function apply(ctx: Context): void {
|
||||
},
|
||||
loadOlder: () => { void scoped.loadOlder() },
|
||||
loadImage: attachment => conversation.resolveImage(sessionId, attachment),
|
||||
forkAt: (seq) => {
|
||||
sessions.fork({ sessionId, atSeq: seq, increaseTitle: true })
|
||||
.then((childId) => { sessions.open(childId) })
|
||||
.catch(() => {
|
||||
// Fork or child-rename failure keeps the source view untouched.
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}, ChatView)
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface AssistantMarkdownProps {
|
||||
loadImage?: ImageLoader
|
||||
/** Unix epoch ms for the finalized IconActions clock; omitted while streaming. */
|
||||
time?: number | undefined
|
||||
/** Event sequence used as the fork boundary; omitted while streaming. */
|
||||
seq?: number | undefined
|
||||
/** Fork the session through the turn containing this finalized message. */
|
||||
onFork?: ((seq: number) => void) | undefined
|
||||
}
|
||||
|
||||
function firstLine(text: string): string {
|
||||
@@ -62,7 +66,7 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
|
||||
}
|
||||
|
||||
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
blocks, streaming, interrupted, loadImage = unavailableImage, time,
|
||||
blocks, streaming, interrupted, loadImage = unavailableImage, time, seq, onFork,
|
||||
}: AssistantMarkdownProps) {
|
||||
const last = blocks.length - 1
|
||||
// Tool-call heads render as tool rows in the chat view's grouping pass, so
|
||||
@@ -94,6 +98,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
text={copyText(blocks)}
|
||||
time={time}
|
||||
clock="end"
|
||||
onBranch={onFork === undefined || seq === undefined ? undefined : () => { onFork(seq) }}
|
||||
className={css.actions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -233,7 +233,7 @@ function StreamingTail({ useSession, onGrow, loadImage }: {
|
||||
* render through the declared keyed hole's renderSlot share).
|
||||
*/
|
||||
export function ChatView({
|
||||
useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder, loadImage,
|
||||
useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder, loadImage, forkAt,
|
||||
}: ChatViewSlotProps) {
|
||||
const nodes = useSession(s => s.nodes)
|
||||
// Workspace root off the session list row: path summaries display relative to it.
|
||||
@@ -382,6 +382,8 @@ export function ChatView({
|
||||
interrupted={node.interrupted}
|
||||
loadImage={loadImage}
|
||||
time={node.time}
|
||||
seq={node.seq}
|
||||
onFork={forkAt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -390,7 +392,7 @@ export function ChatView({
|
||||
}
|
||||
/* v8 ignore next -- tool-result never reaches here: deriveChatFlow folds them into groups. */
|
||||
if (node.kind === 'tool-result') return null
|
||||
return <MessageItem key={item.key} node={node} loadImage={loadImage} />
|
||||
return <MessageItem key={item.key} node={node} loadImage={loadImage} onFork={forkAt} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Shared IconActions chrome for user and assistant messages: copy / branch
|
||||
// live (branch still a stub), date-aware clock, optional edit stub.
|
||||
// Shared IconActions chrome for user and assistant messages: copy live,
|
||||
// branch wired through onBranch, date-aware clock,
|
||||
// optional edit stub.
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
@@ -18,17 +19,19 @@ export interface MessageIconActionsProps {
|
||||
clock: 'start' | 'end'
|
||||
/** When true, append the stub edit control (user bubble). */
|
||||
edit?: boolean | undefined
|
||||
/** Fork the session at this message. */
|
||||
onBranch?: (() => void) | undefined
|
||||
/** Parent layout 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.
|
||||
* @param props - Copy text, event time, clock side, optional edit, branch callback, className.
|
||||
* @returns The actions row element.
|
||||
*/
|
||||
export function MessageIconActions({
|
||||
text, time, clock, edit, className,
|
||||
text, time, clock, edit, onBranch, className,
|
||||
}: MessageIconActionsProps) {
|
||||
const day = useCalendarDay()
|
||||
const onCopy = useCallback(() => {
|
||||
@@ -48,7 +51,7 @@ export function MessageIconActions({
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="在新对话中分支" side="bottom">
|
||||
<button type="button" className={css.action} aria-label="在新对话中分支">
|
||||
<button type="button" className={css.action} aria-label="在新对话中分支" onClick={onBranch}>
|
||||
<IconBranchOutline16 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { ImageGallery, type ImageLoader } from './MessageImage.tsx'
|
||||
export interface MessageItemProps {
|
||||
node: UserMessageNode | SteeringMessageNode | ContextMessageNode | UnknownSurfaceNode
|
||||
loadImage?: ImageLoader
|
||||
/** Fork the session through the turn containing this message (user-bubble branch action). */
|
||||
onFork?: (seq: number) => void
|
||||
}
|
||||
|
||||
type UserImage = Extract<UserMessageNode['content'][number], { type: 'image' }>
|
||||
@@ -73,7 +75,9 @@ function projectUserText(text: string): ReactNode {
|
||||
return <>{parts}</>
|
||||
}
|
||||
|
||||
export const MessageItem = memo(function MessageItem({ node, loadImage = unavailableImage }: MessageItemProps) {
|
||||
export const MessageItem = memo(function MessageItem({
|
||||
node, loadImage = unavailableImage, onFork,
|
||||
}: MessageItemProps) {
|
||||
switch (node.kind) {
|
||||
case 'user': {
|
||||
const { text, images, rest } = contentParts(node.content)
|
||||
@@ -91,6 +95,7 @@ export const MessageItem = memo(function MessageItem({ node, loadImage = unavail
|
||||
time={node.time}
|
||||
clock="start"
|
||||
edit
|
||||
onBranch={onFork === undefined ? undefined : () => { onFork(node.seq) }}
|
||||
className={css.actions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -438,6 +438,8 @@ export interface ChatViewInjected {
|
||||
loadOlder: () => void
|
||||
/** Resolve a session-authorized historical image for inline display. */
|
||||
loadImage: (attachment: ImageAttachmentRef) => Promise<string>
|
||||
/** Fork the session through the turn containing the message at `seq`, then open the child. */
|
||||
forkAt: (seq: number) => void
|
||||
}
|
||||
|
||||
/** Full chat-view component props: runtime share & the declared toolview/commandview holes' render share & store share & injected share. */
|
||||
|
||||
Reference in New Issue
Block a user