|
@@ -80,16 +80,25 @@ function mount(
|
|
|
snapshot: ConversationSnapshot,
|
|
snapshot: ConversationSnapshot,
|
|
|
workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }],
|
|
workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }],
|
|
|
retargetWorkspace = vi.fn(async (_workspaceId: WorkspaceId) => {}),
|
|
retargetWorkspace = vi.fn(async (_workspaceId: WorkspaceId) => {}),
|
|
|
- /** When true, mimic overlay:true chain siblings (hidden fallback + takeover). */
|
|
|
|
|
- overlayTakeover = false,
|
|
|
|
|
|
|
+ options: {
|
|
|
|
|
+ /** When true, mimic overlay:true chain siblings (hidden fallback + takeover). */
|
|
|
|
|
+ overlayTakeover?: boolean
|
|
|
|
|
+ /** The session list summary's `blank` flag — independent of the snapshot's. */
|
|
|
|
|
+ summaryBlank?: boolean
|
|
|
|
|
+ /** Drop the session's summary row entirely (a session the list has not caught up with). */
|
|
|
|
|
+ omitSummaryRow?: boolean
|
|
|
|
|
+ } = {},
|
|
|
) {
|
|
) {
|
|
|
const root = sid('root')
|
|
const root = sid('root')
|
|
|
|
|
+ const rootRow = { id: root, displayTitle: 'Root', running: false, waitingApproval: false, blank: false, updatedAt: 1 }
|
|
|
|
|
+ const childRow = {
|
|
|
|
|
+ id: SID, displayTitle: 'Child', parentId: root, cwd: '/projects/one',
|
|
|
|
|
+ running: false, waitingApproval: false, blank: options.summaryBlank ?? false, updatedAt: 2,
|
|
|
|
|
+ }
|
|
|
|
|
+ const listed = options.omitSummaryRow !== true
|
|
|
const sessions = createSnapshotStore<SessionListState>({
|
|
const sessions = createSnapshotStore<SessionListState>({
|
|
|
- ids: [root, SID],
|
|
|
|
|
- byId: {
|
|
|
|
|
- [root]: { id: root, displayTitle: 'Root', running: false, waitingApproval: false, blank: false, updatedAt: 1 },
|
|
|
|
|
- [SID]: { id: SID, displayTitle: 'Child', parentId: root, cwd: '/projects/one', running: false, waitingApproval: false, blank: false, updatedAt: 2 },
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ ids: listed ? [root, SID] : [root],
|
|
|
|
|
+ byId: { [root]: rootRow, ...listed && { [SID]: childRow } },
|
|
|
current: SID,
|
|
current: SID,
|
|
|
phase: 'ready',
|
|
phase: 'ready',
|
|
|
})
|
|
})
|
|
@@ -167,7 +176,7 @@ function mount(
|
|
|
return <div data-testid={`view-${opts?.only ?? key}`} />
|
|
return <div data-testid={`view-${opts?.only ?? key}`} />
|
|
|
}) as ConversationRootProps['renderSlot']
|
|
}) as ConversationRootProps['renderSlot']
|
|
|
const renderSlotChain = ((_key, _owner, opts) => (
|
|
const renderSlotChain = ((_key, _owner, opts) => (
|
|
|
- overlayTakeover
|
|
|
|
|
|
|
+ options.overlayTakeover === true
|
|
|
? (
|
|
? (
|
|
|
<>
|
|
<>
|
|
|
<div data-chain-overlay-fallback="conversation.composer" style={{ display: 'none' }}>
|
|
<div data-chain-overlay-fallback="conversation.composer" style={{ display: 'none' }}>
|
|
@@ -229,7 +238,7 @@ describe('ConversationRoot resident composer', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('sticky composer seat wraps the whole overlay chain, not only the fallback stack', () => {
|
|
it('sticky composer seat wraps the whole overlay chain, not only the fallback stack', () => {
|
|
|
- const b = mount(conversationSnapshot(), undefined, undefined, true)
|
|
|
|
|
|
|
+ const b = mount(conversationSnapshot(), undefined, undefined, { overlayTakeover: true })
|
|
|
const seat = b.view.container.querySelector('[data-composer-seat]')
|
|
const seat = b.view.container.querySelector('[data-composer-seat]')
|
|
|
const takeover = b.view.getByTestId('composer-takeover')
|
|
const takeover = b.view.getByTestId('composer-takeover')
|
|
|
const fallback = b.view.container.querySelector('[data-chain-overlay-fallback="conversation.composer"]')
|
|
const fallback = b.view.container.querySelector('[data-chain-overlay-fallback="conversation.composer"]')
|
|
@@ -270,6 +279,39 @@ describe('ConversationRoot resident composer', () => {
|
|
|
expect(b.view.getByText('Selected Folder')).toBeTruthy()
|
|
expect(b.view.getByText('Selected Folder')).toBeTruthy()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ it('settling phase: a summary that does not prove the session blank hides the composer while it opens', () => {
|
|
|
|
|
+ const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true, openState: 'loading' }))
|
|
|
|
|
+ const root = b.view.container.querySelector('[data-phase]')
|
|
|
|
|
+ expect(root?.getAttribute('data-phase')).toBe('settling')
|
|
|
|
|
+ expect(b.view.queryByText('开始构建吧')).toBeNull()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('settling phase: a session the list has no row for settles conservatively', () => {
|
|
|
|
|
+ const b = mount(
|
|
|
|
|
+ conversationSnapshot({ composerPhase: 'blank', blank: true, openState: 'loading' }),
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ { omitSummaryRow: true },
|
|
|
|
|
+ )
|
|
|
|
|
+ const root = b.view.container.querySelector('[data-phase]')
|
|
|
|
|
+ expect(root?.getAttribute('data-phase')).toBe('settling')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('startup auto-selection: a summary-proven blank session opens straight into the hero', () => {
|
|
|
|
|
+ const b = mount(
|
|
|
|
|
+ conversationSnapshot({ composerPhase: 'blank', blank: true, openState: 'loading' }),
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ { summaryBlank: true },
|
|
|
|
|
+ )
|
|
|
|
|
+ // The summary already proves the outcome, so the settling hide would only
|
|
|
|
|
+ // blank the column for the history round-trip.
|
|
|
|
|
+ const root = b.view.container.querySelector('[data-phase]')
|
|
|
|
|
+ expect(root?.getAttribute('data-phase')).toBe('hero')
|
|
|
|
|
+ expect(b.view.getByText('开始构建吧')).toBeTruthy()
|
|
|
|
|
+ expect(b.view.getByRole('textbox')).toBeTruthy()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
it('same textarea DOM node survives the hero → active flip into the sticky scrollport', () => {
|
|
it('same textarea DOM node survives the hero → active flip into the sticky scrollport', () => {
|
|
|
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
|
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
|
|
const before = b.view.getByRole('textbox')
|
|
const before = b.view.getByRole('textbox')
|