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

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md
#	THIRD_PARTY_NOTICES.md
#	apps/cli/composition.md
#	apps/cli/config/base.cordis.yml
#	apps/cli/package.json
#	apps/cli/src/app-cli-entry.ts
#	apps/cli/src/bin.ts
#	apps/cli/tests/args.spec.ts
#	apps/web/tests/built-boot.snapshot.ts
#	apps/web/tests/navigation-panes.e2e.ts
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/bundle/README.i18n.yaml
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/http-bridge.ts
#	packages/client/connection/src/index.ts
#	packages/client/connection/tests/fixture.spec.ts
#	packages/client/connection/tests/node-half.spec.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/runtime/src/client/contract/session.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.module.css
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/input/contract.ts
#	packages/client/ui-conversation/src/client/input/facade.ts
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/src/client/locales.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-conversation/tests/apply-inject.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/input-matrix.spec.tsx
#	packages/client/ui-conversation/tests/input-scenarios.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	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/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/apiproxy/tests/rpc-schemas.spec.ts
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/README.zh.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm/README.i18n.yaml
#	packages/ui/tui/README.md
#	packages/ui/tui/README.zh.md
#	packages/ui/tui/src/components/content.ts
#	packages/ui/tui/src/components/transcript.ts
#	packages/ui/tui/tests/tui.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 01:56:29 +08:00
3973 changed files with 144864 additions and 60887 deletions

View File

@@ -1,7 +1,7 @@
// @vitest-environment jsdom
/**
* QueueDock rendering and operations: authoritative rows, inline editing,
* collapse state, removal, failure notices, and live retirement.
* collapse state, removal, strict steering, failure notices, and live retirement.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
@@ -23,14 +23,18 @@ const SID = 's1' as SessionId
const iid = (id: string): QueueItemId => id as QueueItemId
function row(id: string, text: string | null, preview = text ?? '[image]'): QueuedMessage {
return { id: iid(id), preview, text }
return {
id: iid(id), messageId: `message-${id}` as never, placement: 'queued',
content: text === null ? [{ type: 'image', data: 'x' } as never] : [{ type: 'text', text }],
preview, text,
}
}
function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
return {
sessionId: SID, nodes: [], partial: null, runningCalls: [], codeDispatches: new Map(),
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue, running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
hasMore: false, loadingOlder: false, promptError: null, blank: false, subagent: null, lastAgentError: null,
}
}
@@ -86,6 +90,14 @@ describe('QueueDock', () => {
expect(container.innerHTML).toBe('')
})
it('leaves pending steering to the conversation flow', () => {
const steering = { ...row('s-1', 'interrupt'), placement: 'steering' as const }
const snap = snapshotWith([steering])
const source = liveSession(snap)
const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
expect(container.innerHTML).toBe('')
})
it('renders one row directly and defaults multiple rows to a collapsible count header', () => {
const single = snapshotWith([row('i-1', 'one')])
const source = liveSession(single)
@@ -188,10 +200,10 @@ describe('QueueDock', () => {
fireEvent.click(getByRole('button', { name: '2 条排队消息' }))
expect([...container.querySelectorAll('li')].map(item => item.textContent))
.toEqual(['第一条排队消息', 'image [image]'])
expect(container.querySelectorAll('button')).toHaveLength(5)
expect(container.querySelectorAll('button')).toHaveLength(7)
expect(container.querySelectorAll('[aria-label="编辑排队消息"]')).toHaveLength(2)
expect(container.querySelectorAll('[aria-label="删除排队消息"]')).toHaveLength(2)
expect(container.querySelectorAll('[aria-label="立即发送排队消息"]')).toHaveLength(0)
expect(container.querySelectorAll('[aria-label="插话发送"]')).toHaveLength(2)
expect((container.querySelectorAll('[aria-label="编辑排队消息"]')[0] as HTMLButtonElement).disabled).toBe(false)
expect((container.querySelectorAll('[aria-label="编辑排队消息"]')[1] as HTMLButtonElement).disabled).toBe(true)
expect(container.querySelectorAll('[aria-label="编辑排队消息"]')[1]?.getAttribute('title'))
@@ -274,6 +286,68 @@ describe('QueueDock', () => {
})
})
it('strictly steers complete row content only while the agent is running', async () => {
const running = snapshotWith([row('i-steer', null, 'image [image]')])
const source = liveSession(running)
const updateQueue = vi.fn(() => Promise.resolve())
const rendered = render(
<QueueDock {...kitFor(running, { updateQueue })} useSession={source.useSession} />,
)
const button = rendered.getByLabelText('插话发送')
expect(button).toHaveProperty('disabled', false)
fireEvent.click(button)
await waitFor(() => {
expect(updateQueue).toHaveBeenCalledWith(iid('i-steer'), { kind: 'steer' })
})
act(() => { source.push({ ...running, running: false }) })
expect(rendered.getByLabelText('插话发送')).toHaveProperty('disabled', true)
expect(rendered.getByLabelText('插话发送').getAttribute('title')).toBe('仅运行中可插话发送')
})
it('renders a session-backed subagent Queue without unsupported actions', () => {
const snap = {
...snapshotWith([row('i-subagent', 'pending child follow-up')]),
subagent: {
address: {
parentSessionId: 'parent' as SessionId,
childSessionId: SID,
mode: 'continuable' as const,
},
parentAvailable: true,
},
}
const source = liveSession(snap)
const view = render(
<QueueDock {...kitFor(snap)} useSession={source.useSession} />,
)
expect(view.getByText('pending child follow-up')).toBeTruthy()
expect(view.queryByLabelText('编辑排队消息')).toBeNull()
expect(view.queryByLabelText('删除排队消息')).toBeNull()
expect(view.queryByLabelText('插话发送')).toBeNull()
})
it('keeps the row and reports a genuine steer failure', async () => {
const snap = snapshotWith([row('i-steer-race', 'pending steer')])
const source = liveSession(snap)
const notify = vi.fn()
const updateQueue = vi.fn(() => Promise.reject(new Error('transport failed')))
const { getByLabelText, getByText } = render(
<QueueDock {...kitFor(snap, { updateQueue, notify })} useSession={source.useSession} />,
)
fireEvent.click(getByLabelText('插话发送'))
await waitFor(() => {
expect(notify).toHaveBeenCalledWith(
'error',
'插话发送失败,请重试。',
)
})
expect(getByText('pending steer')).toBeTruthy()
})
it('keeps the row and surfaces a notice when an operation loses the claim race', async () => {
const snap = snapshotWith([row('i-race', 'pending')])
const source = liveSession(snap)
@@ -302,8 +376,10 @@ describe('QueueDock', () => {
it('registers as the terminal composer-context entry', () => {
expect(queueDockEntry.name).toBe('conversation-queue-dock')
expect(queueDockEntry.inject).toEqual(['slots', 'conversation', 'sessions'])
const register = vi.fn()
queueDockEntry.apply({ slots: { register } } as never)
const register = vi.fn(() => () => undefined)
const inject = vi.fn((_name: string, callback: () => () => void) => callback())
queueDockEntry.apply({ slots: { inject, register } } as never)
expect(inject).toHaveBeenCalledWith('conversation.input.dock', expect.any(Function))
expect(register).toHaveBeenCalledWith(
expect.objectContaining({ name: 'conversation.input.dock', id: 'queue', order: 20 }),
QueueDock,