Merge remote-tracking branch 'origin/master' into feat/web-message-feedback-ui

Adapt to two contract changes master introduced:

- The generated Remote face now wraps every business result in
  RemoteResult, folding carrier failures into an ok:false branch instead
  of rejecting. The controller reads that envelope at its three call
  sites and maps a carrier failure onto the same settled shape the
  controls already render; three specs cover the new branch.
- Client packages split their tsconfig into host and client halves, and
  the host aggregate now compiles any test not named *.client.spec.*.
  Rename this package's specs to the client convention and drop the
  ../connection project reference, which pointed at a solution file that
  no longer carries the client sources.

Keep master's mount loop with its rollback-on-failure in api-remotes and
add messageFeedbackRemote to it.
This commit is contained in:
Chinesezjc
2026-08-12 10:43:23 +08:00
parent 47f254a252
commit b462d5fd69
507 files changed
+3130 -2238

No files matched your search

@@ -14,7 +14,6 @@ import type { SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { CommandServiceContract } from '../src/client/contract.ts'
import type { PopupSelectInjected } from '../src/client/PopupSelectView.tsx'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
import { apply, CommandService, inject } from '../src/client/index.ts'
const sid = (k: string): SessionId => k as SessionId
@@ -33,14 +32,16 @@ async function bench() {
scope: (id: SessionId) => scopes.get(id),
scopeOf: (c: Context) => scopeOf(c),
})
ctx.provide('connection', { api: { commands: { list: () => Promise.resolve({ result: { ok: true, value: { commands: [] } } }) } } })
const commandsRemote = { list: () => Promise.resolve([]) }
// The service subscribes its cache-invalidation events on construction, so
// the Remote face needs `$on` even where this spec dispatches none.
ctx.provide('remote', { commands: commandsRemote, $on: () => () => {} })
ctx.provide('remote.commands', commandsRemote)
await ctx.plugin(SlotsService).await()
ctx.slots.register({
name: 'root', children: { 'conversation.input.overlay': { kind: 'list', scope: 'session' } },
} as never, (() => null) as never)
ctx.provide('locale', new LocaleService(ctx))
// CommandService injects `remote` for the forwarded directory invalidation.
new TestRemote(ctx)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
const mint = (key: string) => {
@@ -53,7 +54,7 @@ async function bench() {
describe('apply', () => {
it('declares the services it binds', () => {
expect(inject).toEqual(['slash', 'sessions', 'connection', 'locale', 'remote'])
expect(inject).toEqual(['slash', 'sessions', 'remote', 'remote.commands', 'locale'])
})
it('mounts ctx.command, registers the source and the overlay entry, and folds up on disposal', async () => {
@@ -6,7 +6,7 @@
* gate, and the per-key ensureReady strong-wait policy.
*/
import { describe, expect, it } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
import type { CommandDescriptor } from '../src/client/directory.ts'
import { CommandDirectory } from '../src/client/directory.ts'
@@ -10,7 +10,6 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { createScope, scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { ClientSessionContext, ConsumeTokenRequest, SlashPick, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { CommandContribution, CommandDecoration, CommandUiSpec, SelectOption } from '../src/client/contract.ts'
@@ -32,7 +31,7 @@ const S2_CMDS: CommandDescriptor[] = [
{ name: 'attach', description: 'scoped shadow', input: { hint: 'path' } },
]
type ExecuteValue = { matched: boolean }
type ExecuteValue = { matched: boolean; commandId?: string }
interface BenchOptions {
/** Scripted catalog per list payload; default serves the fixed catalogs by session. */
@@ -41,25 +40,54 @@ interface BenchOptions {
addressed?: SessionId
}
/**
* Fold one programmed answer into the generated Remote face's outcome: a
* resolved value is the ok branch, a rejection is the transport failure the
* carrier reports in the error branch instead of throwing at the caller.
* @param produce - the scripted answer for one Remote method.
* @returns the carried result the service reads.
*/
async function carried<T>(produce: () => Promise<T>) {
try {
return { ok: true as const, value: await produce() }
} catch (error) {
return {
ok: false as const,
error: {
code: 'internal',
message: error instanceof Error ? error.message : String(error),
details: {},
},
}
}
}
async function bench(opts: BenchOptions = {}) {
const ctx = new Context()
const registered = new Map<string, SlashSource>()
const listCalls: Array<{ sessionId: SessionId }> = []
const executeCalls: Array<{ sessionId: SessionId; line: string }> = []
const api = {
commands: {
list: async (payload: { sessionId: SessionId }) => {
listCalls.push(payload)
// The service reads the generated commands Remote, which delivers the
// carrier's outcome, so a programmed failure answers the error branch.
const commandsRemote = {
list: async (sessionId: SessionId) => {
listCalls.push({ sessionId })
return await carried(async () => {
const value = await (opts.commands ?? (p => Promise.resolve({
commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
})))(payload)
return { result: { ok: true as const, value } }
},
execute: async (payload: { sessionId: SessionId; line: string }) => {
executeCalls.push(payload)
const value = await (opts.execute ?? (() => Promise.resolve({ matched: true })))(payload)
return { result: { ok: true as const, value } }
},
})))({ sessionId })
return value.commands
})
},
execute: async (sessionId: SessionId, line: string) => {
executeCalls.push({ sessionId, line })
return await carried(async () => {
const fallback = (): Promise<ExecuteValue> => Promise.resolve({ matched: true })
const value = await (opts.execute ?? fallback)({ sessionId, line })
return value.matched
? { commandId: value.commandId ?? 'fake-command', result: { kind: 'success' as const } }
: undefined
})
},
}
ctx.provide('slash', {
@@ -78,10 +106,20 @@ async function bench(opts: BenchOptions = {}) {
? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const }
: undefined,
})
ctx.provide('connection', { api })
// CommandService injects `remote`; the directory invalidation arrives on the
// same `$dispatch` handoff the connection sink makes.
new TestRemote(ctx)
const forwarded = new Map<string, Array<(...args: never[]) => void>>()
ctx.provide('remote', {
commands: commandsRemote,
$on: (event: string, listener: (...args: never[]) => void) => {
const listeners = forwarded.get(event) ?? []
listeners.push(listener)
forwarded.set(event, listeners)
return () => { forwarded.set(event, listeners.filter(entry => entry !== listener)) }
},
$dispatch: (event: string, args: readonly unknown[]) => {
for (const listener of forwarded.get(event) ?? []) listener(...args as never[])
},
})
ctx.provide('remote.commands', commandsRemote)
/** Notices the fake conversation face collected (runDetached routing). */
const notices: Array<{ scope: SessionId | undefined; level: 'info' | 'error'; text: string }> = []
ctx.provide('conversation', {
@@ -515,7 +553,13 @@ describe('detached admission notices', () => {
mode = 'reject'
menuPick(source, 'plan', proj('s1'))
await flush()
expect(notices).toEqual([{ scope: sid('s1'), level: 'error', text: 'network down' }])
// A dead Remote call and a rejected one now read alike: both arrive as a
// failed result, so the notice names the endpoint either way.
expect(notices).toEqual([{
scope: sid('s1'),
level: 'error',
text: 'command.execute failed: internal: network down',
}])
})
it('a torn-down scope drops the failure notice', async () => {