Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/config-catalog.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/index.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/runtime/src/client/sessions/conversation.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/apply.ts
#	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-trajectory/tests/views.spec.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/index.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/fetch-carrier.spec.ts
#	packages/llm/llm-deepseek/src/adapter.ts
#	packages/llm/llm-deepseek/tests/adapter.spec.ts
#	packages/llm/llm-deepseek/tests/serialize.spec.ts
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/index.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm/src/types.ts
#	packages/ui/tui/README.i18n.yaml
#	packages/ui/tui/src/index.ts
#	packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
Yichen Jiang
2026-07-28 11:41:40 +08:00
1499 changed files with 48621 additions and 21956 deletions

View File

@@ -3,7 +3,7 @@
// hero (blank session) and active phases — same textarea DOM node, machine-
// owned draft, and the hero workspace picker (switching = retargetWorkspace).
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type {
@@ -48,14 +48,18 @@ const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState =>
function conversationSnapshot(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
return {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, lastAgentError: null,
...overrides,
}
}
function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }]) {
function mount(
snapshot: ConversationSnapshot,
workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }],
retargetWorkspace = vi.fn(async (_workspaceId: WorkspaceId) => {}),
) {
const root = sid('root')
const sessions = createSnapshotStore<SessionListState>({
ids: [root, SID],
@@ -76,7 +80,6 @@ function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] =
const inputActions = wiring.actions
const stop = vi.fn()
const open = vi.fn()
const retargetWorkspace = vi.fn()
const slotCalls: string[] = []
let pickerOwner: unknown
const renderSlot = ((key: string, owner: object, opts?: { only?: string }) => {
@@ -162,7 +165,13 @@ describe('ConversationRoot resident composer', () => {
})
it('hero phase: same textarea, hero chrome, no header, picker switches the workspace', () => {
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
const b = mount(
conversationSnapshot({ composerPhase: 'blank', blank: true }),
[
{ ...workspace('one'), sessionIds: [SID] },
{ ...workspace('second'), title: 'Selected Folder' },
],
)
// Hero chrome present, view ring absent.
expect(b.view.getByText("Let's start building")).toBeTruthy()
expect(b.view.queryByTestId('view-chat')).toBeNull()
@@ -177,8 +186,9 @@ describe('ConversationRoot resident composer', () => {
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
const owner = b.pickerOwner() as { open: boolean; onPick(id: WorkspaceId): void }
expect(owner.open).toBe(true)
owner.onPick(wid('second'))
act(() => { owner.onPick(wid('second')) })
expect(b.retargetWorkspace).toHaveBeenCalledWith(wid('second'))
expect(b.view.getByText('Selected Folder')).toBeTruthy()
})
it('textarea DOM identity survives the hero → active flip', () => {
@@ -195,6 +205,24 @@ describe('ConversationRoot resident composer', () => {
expect(b.view.getByTestId('view-chat')).toBeTruthy()
})
it('rolls the pending workspace label back when switching fails', async () => {
const selectWorkspace = vi.fn(async () => { throw new Error('connect failed') })
const b = mount(
conversationSnapshot({ composerPhase: 'blank', blank: true }),
[
{ ...workspace('one'), sessionIds: [SID] },
{ ...workspace('second'), title: 'Selected Folder' },
],
selectWorkspace,
)
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
const owner = b.pickerOwner() as { onPick(id: WorkspaceId): void }
await act(async () => { owner.onPick(wid('second')); await Promise.resolve() })
expect(selectWorkspace).toHaveBeenCalledWith(wid('second'))
expect(b.view.queryByText('Selected Folder')).toBeNull()
expect(b.view.getByText('one')).toBeTruthy()
})
it('blank session keeps the interactive picker chip (workspace switchable until the first message)', () => {
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
const chip = b.view.getByRole('button', { name: 'Choose workspace' })