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

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml
#	apps/web/tests/code-mode-fixture.snapshot.ts
#	apps/web/tests/image-display.snapshot.ts
#	docs/architecture.i18n.yaml
#	docs/module-graph.md
#	packages/client/test-runtime/tests/runtime.spec.tsx
#	packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
#	packages/client/ui-conversation/tests/terminal-card.spec.tsx
#	packages/client/ui-trajectory/src/client/layout.ts
This commit is contained in:
creatixchu
2026-07-30 10:46:50 +08:00
38 changed files with 75 additions and 444 deletions

View File

@@ -1419,24 +1419,17 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
},
host: {
async describe(request) {
const activeModel = (await ctx.llm.listModels(defaults.provider))
.find(model => model.id === defaults.model)
describe(request) {
// TODO(step2): version should read apps/cli's package.json; placeholder for now.
return ok(request, {
return Promise.resolve(ok(request, {
version: '0.0.1',
// Same source as session.create's fallback: the UI's default project
// must match where an unspecified-cwd session actually lands.
cwd: defaults.cwd,
provider: defaults.provider,
model: defaults.model,
...activeModel === undefined ? {} : { activeModel },
imageLimits: {
...ctx.attachments.imageLimits,
mediaTypes: [...ctx.attachments.imageLimits.mediaTypes],
},
attachedSessions: ctx.agents.list().length,
})
}))
},
async pickDirectory(request, signal) {

View File

@@ -3,14 +3,9 @@
*/
import { z } from 'zod'
import type { ModelModality } from '@deepseek-ai/dsh-llm'
import type { DirectoryEntry } from './host.ts'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { imageMediaTypeSchema } from './sessions.schema.ts'
/** Merge-extensible modality passthrough: declaration merging cannot extend a runtime Zod union. */
const modalitySchema = z.string() as unknown as z.ZodType<ModelModality>
/** host.describe request payload (empty object literal). */
export const hostDescribeRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'host.describe'>>>
@@ -21,21 +16,6 @@ export const hostDescribeValueSchema = z.object({
cwd: z.string(),
provider: z.string().optional(),
model: z.string().optional(),
activeModel: z.object({
provider: z.string(),
id: z.string(),
name: z.string(),
description: z.string().optional(),
inputModalities: z.array(modalitySchema).optional(),
outputModalities: z.array(modalitySchema).optional(),
}).optional(),
imageLimits: z.object({
maxImageBytes: z.number().int().positive(),
maxImagesPerMessage: z.number().int().positive(),
maxMessageImageBytes: z.number().int().positive(),
maxImagePixels: z.number().int().positive(),
mediaTypes: z.array(imageMediaTypeSchema),
}).optional(),
attachedSessions: z.number().int().nonnegative(),
}) satisfies z.ZodType<Wire<ResponseValue<'host.describe'>>>

View File

@@ -4,8 +4,6 @@
*/
import type { RpcRequest, RpcResponse } from './rpc.ts'
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm/types'
/** One directory row of a listing: a child entry or a breadcrumb ancestor. */
export interface DirectoryEntry {
@@ -49,10 +47,6 @@ export interface HostApi {
cwd: string
provider?: string
model?: string
/** Catalog entry for the active route; absent means its capabilities are unknown. */
activeModel?: LlmModelInfo
/** Resolved authoritative image-upload limits. */
imageLimits?: ImageAttachmentLimits
attachedSessions: number
}>>

View File

@@ -259,40 +259,6 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
expect((await c.host.describe({})).result.ok).toBe(true)
})
it('round-trips a declaration-merged model modality through host.describe', async () => {
const c = client(fakeApi({
hostDescription: {
version: 'v',
cwd: '/w',
activeModel: {
provider: 'future',
id: 'audio-model',
name: 'Audio Model',
inputModalities: ['text', 'audio'],
outputModalities: ['audio'],
},
attachedSessions: 0,
},
}))
const response = await c.host.describe({})
expect(response.result).toEqual({
ok: true,
value: {
version: 'v',
cwd: '/w',
activeModel: {
provider: 'future',
id: 'audio-model',
name: 'Audio Model',
inputModalities: ['text', 'audio'],
outputModalities: ['audio'],
},
attachedSessions: 0,
},
})
})
it('round-trips the native picker without the default unary timeout', async () => {
const api = fakeApi()
api.host.pickDirectory = async (request) => {

View File

@@ -242,32 +242,17 @@ describe('sessions domain schemas', () => {
})
describe('host domain schemas', () => {
it('validates describe request/value and preserves merge-extensible modalities', () => {
it('validates describe request/value', () => {
expect(hostDescribeRequestSchema.parse({})).toEqual({})
const value = hostDescribeValueSchema.parse({
version: '1',
cwd: '/x',
provider: 'p',
model: 'm',
activeModel: {
provider: 'p',
id: 'm',
name: 'Model',
inputModalities: ['text', 'audio'],
outputModalities: ['text', 'audio'],
},
attachedSessions: 2,
})
expect(value.attachedSessions).toBe(2)
expect(value.activeModel?.inputModalities).toEqual(['text', 'audio'])
expect(value.activeModel?.outputModalities).toEqual(['text', 'audio'])
expect(hostDescribeValueSchema.parse({ version: '1', cwd: '/x', attachedSessions: 0 }).provider).toBeUndefined()
expect(() => hostDescribeValueSchema.parse({
version: '1',
cwd: '/x',
activeModel: { provider: 'p', id: 'm', name: 'Model', inputModalities: [{ type: 'audio' }] },
attachedSessions: 0,
})).toThrow()
})
it('validates the browse listing/creation payloads', () => {