Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md
#	.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
#	apps/web/tests/scaffold.ts
#	docs/module-graph.md
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/package.json
#	packages/client/test-runtime/README.i18n.yaml
#	packages/client/test-runtime/README.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/package.json
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-theme/README.i18n.yaml
#	packages/client/ui-theme/README.md
#	packages/client/ui-theme/README.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.zh.md
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-10 12:50:43 +08:00
3485 changed files with 86192 additions and 25255 deletions

View File

@@ -22,9 +22,10 @@ import type { CredentialInfo, CredentialRef, ResolvedCredential } from '@deepsee
import type { HostFrame } from '../src/api/index.ts'
import type { RpcRequest, RpcResponse } from '../src/api/rpc.ts'
import { RpcId } from '../src/api/rpc.ts'
import { API_GATEWAY_SETTINGS_NAMESPACE, createApiProxy } from '../src/api-proxy.ts'
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model'
import { createApiProxy } from '../src/api-proxy.ts'
const DEFAULTS = { defaultTarget: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', workspaceRoot: '/tmp' }
const DEFAULTS = { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', workspaceRoot: '/tmp' }
let nextRpc = 1
function request<P>(payload: P): RpcRequest<P> {
@@ -43,7 +44,7 @@ function expectErr<T>(response: RpcResponse<T>): { code: string; message: string
return response.result.error
}
/** In-memory settings provider: the seam base class owns all tested behavior. */
/** In-memory settings provider: the Service Definition base class owns all tested behavior. */
class MemorySettings extends Settings {
doc: Record<string, unknown>
@@ -391,6 +392,21 @@ describe('settings domain', () => {
])
})
it('serves the agent-preset namespace, so a browser preset picker can persist its choice', async () => {
const ctx = await harness()
ctx.settings.register(settingsNamespace('agent-presets'), z.object({ default: z.string() }))
const api = createApiProxy(ctx, DEFAULTS)
expectOk(await api.settings.update(request({ ns: 'agent-presets', patch: { default: 'minimal' } })))
// Both browser surfaces that offer the choice — the General row and the
// management section — write the default through `settings.update`, so a
// namespace outside this boundary makes the picker move and then silently
// forget, which is worse than refusing the control.
expect(ctx.settings.describe().find(view => String(view.ns) === 'agent-presets')?.value)
.toEqual({ default: 'minimal' })
})
it('refuses even a model-provider namespace once its directory entry is gone', async () => {
const ctx = await harness({ configurableProviders: false })
ctx.settings.register(NS, AdapterConfig)
@@ -402,9 +418,10 @@ describe('settings domain', () => {
it('invalidates the model catalog when a provider namespace changes, and broadcasts a raw-only change', async () => {
// Editing `models` changes no route, so llm/adapters-updated never fires
// and an open model picker kept serving the old catalog. And storing an
// override equal to the resolved value emits nothing on settings/updated,
// so another tab never learned the field became overridden.
// and an open model picker would keep serving the stale catalog. Storing
// an override equal to the resolved value emits nothing on
// settings/updated, so another tab would never learn the field became
// overridden.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig, { base: { baseURL: 'https://base' } })
const api = createApiProxy(ctx, DEFAULTS)
@@ -434,21 +451,21 @@ describe('settings domain', () => {
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'permission' }])
})
it('invalidates the model catalog when the gateway default route changes', async () => {
it('invalidates the model catalog when the Agent default selection changes', async () => {
const ctx = await harness()
const route = ctx.settings.register(API_GATEWAY_SETTINGS_NAMESPACE, z.object({
const defaultModel = ctx.settings.register(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, z.object({
provider: z.string().required(),
model: z.string().required(),
}), { base: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } })
const api = createApiProxy(ctx, DEFAULTS)
// The gateway's own section names the route every session with no logged
// one resolves to, so an externally edited default — another tab, a
// The shared section names the selection every blank session resolves to,
// so an externally edited default — another tab, a
// hand-edited settings.yaml — has to reach an open selector as well.
const frames = await collectHost(api, ['host/settings-changed', 'host/models-changed'], 2, async () => {
await route.replace({ provider: 'deepseek-official', model: 'deepseek-reasoner' })
await defaultModel.replace({ provider: 'deepseek-official', model: 'deepseek-reasoner' })
})
expect(frames).toEqual([
{ type: 'host/settings-changed', ns: 'api-gateway' },
{ type: 'host/settings-changed', ns: 'agent-default-model' },
{ type: 'host/models-changed' },
])
})