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:
creatixchu
2026-07-31 00:09:49 +08:00
138 changed files with 3057 additions and 505 deletions
@@ -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. */