fix(gui): close attachment durability gaps

This commit is contained in:
Yichen Jiang
2026-07-27 14:29:19 +08:00
parent b599d1d0af
commit a1835c3228
5 changed files with 142 additions and 16 deletions

View File

@@ -3,11 +3,13 @@
*/
import { z } from 'zod'
import type { ModelModality } from '@deepseek-ai/dsh-llm'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { imageMediaTypeSchema } from './sessions.schema.ts'
const modalitySchema = z.union([z.literal('text'), z.literal('image')])
/** 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'>>>

View File

@@ -1,12 +1,24 @@
import { describe, expect, it, vi } from 'vitest'
import type { ApiProxy, HostFrame, MuxFrame } from '../src/api/index.ts'
import type { ResponseValue } from '../src/api/rpc-map.ts'
import type { ClientResponse, RpcMessage, RpcReceipt, RpcRequest } from '../src/api/rpc.ts'
import { RpcId } from '../src/api/rpc.ts'
import { toFetchHandler } from '../src/fetch/handler.ts'
import { AbstractApiClient, InProcessApiClient } from '../src/fetch/client.ts'
declare module '@deepseek-ai/dsh-llm' {
interface ModelModalityMap {
audio: 'audio'
}
}
/** Minimal in-memory ApiProxy: echoes rpcIds, scripts one frame per stream. */
function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFrame[]; crashOn: string }> = {}): ApiProxy {
function fakeApi(overrides: Partial<{
muxFrames: MuxFrame[]
hostFrames: HostFrame[]
crashOn: string
hostDescription: ResponseValue<'host.describe'>
}> = {}): ApiProxy {
const muxFrames = overrides.muxFrames ?? [{ type: 'session/subscribed', sessionId: 's1' as never, lastSeq: -1 }]
const hostFrames = overrides.hostFrames ?? [{ type: 'host/session-removed', sessionId: 's1' as never }]
async function * stream<F>(frames: F[], signal: AbortSignal): AsyncGenerator<RpcRequest<F>> {
@@ -45,7 +57,13 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
},
host: {
async describe(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { version: 'v', cwd: '/w', attachedSessions: 0 } } }
return {
rpcId: request.rpcId,
result: {
ok: true,
value: overrides.hostDescription ?? { version: 'v', cwd: '/w', attachedSessions: 0 },
},
}
},
},
workspace: {
@@ -137,6 +155,40 @@ 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 command.list / command.execute / skill.list through the wire form', async () => {
const c = client()
const list = await c.commands.list({ sessionId: 's' as never })

View File

@@ -155,11 +155,32 @@ describe('sessions domain schemas', () => {
})
describe('host domain schemas', () => {
it('validates describe request/value', () => {
it('validates describe request/value and preserves merge-extensible modalities', () => {
expect(hostDescribeRequestSchema.parse({})).toEqual({})
const value = hostDescribeValueSchema.parse({ version: '1', cwd: '/x', provider: 'p', model: 'm', attachedSessions: 2 })
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()
})
})