fix(client): route turn-tail through chain selector
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
// ProducedFiles: the produced-file row a finished turn ends with. The paths
|
||||
// come from the mutation tools' follow-along locations (see
|
||||
// producedForClosing), never from the closing prose, so the answer carries
|
||||
// its own output whether or not the model remembered to name it. Clicking one
|
||||
// goes through the same openFile the tool rows use — the Host's own opener,
|
||||
// on the Host machine.
|
||||
// come pre-matched by the turn-tail chain from the mutation tools'
|
||||
// follow-along locations, never from the closing prose. Clicking one goes
|
||||
// through the same openFile the tool rows use — the Host's own opener, on the
|
||||
// Host machine.
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { producedForClosing } from './turn-deliverables.ts'
|
||||
import type { NS } from './locales.ts'
|
||||
import css from './ProducedFiles.module.css'
|
||||
|
||||
@@ -21,21 +18,17 @@ function basename(path: string): string {
|
||||
return at === -1 ? path : path.slice(at + 1)
|
||||
}
|
||||
|
||||
/** Full props: the turn-tail owner currency plus this plugin's locale seat. */
|
||||
export type ProducedFilesProps = TurnTailOwnerProps & PropsLocale<typeof NS>
|
||||
/** Matched paths plus the opener and locale seats needed to present them. */
|
||||
export type ProducedFilesProps = Pick<TurnTailOwnerProps, 'openFile'> & {
|
||||
matched: readonly string[]
|
||||
} & PropsLocale<typeof NS>
|
||||
|
||||
/**
|
||||
* Render one turn's produced files as openable chips.
|
||||
* @param props - the tail hole's owner currency (snapshot nodes, the closing
|
||||
* assistant's seq, the chat view's file opener) and the locale seat.
|
||||
* @returns The row, or `null` when the turn produced nothing.
|
||||
* @param props - selector-matched paths, the chat view's file opener, and the locale seat.
|
||||
* @returns The produced-files row.
|
||||
*/
|
||||
export function ProducedFiles({ nodes, seq, openFile, t }: ProducedFilesProps) {
|
||||
// Per-closing-message derivation over the windowed snapshot: O(nodes) on
|
||||
// node-identity change only, which is the same cadence the owning view
|
||||
// re-derives its own flow at.
|
||||
const paths = useMemo(() => producedForClosing(nodes, seq), [nodes, seq])
|
||||
if (paths.length === 0) return null
|
||||
export function ProducedFiles({ matched: paths, openFile, t }: ProducedFilesProps) {
|
||||
const shown = paths.slice(0, SHOWN)
|
||||
const hidden = paths.length - shown.length
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { ProducedFiles } from './ProducedFiles.tsx'
|
||||
import { en, NS, zh, type DeliverablesKey } from './locales.ts'
|
||||
import { selectProducedFiles } from './turn-deliverables.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
@@ -34,8 +35,7 @@ export function apply(ctx: ClientContext): void {
|
||||
'conversation.chat.turnTail',
|
||||
() => ctx.slots.register({
|
||||
name: 'conversation.chat.turnTail',
|
||||
id: 'produced-files',
|
||||
order: 0,
|
||||
select: selectProducedFiles,
|
||||
locale: NS,
|
||||
}, ProducedFiles),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* own follow-along `locations`, never the closing prose.
|
||||
*/
|
||||
import type { ConversationNode, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
|
||||
/**
|
||||
* Paths a call view reports having created or changed, by render intent rather
|
||||
@@ -76,3 +77,13 @@ export function producedForClosing(nodes: readonly ConversationNode[], seq: numb
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim the turn-tail chain only when its closing turn produced files.
|
||||
* @param owner - Turn-tail owner currency for the closing assistant.
|
||||
* @returns Produced paths as the component's match, or null to decline before mount.
|
||||
*/
|
||||
export function selectProducedFiles({ nodes, seq }: TurnTailOwnerProps): readonly string[] | null {
|
||||
const paths = producedForClosing(nodes, seq)
|
||||
return paths.length === 0 ? null : paths
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
import { apply as applyLocale } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { ProducedFiles } from '../src/client/ProducedFiles.tsx'
|
||||
import { producedForClosing } from '../src/client/turn-deliverables.ts'
|
||||
import { producedForClosing, selectProducedFiles } from '../src/client/turn-deliverables.ts'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
import { apply as applyNode } from '../src/index.ts'
|
||||
import { apply as applyInvariant } from '../src/invariant.ts'
|
||||
@@ -64,6 +64,8 @@ describe('producedForClosing derivation', () => {
|
||||
assistant(9, 'second turn', 2),
|
||||
]
|
||||
expect(producedForClosing(nodes, 7)).toEqual(['out/index.html', 'out/app.css'])
|
||||
expect(selectProducedFiles({ nodes, seq: 7, openFile: () => {} })).toEqual(['out/index.html', 'out/app.css'])
|
||||
expect(selectProducedFiles({ nodes, seq: 9, openFile: () => {} })).toBeNull()
|
||||
// A turn that produced nothing yields the empty list, and so does an
|
||||
// anchor the window does not contain.
|
||||
expect(producedForClosing(nodes, 9)).toEqual([])
|
||||
@@ -126,8 +128,7 @@ describe('ProducedFiles row', () => {
|
||||
// it shows and says so rather than dropping the rest silently.
|
||||
const paths = ['deep/a.html', 'b.css', 'c.ts', 'd.ts', 'e.ts', 'f.ts', 'g.ts']
|
||||
const openFile = vi.fn<(path: string) => void>()
|
||||
const nodes: ConversationNode[] = [user(1, 'build it'), wrote(2, 'w', ...paths), assistant(3, 'done', 1)]
|
||||
const view = render(<ProducedFiles nodes={nodes} seq={3} openFile={openFile} t={t} />)
|
||||
const view = render(<ProducedFiles matched={paths} openFile={openFile} t={t} />)
|
||||
expect(view.getByText('产物')).toBeTruthy()
|
||||
// Chips carry the basename; the full path stays reachable as the title.
|
||||
const chip = view.getByRole('button', { name: '打开 deep/a.html' })
|
||||
@@ -138,12 +139,6 @@ describe('ProducedFiles row', () => {
|
||||
fireEvent.click(chip)
|
||||
expect(openFile).toHaveBeenCalledWith('deep/a.html')
|
||||
})
|
||||
|
||||
it('a turn that produced nothing renders no row at all', () => {
|
||||
const nodes: ConversationNode[] = [user(1, 'hi'), assistant(2, 'hello', 1)]
|
||||
const view = render(<ProducedFiles nodes={nodes} seq={2} openFile={() => {}} t={t} />)
|
||||
expect(view.container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('package shells', () => {
|
||||
@@ -169,7 +164,7 @@ describe('plugin registration', () => {
|
||||
// The owning view's child declaration, stood up by a bench root entry.
|
||||
ctx.slots.register({
|
||||
name: 'root',
|
||||
children: { 'conversation.chat.turnTail': { kind: 'list', scope: 'session' } },
|
||||
children: { 'conversation.chat.turnTail': { kind: 'chain', scope: 'session' } },
|
||||
} as never, () => null)
|
||||
await ctx.plugin({ inject: ['slots'], apply: applyLocale }).await()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user