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:
@@ -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) {
|
||||
|
||||
@@ -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'>>>
|
||||
|
||||
|
||||
@@ -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
|
||||
}>>
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user