fix(client): address remaining conversation review feedback

This commit is contained in:
imccyu
2026-08-09 19:12:54 +08:00
parent 27682b9384
commit e11f630fcd
11 changed files with 47 additions and 18 deletions

View File

@@ -9,9 +9,11 @@ type TurnTailNodeViewProps = ChatNodeViewProps<'turn-tail'> & PropsRenderSlots<'
/** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */
export const TurnTailNodeView = memo(function TurnTailNodeView({
node, openFile, forkAt, renderSlotChain, t,
node, openFile, forkAt, renderSlotChain, t, useSession,
}: TurnTailNodeViewProps) {
const data = node.data
const hasLaterChatNode = useSession(snapshot =>
snapshot.chat.locations.getTurn(data.turn).at(-1) !== node.key)
const turn = node.location.kind === 'turn' || node.location.kind === 'step'
? node.location.turn
: undefined
@@ -34,7 +36,7 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
tokensPerSecond={data.tokensPerSecond}
clock="end"
onBranch={() => { forkAt(closing.finalNode.seq) }}
branchUnavailable={data.branchUnavailable}
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
className={css.actions}
t={t}
/>

View File

@@ -57,7 +57,7 @@ export interface TurnTailChatData {
readonly time: number
/** Last finalized content-bearing Assistant in this Turn. */
readonly closing: FinalAssistantChatData | null
/** Whether later Assistant/Step material makes the closing seq non-tail. */
/** Whether non-rendered later evidence makes the closing seq non-tail. */
readonly branchUnavailable: boolean
readonly ttftMs?: number
readonly tokensPerSecond?: number

View File

@@ -181,14 +181,12 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
if (key !== 'conversation.chat.node') return opts?.fallback ?? null
const nodeOwner = owner as RoutedChatNodeOwner
const nodeKey = opts?.hookContext as string | undefined
const useTurnData = ((dataKey: string) => {
return props.useSession((snapshot) => {
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
return location?.kind === 'turn' || location?.kind === 'step'
? location.turn.data.get(dataKey as never)
: undefined
})
}) as UseChatNodeTurnData
const useTurnData: UseChatNodeTurnData = dataKey => props.useSession((snapshot) => {
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
return location?.kind === 'turn' || location?.kind === 'step'
? location.turn.data.get(dataKey)
: undefined
})
const nodeProps = <Kind extends ChatNode['kind']>(): ChatNodeViewProps<Kind> => (
{ ...props, ...nodeOwner, useTurnData } as unknown as ChatNodeViewProps<Kind>
)
@@ -722,6 +720,28 @@ describe('ChatView', () => {
expect(h.forkAt.mock.calls).toEqual([[2]])
})
it('disables fork when the indexed Turn has a later steering Node', () => {
const base = chatSnapshotFixture({
nodes: [user(1, 'question'), assistant(2, 'answer')],
turnEnds: new Map([[1, 4]]),
})
const chat = {
...base,
locations: {
getTurn: (turn: number) => turn === 1
? [...base.locations.getTurn(turn), 'fixture:steering:later']
: base.locations.getTurn(turn),
getStep: (turn: number, step: number) => base.locations.getStep(turn, step),
},
}
const h = makeHarness({ chat })
const view = render(<h.ChatView {...h.props} />)
const branch = view.getByRole('button', { name: '在新对话中分支' })
expect(branch.getAttribute('aria-disabled')).toBe('true')
fireEvent.click(branch)
expect(h.forkAt).not.toHaveBeenCalled()
})
it('keeps final content actions but disables branch when Tool and interrupted Think follow it', () => {
const interruptedThink: AssistantMessageNode = {
kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2,

View File

@@ -36,7 +36,9 @@ interface DeliverablesState extends DeliverablesTurnData {
* Paths a call view reports having created or changed, by render intent rather
* than tool name: a diff card, or a generic card whose kind is `edit` (the
* shape `str_replace_editor`'s insert presents). Every other card produces
* nothing to open — a read looked, a delete removed, a terminal ran.
* nothing to open — a read looked, a delete removed, a terminal ran. Only
* root call views enter this Turn accumulator; nested Code Mode dispatches
* preserve the pre-assembly behavior and do not contribute independently.
*/
function producedPaths(view: ToolResultNode['callView']): readonly string[] {
if (view === null) return []