feat: slot system + entries/priority/errorreport + typert generator
This commit is contained in:
@@ -14,14 +14,14 @@ export interface ViewTab { id: string; label: string }
|
||||
|
||||
/**
|
||||
* Per-session state shared by conversation, chat-view, and details slots.
|
||||
* Unknown persisted view ids fall back to the first registered view.
|
||||
* Unknown persisted view ids fall back to the stable Chat view.
|
||||
*/
|
||||
export interface ChatStoreState {
|
||||
/** Details-linkage channel (conversation writes, details reads). */
|
||||
selection: SelectionTarget | null
|
||||
/** Composer draft (persisted; survives session switches and reloads). */
|
||||
draft: string
|
||||
/** Active conversation view id ('conversation.view' entry id); null falls back to the first view. */
|
||||
/** Active conversation view id ('conversation.view' entry id); null falls back to Chat. */
|
||||
view: string | null
|
||||
/**
|
||||
* One-shot inspect handoff: chat writes the call to reveal, the trajectory
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scrollBody:has([data-conversation-composer-overlay]) > .viewArea {
|
||||
.scrollBody:has([data-conversation-composer-overlay]) > :global([data-slot='conversation.session']) > .viewArea {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SessionId, SessionListState, SessionSummary } from '@deepseek-ai/d
|
||||
import type {
|
||||
ConversationSessionHeaderSlotProps, ConversationSessionSlotProps,
|
||||
} from '../contract/slots.ts'
|
||||
import type { ViewTab } from '../contract/views.ts'
|
||||
import css from './ConversationRoot.module.css'
|
||||
|
||||
/** Full props composed from the strict session body contract. */
|
||||
@@ -19,6 +20,15 @@ interface Breadcrumb {
|
||||
readonly displayTitle: string
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW_ID = 'chat'
|
||||
|
||||
/** Resolve by id and keep stale persisted selections on the stable Chat fallback. */
|
||||
function resolveActiveView(tabs: readonly ViewTab[], selectedId: string | null): ViewTab | undefined {
|
||||
const requestedId = selectedId ?? DEFAULT_VIEW_ID
|
||||
return tabs.find(view => view.id === requestedId)
|
||||
?? tabs.find(view => view.id === DEFAULT_VIEW_ID)
|
||||
}
|
||||
|
||||
function deriveAncestry(list: SessionListState, id: SessionId): readonly Breadcrumb[] {
|
||||
const chain: Breadcrumb[] = []
|
||||
const seen = new Set<SessionId>()
|
||||
@@ -54,8 +64,8 @@ export function ConversationSessionHeader({
|
||||
}: ConversationSessionHeaderProps) {
|
||||
useSyncExternalStore(views.subscribe, views.version)
|
||||
const tabs = views.list()
|
||||
const activeId = useStore(s => s.view) ?? 'chat'
|
||||
const active = tabs.find(view => view.id === activeId) ?? tabs[0]
|
||||
const selectedId = useStore(s => s.view)
|
||||
const active = resolveActiveView(tabs, selectedId)
|
||||
const ancestry = useSessions(s => deriveAncestry(s, sessionId), equalBreadcrumbs)
|
||||
const composerPhase = useSession(s => s.composerPhase)
|
||||
const blank = useSession(s => s.blank)
|
||||
@@ -131,8 +141,8 @@ export function ConversationSession({
|
||||
}: ConversationSessionProps) {
|
||||
useSyncExternalStore(views.subscribe, views.version)
|
||||
const tabs = views.list()
|
||||
const activeId = useStore(s => s.view) ?? 'chat'
|
||||
const active = tabs.find(view => view.id === activeId) ?? tabs[0]
|
||||
const selectedId = useStore(s => s.view)
|
||||
const active = resolveActiveView(tabs, selectedId)
|
||||
const composerPhase = useSession(s => s.composerPhase)
|
||||
const blank = useSession(s => s.blank)
|
||||
const inputState = useInput(s => s)
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type {
|
||||
ComposerBarOwnerProps,
|
||||
} from '../src/client/contract/slots.ts'
|
||||
import type { ViewTab } from '../src/client/contract/views.ts'
|
||||
|
||||
/** Machine-backed wiring over a sink spy. */
|
||||
function fakeWiring() {
|
||||
@@ -96,6 +97,8 @@ function mount(
|
||||
summaryOrigin?: 'subagent'
|
||||
/** A composer block another plugin raised for this session. */
|
||||
composerBlock?: { reason: string }
|
||||
/** Mutable view ledger used by registration-order regressions. */
|
||||
viewTabs?: ViewTab[]
|
||||
} = {},
|
||||
) {
|
||||
const root = sid('root')
|
||||
@@ -123,6 +126,15 @@ function mount(
|
||||
const stop = vi.fn()
|
||||
const open = vi.fn()
|
||||
const slotCalls: string[] = []
|
||||
const viewTabs = options.viewTabs ?? [
|
||||
{ id: 'chat', label: 'Chat' },
|
||||
{ id: 'trajectory', label: 'Trajectory' },
|
||||
]
|
||||
const views = {
|
||||
list: () => viewTabs,
|
||||
subscribe: () => () => {},
|
||||
version: () => 1,
|
||||
}
|
||||
/** Owner share handed to the two composer tool-row seats, per render. */
|
||||
const seatOwners: { key: string; owner: unknown }[] = []
|
||||
let pickerOwner: unknown
|
||||
@@ -146,14 +158,7 @@ function mount(
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
renderSlot={renderSlot as never}
|
||||
views={{
|
||||
list: () => [
|
||||
{ id: 'chat', label: 'Chat' },
|
||||
{ id: 'trajectory', label: 'Trajectory' },
|
||||
],
|
||||
subscribe: () => () => {},
|
||||
version: () => 1,
|
||||
}}
|
||||
views={views}
|
||||
open={open}
|
||||
t={t}
|
||||
/>
|
||||
@@ -173,14 +178,7 @@ function mount(
|
||||
useStore={bindSnapshotSelector(chat)}
|
||||
actions={chat.actions}
|
||||
renderSlot={renderSlot as never}
|
||||
views={{
|
||||
list: () => [
|
||||
{ id: 'chat', label: 'Chat' },
|
||||
{ id: 'trajectory', label: 'Trajectory' },
|
||||
],
|
||||
subscribe: () => () => {},
|
||||
version: () => 1,
|
||||
}}
|
||||
views={views}
|
||||
releaseSessionImages={vi.fn()}
|
||||
bindDraftMirror={write => wiring.bindMirror(write)}
|
||||
/>
|
||||
@@ -442,6 +440,26 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(b.view.getByRole('textbox')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps the Chat fallback selected by id when a view is inserted before it', () => {
|
||||
const viewTabs: ViewTab[] = [
|
||||
{ id: 'chat', label: 'Chat' },
|
||||
{ id: 'trajectory', label: 'Trajectory' },
|
||||
]
|
||||
const b = mount(conversationSnapshot(), undefined, undefined, { viewTabs })
|
||||
// A removed dynamic view leaves its persisted id behind. The visible
|
||||
// fallback is Chat and must stay Chat when another lower-order view lands.
|
||||
act(() => { b.chat.actions.setView('removed-view') })
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
|
||||
viewTabs.unshift({ id: 'new-view', label: 'New view' })
|
||||
b.rerender()
|
||||
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
expect(b.view.queryByTestId('view-new-view')).toBeNull()
|
||||
expect(b.view.getByRole('tab', { name: 'Chat' }).getAttribute('aria-selected')).toBe('true')
|
||||
expect(b.view.getByRole('tab', { name: 'New view' }).getAttribute('aria-selected')).toBe('false')
|
||||
})
|
||||
|
||||
it('rolls the pending workspace label back when switching fails', async () => {
|
||||
const selectWorkspace = vi.fn(async () => { throw new Error('connect failed') })
|
||||
const b = mount(
|
||||
|
||||
Reference in New Issue
Block a user