1
0
Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master' into xtr/durable-inbox-recovery

_Kerman 3 долоо хоног өмнө
parent
commit
c91b68409c
100 өөрчлөгдсөн 622 нэмэгдсэн , 250 устгасан
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  2. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  3. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.i18n.yaml
  5. 5 5
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md
  6. 5 5
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md
  7. 2 2
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.i18n.yaml
  8. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.md
  9. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml
  11. 5 3
      .agents/notes/implemented/feature/2026-07-31-web-default-search.md
  12. 5 3
      .agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md
  13. 6 0
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.i18n.yaml
  14. 27 0
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md
  15. 27 0
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.zh.md
  16. 6 0
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.i18n.yaml
  17. 42 0
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md
  18. 42 0
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.zh.md
  19. 6 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.i18n.yaml
  20. 34 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md
  21. 34 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.zh.md
  22. 2 2
      apps/cli/reference/README.i18n.yaml
  23. 1 1
      apps/cli/reference/README.md
  24. 1 1
      apps/cli/reference/README.zh.md
  25. 15 4
      apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts
  26. 3 1
      apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts
  27. 5 4
      apps/web/tests/expected/models-settings/model-picker.expected.md
  28. 12 3
      apps/web/tests/models-settings.e2e.ts
  29. 2 2
      docs/config-catalog.i18n.yaml
  30. 1 1
      docs/config-catalog.md
  31. 1 1
      docs/config-catalog.zh.md
  32. 2 2
      docs/web-styling.i18n.yaml
  33. 3 0
      docs/web-styling.md
  34. 3 0
      docs/web-styling.zh.md
  35. 2 2
      packages/bundle/base/README.i18n.yaml
  36. 3 3
      packages/bundle/base/README.md
  37. 3 3
      packages/bundle/base/README.zh.md
  38. 5 4
      packages/bundle/base/cordis.patch.yml
  39. 1 1
      packages/bundle/base/tests/base.spec.ts
  40. 1 1
      packages/client/locale/src/client/LanguageRow.module.css
  41. 17 13
      packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css
  42. 1 0
      packages/client/ui-approval/src/client/ApprovalPanel.module.css
  43. 6 3
      packages/client/ui-attachment/src/AttachmentRail.module.css
  44. 2 1
      packages/client/ui-attachment/src/ImageLightbox.module.css
  45. 2 2
      packages/client/ui-attachment/src/MessageImage.module.css
  46. 4 2
      packages/client/ui-chat/src/client/chat/ChatView.module.css
  47. 1 1
      packages/client/ui-chat/src/client/chat/ContextBody.module.css
  48. 1 1
      packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css
  49. 2 2
      packages/client/ui-chat/src/client/chat/TurnNavigator.module.css
  50. 1 1
      packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css
  51. 5 4
      packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css
  52. 3 2
      packages/client/ui-chat/src/client/details/DetailsPanel.module.css
  53. 1 1
      packages/client/ui-chat/src/client/settings/TranscriptViewRow.module.css
  54. 7 6
      packages/client/ui-commands/src/client/PopupSelectView.module.css
  55. 4 3
      packages/client/ui-conversation/src/client/queue/QueueDock.module.css
  56. 1 1
      packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css
  57. 6 3
      packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css
  58. 2 2
      packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css
  59. 2 2
      packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css
  60. 10 6
      packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
  61. 1 1
      packages/client/ui-conversation/src/client/skeleton/TodoPanel.module.css
  62. 4 4
      packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.module.css
  63. 3 2
      packages/client/ui-goal/src/client/GoalBar.module.css
  64. 5 4
      packages/client/ui-input-trigger/src/client/MenuView.module.css
  65. 4 3
      packages/client/ui-jobs/src/client/JobListAction.module.css
  66. 3 3
      packages/client/ui-layout/src/client/AppFrame.module.css
  67. 4 3
      packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css
  68. 4 3
      packages/client/ui-model-selection/src/client/ModelSelect.module.css
  69. 1 1
      packages/client/ui-permission-presets/src/client/PermissionRow.module.css
  70. 1 0
      packages/client/ui-plan/src/client/PlanModeControl.module.css
  71. 1 1
      packages/client/ui-primitives/src/Button.module.css
  72. 1 1
      packages/client/ui-primitives/src/Input.module.css
  73. 7 6
      packages/client/ui-primitives/src/Menu.module.css
  74. 3 3
      packages/client/ui-primitives/src/Modal.module.css
  75. 2 0
      packages/client/ui-primitives/src/StateDot.module.css
  76. 1 1
      packages/client/ui-primitives/src/TerminalBlock.module.css
  77. 1 1
      packages/client/ui-primitives/src/markdown/JsonBlock.module.css
  78. 3 3
      packages/client/ui-primitives/src/markdown/MarkdownText.module.css
  79. 5 3
      packages/client/ui-schedule/src/client/ScheduleCatalogAction.module.css
  80. 5 4
      packages/client/ui-settings-general/src/client/SettingsRoot.module.css
  81. 2 2
      packages/client/ui-settings-models/README.i18n.yaml
  82. 2 2
      packages/client/ui-settings-models/README.md
  83. 2 2
      packages/client/ui-settings-models/README.zh.md
  84. 55 26
      packages/client/ui-settings-models/src/client/ModelListEditor.tsx
  85. 28 13
      packages/client/ui-settings-models/src/client/ModelsSection.module.css
  86. 4 0
      packages/client/ui-settings-models/src/client/locales.ts
  87. 25 6
      packages/client/ui-settings-models/tests/provider-form.client.spec.tsx
  88. 1 1
      packages/client/ui-settings-models/tests/styles.client.spec.ts
  89. 11 9
      packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css
  90. 5 4
      packages/client/ui-settings-plugins/src/client/PluginCard.module.css
  91. 1 1
      packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.module.css
  92. 3 2
      packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css
  93. 4 2
      packages/client/ui-settings-plugins/src/client/fields.module.css
  94. 2 1
      packages/client/ui-sidebar/src/client/SidebarRoot.module.css
  95. 4 3
      packages/client/ui-skill/src/client/SkillRow.module.css
  96. 5 4
      packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css
  97. 1 1
      packages/client/ui-subagent/src/client/SubagentReadOnlyComposer.module.css
  98. 2 2
      packages/client/ui-theme/README.i18n.yaml
  99. 4 2
      packages/client/ui-theme/README.md
  100. 4 2
      packages/client/ui-theme/README.zh.md

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
-2026-07-30-web-config-plane.md: 81b501db529bf1b2974fd4541045991c5a8bf087
-2026-07-30-web-config-plane.zh.md: 3f02a17e4826bb35ecfd45da25c4b0170be270cb
+2026-07-30-web-config-plane.md: a919487ba48cd7735a9f7fbc65a548bc5bfb7114
+2026-07-30-web-config-plane.zh.md: ca9e9f4427bba80a63865e891e42656aeb1b5c64

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md
-2026-08-04-draft-provider-endpoint-interrogation.md: 502d9bab15dcb91a59deb26443d869a36b028b48
-2026-08-04-draft-provider-endpoint-interrogation.zh.md: e0605369c7f4707eb682cc1c32d11123140b449a
+2026-08-04-draft-provider-endpoint-interrogation.md: d4112d813ad4f5781b74639209d13952e459f7dd
+2026-08-04-draft-provider-endpoint-interrogation.zh.md: 1626a34cb3163949d70688cefeec77d328c62caa

+ 5 - 5
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md

@@ -17,11 +17,11 @@ The awkward part is that the question is about something that does not exist yet
 Interrogation is keyed by **settings namespace**, not by provider route:
 
 - `ctx.llm.registerModelDiscovery(settingsNs, discover)` lets an adapter plugin offer to interrogate endpoints for the namespace it owns, and `ctx.llm.discoverModels(settingsNs, request)` asks. There is no way to enumerate which namespaces registered: a surface that cannot interrogate learns it from the refusal, and a list nothing consumed would be a required wire field doing nothing. The namespace is the right key because a configuration surface already holds it from the configurable-provider directory, and because a provider being added has no route to name.
-- `LlmModelDiscoveryRequest` carries the draft — an optional `provider`, an optional `baseURL`, an optional `api`, an optional `apiKey`, and a signal — and needs at least one of `provider` or `baseURL` to have anything to answer about. `provider` exists because a route the adapter already describes is answered from its own registry with no network call at all; only a route it does not describe reaches an endpoint. Nothing in this path writes settings or credentials. The one read is the credential of a route the request names: a configuration surface holds a redacted descriptor rather than the stored secret, so the draft's `apiKey` is present only while the user is typing one, and without that read an already-configured route would be interrogated unauthenticated and answer 401. The typed key wins, being the one under test.
+- `LlmModelDiscoveryRequest` carries the draft — an optional `provider`, an optional `baseURL`, an optional `api`, an optional `apiKey`, and a signal — and needs at least one of `provider` or `baseURL` to have anything to answer about. `provider` exists because a route the adapter already describes is answered from its own registry with no network call at all; only a route it does not describe reaches an endpoint. Nothing in this path writes settings or credentials. A named configured route reads its stored credential and deployment-owned profile `headers` inside the Host: the credential is write-only and the curated Models page does not edit headers, so neither can be reconstructed from that page's draft. The typed key wins over the stored credential, while the profile headers still accompany the request.
 - `LlmDiscoveredModel` makes every field but `id` optional, because most listings disclose an id and nothing else. The reply is candidates, not a catalog: a surface adopting one still owes the capacities the adapter requires.
 - `llm.discoverModels` carries the same draft over the wire. Its `apiKey` is the third and last payload on which a secret may ride, alongside `settings.update`/`mutate` and `credentials.set`, and it is never stored or echoed back. It does ride the client's outgoing envelope like every other secret-bearing payload, where a `subscribeEnvelopes()` observer can see it; redacting that tap is a configuration-plane-wide change, not this method's to make alone. Connection authenticates the method with the complete Host API: it makes the host issue a GET to a caller-chosen URL and reports the outcome, which an anonymous caller must not receive. Every refusal folds into `model-discovery-failed`, whose message is the adapter's own text and whose details name the endpoint asked but never the credential offered.
 
-`dsh-llm-pi-ai` implements the wire path as a plain `GET {baseURL}/models`, reading `openai-completions` and `openai-responses`: their `GET /models` shape with bearer auth is the one a gateway, a self-hosted server, and the official endpoints all agree on. Azure is excluded despite its OpenAI lineage — it authenticates with an `api-key` header and requires an `api-version` query — and Codex uses OAuth; both would have reported an authentication failure as a provider with no models. Every other protocol answers `DISCOVERY_UNSUPPORTED`, so the surface falls back to hand-entry rather than reporting a guessed response shape as an empty provider. `baseURL` is treated as a prefix rather than a URL to resolve against, so a deployment path such as `https://gateway.example/openai/v1` keeps its segments. The reply is read under a four-megabyte ceiling enforced on the bytes actually received — the endpoint is a URL the user typed, so a declared `content-length` is checked first as a courtesy but never trusted as the bound, matching `dsh-web-fetch`'s two-stage shape for its own caller-supplied URLs.
+`dsh-llm-pi-ai` implements the wire path as a plain `GET {baseURL}/models`, reading `openai-completions` and `openai-responses`: their `GET /models` shape with bearer auth is the one a gateway, a self-hosted server, and the official endpoints all agree on. Profile resolution rejects names and values Fetch cannot represent, so a malformed deployment header is reported as a configuration error before interrogation. Configured profile headers are installed first; the fixed JSON accept header, a typed-or-stored bearer credential, and Harness attribution then win case-insensitive collisions in that order. Azure is excluded despite its OpenAI lineage — it authenticates with an `api-key` header and requires an `api-version` query — and Codex uses OAuth; both would have reported an authentication failure as a provider with no models. Every other protocol answers `DISCOVERY_UNSUPPORTED`, so the surface falls back to hand-entry rather than reporting a guessed response shape as an empty provider. `baseURL` is treated as a prefix rather than a URL to resolve against, so a deployment path such as `https://gateway.example/openai/v1` keeps its segments. The reply is read under a four-megabyte ceiling enforced on the bytes actually received — the endpoint is a URL the user typed, so a declared `content-length` is checked first as a courtesy but never trusted as the bound, matching `dsh-web-fetch`'s two-stage shape for its own caller-supplied URLs.
 
 ### Why not pi-ai's own refresh machinery
 
@@ -33,7 +33,7 @@ pi-ai supplies `createProvider({ fetchModels })` plus `Models.refresh()` and a `
 
 **Put the capability on `LlmAdapter`.** Adapters are reached through a route registration, so this has the same problem, plus it would make an adapter instance answer questions about endpoints it does not serve.
 
-**Have the host read the stored profile instead of accepting a draft.** No secret would cross the wire for an already-configured provider. But adding a provider would then require saving an unusable configuration first, and a form whose endpoint was edited but not yet saved would silently interrogate the old one. Accepting the draft keeps what the user sees and what is asked identical — with the credential as the one exception, because it is the one field a surface is never shown and so can never put in the draft.
+**Have the host read the entire stored profile instead of accepting a draft.** No secret would cross the wire for an already-configured provider. But adding a provider would then require saving an unusable configuration first, and a form whose endpoint was edited but not yet saved would silently interrogate the old one. The draft remains authoritative for the endpoint and protocol. The narrow Host-side exceptions are the stored credential, which is write-only, and profile headers, which remain deployment configuration rather than Models-page fields.
 
 **Interrogate every pi-ai protocol.** Anthropic's listing happens to share OpenAI's envelope, and Google's does not. Supporting the ones that are easy would make coverage arbitrary and, worse, make a wrong guess at a response shape indistinguishable from a provider with no models. A protocol that says it cannot be interrogated sends the user to hand-entry, which is the documented fallback.
 
@@ -41,10 +41,10 @@ pi-ai supplies `createProvider({ fetchModels })` plus `Models.refresh()` and a `
 
 ## Consequences
 
-A person adding a gateway can ask it what it serves instead of hunting through its documentation, and the answer arrives as candidates they choose from rather than as configuration written behind their back. The seam gained a registry that is deliberately small: one offer per namespace, no storage, no lifecycle beyond the fiber.
+A person adding a gateway can ask it what it serves instead of hunting through its documentation, and the answer arrives as candidates they choose from rather than as configuration written behind their back. An already-configured enterprise gateway uses the same deployment headers for interrogation and model requests without adding a header injection field to the browser protocol. The seam gained a registry that is deliberately small: one offer per namespace, no storage, no lifecycle beyond the fiber.
 
 What it costs: the wire gained a third secret-carrying payload, so the configuration plane's write-only surface is now three methods rather than two. Discovery coverage is protocol-shaped rather than provider-shaped — an Anthropic-compatible gateway must be filled in by hand even though its listing would parse. And because nothing re-runs the question, a model list is still only as current as its last edit; that is the same trade the layer below made deliberately.
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals, and the `model-discovery-failed` Remote mapping. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its own where the draft has none and a typed key winning over it, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/client/connection/tests/node-half.host.spec.ts` pins the `llm/discoverModels` `/api` carrier registration, while `packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` verifies that the draft reaches the Remote whole, absent fields stay absent, and no settings namespace or credential is written before selection.
+`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals, and the `model-discovery-failed` Remote mapping. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its stored credential and headers while a typed key wins without resolving the stored one, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/llm/llm-pi-ai/tests/loader-composition.spec.ts` boots settings and credentials through the Loader and proves settings-only headers reach `GET /models` with request-owned headers winning collisions. `packages/llm/llm-pi-ai/tests/adapter.spec.ts` rejects profile headers Fetch cannot represent, and `packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts` proves a settings write reports that configuration error while its last good routes keep serving. `packages/client/connection/tests/node-half.host.spec.ts` pins the `llm/discoverModels` `/api` carrier registration, while `packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` verifies that the draft reaches the Remote whole, absent fields stay absent, and no settings namespace or credential is written before selection.

+ 5 - 5
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md

@@ -17,11 +17,11 @@ Status: implemented
 询问以 **settings namespace** 为键,而不是提供方路由:
 
 - `ctx.llm.registerModelDiscovery(settingsNs, discover)` 让适配器插件为自己拥有的 namespace 提供「询问端点」的能力,`ctx.llm.discoverModels(settingsNs, request)` 发起询问。没有任何办法枚举哪些 namespace 注册过:询问不了的界面会从那句拒绝里知道,而一份无人消费的列表只会变成一个什么都不做的必填协议字段。以 namespace 为键是对的,因为配置界面已经从可配置提供方目录里拿到了它,也因为正在新增的提供方没有路由可点名。
-- `LlmModelDiscoveryRequest` 携带草稿——可选的 `provider`、可选的 `baseURL`、可选的 `api`、可选的 `apiKey`,以及一个 signal——且 `provider` 与 `baseURL` 至少要有一个,才有东西可答。`provider` 之所以存在,是因为适配器已经描述过的路由直接由它自己的注册表作答、完全不联网;只有它未描述的路由才会抵达某个端点。这条路径不写 settings 与 credentials。唯一的读取是请求所点名路由的凭据:配置界面拿到的是脱敏描述符而非已存的机密,因此草稿里的 `apiKey` 只在用户正键入时才存在;没有这次读取,已配置好的路由就会被不带认证地询问,只换回一个 401。键入的密钥优先,因为那正是被测试的那一把。
+- `LlmModelDiscoveryRequest` 携带草稿——可选的 `provider`、可选的 `baseURL`、可选的 `api`、可选的 `apiKey`,以及一个 signal——且 `provider` 与 `baseURL` 至少要有一个,才有东西可答。`provider` 之所以存在,是因为适配器已经描述过的路由直接由它自己的注册表作答、完全不联网;只有它未描述的路由才会抵达某个端点。这条路径不写 settings 与 credentials。已配置且具名的路由会在 Host 内读取已存凭据和部署方持有的 profile `headers`:凭据只写,而精选的 Models 页面不编辑 headers,因此页面草稿无法重建两者。键入的密钥优先于已存凭据,profile headers 则仍随请求发送。
 - `LlmDiscoveredModel` 除 `id` 外每个字段都可选,因为大多数列表只公布 id。回复是候选而非 catalog:采纳其中一条的界面仍要补上适配器所需的容量。
 - `llm.discoverModels` 把同一份草稿送过协议层。它的 `apiKey` 是可承载机密的第三个、也是最后一个载荷(另两个是 `settings.update`/`mutate` 与 `credentials.set`),且绝不被存储或回显。它确实会像其他承载机密的载荷一样随客户端外发信封同行,`subscribeEnvelopes()` 观察者看得到;把那个抽头脱敏是整个配置面的改动,不该由这一个方法独自决定。Connection 用与完整 Host API 相同的会话认证该方法:它让宿主向调用方选定的 URL 发起 GET 并回报结果,匿名调用者绝不能获得这类探测能力。每一种拒绝都折叠为 `model-discovery-failed`,其消息是适配器自己的文本,details 点名被询问的端点,绝不点名所提供的凭据。
 
-`dsh-llm-pi-ai` 的实现只是一次朴素的 `GET {baseURL}/models`,且仅限 OpenAI 兼容协议。它们的列表形状是网关、自建服务与官方端点三方一致认可的那一种,而这正是该动作存在的场景。其余协议一律以 `DISCOVERY_UNSUPPORTED` 回答,让界面回退到手工填写,而不是把猜错的响应形状报成一个空提供方。`baseURL` 按前缀而非待解析 URL 处理,因此 `https://gateway.example/openai/v1` 这类部署路径会保留其路径段。回复在四兆字节上限下读取,且上限落在实际收到的字节上——端点是用户自己填的 URL,因此会先看声明的 `content-length` 作为善意提示,但绝不把它当作边界;这与 `dsh-web-fetch` 面对自己的调用方提供 URL 时所用的两段式形状一致。
+`dsh-llm-pi-ai` 的实现只是一次朴素的 `GET {baseURL}/models`,且仅限 OpenAI 兼容协议。它们的列表形状是网关、自建服务与官方端点三方一致认可的那一种,而这正是该动作存在的场景。Profile 解析会拒绝 Fetch 无法表示的名称与值,因此格式错误的部署 header 会在询问前以配置错误报告。已配置的 profile headers 最先装入;固定的 JSON accept header、键入或已存的 bearer 凭据以及 Harness attribution 随后依次以大小写不敏感方式赢得冲突。其余协议一律以 `DISCOVERY_UNSUPPORTED` 回答,让界面回退到手工填写,而不是把猜错的响应形状报成一个空提供方。`baseURL` 按前缀而非待解析 URL 处理,因此 `https://gateway.example/openai/v1` 这类部署路径会保留其路径段。回复在四兆字节上限下读取,且上限落在实际收到的字节上——端点是用户自己填的 URL,因此会先看声明的 `content-length` 作为善意提示,但绝不把它当作边界;这与 `dsh-web-fetch` 面对自己的调用方提供 URL 时所用的两段式形状一致。
 
 ### 为什么不用 pi-ai 自己的 refresh 机制
 
@@ -33,7 +33,7 @@ pi-ai 提供了 `createProvider({ fetchModels })` 加上 `Models.refresh()` 与
 
 **把能力挂在 `LlmAdapter` 上。** 适配器要经由路由注册才能抵达,因此问题相同;而且这会让一个适配器实例去回答它并不服务的端点的问题。
 
-**让 host 读已存 profile,而不是接受草稿。** 对已配置好的提供方来说,不会有机密跨越协议层。但这样一来新增提供方就必须先保存一份不可用的配置,而端点已改却尚未保存的表单会静默地去询问旧地址。接受草稿让用户看见的与被询问的保持一致——凭据是唯一的例外,因为它是从不向界面展示、因而永远无法放进草稿的那个字段。
+**让 Host 读取整个已存 profile,而不是接受草稿。** 对已配置好的提供方来说,不会有机密跨越协议层。但这样一来新增提供方就必须先保存一份不可用的配置,而端点已改却尚未保存的表单会静默地去询问旧地址。草稿仍是端点和协议的权威来源。Host 侧的狭窄例外是只写的已存凭据,以及仍属部署配置、而非 Models 页面字段的 profile headers。
 
 **询问 pi-ai 的每一种协议。** Anthropic 的列表恰好与 OpenAI 共用同一层信封,而 Google 的不是。只支持容易的那几种会让覆盖范围变得任意;更糟的是,猜错的响应形状会与「该提供方没有模型」无法区分。一个明说自己无法被询问的协议,会把用户送去手工填写——那正是既定的回退路径。
 
@@ -41,10 +41,10 @@ pi-ai 提供了 `createProvider({ fetchModels })` 加上 `Models.refresh()` 与
 
 ## Consequences
 
-接入网关的人可以直接问它服务什么,而不必去翻它的文档;答案以候选形式抵达,由用户自己挑选,而不是被背着写进配置。seam 因此多了一个刻意保持很小的注册表:每个 namespace 一份、不存储、生命周期不超出 fiber。
+接入网关的人可以直接问它服务什么,而不必去翻它的文档;答案以候选形式抵达,由用户自己挑选,而不是被背着写进配置。已配置的企业网关会为询问与模型请求使用同一组部署 headers,而无需给浏览器协议增加 header 注入字段。seam 因此多了一个刻意保持很小的注册表:每个 namespace 一份、不存储、生命周期不超出 fiber。
 
 代价是:协议层多了第三个承载机密的载荷,配置面的只写接口从两个方法变成三个。发现覆盖范围按协议而非按提供方划分——一个 Anthropic 兼容网关即便其列表能被解析,也仍须手工填写。而且由于没有任何环节会重跑该询问,模型列表的新鲜度依旧只到最近一次编辑为止;这与下层刻意做出的取舍是同一个。
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化、`NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝,以及 `model-discovery-failed` Remote 映射。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、草稿没带密钥时已配置路由自行取用凭据且键入的密钥压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/client/connection/tests/node-half.host.spec.ts` 固定 `llm/discoverModels` 的 `/api` 承载注册,`packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` 则验证草稿完整抵达 Remote、缺席字段保持缺席,以及选择前没有 settings namespace 或凭据被写入。
+`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化、`NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝,以及 `model-discovery-failed` Remote 映射。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、已配置路由提供自己的已存凭据与 headers 且键入的密钥无需解析已存凭据便可压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/llm/llm-pi-ai/tests/loader-composition.spec.ts` 通过 Loader 启动 settings 与 credentials,并证明仅配置在 settings 中的 headers 会抵达 `GET /models`,且请求所持有的 headers 赢得冲突。`packages/llm/llm-pi-ai/tests/adapter.spec.ts` 拒绝 Fetch 无法表示的 profile headers,`packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts` 证明 settings 写入会报告该配置错误,同时上一组可用路由仍继续服务。`packages/client/connection/tests/node-half.host.spec.ts` 固定 `llm/discoverModels` 的 `/api` 承载注册,`packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` 则验证草稿完整抵达 Remote、缺席字段保持缺席,以及选择前没有 settings namespace 或凭据被写入。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.md
-2026-08-13-feedback-note-editor-popover.md: 42c08e39cfb054db689503e23306c5049a97b6cb
-2026-08-13-feedback-note-editor-popover.zh.md: 553029f8a42dae42a38e909d716b41e2c6dd252e
+2026-08-13-feedback-note-editor-popover.md: 8f51f090cc24292ad02d96fefb1f45bc05df08c9
+2026-08-13-feedback-note-editor-popover.zh.md: e64e95ac6599be2d7c343b4432415f0e99c6ef9b

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.md

@@ -18,7 +18,7 @@ The note editor does not enter the row's flex layout at all. It is a popover: a
 
 **The action strip.** The like/dislike buttons and the note trigger stay in the row, unchanged. The trigger is a plain button (`aria-haspopup="dialog"`, `aria-expanded` while open) that shows "Add a note" before a note exists and the note text afterward.
 
-**The popover.** While open, the panel contains the textarea plus Save and Cancel, and any note-save failure, as `role="dialog"` with a title distinct from the textarea's own label so both are addressable by name. It opens beneath the trigger (4px gap), clamps to 12px from the viewport edges, auto-focuses the textarea, and closes on Escape or an outside pointer-down. Closing returns focus to the trigger only when the panel was really open, never on the initial mount (a freshly rendered rated message must not pull focus into its action row). A rating action during an open editor closes the panel. The four undefined tokens are replaced with the ones the theme actually defines, matching the primitives' precedent: `border-l2` and `bg-layer-1` for the input, `button-primary-fill` with `label-primary-foreground` plus a `button-primary-hover` state for Save; the panel surface reuses the Menu card recipe (`--dsw-specific-menu`, `--dsw-shadow-lv3`, inverted hairline `--dsw-alias-border-inverted`, `border-radius: 12px`).
+**The popover.** While open, the panel contains the textarea plus Save and Cancel, and any note-save failure, as `role="dialog"` with a title distinct from the textarea's own label so both are addressable by name. It opens beneath the trigger (4px gap), clamps to 12px from the viewport edges, auto-focuses the textarea, and closes on Escape or an outside pointer-down. Closing returns focus to the trigger only when the panel was really open, never on the initial mount (a freshly rendered rated message must not pull focus into its action row). A rating action during an open editor closes the panel. The four undefined tokens are replaced with the ones the theme actually defines, matching the primitives' precedent: `border-l2` and `bg-layer-1` for the input, `button-primary-fill` with `label-primary-foreground` plus a `button-primary-hover` state for Save; the panel surface reuses the Menu card surface recipe (`--dsw-specific-menu`, the `--dsw-elevation-prominent` shadow with the `--dsw-alias-border-l1` stroke rebind and `border: 0`) at `border-radius: 12px`.
 
 **Failure surfaces split by where the human is looking.** A rating or list-load failure shows beside the buttons in the row, legible whether or not the popover is open. A note-save failure shows inside the popover, next to Save/Cancel, and the panel stays open so the draft survives to be corrected.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.zh.md

@@ -18,7 +18,7 @@ Status: implemented
 
 **操作条。** 点赞/点踩按钮与备注触发按钮保持原样留在行内。触发按钮是普通 `button`(`aria-haspopup="dialog"`,打开时 `aria-expanded`),在没有备注时显示「补充说明」,已有备注时显示备注文本。
 
-**浮层。** 打开时,面板内含 textarea、Save 与 Cancel,以及任何备注保存失败提示,作为 `role="dialog"`,其标题与 textarea 自身的标签不同,以便两者都能按名称寻址。它在触发按钮下方打开(4px 间距),钳制到距视口边缘 12px,自动聚焦 textarea,并在 Escape 或外部 pointer-down 时关闭。关闭时仅当面板确实曾经打开才把焦点还给触发按钮,绝不会在初始挂载时(新渲染出的一条已评分消息不得把焦点拉进其操作条)。编辑器打开时进行评分操作会关闭面板。四个未定义 token 换成主题确实定义的那些,与 primitives 的既有做法一致:输入框用 `border-l2` 与 `bg-layer-1`,Save 用 `button-primary-fill` 配 `label-primary-foreground` 并加 `button-primary-hover` 状态;面板表面复用 Menu 卡片的配方(`--dsw-specific-menu`、`--dsw-shadow-lv3`、反色发丝线 `--dsw-alias-border-inverted`、`border-radius: 12px`)。
+**浮层。** 打开时,面板内含 textarea、Save 与 Cancel,以及任何备注保存失败提示,作为 `role="dialog"`,其标题与 textarea 自身的标签不同,以便两者都能按名称寻址。它在触发按钮下方打开(4px 间距),钳制到距视口边缘 12px,自动聚焦 textarea,并在 Escape 或外部 pointer-down 时关闭。关闭时仅当面板确实曾经打开才把焦点还给触发按钮,绝不会在初始挂载时(新渲染出的一条已评分消息不得把焦点拉进其操作条)。编辑器打开时进行评分操作会关闭面板。四个未定义 token 换成主题确实定义的那些,与 primitives 的既有做法一致:输入框用 `border-l2` 与 `bg-layer-1`,Save 用 `button-primary-fill` 配 `label-primary-foreground` 并加 `button-primary-hover` 状态;面板表面复用 Menu 卡片的表面配方(`--dsw-specific-menu`、`--dsw-elevation-prominent` 投影配 `--dsw-alias-border-l1` 描边重绑与 `border: 0`),圆角取 `border-radius: 12px`。
 
 **失败提示按人的视线所落之处拆分。** 评分或列表加载失败显示在按钮旁的图标行里,无论浮层是否打开都清晰可读。备注保存失败显示在浮层内、Save/Cancel 旁,且面板保持打开,以便草稿留存待修正。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-default-search.md
-2026-07-31-web-default-search.md: eb0de16b5bf6133bbdb5275106f42eba75ddf607
-2026-07-31-web-default-search.zh.md: e1cc622e70d8af8e71a8c60aa7e7ceb9a2b91a5e
+2026-07-31-web-default-search.md: f196bfcd0c42bcfd6aacaac46971b4b9948732d7
+2026-07-31-web-default-search.zh.md: cd313c714acc22ca470ece681624bae19c1e8b4f

+ 5 - 3
.agents/notes/implemented/feature/2026-07-31-web-default-search.md

@@ -4,13 +4,15 @@ Status: implemented
 
 English | [中文](2026-07-31-web-default-search.zh.md)
 
+The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) supersedes this record's fetch opt-in decision. This record remains authoritative for the default search provider, credential resolution, endpoint, timeout, and the separation between provider availability and model-tool registration.
+
 ## Problem
 
 The harness had a complete Web capability family—provider registry, DeepSeek/Exa/Perplexity search providers, local fetch, stable model tools, and structured result presentation—but the shipped `dsh web` composition mounted none of it. The model could not discover current information unless a deployment supplied a custom overlay. Merely mounting the existing DeepSeek provider would not complete the WebUI path: the Models page stores `DEEPSEEK_API_KEY` through `ctx.credentials`, while the search provider froze only the process environment at plugin load, so a key entered or rotated in the running UI would not reach search.
 
 ## Decision
 
-`apps/cli/config/base.cordis.yml` explicitly mounts `dsh-web` with `searchProvider: deepseek-official` and `fetchProvider: http`, `dsh-web-search-deepseek`, `dsh-web-fetch-http`, and `dsh-tool-web` with `fetch: false` and `searchTimeoutMs: 60000`. The shared base therefore keeps only `web_search` visible unless a product preset enables fetch; the shipped Web `cordis`, `code`, and `standard` presets do so. Explicit provider ids keep selection independent of registration order and leave personal or `--config` overlays able to replace or disable the rows. The one-minute shipped budget covers an auxiliary DeepSeek Messages request plus server-side retrieval while leaving `dsh-tool-web`'s provider-neutral 30-second default unchanged for custom compositions. The [Web capability seam decision](../architecture/2026-06-24-web-capability-seam.md) owns the public-fetch security policy and Web preset default.
+`packages/bundle/base/cordis.patch.yml` explicitly mounts `dsh-web` with `searchProvider: deepseek-official` and `fetchProvider: http`, `dsh-web-search-deepseek`, `dsh-web-fetch-http`, and `dsh-tool-web` with `searchTimeoutMs: 60000`. The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) owns the current `fetch: true`; this record continues to own provider selection, search credentials, and timeout. Explicit provider ids keep selection independent of registration order and leave personal or `--patch` overlays able to replace or disable the rows. The one-minute shipped budget covers an auxiliary DeepSeek Messages request plus server-side retrieval while leaving `dsh-tool-web`'s provider-neutral 30-second default unchanged for custom compositions. The [Web capability seam decision](../architecture/2026-06-24-web-capability-seam.md) owns the public-fetch security policy.
 
 DeepSeek search uses the same `DEEPSEEK_API_KEY` credential reference as the official conversation adapter. The provider resolves that reference inside every search through the optional `ctx.credentials` service; only a composition without the seam falls back to the launching process environment, and a non-empty literal `apiKey` remains the programmatic last resort. A stored or rotated Web Models key therefore reaches the next search without restarting or retaining the value on the provider. Because `WebSearchProvider.available()` is synchronous, it treats an installed resolver as locally usable and missing dynamic credentials fail the operation with the provider-specific `WEB_PROVIDER_CREDENTIAL_MISSING` code while the stable tool schema stays registered.
 
@@ -30,8 +32,8 @@ The default mount does not create a Web-specific permission policy. `web_search`
 
 **Raise `dsh-tool-web`'s provider-neutral timeout.** Rejected because custom providers and deployments own different latency expectations; the shipped DeepSeek composition owns this deployment budget.
 
-**Enable fetch on every shared-base surface.** Rejected because the shared base serves products with different network postures. It mounts the public-only provider but keeps the tool opt-in; the shipped Web presets deliberately enable it, while another product can leave it hidden or add stricter network policy.
+**Enable fetch on every shared-base surface.** This record rejected the alternative because shared-base products could require different network policies. The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) supersedes that rejection after the shipped products converged on one full tool roster; its public-destination and no-approval constraints remain current.
 
 ## Consequences
 
-Native model requests on every shared-base surface carry the `web_search` schema and search guidance; Web/headless PTC mode exposes the same search capability beneath `run_code`. Search adds a complete auxiliary model call and may use the native server tool multiple times; its exact secret-free request remains reconstructable from the initiating session log. The shipped Web `cordis`, `ptc`, and `standard` presets additionally expose `web_fetch` with public-address enforcement and no per-call approval. The Web snapshot lane boots the shipped tree, drives a replayed `web_search` call through the real DeepSeek provider against a local Messages fixture, asserts the durable auxiliary request and structured result, and pins the settled browser presentation. Composition smokes pin the shared search roster and per-preset fetch choices; the built composition dump pins the one-minute shipped search budget; provider tests pin missing, stored, and rotated credential behavior plus literal and ambient compatibility.
+Native model requests on headless, full SDK, ACP, and custom base-only profiles carry the `web_search` and `web_fetch` schemas and guidance; Web presets expose the same pair, including beneath `run_code` in PTC mode. Search adds a complete auxiliary model call and may use the native server tool multiple times; its exact secret-free request remains reconstructable from the initiating session log. Fetch enforces public addresses and requires no per-call approval. The Web snapshot lane boots the shipped tree, drives a replayed `web_search` call through the real DeepSeek provider against a local Messages fixture, asserts the durable auxiliary request and structured result, and pins the settled browser presentation. Shared snapshot headers pin the common fetch schema and prompt guidance. Composition smokes pin the tool roster; the built composition dump pins the one-minute shipped search budget; provider tests pin missing, stored, and rotated credential behavior plus literal and ambient compatibility.

+ 5 - 3
.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md

@@ -4,13 +4,15 @@ Status: implemented
 
 [English](2026-07-31-web-default-search.md) | 中文
 
+[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)取代本文关于抓取按需启用的决策。本文继续负责默认搜索提供方、凭据解析、端点、超时,以及提供方可用性与模型工具注册之间的区分。
+
 ## 问题
 
 该 harness 已具备完整的 Web 能力体系:提供方注册表、DeepSeek、Exa 和 Perplexity 搜索提供方、本地抓取、稳定的面向模型工具,以及结构化结果呈现,但已交付的 `dsh web` 组合没有挂载其中任何一项。除非部署提供自定义覆盖层,否则模型无法发现最新信息。仅挂载现有 DeepSeek 提供方仍无法打通 WebUI 链路:Models 页面通过 `ctx.credentials` 存储 `DEEPSEEK_API_KEY`,而搜索提供方只会在插件加载时固定读取进程环境,因此在运行中的 UI 输入或轮换的密钥无法用于搜索。
 
 ## 决策
 
-`apps/cli/config/base.cordis.yml` 明确挂载 `dsh-web`,配置 `searchProvider: deepseek-official` 与 `fetchProvider: http`,同时挂载 `dsh-web-search-deepseek`、`dsh-web-fetch-http`,并以 `fetch: false` 和 `searchTimeoutMs: 60000` 挂载 `dsh-tool-web`。因此,共享 base 只会暴露 `web_search`,除非产品 preset 启用抓取;已交付的 Web `cordis`、`ptc` 与 `standard` preset 会启用抓取。显式提供方 id 使选择不受注册顺序影响,同时个人覆盖层或 `--config` 覆盖层仍可替换或禁用这些配置项。已交付的一分钟预算用于覆盖一次辅助 DeepSeek Messages 请求及服务端检索,同时保持 `dsh-tool-web` 提供方无关的 30 秒默认值不变,以供自定义组合使用。[Web 能力 seam 决策](../architecture/2026-06-24-web-capability-seam.zh.md)负责公开抓取安全策略与 Web preset 默认值。
+`packages/bundle/base/cordis.patch.yml` 明确挂载 `dsh-web`,配置 `searchProvider: deepseek-official` 与 `fetchProvider: http`,同时挂载 `dsh-web-search-deepseek`、`dsh-web-fetch-http`,并以 `searchTimeoutMs: 60000` 挂载 `dsh-tool-web`。[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)负责当前的 `fetch: true`;本文继续负责提供方选择、搜索凭据与超时。显式提供方 id 使选择不受注册顺序影响,同时个人覆盖层或 `--patch` 覆盖层仍可替换或禁用这些配置项。已交付的一分钟预算用于覆盖一次辅助 DeepSeek Messages 请求及服务端检索,同时保持 `dsh-tool-web` 提供方无关的 30 秒默认值不变,以供自定义组合使用。[Web 能力 seam 决策](../architecture/2026-06-24-web-capability-seam.zh.md)负责公开抓取安全策略。
 
 DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据引用。提供方在每次搜索内部通过可选的 `ctx.credentials` 服务解析该引用;只有未挂载该 seam 的组合才会回退到启动进程的环境变量,非空的 `apiKey` 字面值仍作为程序化配置的最后兜底。因此,由 Web 的 Models 页存储或轮换的密钥无需重启即可用于下一次搜索,提供方也无需保留该值。由于 `WebSearchProvider.available()` 是同步方法,它会将已安装解析器视为本地可用;若动态凭据缺失,操作会以提供方专属错误码 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败,而稳定的工具 schema 仍保持注册。
 
@@ -30,8 +32,8 @@ DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据
 
 **提高 `dsh-tool-web` 的提供方无关超时。** 不予采纳:自定义提供方和部署有各自不同的延迟预期;这一部署预算应归已交付的 DeepSeek 组合所有。
 
-**在每个共享 base surface 上启用抓取。** 不予采纳:共享 base 服务于网络策略不同的产品。它会挂载仅限公网的提供方,但保持工具按需启用;已交付的 Web preset 会有意启用该工具,其他产品则可以继续隐藏它或添加更严格的网络策略。
+**在每个共享 base surface 上启用抓取。** 本文曾因各产品可能需要不同网络策略而否决该方案。已交付产品采用同一个完整工具集合后,[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)取代了该否决;仅限公开目的地址与无需逐次审批的约束仍然有效。
 
 ## 后果
 
-每个共享 base surface 的原生模型请求都会携带 `web_search` schema 与搜索指引;Web/无头 PTC 模式 通过 `run_code` 公开相同的搜索能力。搜索会增加一次完整的辅助模型调用,并可能多次使用原生服务器工具;发起会话的日志仍可精确重建其不含密钥的请求。已交付的 Web `cordis`、`ptc` 与 `standard` preset 还会暴露 `web_fetch`,实施公开地址强制校验且无需逐次审批。Web 快照通道会启动已交付配置树,使用本地 Messages fixture(测试前置数据),经由真实 DeepSeek 提供方驱动一次回放的 `web_search` 调用,断言持久化的辅助请求与结构化结果,并固定最终浏览器呈现。组合冒烟测试会固定共享搜索清单与各 preset 的抓取选择;构建后组合配置的转储固定已交付的一分钟搜索预算;提供方测试固定缺失、已存储及已轮换凭据的行为,以及字面值与环境变量的兼容性。
+headless、完整 SDK、ACP 与仅使用 base 的自定义 profile 的原生模型请求都会携带 `web_search` 和 `web_fetch` schema 与指引;Web preset 会暴露同一对工具,PTC mode 还会通过 `run_code` 暴露它们。搜索会增加一次完整的辅助模型调用,并可能多次使用原生服务器工具;发起会话的日志仍可精确重建其不含密钥的请求。抓取会强制使用公开地址,并且无需逐次审批。Web 快照通道会启动已交付配置树,使用本地 Messages fixture(测试前置数据),经由真实 DeepSeek 提供方驱动一次回放的 `web_search` 调用,断言持久化的辅助请求与结构化结果,并固定最终浏览器呈现。共享 snapshot header 会固定通用的抓取 schema 与提示指引。组合冒烟测试会固定工具集合;构建后组合配置的转储固定已交付的一分钟搜索预算;提供方测试固定缺失、已存储及已轮换凭据的行为,以及字面值与环境变量的兼容性。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md
+2026-09-01-shared-base-web-fetch-default.md: eceb2009d8a845b4a82f62b99eae13d86e89d050
+2026-09-01-shared-base-web-fetch-default.zh.md: 9be9c6350b6abce2e21bafe1f8c09efcb4265386

+ 27 - 0
.agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md

@@ -0,0 +1,27 @@
+# Agent Note: Shared-base Web fetch default
+
+Status: implemented
+
+English | [中文](2026-09-01-shared-base-web-fetch-default.zh.md)
+
+This decision partially supersedes the fetch opt-in choice in [Default Web search in shipped compositions](2026-07-31-web-default-search.md). That record continues to own search provider selection, credentials, endpoint, timeout, and the separation between provider availability and model-tool registration; no active Agent Note is fully superseded or eligible for archival.
+
+## Problem
+
+Every shipped full agent product accepts anonymous public Web fetch, but `dsh-base` disabled `web_fetch` and required each application bundle to repeat the same override. The repeated configuration omitted ACP, made new base-backed profiles search-only unless their authors noticed the exception, and forced otherwise identical snapshot headers to split by product.
+
+## Decision
+
+`packages/bundle/base/cordis.patch.yml` mounts `dsh-tool-web` with `fetch: true` and the shipped 60-second search timeout. Headless, full SDK, ACP, and custom base-only profiles inherit both `web_search` and `web_fetch` without application-level overrides. The Web app disables the base tool row and composes the same pair per agent preset. The standalone `sdk-minimal` profile remains independent of base.
+
+The base HTTP provider permits anonymous `http:` and `https:` requests only to validated public destinations. Fetch executes outside shell and filesystem sandbox or approval presets and requires no per-call approval; public-destination validation does not prevent public data egress. A product that requires a different network policy overrides the complete `tool-web` config in a later bundle or profile patch.
+
+## Alternatives considered
+
+**Keep fetch disabled in base and enable it in each product.** Rejected because every shipped full product selects the same capability, so the repeated rows encode no product difference and can omit future base-backed profiles.
+
+**Add only an ACP override.** Rejected because it repairs the current omission while retaining three redundant application-level settings and the same failure mode for future profiles.
+
+## Consequences
+
+Base-backed model requests expose the fetch schema and prompt guidance by default, including ACP automation and custom profiles that name only `dsh-base`. Restricted deployments must opt out explicitly. Headless, SDK, and ACP can share the same model-header snapshot sources, while focused real-profile tests pin the shipped tool roster.

+ 27 - 0
.agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: 共享 base 的 Web 抓取默认值
+
+Status: implemented
+
+[English](2026-09-01-shared-base-web-fetch-default.md) | 中文
+
+本决策部分取代[已交付组合中的默认 Web 搜索](2026-07-31-web-default-search.zh.md)里关于抓取按需启用的选择。该记录继续负责搜索提供方选择、凭据、端点、超时,以及提供方可用性与模型工具注册之间的区分;没有任何 active Agent Note 被完全取代或符合归档条件。
+
+## 问题
+
+所有随附的完整 agent 产品都接受匿名公开 Web 抓取,但 `dsh-base` 会禁用 `web_fetch`,要求每个应用组合包重复相同的覆盖。重复配置遗漏了 ACP,使新的 base-backed profile 默认只有搜索能力,除非作者注意到这个例外,还迫使产品之间原本相同的 snapshot header 分开维护。
+
+## 决策
+
+`packages/bundle/base/cordis.patch.yml` 以 `fetch: true` 和随附的 60 秒搜索超时挂载 `dsh-tool-web`。Headless、完整 SDK、ACP 与仅使用 base 的自定义 profile 会继承 `web_search` 和 `web_fetch`,无需应用级覆盖。Web app 会禁用 base 工具配置项,并按 agent preset 组合相同的一对工具。独立的 `sdk-minimal` profile 不使用 base,因此保持不变。
+
+base HTTP 提供方只允许匿名请求经过验证的公开 `http:` 与 `https:` 目的地址。抓取在 shell 和文件系统 sandbox 或审批 preset 之外执行,无需逐次审批;公开目的地址校验不会阻止向公网发送数据。需要不同网络策略的产品应在后续组合包或 profile patch 中覆盖完整的 `tool-web` 配置。
+
+## 考虑过的替代方案
+
+**在 base 中禁用抓取,再由每个产品分别启用。** 不予采纳:所有随附的完整产品都选择相同能力,重复配置没有表达产品差异,还可能遗漏未来的 base-backed profile。
+
+**只增加 ACP 覆盖。** 不予采纳:这种方式能修复当前遗漏,但会保留三处重复的应用级设置,也会让未来 profile 面临相同问题。
+
+## 后果
+
+基于 base 的模型请求默认暴露抓取 schema 与 prompt 指引,包括 ACP 自动化和只列出 `dsh-base` 的自定义 profile。受限部署必须显式关闭。Headless、SDK 与 ACP 可以共享相同的模型 header snapshot 来源,聚焦的真实 profile 测试会固定随附工具集合。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md
+2026-09-01-web-elevation-stroke-shadows.md: 2bc3105203b63c87aaf39e3d68780650b163dcca
+2026-09-01-web-elevation-stroke-shadows.zh.md: bda3ee27af03d20937253fa23968afd0cd7d39c7

+ 42 - 0
.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md

@@ -0,0 +1,42 @@
+# Agent Note: Web elevation — hairline stroke drawn in shadow
+
+Status: implemented
+
+English | [中文](2026-09-01-web-elevation-stroke-shadows.zh.md)
+
+## Problem
+
+Elevated web-client surfaces — menus, popovers, modals, panels, floating buttons, the composer — each paired a real `border: 1px solid <neutral token>` with a `--dsw-shadow-lv2`/`lv3` shadow. The border consumes layout (1px per side, and it is the UA-default replacement on `<button>` elements), the light theme drew most floats with no stroke at all (`--dsw-alias-border-inverted` is transparent in light) while `lv3` faked one with a blurred 1px ring, and the composer wore a broad soft `lv2` patch that read as a smudge rather than a lifted surface. Current desktop chat UIs instead draw elevation as one `box-shadow` list: a 0.5px hairline stroke plus two faint soft layers, with `border: 0` on the surface.
+
+## Decision
+
+`gradient-shadow-text.css` (the ui-theme shadow owner) defines the elevation tokens beside the `--dsw-shadow-lv*` scale:
+
+- `--dsw-elevation-stroke-color` — the hairline color, defaulting to `--dsw-alias-border-l4` (black 16% light, white 20% dark); components rebind it per surface or state: every menu-fill surface (`--dsw-specific-menu` background) rebinds the lightest `--dsw-alias-border-l1` and the composer rebinds `--dsw-alias-border-l2`, both quieter than panels and buttons. The default is declared on `body` alone while the derived tokens below are re-declared on `body, body *`: a custom property computes with `var()` already substituted, so body-only derived tokens would bake in body's color and make every rebind a no-op (the same per-element re-substitution scrollbar.css states for `--dsh-scrollbar-thumb`).
+- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)` — the stroke alone, used standalone by inline cards that want only an outline (the plugin-inventory card).
+- `--dsw-elevation-panel` / `--dsw-elevation-prominent` — the stroke plus two faint soft layers (3px directional + 16/20px glow at 2–5% black), panel for small floating widgets and cards, prominent for floats, and soft — larger blur at lower alpha — for the composer.
+
+Converted surfaces set `border: 0` and one elevation shadow: every `--dsw-shadow-lv3` float (Menu, Modal, popup selects, model select, usage/context popovers, feedback actions, schedule/job popovers, subagent lineage, settings panel, cordis panel, experimental team panel) takes prominent, and the lv2 surfaces (scroll-to-bottom button, turn-preview card, attachment-rail arrows, question composer, trajectory tooltip) take panel. The composer card takes the soft tier with the l2 stroke rebind, and its workspace-trigger state sets the stroke color `transparent` instead of the former `border-color: transparent`. Dark theme needs no shadow overrides: the soft layers are near-invisible there and the stroke carries the separation.
+
+`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` pins the token composition and scans every stylesheet under `packages/`: a rule pairing an lv/elevation `box-shadow` with a `--dsw-alias-border-*` border fails, and every `solid` border on a neutral `--dsw-alias-border-*` token must be `0.5px` wide. Deliberate keeps: Toast and HoverCard (inverted fills where a theme-following stroke is meaningless), ImageLightbox (bare image), and the warn-bordered approval/plan panels, whose state-colored borders stay real borders and pass the scan.
+
+Flat widgets keep real borders at hairline width: every neutral-token `1px solid` border — buttons (the shared outline variant, add/retry/inspect buttons), inputs, inline cards, code blocks, and the settings row separators — is `0.5px solid`, with full-box strokes deepened to `--dsw-alias-border-l4` (buttons one step lighter at `--dsw-alias-border-l3`) while row separators keep `--dsw-alias-border-l2`; state logic (`border-color` swaps on focus/hover) is unchanged. Separators drawn as filled boxes take the same weight: the 1px-tall or 1px-wide lines with a border-token background (menu separators, the conversation header seam, markdown `hr`, the tool IO dividers, the trajectory rail, the directory-browser divider) are 0.5px, and the context-injection divider that read the never-defined `--dsw-alias-line-secondary` (so it never rendered) now draws `0.5px solid var(--dsw-alias-border-l2)`. Chromium paints sub-device-pixel borders as one device pixel, so 1x displays render exactly the former line and 2x displays get the hairline. Dashed affordances stay 1px (a 0.5px dash pattern degrades), and the two border-drawn spinner rings keep their track width through the spec's explicit allowlist.
+
+## Alternatives considered
+
+**Keeping 1px real borders and only softening the shadows.** Leaves the layout-consuming border, the light-theme stroke gap on `border-inverted` floats, and the double outline wherever both existed; the stroke-in-shadow form is what produces the crisp hairline edge.
+
+**A 1px stroke instead of 0.5px.** At 2x displays 0.5px renders one physical pixel, and on 1x it blends lighter, which is the intended hairline. 1px reads as the old border.
+
+**Drawing flat-widget hairlines as box-shadow strokes too.** Buttons and inputs swap `border-color` on hover and focus and several pair a box-shadow focus ring; moving their stroke into `box-shadow` would collide with those rings (one property) and rewrite every state rule, while `0.5px solid` keeps the whole state logic and changes only the weight.
+
+**Suppressing the composer trigger stroke with `box-shadow: none`.** Also drops the soft layers the trigger state keeps today; rebinding `--dsw-elevation-stroke-color: transparent` removes exactly the stroke.
+
+**Converting Toast/HoverCard too.** Their fills are inverted relative to the theme, so the theme-following stroke color is invisible-or-wrong on them; they keep `lv3` until an inverted-surface stroke token exists.
+
+## Consequences
+
+- Every converted surface gains a hairline outline in the light theme (most floats previously had none) and loses 1px of border from its box; the visual size change is at most 2px on small buttons and imperceptible on panels.
+- The neutral-border-plus-shadow pairing is now rejected by the ui-theme elevation spec, so a new elevated surface must choose the elevation tokens; the rule lives in [docs/web-styling.md](../../../../docs/web-styling.md).
+- The composer's broad `lv2` patch becomes stroke + tight glow; its dark stroke keeps the figma one-notch-weaker value through the rebind.
+- `--dsw-shadow-lv1`/`lv1-blur` currently have no consumer and `lv2`/`lv3` remain only on the deliberate keeps; the scale stays for inverted and bespoke surfaces.

+ 42 - 0
.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.zh.md

@@ -0,0 +1,42 @@
+# Agent Note: Web elevation — hairline stroke drawn in shadow
+
+Status: implemented
+
+[English](2026-09-01-web-elevation-stroke-shadows.md) | 中文
+
+## Problem
+
+Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬浮按钮、输入框——原先都把真 `border: 1px solid <中性 token>` 与 `--dsw-shadow-lv2`/`lv3` 投影配对。border 占布局(每侧 1px,且在 `<button>` 上是对 UA 默认边框的替换);浅色主题下多数浮层实际没有描边(`--dsw-alias-border-inverted` 在浅色下是透明的),靠 `lv3` 里模糊的 1px 环冒充;输入框则披着一大片柔和的 `lv2` 投影,读起来更像污渍而非悬浮表面。当前桌面聊天 UI 改用单个 `box-shadow` 列表绘制 elevation:0.5px 发丝描边加两层极淡柔光,表面本身 `border: 0`。
+
+## Decision
+
+`gradient-shadow-text.css`(ui-theme 的阴影归属地)在 `--dsw-shadow-lv*` 阶旁定义 elevation token:
+
+- `--dsw-elevation-stroke-color`——发丝描边颜色,默认 `--dsw-alias-border-l4`(浅色黑 16%、深色白 20%);组件可按表面或状态重绑:所有菜单面(`--dsw-specific-menu` 背景)重绑最浅的 `--dsw-alias-border-l1`、输入框重绑 `--dsw-alias-border-l2`,两者都比面板与按钮安静。默认色只声明在 `body` 上,而下述派生 token 在 `body, body *` 上逐元素重声明:自定义属性的计算值已替换完 `var()`,派生 token 若只在 body 声明会把 body 的颜色固化进去,让所有重绑失效(与 scrollbar.css 对 `--dsh-scrollbar-thumb` 声明的逐元素重替换是同一契约)。
+- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)`——单独的描边,供只要轮廓的行内卡片独立使用(插件清单卡片)。
+- `--dsw-elevation-panel` / `--dsw-elevation-prominent`——描边加两层极淡柔光(3px 方向光 + 16/20px 辉光,黑 2–5%),panel 用于小型悬浮部件与卡片,prominent 用于浮层,soft——更大模糊、更低透明度——用于输入框。
+
+被转换的表面设 `border: 0` 加一个 elevation 投影:所有 `--dsw-shadow-lv3` 浮层(Menu、Modal、弹出选择、模型选择、用量/上下文浮层、反馈操作条、日程/任务浮层、子代理谱系、设置面板、cordis 面板、实验性 team 面板)取 prominent;lv2 表面(回到底部按钮、回合预览卡、附件栏箭头、问题 composer、轨迹 tooltip)取 panel。输入框卡片取 soft 档并重绑 l2 描边,其 workspace-trigger 态把描边色设为 `transparent`,替代原先的 `border-color: transparent`。深色主题无需投影覆盖:柔光在深色下几乎不可见,分离由描边承担。
+
+`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` 钉住 token 组成并扫描 `packages/` 下全部样式表:lv/elevation `box-shadow` 与 `--dsw-alias-border-*` border 配对的规则即失败;中性 `--dsw-alias-border-*` token 上的每个 `solid` border 必须为 `0.5px` 宽。有意保留:Toast 与 HoverCard(反色填充,跟随主题的描边色在其上无意义)、ImageLightbox(裸图片)、warn 描边的审批/计划面板——状态色 border 保持真 border,扫描放行。
+
+平面部件保留真 border 但取发丝线宽度:所有中性 token 的 `1px solid` border——按钮(共享 outline 变体、添加/重试/inspect 按钮)、输入框、行内卡片、代码块与设置行分割线——改为 `0.5px solid`,整框描边加深为 `--dsw-alias-border-l4`(按钮浅一档取 `--dsw-alias-border-l3`),行分割线保持 `--dsw-alias-border-l2`;状态逻辑(focus/hover 换 `border-color`)不变。以填充盒绘制的分隔线取同一粗细:高或宽为 1px、背景用 border token 的线(菜单分隔、对话标题栏接缝、markdown `hr`、工具 IO 分隔、轨迹竖轨、目录浏览器分隔)改为 0.5px;上下文注入分隔线原先读取从未定义的 `--dsw-alias-line-secondary`(因此从未渲染),现在绘制 `0.5px solid var(--dsw-alias-border-l2)`。Chromium 把亚设备像素 border 绘制为一个设备像素,因此 1x 屏与原先渲染完全一致,2x 屏得到发丝线。dashed 记号保持 1px(0.5px 虚线图案会退化),两个用 border 画的 spinner 圆环经 spec 显式豁免保留轨道宽度。
+
+## Alternatives considered
+
+**保留 1px 真 border、只调柔投影。** 留下占布局的 border、浅色主题 `border-inverted` 浮层的描边缺口,以及两者并存处的双轮廓;描边入投影正是产生锐利发丝边缘的形式。
+
+**用 1px 而非 0.5px 描边。** 0.5px 在 2x 屏渲染为一物理像素,1x 屏混合得更浅,正是发丝线意图。1px 读起来就是原来的 border。
+
+**平面部件的发丝线也用 box-shadow 描边画。** 按钮与输入框在 hover/focus 时切换 `border-color`,多处还配 box-shadow 焦点环;把描边挪进 `box-shadow`(单一属性)会与焦点环冲突并重写全部状态规则,而 `0.5px solid` 保留整套状态逻辑,只改粗细。
+
+**composer trigger 态用 `box-shadow: none` 抑制描边。** 会连带丢掉该状态今天保留的柔光;重绑 `--dsw-elevation-stroke-color: transparent` 恰好只去掉描边。
+
+**Toast/HoverCard 一并转换。** 其填充相对主题反色,跟随主题的描边色在其上不可见或错误;在出现反色表面描边 token 之前保留 `lv3`。
+
+## Consequences
+
+- 每个被转换表面在浅色主题下获得发丝轮廓(多数浮层此前没有),盒子少了 1px border;视觉尺寸变化在小按钮上至多 2px,面板上不可察觉。
+- 中性 border 加投影的配对现被 ui-theme elevation spec 拒绝,新的高层级表面必须选用 elevation token;规则记录于 [docs/web-styling.md](../../../../docs/web-styling.zh.md)。
+- 输入框的大片 `lv2` 变为描边加收紧的辉光;深色描边经重绑保留 figma 低一档取值。
+- `--dsw-shadow-lv1`/`lv1-blur` 目前无消费方,`lv2`/`lv3` 只剩有意保留者;该阶为反色与定制表面继续存在。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md
+2026-09-01-web-superellipse-corner-smoothing.md: 7088438a8068343434a27495b771515d4a5158ee
+2026-09-01-web-superellipse-corner-smoothing.zh.md: 26aa64fda9f557069cfc3040422c8e1b7e34676e

+ 34 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md

@@ -0,0 +1,34 @@
+# Agent Note: Web superellipse corner smoothing
+
+Status: implemented
+
+English | [中文](2026-09-01-web-superellipse-corner-smoothing.zh.md)
+
+## Problem
+
+Every rounded surface in the web client — cards, composer, buttons, popovers — draws its corners as plain circular arcs, which read as visibly harder than the smooth (squircle-like) corners current desktop chat UIs ship. That smoothness comes from CSS `corner-shape: superellipse(1.5)` applied behind an `@supports` guard, not from larger radii or masking tricks; utility-class implementations attach it to every rounded-corner class except full-round. This client has no utility classes: `border-radius` values are px literals spread across CSS Modules in every client package, so there is no single class list to attach the property to, and full-round shapes (`border-radius: 50%` circles, 999px pills) must keep circular arcs — a superellipse deforms a circle into a squircle, so a border-drawn spinner would visibly wobble, and it squares off capsule ends.
+
+## Decision
+
+`packages/client/ui-theme/src/styles/corner-shape.css` is a global sheet mounted by ui-theme's client entry (after `base.css`). Inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape: superellipse(1.5)` on `:root` and applies `corner-shape: var(--dsw-corner-shape)` through `*, *::before, *::after` — `corner-shape` does not inherit, so the universal selector is the mechanism that reaches every rounded surface without a utility-class system. Engines without `corner-shape` keep circular corners because both declarations live inside the guard. `superellipse(1.5)` sits between `round` (`superellipse(1)`) and `squircle` (`superellipse(2)`), matching the smoothing current desktop chat UIs ship.
+
+Full-round shapes opt back out at their declaration: every `border-radius` of `50%`, `100%`, or a pill radius (≥ 99px) pairs `corner-shape: round` in the same rule of its owning component sheet. The pairing is enforced by `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts`, which scans every stylesheet under `packages/` (the shared scan helpers live in `tests/stylesheet-scan.ts`, extracted from the scrollbar spec); the same spec pins the guard and the universal application in `corner-shape.css`. Component-local radius indirections (`--dsl-*-radius`) all hold values far below the pill threshold, so the lexical scan covers current usage.
+
+Token-based implementations pair the shape change with a 1.25× radius scale; this client has no radius tokens (px literals per component), so radii are unchanged and only the corner curvature moves.
+
+## Alternatives considered
+
+**A radius token system first, then per-token application.** Faithful, but converting ~130 px-literal radii across every client package into tokens is a large refactor serving no other current need; the universal selector reaches the same surfaces with one rule.
+
+**Applying superellipse to full-round shapes too (no opt-outs).** Fewer declarations, but spinners built from `border-radius: 50%` borders wobble when the rotating shape is not a circle, and capsule ends square off.
+
+**Subtree opt-out via `--dsw-corner-shape: round` instead of per-declaration `corner-shape: round`.** The custom property inherits, so a pill's rounded descendants would silently lose smoothing; the explicit per-rule declaration keeps the opt-out exactly as wide as the full-round shape and is what the pairing spec can check.
+
+**Scaling radii by 1.25 alongside the curvature change.** Requires the token system above; the curvature change alone already delivers the smoothness, and radii stay as designed.
+
+## Consequences
+
+- On engines with `corner-shape` (Chromium ≥ 139 behind no flag), every rounded corner in the client curves along `superellipse(1.5)`; other engines render exactly as before, with no fallback code.
+- New full-round shapes must pair `corner-shape: round` or the ui-theme corner-shape spec fails; the rule is stated in [docs/web-styling.md](../../../../docs/web-styling.md) and costs one extra declaration per circle or pill.
+- The universal selector adds one non-inherited property to every element; the declaration is a constant and profiling concerns are theoretical at current tree sizes.
+- A rounded box whose radius equals half its height (an implicit pill under 99px) still receives the superellipse; the scan cannot see computed geometry, and such ends read as intentional smoothing rather than distortion.

+ 34 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.zh.md

@@ -0,0 +1,34 @@
+# Agent Note: Web superellipse corner smoothing
+
+Status: implemented
+
+[English](2026-09-01-web-superellipse-corner-smoothing.md) | 中文
+
+## Problem
+
+Web 客户端里每个圆角表面——卡片、输入框、按钮、浮层——都以普通圆弧绘制圆角,观感明显硬于当前桌面聊天 UI 普遍采用的平滑(类 squircle)圆角。这种平滑感来自在 `@supports` 守卫内应用 CSS `corner-shape: superellipse(1.5)`,而非更大的半径或遮罩技巧;工具类体系的实现把它挂到除正圆之外的所有圆角工具类上。本客户端没有工具类:`border-radius` 以 px 字面量散布在各客户端包的 CSS Modules 中,没有可以统一挂载该属性的类列表;而正圆形状(`border-radius: 50%` 的圆、999px 胶囊)必须保持圆弧——超级椭圆会把圆变形为 squircle,用 border 绘制的加载圈旋转时会明显晃动,胶囊两端也会变方。
+
+## Decision
+
+`packages/client/ui-theme/src/styles/corner-shape.css` 是由 ui-theme 客户端 entry 挂载的全局样式表(位于 `base.css` 之后)。它在 `@supports (corner-shape: superellipse(1.5))` 内于 `:root` 定义 `--dsw-corner-shape: superellipse(1.5)`,并通过 `*, *::before, *::after` 应用 `corner-shape: var(--dsw-corner-shape)`——`corner-shape` 不继承,通配选择器正是在没有工具类系统的前提下触达每个圆角表面的机制。两条声明都在守卫内,因此不支持 `corner-shape` 的引擎保持普通圆弧。`superellipse(1.5)` 介于 `round`(`superellipse(1)`)与 `squircle`(`superellipse(2)`)之间,与当前桌面聊天 UI 的平滑度一致。
+
+正圆形状在其声明处退出:每个取值为 `50%`、`100%` 或胶囊半径(≥ 99px)的 `border-radius`,都在所属组件样式表的同一规则内配对 `corner-shape: round`。该配对由 `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts` 强制,它扫描 `packages/` 下的全部样式表(共享扫描辅助函数位于 `tests/stylesheet-scan.ts`,自 scrollbar spec 抽出);同一 spec 也钉住 `corner-shape.css` 的守卫与通配应用。组件局部半径变量(`--dsl-*-radius`)取值都远低于胶囊阈值,因此词法扫描覆盖当前用法。
+
+基于 token 的实现会在改变曲线的同时把半径 token 乘以 1.25;本客户端没有半径 token(各组件 px 字面量),故半径不变,只改变圆角曲率。
+
+## Alternatives considered
+
+**先建半径 token 系统,再按 token 应用。** 更忠实,但把所有客户端包约 130 处 px 字面量半径改造成 token 是没有其他现实需求的大重构;通配选择器用一条规则触达同样的表面。
+
+**对正圆形状也应用超级椭圆(不设豁免)。** 声明更少,但用 `border-radius: 50%` border 绘制的加载圈在形状不是圆时旋转会晃动,胶囊两端会变方。
+
+**用子树级 `--dsw-corner-shape: round` 替代逐声明 `corner-shape: round` 豁免。** 自定义属性会继承,胶囊的圆角后代会静默失去平滑;逐规则显式声明让豁免范围恰好等于正圆形状本身,也是配对 spec 能检查的形式。
+
+**在改变曲率的同时把半径乘 1.25。** 需要上述 token 系统;仅曲率变化已带来平滑感,半径维持设计值。
+
+## Consequences
+
+- 在支持 `corner-shape` 的引擎(Chromium ≥ 139,无需 flag)上,客户端每个圆角都沿 `superellipse(1.5)` 弯曲;其他引擎渲染与之前完全一致,没有回退代码。
+- 新增正圆形状必须配对 `corner-shape: round`,否则 ui-theme 的 corner-shape spec 失败;该规则记录于 [docs/web-styling.md](../../../../docs/web-styling.zh.md),每个圆或胶囊多付一条声明。
+- 通配选择器给每个元素增加一个不继承的属性;声明是常量,在当前树规模下性能顾虑属于理论层面。
+- 半径等于自身高度一半的圆角盒(低于 99px 的隐式胶囊)仍会得到超级椭圆;扫描看不到计算后几何,此类端部读作有意的平滑而非变形。

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: f5cbe1659e5181cdaa6eaefcdb9bd8c8fe289e6d
-README.zh.md: cf040f085241f0af58eb3db485bcedd4316726be
+README.md: b43c147036ad230ed85cebafa3df89d67a802f6b
+README.zh.md: 9191139e28b4bb449593514cb61750de42dba24e

+ 1 - 1
apps/cli/reference/README.md

@@ -89,7 +89,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
 
 ## Shared deployment behavior
 
-The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, the public-only HTTP fetch provider, and feedback-gated session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. The Web app's `cordis`, `ptc`, and `standard` agent presets expose `web_fetch` in every sandbox and approval mode without per-call confirmation; the provider still rejects non-public destinations before connecting.
+The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, and feedback-gated session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
 
 Session telemetry defaults to feedback-gated sharing: nothing is uploaded until the user records `/feedback`, and each recorded feedback uploads the session records not yet shared, through that event; a resumed session shares only its current lifecycle. `DSH_TELEMETRY_MODE=FULL` instead streams every projected session event as OTLP/HTTP logs, `DSH_TELEMETRY_MODE=DISABLED` keeps everything local, and any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative hard opt-out. `DSH_TELEMETRY_OTLP_URL` selects another collector. The shipped base has no telemetry redaction rule, so released exports can contain message text, tool arguments and results, and workspace paths; the [feedback-gated-default Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-feedback-gated-telemetry-default.md) owns that deployment decision.
 

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -89,7 +89,7 @@ dsh web --help
 
 ## 共享部署行为
 
-基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search`、仅限公网的 HTTP fetch 提供方,以及按反馈门控的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。Web app 的 `cordis`、`ptc` 与 `standard` agent preset 会在所有 sandbox 和审批模式下暴露 `web_fetch`,无需逐次确认;提供方仍会在连接前拒绝非公开目的地址。
+基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,以及按反馈门控的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
 
 会话遥测默认按反馈门控共享:在用户记录 `/feedback` 之前不上传任何数据,每条已记录的反馈通过该事件上传尚未共享的会话记录;恢复的会话只共享当前生命周期。`DSH_TELEMETRY_MODE=FULL` 改为将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=DISABLED` 让全部数据留在本地,任何非空的 `DSH_TELEMETRY_DISABLED` 仍是具有最终效力的遥测强制关闭开关。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。随附基础配置没有遥测脱敏规则,因此释放的导出可能包含消息文本、工具参数和结果,以及 workspace 路径;相关部署决策见[反馈门控默认值 Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-feedback-gated-telemetry-default.zh.md)。
 

+ 15 - 4
apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts

@@ -1,22 +1,22 @@
 import { readFile, readdir } from 'node:fs/promises'
-import { zstdDecompress } from 'node:zlib'
-import { promisify } from 'node:util'
+import { zstdDecompressSync } from 'node:zlib'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { describe, expect, it } from 'vitest'
 import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import { scanZstdFrames } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.js'
 
 const PRODUCTION_PROFILE_PROCESS_TIMEOUT_MS = 60_000
 const PRODUCTION_PROFILE_TEST_TIMEOUT_MS = PRODUCTION_PROFILE_PROCESS_TIMEOUT_MS + 15_000
 const binScript = fileURLToPath(new URL('../../../../../../packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', import.meta.url))
 const configPath = fileURLToPath(new URL('./fixtures/cli.patch.yml', import.meta.url))
 const tsconfigPath = fileURLToPath(new URL('../../../../../../tsconfig.json', import.meta.url))
-const decompress = promisify(zstdDecompress)
 
 describe('headless-agent keyless smoke', () => {
   it('boots the real Loader tree, runs the production shell tool, and persists the turn', async () => {
     let persistedHeader: Record<string, unknown> | undefined
+    let persistedToolNames: string[] = []
     const { stdout, stderr } = await runLoaderSmoke({
       label: 'headless-agent',
       tempDirPrefix: 'headless-agent-smoke-',
@@ -33,7 +33,17 @@ describe('headless-agent keyless smoke', () => {
         if (relativePath === undefined) return
         const compressed = await readFile(join(sessionsDir, relativePath))
         expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
-        persistedHeader = JSON.parse((await decompress(compressed)).toString()) as Record<string, unknown>
+        const { frames, tornStart } = scanZstdFrames(compressed)
+        expect(tornStart).toBeUndefined()
+        const records = frames.flatMap(({ start, end }) =>
+          zstdDecompressSync(compressed.subarray(start, end)).toString().trim().split('\n'))
+          .map(line => JSON.parse(line) as Record<string, unknown>)
+        persistedHeader = records[0]
+        const requestHeader = records.find(record => record.type === 'request/header')
+        const data = requestHeader?.data as Record<string, unknown> | undefined
+        const header = data?.header as Record<string, unknown> | undefined
+        const tools = header?.tools as Array<{ name?: string }> | undefined
+        persistedToolNames = tools?.flatMap(tool => tool.name === undefined ? [] : [tool.name]) ?? []
       },
     })
     const lines = stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
@@ -50,5 +60,6 @@ describe('headless-agent keyless smoke', () => {
     })
     expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
     expect(persistedHeader).toMatchObject({ type: 'session' })
+    expect(persistedToolNames).toEqual(expect.arrayContaining(['web_fetch', 'web_search']))
   }, PRODUCTION_PROFILE_TEST_TIMEOUT_MS)
 })

+ 3 - 1
apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts

@@ -151,9 +151,11 @@ describe('Python SDK dsh profile keyless smoke', () => {
         },
       })
       const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
+      const toolNames = tools.map(tool => tool.function?.name)
       expect(modelRequests[0]?.reasoning_effort).toBe('max')
       expect(modelRequests[0]?.max_tokens).toBe(1234)
-      expect(tools.map(tool => tool.function?.name)).not.toContain('list_subagent_models')
+      expect(toolNames).toEqual(expect.arrayContaining(['web_fetch', 'web_search']))
+      expect(toolNames).not.toContain('list_subagent_models')
 
       child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
       const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)

+ 5 - 4
apps/web/tests/expected/models-settings/model-picker.expected.md

@@ -3,16 +3,17 @@
   - button "关闭":
     - img
   - paragraph: 以下是模型提供方的可用模型,勾选要添加的模型。
-  - button "全选"
+  - searchbox "搜索模型"
+  - button "取消全选"
   - list:
     - listitem:
-      - checkbox "MiniMax-M2.7"
+      - checkbox "MiniMax-M2.7" [checked]
       - text: MiniMax-M2.7
     - listitem:
-      - checkbox "MiniMax-M2.7-highspeed"
+      - checkbox "MiniMax-M2.7-highspeed" [checked]
       - text: MiniMax-M2.7-highspeed
     - listitem:
-      - checkbox "MiniMax-M3"
+      - checkbox "MiniMax-M3" [checked]
       - text: MiniMax-M3
   - button "取消"
   - button "添加所选"

+ 12 - 3
apps/web/tests/models-settings.e2e.ts

@@ -179,7 +179,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
     expect(tripwire.pageErrors).toEqual([])
   }, 60_000)
 
-  it('selects and clears the discovered model catalog in one action', async () => {
+  it('filters the discovered model catalog and preserves hidden selections', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-models-picker'))
     const settingsDialog = page.getByRole('dialog', { name: '设置' })
     await settingsDialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
@@ -195,11 +195,21 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
       Array.from({ length: count }, () => true),
     )
 
+    const search = picker.getByRole('searchbox', { name: '搜索模型' })
+    await search.fill('highspeed')
+    await expect.poll(async () => boxes.count()).toBe(1)
     await picker.getByRole('button', { name: '取消全选' }).click()
     expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
-      Array.from({ length: count }, () => false),
+      [false],
     )
+
+    await search.fill('')
+    await expect.poll(async () => boxes.count()).toBe(count)
+    const restored = await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))
+    expect(restored.filter(Boolean)).toHaveLength(count - 1)
+    expect(restored.filter(checked => !checked)).toHaveLength(1)
     await picker.getByRole('button', { name: '全选' }).waitFor()
+    await picker.getByRole('button', { name: '全选' }).click()
     const snapshot = await captureStableAria(
       page,
       '[role="dialog"][aria-label="选择要添加的模型"]',
@@ -207,7 +217,6 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
     )
     await compareOrRefreshGolden(MODEL_PICKER_EXPECTED, snapshot, MODE)
 
-    await picker.getByRole('button', { name: '全选' }).click()
     expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
       Array.from({ length: count }, () => true),
     )

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 85aead7a93bd8dff0da4eb4f0e6b6b348d9616b9
-config-catalog.zh.md: f91ebc6c46989cbf98d2a773f6f2873539ad6351
+config-catalog.md: f9eb7ae0bc634e655462412e7dd5d341b7a449a2
+config-catalog.zh.md: 827f39d068de067aefa4dfc77fc53c3db74b5336

+ 1 - 1
docs/config-catalog.md

@@ -1091,7 +1091,7 @@ export interface PiAiProviderProfile {
    * to answer instead.
    */
   defaultInput?: PiAiModality[]
-  /** Provider request headers; Harness attribution wins reserved names. */
+  /** Provider request headers, validated against Fetch when the profile resolves; Harness attribution wins reserved names. */
   headers?: Record<string, string>
   /** Provider-neutral pi-ai reasoning level. */
   reasoning?: ModelThinkingLevel

+ 1 - 1
docs/config-catalog.zh.md

@@ -1093,7 +1093,7 @@ export interface PiAiProviderProfile {
    * to answer instead.
    */
   defaultInput?: PiAiModality[]
-  /** Provider request headers; Harness attribution wins reserved names. */
+  /** Provider request headers, validated against Fetch when the profile resolves; Harness attribution wins reserved names. */
   headers?: Record<string, string>
   /** Provider-neutral pi-ai reasoning level. */
   reasoning?: ModelThinkingLevel

+ 2 - 2
docs/web-styling.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/web-styling.md
-web-styling.md: 5296cc7f83f712532262eadda6da098ae9f63ec7
-web-styling.zh.md: a2ba074619a7e2737c7e9286d2aeaa90611ac633
+web-styling.md: dd057a3121422e4decfac06b9a3cf57e52254011
+web-styling.zh.md: 5ec0b65390b29e915fe3da633bb7c56ef92e17d6

+ 3 - 0
docs/web-styling.md

@@ -19,6 +19,9 @@ Global style sheets belong in `ui-theme/src/styles/`. Component styles live besi
 - Keep source text, terminal output, and diff lines unwrapped when their component contract requires column preservation; use the shared scrollbar styles rather than component-specific scrollbar selectors.
 - Put presentation in CSS. Inline React styles may pass component-local custom-property values but must not encode theme branches.
 - Preserve keyboard focus visibility and reduced-motion behavior when adding transitions or hover-only controls.
+- Rounded corners inherit the global superellipse smoothing from ui-theme's `corner-shape.css` on supporting engines. Pair `corner-shape: round` with every full-round `border-radius` (`50%`, `100%`, or a pill radius) so circles and capsules keep circular arcs; the ui-theme corner-shape spec enforces the pairing.
+- Elevated surfaces (menus, popovers, modals, panels, floating buttons, the composer) set `border: 0` and take `box-shadow: var(--dsw-elevation-panel)`, `var(--dsw-elevation-prominent)`, or the composer's `var(--dsw-elevation-soft)` (larger blur at lower alpha): the 0.5px hairline stroke is the first shadow layer, and `--dsw-elevation-stroke-color` rebinds or suppresses it per surface or state. Never pair a `--dsw-alias-border-*` border with an lv/elevation shadow — the ui-theme elevation spec rejects the pairing; state-colored borders (warn panels) stay real borders.
+- Flat borders and separators that use a neutral `--dsw-alias-border-*` token draw at `0.5px` — buttons, inputs, cards, row dividers, and separators drawn as filled boxes (menu separators, the conversation header seam, markdown `hr`, vertical rails) share the hairline weight, which Chromium paints as one device pixel. Dashed affordances and state-colored borders keep 1px; spinner ring tracks keep their width through the spec's explicit allowlist. The ui-theme elevation spec rejects wider neutral solid borders.
 
 ## Changing the system
 

+ 3 - 0
docs/web-styling.zh.md

@@ -19,6 +19,9 @@
 - 当组件约定要求保留列结构时,源码文本、终端输出和 diff 行不得换行;使用共享滚动条样式,不得定义组件专用滚动条选择器。
 - 呈现规则写在 CSS 中。React 内联样式可以传递组件局部自定义属性值,但不得编码主题分支。
 - 添加过渡动画或仅悬停可见的控件时,保留清晰可见的键盘焦点和减少动态效果行为。
+- 支持的引擎上,圆角继承 ui-theme `corner-shape.css` 的全局超级椭圆平滑。每个正圆 `border-radius`(`50%`、`100%` 或胶囊半径)必须配对 `corner-shape: round`,使圆形与胶囊保持圆弧;ui-theme 的 corner-shape spec 强制这一配对。
+- 高层级表面(菜单、浮层、对话框、面板、悬浮按钮、输入框)设 `border: 0` 并使用 `box-shadow: var(--dsw-elevation-panel)`、`var(--dsw-elevation-prominent)` 或输入框专用的 `var(--dsw-elevation-soft)`(更大模糊、更低透明度):0.5px 发丝描边是第一层投影,`--dsw-elevation-stroke-color` 可按表面或状态重绑或抑制描边。不得将 `--dsw-alias-border-*` border 与 lv/elevation 投影配对——ui-theme 的 elevation spec 会拒绝;状态色 border(warn 面板)保持真 border。
+- 使用中性 `--dsw-alias-border-*` token 的平面边框与分割线一律 `0.5px`——按钮、输入框、卡片、行分割线,以及以填充盒绘制的分隔线(菜单分隔、对话标题栏接缝、markdown `hr`、竖向轨道线)共用发丝线粗细,Chromium 将其绘制为一个设备像素。dashed 记号与状态色 border 保持 1px;spinner 圆环经 spec 的显式豁免保留原宽度。更宽的中性 solid border 会被 ui-theme elevation spec 拒绝。
 
 ## 变更系统
 

+ 2 - 2
packages/bundle/base/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/base/README.md
-README.md: 6c02fcf946368c1ea931bd8114c1e4cc828af5ea
-README.zh.md: f69853902fa48db4349cc52e2d06a4239a281870
+README.md: 0995f5bc69905e643a117c1f5a833be0f114cdc6
+README.zh.md: c1d180f2dd85d6767f197b561b46f722d653d4fc

+ 3 - 3
packages/bundle/base/README.md

@@ -25,7 +25,7 @@ Every base-backed `dsh --profile` surface runs on `dsh-base`, so those surfaces
 <a id="use-this-package"></a>
 ## Use this package
 
-You get the dsh core automatically: the shipped `web` and `headless` profiles already include it, and a custom profile names it as its first bundle. After that, everything works with no further configuration.
+You get the dsh core automatically: the shipped `web`, `headless`, `sdk`, and `acp` profiles already include it, and a custom profile names it as its first bundle. After that, everything works with no further configuration.
 
 ### A minimal custom profile
 
@@ -43,11 +43,11 @@ To build a profile on the shared core, create a profile with a `package.json` th
 }
 ```
 
-Run `dsh --profile my-profile "your task"` and you get a working agent with model access, tools, persistence, and the default permission policy. The shipped `web` and `headless` profiles are created for you on first use. To add more bundles, run `dsh plugin --profile <name> add <package>`; in-box bundles resolve from the dsh installation. The profile contract is documented in the [app-boot profile section](../../boot/app-boot/README.md).
+Run `dsh --profile my-profile "your task"` and you get a working agent with model access, tools, persistence, and the default permission policy. The shipped `web`, `headless`, `sdk`, and `acp` profiles are created for you on first use. To add more bundles, run `dsh plugin --profile <name> add <package>`; in-box bundles resolve from the dsh installation. The profile contract is documented in the [app-boot profile section](../../boot/app-boot/README.md).
 
 ### What you get
 
-Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Telemetry stays off unless you opt in.
+Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, public HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval; its provider rejects non-public destinations. Telemetry stays off unless you opt in.
 
 ### Shell tools per platform
 

+ 3 - 3
packages/bundle/base/README.zh.md

@@ -25,7 +25,7 @@ kind: "package-bundle"
 <a id="use-this-package"></a>
 ## 使用本包
 
-你会自动获得 dsh 核心:随发行版交付的 `web` 与 `headless` profile 已包含它,自定义 profile 则把它列为第一个组合包。之后一切无需任何额外配置即可工作。
+你会自动获得 dsh 核心:随发行版交付的 `web`、`headless`、`sdk` 与 `acp` profile 已包含它,自定义 profile 则把它列为第一个组合包。之后一切无需任何额外配置即可工作。
 
 ### 最小自定义 profile
 
@@ -43,11 +43,11 @@ kind: "package-bundle"
 }
 ```
 
-运行 `dsh --profile my-profile "your task"`,你就得到一个可用的 agent(智能体),带模型访问、工具、持久化与默认权限策略。随发行版交付的 `web` 与 `headless` profile 会在首次使用时为你创建。要添加更多组合包,运行 `dsh plugin --profile <name> add <package>`;内置组合包从 dsh 安装目录解析。profile 约定见 [app-boot 的 profile 章节](../../boot/app-boot/README.zh.md)。
+运行 `dsh --profile my-profile "your task"`,你就得到一个可用的 agent(智能体),带模型访问、工具、持久化与默认权限策略。随发行版交付的 `web`、`headless`、`sdk` 与 `acp` profile 会在首次使用时为你创建。要添加更多组合包,运行 `dsh plugin --profile <name> add <package>`;内置组合包从 dsh 安装目录解析。profile 约定见 [app-boot 的 profile 章节](../../boot/app-boot/README.zh.md)。
 
 ### 你得到什么
 
-开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(provider 与模型可配置,你还可以在设置中启用额外 provider)、完整工具集——文件编辑、shell 命令、web 搜索、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。遥测默认关闭,除非你主动开启。
+开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(provider 与模型可配置,你还可以在设置中启用额外 provider)、完整工具集——文件编辑、shell 命令、web 搜索、公开 HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,其提供方会拒绝非公开目的地址。遥测默认关闭,除非你主动开启。
 
 ### 各平台的 shell 工具
 

+ 5 - 4
packages/bundle/base/cordis.patch.yml

@@ -433,9 +433,10 @@
         thresholds: [3, 5, 8]
         argumentsPreviewChars: 500
 
-    # Every mode enables the stable model-facing web_search tool. The Web app's
-    # per-agent presets additionally enable web_fetch; other products opt in by
-    # overriding tool-web. DeepSeek search resolves the same DEEPSEEK_API_KEY
+    # The shared base enables the stable model-facing web_search and web_fetch
+    # tools. The Web app disables this host row and composes both tools per agent
+    # preset; products with a stricter network policy override tool-web. DeepSeek
+    # search resolves the same DEEPSEEK_API_KEY
     # credential the Models page manages for chat, at each search; its Messages
     # endpoint is separate from the chat-completions endpoint, so it takes its own
     # base-URL override. Anonymous fetch accepts only public HTTP(S) destinations,
@@ -460,7 +461,7 @@
     - id: tool-web
       name: '@deepseek-ai/dsh-tool-web'
       config:
-        fetch: false
+        fetch: true
         searchTimeoutMs: 60000
 
     # ── rows every mode mounts, whose values each overlay may state ──────────────

+ 1 - 1
packages/bundle/base/tests/base.spec.ts

@@ -43,7 +43,7 @@ describe('dsh-base bundle', () => {
     expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(0)
     expect(rows.find(row => row.id === 'web')?.config).toMatchObject({ fetchProvider: 'http' })
     expect(rows.find(row => row.id === 'web-fetch-http')).toBeDefined()
-    expect(rows.find(row => row.id === 'tool-web')?.config).toMatchObject({ fetch: false })
+    expect(rows.find(row => row.id === 'tool-web')?.config).toMatchObject({ fetch: true })
     expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
     expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-claude-code')
     expect(manifest.dependencies).toHaveProperty('@deepseek-ai/dsh-web-fetch-http')

+ 1 - 1
packages/client/locale/src/client/LanguageRow.module.css

@@ -6,7 +6,7 @@
   align-items: center;
   gap: 8px;
   padding: 16px 0;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .rowText {

+ 17 - 13
packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css

@@ -54,23 +54,25 @@
 }
 
 .card {
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 12px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 20px;
   display: flex;
   flex-direction: column;
-  background: var(--dsw-alias-bg-layer-3);
+  background: transparent;
   transition: border-color .16s, background .16s;
 }
 
 
 .card:hover:not(.cardActive) {
-  border-color: var(--dsw-alias-label-dimmed);
+  background: var(--dsw-alias-interactive-bg-hover);
 }
 
-/* The default preset reads as selected, not merely badged. */
+/* The default preset reads as selected exactly like the Appearance cubes:
+   platform fill + the bluish-400 border (static token — the bluish-400 step
+   has no alias-layer name). */
 .cardActive {
-  background: var(--dsw-alias-bg-layer-2);
-  border-color: var(--dsw-alias-label-primary);
+  background: var(--dsw-alias-bg-module-platform);
+  border-color: var(--dsw-static-neutral-bluish-400);
 }
 
 /* A broken preset reads as damaged before anything else: the card cannot be
@@ -85,6 +87,7 @@
 
 .brokenBadge {
   border-radius: 999px;
+  corner-shape: round;
   padding: 1px 8px;
   font-size: 11px;
   line-height: 17px;
@@ -198,6 +201,7 @@
 .badge,
 .inUse {
   border-radius: 999px;
+  corner-shape: round;
   padding: 1px 8px;
   font-size: 11px;
   line-height: 17px;
@@ -206,7 +210,7 @@
 }
 
 .badge {
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   color: var(--dsw-alias-label-tertiary);
 }
 
@@ -240,7 +244,7 @@
   margin-top: auto;
   font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
   font-size: 11px;
-  color: var(--dsw-alias-label-dimmed);
+  color: var(--dsw-alias-label-tertiary);
 }
 
 .cardFoot {
@@ -248,7 +252,7 @@
   justify-content: flex-end;
   gap: 2px;
   padding: 6px 10px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 /* Icon-only actions: the label rides `title` so the row stays quiet until
@@ -368,7 +372,7 @@
 .input {
   box-sizing: border-box;
   padding: 9px 12px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 10px;
   font: inherit;
   font-size: 13px;
@@ -401,7 +405,7 @@
   padding: 12px;
   max-height: min(52vh, 480px);
   overflow: auto;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 10px;
   background: var(--dsw-alias-bg-layer-2);
   color: var(--dsw-alias-label-secondary);
@@ -445,7 +449,7 @@
   gap: 6px;
   height: 44px;
   border: 1px dashed var(--dsw-alias-border-l3);
-  border-radius: 12px;
+  border-radius: 20px;
   font: inherit;
   font-size: 14px;
   line-height: 22px;

+ 1 - 0
packages/client/ui-approval/src/client/ApprovalPanel.module.css

@@ -32,6 +32,7 @@
   width: 8px;
   height: 8px;
   border-radius: 50%;
+  corner-shape: round;
   background: var(--dsw-alias-state-warn-primary);
 }
 

+ 6 - 3
packages/client/ui-attachment/src/AttachmentRail.module.css

@@ -33,7 +33,7 @@
   height: 64px;
   padding: 0;
   overflow: hidden;
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin);
   border-radius: 16px;
   background: var(--dsw-alias-interactive-bg-hover);
   cursor: zoom-in;
@@ -58,6 +58,7 @@
   padding: 0;
   border: none;
   border-radius: 50%;
+  corner-shape: round;
   background: var(--dsw-alias-button-contrast-fill);
   color: var(--dsw-alias-label-primary-inverted);
   cursor: pointer;
@@ -92,11 +93,13 @@
   width: 24px;
   height: 24px;
   padding: 0;
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0;
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l2-darkmode-thin);
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-specific-input-major);
   color: var(--dsw-alias-label-secondary);
-  box-shadow: var(--dsw-shadow-lv2);
+  box-shadow: var(--dsw-elevation-panel);
   cursor: pointer;
   transform: translateY(-50%);
 }

+ 2 - 1
packages/client/ui-attachment/src/ImageLightbox.module.css

@@ -36,8 +36,9 @@
   place-items: center;
   width: 36px;
   height: 36px;
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin);
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-specific-input-major);
   color: var(--dsw-alias-label-primary);
   cursor: pointer;

+ 2 - 2
packages/client/ui-attachment/src/MessageImage.module.css

@@ -23,7 +23,7 @@
   min-height: 44px;
   padding: 0;
   overflow: hidden;
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin);
   border-radius: 16px;
   background: var(--dsw-alias-interactive-bg-hover);
   cursor: zoom-in;
@@ -53,7 +53,7 @@
 .error {
   max-width: 240px;
   padding: 10px 12px;
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin);
   border-radius: 10px;
   background: var(--dsw-alias-interactive-bg-hover-danger);
   cursor: pointer;

+ 4 - 2
packages/client/ui-chat/src/client/chat/ChatView.module.css

@@ -202,11 +202,13 @@
   height: 34px;
   margin-top: -34px;
   padding: 0;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0;
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l3);
   border-radius: 100px;
+  corner-shape: round;
   color: var(--dsw-alias-label-primary);
   background: var(--dsw-alias-button-floating-fill);
-  box-shadow: var(--dsw-shadow-lv2);
+  box-shadow: var(--dsw-elevation-panel);
   cursor: pointer;
   pointer-events: auto;
 }

+ 1 - 1
packages/client/ui-chat/src/client/chat/ContextBody.module.css

@@ -16,7 +16,7 @@
   gap: 2px;
   margin: 8px 0 0;
   padding-top: 8px;
-  border-top: 1px solid var(--dsw-alias-line-secondary);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .field {

+ 1 - 1
packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css

@@ -71,7 +71,7 @@
   margin: 4px 0 4px 4px;
   padding: 12px 16px;
   overflow: auto;
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 12px;
   background: var(--dsw-alias-markdown-code-block);
   color: var(--dsw-alias-label-primary);

+ 2 - 2
packages/client/ui-chat/src/client/chat/TurnNavigator.module.css

@@ -167,11 +167,11 @@
   max-height: var(--turn-preview-height);
   overflow: hidden;
   padding: 10px 12px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0;
   border-radius: 10px;
   color: var(--dsw-alias-label-primary);
   background: var(--dsw-alias-bg-layer-1);
-  box-shadow: var(--dsw-shadow-lv2);
+  box-shadow: var(--dsw-elevation-panel);
   pointer-events: none;
   animation: dsh-turn-preview-enter 120ms ease-out;
   transition: top 140ms cubic-bezier(0.2, 0.8, 0.2, 1);

+ 1 - 1
packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css

@@ -7,7 +7,7 @@
   height: 33px;
   padding: 0 0 8px;
   border: none;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
   background: none;
   color: var(--dsw-alias-label-secondary);
   cursor: pointer;

+ 5 - 4
packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css

@@ -1,5 +1,5 @@
 /* Icon-row Turn-usage pill (data icon + turn total) plus its click-open
-   Turn-details dialog (menu surface: r12, inverted hairline, shadow-lv3 —
+   Turn-details dialog (menu surface: r12, elevation-prominent —
    ContextMeter's panel skin). */
 
 .root {
@@ -90,10 +90,11 @@
   min-width: min(300px, calc(100vw - 24px));
   max-width: min(440px, calc(100vw - 24px));
   padding: 16px;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0;
   border-radius: 12px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   font-size: 12px;
   line-height: 18px;
   color: var(--dsw-alias-label-secondary);
@@ -114,7 +115,7 @@
 /* Rule under each section heading, above its rows. */
 .titleRule {
   margin-bottom: 10px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .titleValue {

+ 3 - 2
packages/client/ui-chat/src/client/details/DetailsPanel.module.css

@@ -7,7 +7,7 @@
   flex-direction: column;
   height: 100%;
   min-width: 0;
-  border-left: 1px solid var(--dsw-alias-border-l2);
+  border-left: 0.5px solid var(--dsw-alias-border-l2);
   background: var(--dsw-alias-bg-base);
 }
 
@@ -18,7 +18,7 @@
   justify-content: space-between;
   gap: 8px;
   padding: 14px 12px 12px;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 /* figma I54:42735;43:41479: 14/20 wt500. */
@@ -40,6 +40,7 @@
   height: 28px;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: transparent;
   color: var(--dsw-alias-label-secondary);
   cursor: pointer;

+ 1 - 1
packages/client/ui-chat/src/client/settings/TranscriptViewRow.module.css

@@ -5,7 +5,7 @@
   align-items: center;
   gap: 8px;
   padding: 16px 0;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .rowText {

+ 7 - 6
packages/client/ui-commands/src/client/PopupSelectView.module.css

@@ -1,6 +1,6 @@
 /* Official popupSelect shell card: menu-surface tokens (same family as
  * ui-primitives Menu.module.css — figma MenuDropdown r12 / hairline /
- * shadow-lv3), anchored by the conversation.input.overlay slot. */
+ * elevation-prominent), anchored by the conversation.input.overlay slot. */
 
 .card {
   /* The overlay anchor is a zero-height strip on the composer card's top
@@ -24,10 +24,11 @@
      (see ui-theme styles/scrollbar.css for the rebinding contract). */
   --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
-  border: 1px solid var(--dsw-alias-border-inverted);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   outline: none;
 }
 
@@ -84,7 +85,7 @@
 .search {
   margin: 2px 2px 4px;
   padding: 6px 8px;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0.5px solid var(--dsw-alias-border-inverted);
   border-radius: 8px;
   background: transparent;
   font-size: 13px;
@@ -109,7 +110,7 @@
 
 .retry {
   padding: 2px 8px;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0.5px solid var(--dsw-alias-border-inverted);
   border-radius: 6px;
   background: transparent;
   font-size: 12px;

+ 4 - 3
packages/client/ui-conversation/src/client/queue/QueueDock.module.css

@@ -39,7 +39,7 @@
 .panel::after {
   position: absolute;
   inset: 0;
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   /* The input card's own top border closes the shape below. */
   border-bottom: none;
   border-radius: inherit;
@@ -131,7 +131,7 @@
 .thumb {
   width: 24px;
   height: 24px;
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 4px;
   background: var(--dsw-alias-bg-base);
   object-fit: cover;
@@ -157,7 +157,7 @@
   box-sizing: border-box;
   height: 28px;
   padding: 0 8px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 6px;
   outline: none;
   background: var(--dsw-alias-bg-base);
@@ -184,6 +184,7 @@
   padding: 0;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: transparent;
   color: var(--dsw-alias-label-tertiary);
   cursor: pointer;

+ 1 - 1
packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.module.css

@@ -5,7 +5,7 @@
   align-items: center;
   gap: 8px;
   padding: 16px 0;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .rowText {

+ 6 - 3
packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css

@@ -1,5 +1,5 @@
 /* Context-occupancy ring beside the send button plus its click-open breakdown
-   panel (menu surface: r12, inverted hairline, shadow-lv3). */
+   panel (menu surface: r12, elevation-prominent). */
 
 .root {
   position: relative;
@@ -15,6 +15,7 @@
   height: 28px;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: transparent;
   color: var(--dsw-alias-label-secondary);
   cursor: pointer;
@@ -45,10 +46,11 @@
   box-sizing: border-box;
   width: 264px;
   padding: 12px;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0;
   border-radius: 12px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   font-size: 12px;
   line-height: 20px;
   color: var(--dsw-alias-label-secondary);
@@ -89,6 +91,7 @@
   margin: 10px 0 12px;
   height: 4px;
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-alias-interactive-bg-hover);
   overflow: hidden;
 }

+ 2 - 2
packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css

@@ -48,8 +48,8 @@
   bottom: 1px;
   left: 0;
   z-index: 0;
-  height: 1px;
-  background: var(--dsw-alias-border-l2);
+  height: 0.5px;
+  background: var(--dsw-alias-border-l3);
   pointer-events: none;
 }
 

+ 2 - 2
packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css

@@ -50,7 +50,7 @@
   margin-top: 2px;
   margin-left: -3px;
   padding: 1px 7px 0;
-  border: 1px solid var(--dsw-alias-interactive-bg-hover);
+  border: 0.5px solid var(--dsw-alias-interactive-bg-hover);
   border-radius: 24px;
   background: var(--dsw-alias-state-business-tertiary);
   color: var(--dsw-alias-label-primary-bluish);
@@ -185,7 +185,7 @@
   width: 100%;
   height: 44px;
   padding: 7px 14px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 22px;
   outline: none;
   background: transparent;

+ 10 - 6
packages/client/ui-conversation/src/client/skeleton/InputBar.module.css

@@ -40,13 +40,14 @@
   width: 100%;
   max-width: var(--dsh-composer-card-max-width);
   padding-top: 10px;
-  /* Input stroke: black/0.10 light, white/0.06 dark (figma darkmode note says
-     the input border is one notch weaker than buttons) — exactly the
-     l2-darkmode-thin pair. Fill: the input surface token (elevated in dark). */
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  /* Input stroke: the light neutral (l2, one step above the menus' l1),
+     drawn as the elevation hairline so it costs no layout; the shadow is the
+     soft elevation tier. Fill: the input surface token (elevated in dark). */
+  border: 0;
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l2);
   border-radius: 22px;
   background: var(--dsw-specific-input-major);
-  box-shadow: var(--dsw-shadow-lv2);
+  box-shadow: var(--dsw-elevation-soft);
   /* The draft text follows the Settings font-size preference (the textarea,
      mirror, and backdrop all inherit from here, so the three layers stay in
      step); the line height keeps the default 10px leading via the shared px
@@ -69,7 +70,7 @@
    by an SVG dash ring (stroke-width 2 centered on the box edge = 1px visible
    inside), which keeps the 22px radius and both themes. */
 .cardWorkspaceTrigger {
-  border-color: transparent;
+  --dsw-elevation-stroke-color: transparent;
   cursor: pointer;
 }
 
@@ -141,6 +142,7 @@
   width: 8px;
   height: 8px;
   border-radius: 50%;
+  corner-shape: round;
   background: var(--dsw-alias-state-business-primary);
   animation: input-pending 1s ease-in-out infinite alternate;
 }
@@ -266,6 +268,7 @@
   height: 28px;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-specific-selector);
   color: var(--dsw-alias-label-primary);
   cursor: pointer;
@@ -323,6 +326,7 @@
   height: 34px;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-alias-button-info-fill);
   /* Static white, not the foreground token: the arrow stays white on the blue
      fill in both themes (design 34:10465). */

+ 1 - 1
packages/client/ui-conversation/src/client/skeleton/TodoPanel.module.css

@@ -19,7 +19,7 @@
     var(--dsh-composer-dock-inset) -
     var(--dsh-composer-dock-inset)
   );
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 12px;
   background: var(--dsw-specific-tip);
   /* Elevated surface: `--dsw-specific-tip` is the same dark rung as the menu

+ 4 - 4
packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.module.css

@@ -29,7 +29,7 @@
   gap: 8px;
   flex: none;
   padding: 16px 14px 8px 24px;
-  border-bottom: 1px solid var(--dsw-alias-border-l3);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l3);
 }
 
 .title {
@@ -215,7 +215,7 @@
 
 .divider {
   flex: none;
-  width: 1px;
+  width: 0.5px;
   background: var(--dsw-alias-border-l3);
 }
 
@@ -323,7 +323,7 @@
   gap: 8px;
   flex: none;
   padding: 16px 24px;
-  border-top: 1px solid var(--dsw-alias-border-l3);
+  border-top: 0.5px solid var(--dsw-alias-border-l3);
 }
 
 /* Show-hidden toggle: a subtle fixed-label text button left of the gap;
@@ -400,7 +400,7 @@
   width: 100%;
   height: 44px;
   padding: 7px 14px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 22px;
   outline: none;
   background: transparent;

+ 3 - 2
packages/client/ui-goal/src/client/GoalBar.module.css

@@ -26,7 +26,7 @@
   height: 36px;
   margin: 0 auto;
   padding: 4px 5px 4px 12px;
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 12px;
   background: var(--dsw-specific-tip);
 }
@@ -76,7 +76,7 @@
   min-width: 0;
   height: 26px;
   padding: 0 8px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 6px;
   background: var(--dsw-alias-bg-base);
   font-size: 13px;
@@ -111,6 +111,7 @@
   padding: 0;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: transparent;
   color: var(--dsw-alias-label-tertiary);
   cursor: pointer;

+ 5 - 4
packages/client/ui-input-trigger/src/client/MenuView.module.css

@@ -17,10 +17,11 @@
   padding: 4px;
   display: flex;
   flex-direction: column;
-  border: 1px solid var(--dsw-alias-border-inverted);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
 }
 
 .viewport {
@@ -198,7 +199,7 @@
   margin-bottom: 2px;
   /* l1, not border-inverted (the menu frame's dark-only stroke): the divider
      needs to survive light mode too; in dark both resolve to white/0.06. */
-  border-bottom: 1px solid var(--dsw-alias-border-l1);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l1);
 }
 /* Crumbs sit outside the listbox and take no keyboard highlight, so unlike
    .item they carry their own :hover tint with nothing to compete with. */

+ 4 - 3
packages/client/ui-jobs/src/client/JobListAction.module.css

@@ -54,12 +54,13 @@
   padding: 4px;
   overflow: auto;
   list-style: none;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
   --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
 }
 
 .row {

+ 3 - 3
packages/client/ui-layout/src/client/AppFrame.module.css

@@ -26,7 +26,7 @@
   min-width: 0;
   overflow: hidden;
   background: var(--dsw-specific-sidebar-fill);
-  border-right: 1px solid var(--dsw-alias-border-l1);
+  border-right: 0.5px solid var(--dsw-alias-border-l3);
 }
 
 .centerCol {
@@ -39,7 +39,7 @@
 .detailsCol {
   min-width: 0;
   overflow: hidden;
-  border-left: 1px solid var(--dsw-alias-border-l2);
+  border-left: 0.5px solid var(--dsw-alias-border-l3);
 }
 
 /* The details subtree stays mounted at zero width, so its border must not paint
@@ -86,7 +86,7 @@
   border-radius: 10px;
   box-sizing: border-box;
   background: var(--dsw-alias-button-floating-fill);
-  border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
+  border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin);
   /* Hover affordance: the details pill hides until the pointer is over its
      column, the strip itself, or a drag. */
   opacity: 0;

+ 4 - 3
packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css

@@ -78,10 +78,11 @@
   display: flex;
   flex-direction: column;
   gap: 8px;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0;
   border-radius: 12px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
 }
@@ -90,7 +91,7 @@
   width: 100%;
   box-sizing: border-box;
   padding: 6px 8px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 8px;
   background: var(--dsw-alias-bg-layer-1);
   color: var(--dsw-alias-label-primary);

+ 4 - 3
packages/client/ui-model-selection/src/client/ModelSelect.module.css

@@ -82,10 +82,11 @@
   padding: 4px;
   /* Surface tokens match the Menu primitive card (ui-primitives
    * Menu.module.css) so every dropdown reads as the same material. */
-  border: 1px solid var(--dsw-alias-border-inverted);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   color: var(--dsw-alias-label-primary);
   /* Elevated surface: the scrollbar thumb takes the l2 elevation tokens.
      Declared here rather than on the scrolling `.groups` child so the

+ 1 - 1
packages/client/ui-permission-presets/src/client/PermissionRow.module.css

@@ -5,7 +5,7 @@
   align-items: center;
   gap: 8px;
   padding: 16px 0;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .rowText {

+ 1 - 0
packages/client/ui-plan/src/client/PlanModeControl.module.css

@@ -14,6 +14,7 @@
   padding: 2px 8px;
   border: none;
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-alias-state-warn-tertiary);
   color: var(--dsw-alias-state-warn-label);
   font-size: 13px;

+ 1 - 1
packages/client/ui-primitives/src/Button.module.css

@@ -54,7 +54,7 @@
 
 /* Dialog Cancel (figma 451:18655): bordered capsule on transparent fill. */
 .outline {
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   background: transparent;
 }
 

+ 1 - 1
packages/client/ui-primitives/src/Input.module.css

@@ -4,7 +4,7 @@
   gap: 6px;
   height: 32px;
   padding: 0 8px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 8px;
   background: var(--dsw-alias-bg-layer-1);
 }

+ 7 - 6
packages/client/ui-primitives/src/Menu.module.css

@@ -4,7 +4,7 @@
 }
 
 /* Dropdown card (figma MenuDropdown 122:9481 / 419:16920): menu surface,
- * r12, inverted hairline border, shadow-lv3, 4px inset padding. */
+ * r12, elevation-prominent (hairline stroke in shadow), 4px inset padding. */
 .list,
 .submenu {
   /* min-widths below are the design's outer card widths — include the pad. */
@@ -13,10 +13,11 @@
   display: flex;
   flex-direction: column;
   gap: 0;
-  border: 1px solid var(--dsw-alias-border-inverted);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
   /* Elevated surface: the scrollbar thumb takes the l2 elevation tokens. The
      declaration sits on the card rather than on `.scrollable .viewport`
      because the elevation is a property of this surface, and the custom
@@ -85,7 +86,7 @@
   flex-direction: column;
   margin-top: 4px;
   padding-top: 4px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .itemWrap {
@@ -212,7 +213,7 @@
 
 /* Separator cell (figma 122:9481): py 4 / px 2 around the hairline. */
 .separator {
-  height: 1px;
+  height: 0.5px;
   margin: 4px 2px;
   background: var(--dsw-alias-border-l1);
 }

+ 3 - 3
packages/client/ui-primitives/src/Modal.module.css

@@ -18,7 +18,7 @@
   backdrop-filter: var(--dsw-mask-blur);
 }
 
-/* Dialog card: r24, shadow-lv3, layer-2 fill, inverted border, pb 24. */
+/* Dialog card: r24, elevation-prominent, layer-2 fill, pb 24. */
 .dialog {
   position: relative;
   z-index: 1;
@@ -28,10 +28,10 @@
   width: min(380px, 100%);
   padding: 0 0 24px;
   overflow: hidden;
-  border: 1px solid var(--dsw-alias-border-inverted);
+  border: 0;
   border-radius: 24px;
   background: var(--dsw-alias-bg-layer-2);
-  box-shadow: var(--dsw-shadow-lv3);
+  box-shadow: var(--dsw-elevation-prominent);
 }
 
 .content {

+ 2 - 0
packages/client/ui-primitives/src/StateDot.module.css

@@ -18,6 +18,7 @@
   position: absolute;
   inset: 0;
   border-radius: 50%;
+  corner-shape: round;
   background: currentColor;
   opacity: 0.1;
 }
@@ -27,6 +28,7 @@
   position: absolute;
   inset: 20%;
   border-radius: 50%;
+  corner-shape: round;
   background: currentColor;
 }
 

+ 1 - 1
packages/client/ui-primitives/src/TerminalBlock.module.css

@@ -62,7 +62,7 @@
    divider the IN/OUT card draws between its sections. A running card is
    banner-only, so it draws none. */
 .block:not([data-running]) .header {
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 /* One row per command line. The prompt column is the only element allowed to

+ 1 - 1
packages/client/ui-primitives/src/markdown/JsonBlock.module.css

@@ -23,7 +23,7 @@
   max-height: 200px;
   overflow: auto;
   background: var(--dsw-alias-markdown-code-block);
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 6px;
   font-family: var(--ds-font-family-code);
   font-size: 11px;

+ 3 - 3
packages/client/ui-primitives/src/markdown/MarkdownText.module.css

@@ -126,7 +126,7 @@
 .markdown hr {
   display: block;
   border: none;
-  height: 1px;
+  height: 0.5px;
   margin: 32px 0;
   background: var(--dsw-alias-border-l2);
 }
@@ -225,7 +225,7 @@
 .tableScroll th {
   text-align: start;
   padding: 10px 16px;
-  border-bottom: 1px solid var(--dsw-alias-border-l3);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l3);
   border-top: none;
   font: var(--dsw-font-markdown-table-head);
   max-width: 320px;
@@ -235,7 +235,7 @@
 
 .tableScroll td {
   padding: 10px 16px;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
   font: var(--dsw-font-markdown-table);
   max-width: 320px;
   max-width: min(30vw, 320px);

+ 5 - 3
packages/client/ui-schedule/src/client/ScheduleCatalogAction.module.css

@@ -52,12 +52,13 @@
   padding: 4px;
   overflow: auto;
   list-style: none;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 12px;
+  border: 0;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
   --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
 }
 
 .row {
@@ -91,6 +92,7 @@
   height: 8px;
   flex: none;
   border-radius: 50%;
+  corner-shape: round;
   background: var(--dsw-alias-state-business-primary);
 }
 

+ 5 - 4
packages/client/ui-settings-general/src/client/SettingsRoot.module.css

@@ -48,6 +48,7 @@
   gap: 0;
   padding: 0;
   border-radius: 50%;
+  corner-shape: round;
 }
 
 .triggerLabel {
@@ -73,8 +74,8 @@
   backdrop-filter: var(--dsw-mask-blur);
 }
 
-/* Panel (figma Settings 501:29947): r24, white, lv3 shadow (figma effects
-   match --dsw-shadow-lv3 exactly); figma's 1080x700 is shrunk to 800 wide.
+/* Panel (figma Settings 501:29947): r24, white; the figma lv3 effects are
+   superseded by the shared elevation treatment; figma's 1080x700 is shrunk to 800 wide.
    One height for every section, taken from the viewport rather than the
    content: sections differ by hundreds of pixels (a settings list against the
    composition editor), and a content-sized panel would resize under the
@@ -86,10 +87,10 @@
   width: 800px;
   height: min(800px, calc(100vh - 48px));
   max-width: calc(100vw - 48px);
-  border-radius: 24px;
+  border-radius: 32px;
   overflow: hidden;
   background: var(--dsw-alias-bg-layer-2);
-  box-shadow: var(--dsw-shadow-lv3);
+  box-shadow: var(--dsw-elevation-prominent);
   /* Elevated surface: the scrollbar thumb takes the l2 elevation tokens.
      Declared on the panel rather than the scrolling `.options` child so the
      elevation choice sits with the surface; the custom properties inherit

+ 2 - 2
packages/client/ui-settings-models/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-settings-models/README.md
-README.md: cedb2fb164a437710eb3723c1348c32dea896e45
-README.zh.md: d106ce3a86d30b263f985bd944669e4a13d0944b
+README.md: b3f07e1a788b223f0f49e37b9a4a54662acf6445
+README.zh.md: c23dcf95f6b9e9d47b390c3243bf3d8bb0ef26f1

+ 2 - 2
packages/client/ui-settings-models/README.md

@@ -33,11 +33,11 @@ The primary field on an editor card is a single **API key** input — the page n
 
 ### Editing a provider
 
-The collapsed 自定义设置 fold carries the curated extras: `baseURL` for both families (the deepseek placeholder shows the public endpoint), each adapter's model catalog, and the **display name** and **API protocol** of a pi-ai route the adapter does not ship. The Provider ID stays fixed: it is the settings key, the name every other namespace and every logged session references, and the stem of a credential reference the page cannot read back to move. Reasoning effort is deliberately not among the editable fields: it is a per-model capability, so a provider-scoped control could only be set to a value some models reject. Each DeepSeek row edits `id`, optional display `name`, and optional `contextWindow`/`maxTokens`; existing fields outside that curated set survive edits.
+The collapsed 自定义设置 fold carries the curated extras: `baseURL` for both families (the deepseek placeholder shows the public endpoint), each adapter's model catalog, and the **display name** and **API protocol** of a pi-ai route the adapter does not ship. Profile `headers` remain deployment configuration in `settings.yaml` or Cordis config and have no Models-page editor. The Provider ID stays fixed: it is the settings key, the name every other namespace and every logged session references, and the stem of a credential reference the page cannot read back to move. Reasoning effort is deliberately not among the editable fields: it is a per-model capability, so a provider-scoped control could only be set to a value some models reject. Each DeepSeek row edits `id`, optional display `name`, and optional `contextWindow`/`maxTokens`; existing fields outside that curated set survive edits.
 
 ### Adding and deleting providers
 
-The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a picker rather than being written, and nothing is written until **Add selected**. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider.
+The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Search matches model ids and optional display names without clearing hidden selections, while **Select all** and **Deselect all** affect only the visible results. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider.
 
 ### First-run dialogs
 

+ 2 - 2
packages/client/ui-settings-models/README.zh.md

@@ -33,11 +33,11 @@ kind: "package-reference"
 
 ### 编辑提供方
 
-收起的「自定义设置」折叠区承载精选的额外字段:两个家族都有 `baseURL`(deepseek 的占位符显示公共端点)、各适配器自己的模型目录,以及适配器未提供的 pi-ai 路由的**显示名称**与 **API 协议**。Provider ID 保持固定:它是 settings 的键、其他每个 namespace 与每一条已记录会话引用的名字,也是页面读不回、因而搬不走的凭据引用词干。推理等级刻意不在可编辑字段之列:它是按模型的能力,提供方级的控件只可能被设成某些模型会拒绝的值。每个 DeepSeek 行编辑 `id`、可选显示 `name` 与可选 `contextWindow`/`maxTokens`;该精选集之外的现有字段在编辑后仍会保留。
+收起的「自定义设置」折叠区承载精选的额外字段:两个家族都有 `baseURL`(deepseek 的占位符显示公共端点)、各适配器自己的模型目录,以及适配器未提供的 pi-ai 路由的**显示名称**与 **API 协议**。Profile `headers` 仍是 `settings.yaml` 或 Cordis 配置中的部署配置,Models 页面不提供编辑器。Provider ID 保持固定:它是 settings 的键、其他每个 namespace 与每一条已记录会话引用的名字,也是页面读不回、因而搬不走的凭据引用词干。推理等级刻意不在可编辑字段之列:它是按模型的能力,提供方级的控件只可能被设成某些模型会拒绝的值。每个 DeepSeek 行编辑 `id`、可选显示 `name` 与可选 `contextWindow`/`maxTokens`;该精选集之外的现有字段在编辑后仍会保留。
 
 ### 新增与删除提供方
 
-「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是选择器而非直接写入,只有点击**添加所选**才会写入。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。
+「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态;**全选**与**取消全选**只影响可见结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。
 
 ### 首次运行弹窗
 

+ 55 - 26
packages/client/ui-settings-models/src/client/ModelListEditor.tsx

@@ -162,6 +162,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
   const [failure, setFailure] = useState<string | undefined>(undefined)
   const [candidates, setCandidates] = useState<readonly LlmDiscoveredModel[] | undefined>(undefined)
   const [picked, setPicked] = useState<ReadonlySet<string>>(new Set())
+  const [candidateQuery, setCandidateQuery] = useState('')
   // Rows carry an id and a name; capacities are the exception, so they stay
   // folded until asked for rather than crowding every row with four inputs.
   const [expanded, setExpanded] = useState<ReadonlySet<number>>(new Set())
@@ -247,6 +248,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
       // Everything already configured starts unchecked, so adopting a
       // selection never silently rewrites a capacity the user corrected.
       const known = new Set(models.map(model => textOf(model, 'id')))
+      setCandidateQuery('')
       setCandidates(found)
       setPicked(new Set(found.filter(model => !known.has(model.id)).map(model => model.id)))
     } finally {
@@ -257,6 +259,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
   const closePicker = (): void => {
     setCandidates(undefined)
     setPicked(new Set())
+    setCandidateQuery('')
   }
 
   const adoptPicked = (): void => {
@@ -284,14 +287,23 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
   }
 
   const activeCandidates = candidates ?? []
-  const allCandidatesPicked = activeCandidates.length > 0
-    && activeCandidates.every(candidate => picked.has(candidate.id))
+  const normalizedCandidateQuery = candidateQuery.trim().toLowerCase()
+  const visibleCandidates = normalizedCandidateQuery.length === 0
+    ? activeCandidates
+    : activeCandidates.filter(candidate => candidate.id.toLowerCase().includes(normalizedCandidateQuery)
+      || candidate.name?.toLowerCase().includes(normalizedCandidateQuery) === true)
+  const allVisibleCandidatesPicked = visibleCandidates.length > 0
+    && visibleCandidates.every(candidate => picked.has(candidate.id))
 
-  const toggleAllCandidates = (): void => {
+  const toggleVisibleCandidates = (): void => {
     setPicked((current) => {
-      return activeCandidates.every(candidate => current.has(candidate.id))
-        ? new Set()
-        : new Set(activeCandidates.map(candidate => candidate.id))
+      const next = new Set(current)
+      if (visibleCandidates.every(candidate => current.has(candidate.id))) {
+        for (const candidate of visibleCandidates) next.delete(candidate.id)
+      } else {
+        for (const candidate of visibleCandidates) next.add(candidate.id)
+      }
+      return next
     })
   }
 
@@ -450,28 +462,45 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
           </>
         )}
       >
-        <div className={styles['candidateActions']}>
-          <Button variant="ghost" size="sm" onClick={toggleAllCandidates}>
-            {t(allCandidatesPicked ? 'fetchDeselectAll' : 'fetchSelectAll')}
+        <div className={styles['candidateToolbar']}>
+          <input
+            className={`${styles['input']} ${styles['candidateSearch']}`}
+            type="search"
+            value={candidateQuery}
+            placeholder={t('fetchSearch')}
+            aria-label={t('fetchSearch')}
+            onChange={(event) => { setCandidateQuery(event.target.value) }}
+          />
+          <Button
+            variant="ghost"
+            size="sm"
+            disabled={visibleCandidates.length === 0}
+            onClick={toggleVisibleCandidates}
+          >
+            {t(allVisibleCandidatesPicked ? 'fetchDeselectAll' : 'fetchSelectAll')}
           </Button>
         </div>
-        <ul className={styles['candidateList']}>
-          {(candidates ?? []).map(candidate => (
-            <li key={candidate.id} className={styles['candidate']}>
-              <label className={styles['candidateLabel']}>
-                <input
-                  type="checkbox"
-                  checked={picked.has(candidate.id)}
-                  onChange={() => { toggle(candidate.id) }}
-                />
-                {/* The id alone: it is the string adoption writes, and the
-                    capacities the endpoint reported are adopted with it and
-                    editable in the row that appears. */}
-                <span className={styles['candidateId']}>{candidate.id}</span>
-              </label>
-            </li>
-          ))}
-        </ul>
+        {visibleCandidates.length === 0
+          ? <p className={styles['candidateEmpty']} role="status">{t('fetchNoMatches')}</p>
+          : (
+            <ul className={styles['candidateList']}>
+              {visibleCandidates.map(candidate => (
+                <li key={candidate.id} className={styles['candidate']}>
+                  <label className={styles['candidateLabel']}>
+                    <input
+                      type="checkbox"
+                      checked={picked.has(candidate.id)}
+                      onChange={() => { toggle(candidate.id) }}
+                    />
+                    {/* The id alone: it is the string adoption writes, and the
+                        capacities the endpoint reported are adopted with it and
+                        editable in the row that appears. */}
+                    <span className={styles['candidateId']}>{candidate.id}</span>
+                  </label>
+                </li>
+              ))}
+            </ul>
+          )}
       </Modal>
     </section>
   )

+ 28 - 13
packages/client/ui-settings-models/src/client/ModelsSection.module.css

@@ -53,8 +53,8 @@
 /* A configured provider: outlined on the panel fill, so the filled editor
    card it expands into reads as the nested object. */
 .rowCard {
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 12px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 16px;
   padding: 12px 14px;
   display: flex;
   flex-direction: column;
@@ -88,7 +88,7 @@
 .rowTag {
   flex: none;
   padding: 1px 6px;
-  border: 1px solid var(--dsw-alias-border-l3);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   border-radius: 4px;
   font-size: 11px;
   line-height: 16px;
@@ -102,6 +102,7 @@
   width: 8px;
   height: 8px;
   border-radius: 50%;
+  corner-shape: round;
 }
 
 .credentialDotConfigured {
@@ -151,7 +152,7 @@
 
 .secondaryButton,
 .addButton {
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   background: transparent;
   color: var(--dsw-alias-label-primary);
 }
@@ -323,7 +324,7 @@
   gap: 6px;
   height: 44px;
   border: 1px dashed var(--dsw-alias-border-l3);
-  border-radius: 12px;
+  border-radius: 16px;
 }
 
 .addCard,
@@ -345,7 +346,7 @@
 }
 
 .customized {
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
   padding-top: 10px;
 }
 
@@ -406,7 +407,7 @@
   flex-direction: column;
   gap: 10px;
   padding-top: 12px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .modelCatalogHeading {
@@ -450,8 +451,8 @@
 }
 
 .modelEntry {
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 8px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 10px;
   padding: 6px;
 }
 
@@ -528,7 +529,7 @@
   gap: 4px;
   height: 28px;
   padding: 0 10px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   border-radius: 14px;
   background: transparent;
   color: var(--dsw-alias-label-primary);
@@ -547,7 +548,7 @@
   width: 100%;
   height: 32px;
   padding: 0 10px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 8px;
   font: inherit;
   font-size: 14px;
@@ -640,12 +641,18 @@ select.input {
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
 }
 
-.candidateActions {
+.candidateToolbar {
   display: flex;
-  justify-content: flex-end;
+  align-items: center;
+  gap: 8px;
   margin-bottom: 6px;
 }
 
+.candidateSearch {
+  min-width: 0;
+  flex: 1 1 240px;
+}
+
 .candidateList {
   display: flex;
   flex-direction: column;
@@ -675,3 +682,11 @@ select.input {
   font-size: 13px;
   overflow-wrap: anywhere;
 }
+
+.candidateEmpty {
+  margin: 24px 0;
+  color: var(--dsw-alias-label-secondary);
+  font-size: 13px;
+  line-height: 20px;
+  text-align: center;
+}

+ 4 - 0
packages/client/ui-settings-models/src/client/locales.ts

@@ -70,6 +70,8 @@ export const en = {
   fetchEmpty: 'The provider listed no models. Add them by hand.',
   fetchTitle: 'Choose models to add',
   fetchDescription: 'These are the models this provider has available. Choose the ones to add.',
+  fetchSearch: 'Search models',
+  fetchNoMatches: 'No matching models.',
   fetchSelectAll: 'Select all',
   fetchDeselectAll: 'Deselect all',
   fetchAdopt: 'Add selected',
@@ -174,6 +176,8 @@ export const zh: { [Key in keyof typeof en]: string } = {
   fetchEmpty: '该提供方没有列出任何模型,请手动添加。',
   fetchTitle: '选择要添加的模型',
   fetchDescription: '以下是模型提供方的可用模型,勾选要添加的模型。',
+  fetchSearch: '搜索模型',
+  fetchNoMatches: '没有匹配的模型。',
   fetchSelectAll: '全选',
   fetchDeselectAll: '取消全选',
   fetchAdopt: '添加所选',

+ 25 - 6
packages/client/ui-settings-models/tests/provider-form.client.spec.tsx

@@ -661,25 +661,44 @@ describe('endpoint interrogation', () => {
     expect(firstMutate(mutate).ops[0]?.value).toEqual([{ id: 'a' }, { id: 'b', maxTokens: 2048 }])
   })
 
-  it('selects and clears every discovered candidate in one action', async () => {
+  it('filters by model id or name and scopes bulk selection to visible candidates', async () => {
     const discover = vi.fn(() => Promise.resolve(ok([
-      { id: 'a' }, { id: 'b' }, { id: 'c' },
+      { id: 'alpha' }, { id: 'opaque-id', name: 'Beta Display' }, { id: 'gamma' },
     ])))
     await mountSection({ discover })
     openEditor('openai')
 
     fireEvent.click(screen.getByText(en.fetchModels))
     const dialog = await screen.findByRole('dialog')
-    const boxes = [...dialog.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]
-    expect(boxes.map(box => box.checked)).toEqual([true, true, true])
+    const search = screen.getByLabelText<HTMLInputElement>(en.fetchSearch)
+    expect([...dialog.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]
+      .map(box => box.checked)).toEqual([true, true, true])
+
+    fireEvent.change(search, { target: { value: 'ALP' } })
+    expect(dialog.textContent).toContain('alpha')
+    expect(dialog.textContent).not.toContain('opaque-id')
+
+    // The display name is searchable even though adoption and the row use id.
+    fireEvent.change(search, { target: { value: 'beta' } })
+    expect(dialog.textContent).toContain('opaque-id')
+    expect(dialog.textContent).not.toContain('alpha')
 
     fireEvent.click(within_(dialog, en.fetchDeselectAll))
-    expect(boxes.map(box => box.checked)).toEqual([false, false, false])
-    expect(within_(dialog, en.fetchSelectAll)).toBeTruthy()
+    expect([...dialog.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]
+      .map(box => box.checked)).toEqual([false])
+
+    // Clearing the filter restores every row and preserves hidden selections.
+    fireEvent.change(search, { target: { value: '' } })
+    const boxes = [...dialog.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]
+    expect(boxes.map(box => box.checked)).toEqual([true, false, true])
 
     fireEvent.click(within_(dialog, en.fetchSelectAll))
     expect(boxes.map(box => box.checked)).toEqual([true, true, true])
     expect(within_(dialog, en.fetchDeselectAll)).toBeTruthy()
+
+    fireEvent.change(search, { target: { value: 'missing' } })
+    expect(screen.getByText(en.fetchNoMatches)).toBeTruthy()
+    expect((within_(dialog, en.fetchSelectAll) as HTMLButtonElement).disabled).toBe(true)
   })
 })
 

+ 1 - 1
packages/client/ui-settings-models/tests/styles.client.spec.ts

@@ -57,7 +57,7 @@ describe('ModelsSection theme styles', () => {
     // under the dark theme, so filling the row with either erases the nested
     // editor's boundary. The row is outlined; the fill is the editor's alone.
     expect(block('.editor')).toContain('background: var(--dsw-alias-bg-module-platform)')
-    expect(block('.rowCard')).toContain('border: 1px solid var(--dsw-alias-border-l2)')
+    expect(block('.rowCard')).toContain('border: 0.5px solid var(--dsw-alias-border-l4)')
     expect(block('.rowCard')).not.toMatch(/\bbackground\s*:/)
   })
 

+ 11 - 9
packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css

@@ -28,7 +28,7 @@
 }
 
 .failure button {
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   border-radius: 6px;
   padding: 4px 10px;
   background: transparent;
@@ -60,8 +60,8 @@
 .search input {
   width: 100%;
   height: 36px;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 8px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 10px;
   padding: 0 34px 0 36px;
   outline: none;
   background: var(--dsw-alias-bg-layer-1);
@@ -112,14 +112,15 @@
 .card {
   min-width: 0;
   overflow: hidden;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 10px;
+  border: 0;
+  box-shadow: var(--dsw-elevation-stroke);
+  border-radius: 14px;
   background: var(--dsw-alias-bg-layer-3);
 }
 
 .card[data-open='true'] {
-  border-color: var(--dsw-alias-border-l1);
-  box-shadow: var(--dsw-shadow-lv1);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-panel);
 }
 
 .cardContent {
@@ -173,6 +174,7 @@
   height: 7px;
   flex: none;
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-alias-label-tertiary);
 }
 
@@ -235,7 +237,7 @@
 }
 
 .group + .group {
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
   padding-top: 14px;
 }
 
@@ -393,7 +395,7 @@
 }
 
 .cardDetails {
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
   padding: 10px 14px 12px;
   background: var(--dsw-alias-bg-module-platform);
 }

+ 5 - 4
packages/client/ui-settings-plugins/src/client/PluginCard.module.css

@@ -2,8 +2,8 @@
 
 .card {
   list-style: none;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 12px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 16px;
   background: var(--dsw-alias-bg-layer-3);
   transition: border-color .16s, background .16s;
 }
@@ -73,7 +73,7 @@
 }
 
 .body {
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
   margin: 0 16px;
   padding-bottom: 8px;
 }
@@ -89,6 +89,7 @@
 .pending {
   flex: none;
   border-radius: 999px;
+  corner-shape: round;
   padding: 1px 8px;
   font-size: 11px;
   line-height: 17px;
@@ -104,7 +105,7 @@
   justify-content: flex-end;
   gap: 8px;
   padding: 12px 0 4px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .failed {

+ 1 - 1
packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.module.css

@@ -24,7 +24,7 @@
   display: flex;
   align-items: flex-end;
   gap: 22px;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
   margin-top: 2px;
 }
 

+ 3 - 2
packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css

@@ -51,6 +51,7 @@
   width: 16px;
   height: 16px;
   border-radius: 50%;
+  corner-shape: round;
   background: var(--dsw-alias-label-primary-foreground);
   transition: transform 120ms ease;
 }
@@ -108,7 +109,7 @@
   margin: 0;
   padding: 10px;
   overflow: auto;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 8px;
 }
 
@@ -126,7 +127,7 @@
 .modelGroup + .modelGroup {
   margin-top: 4px;
   padding-top: 10px;
-  border-top: 1px solid var(--dsw-alias-border-l3);
+  border-top: 0.5px solid var(--dsw-alias-border-l3);
 }
 
 .providerName {

+ 4 - 2
packages/client/ui-settings-plugins/src/client/fields.module.css

@@ -8,7 +8,7 @@
 }
 
 .field + .field {
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .head {
@@ -34,6 +34,7 @@
 
 .badge {
   border-radius: 999px;
+  corner-shape: round;
   padding: 1px 8px;
   font-size: 11px;
   line-height: 17px;
@@ -45,6 +46,7 @@
 
 .badgeMuted {
   border-radius: 999px;
+  corner-shape: round;
   padding: 1px 8px;
   font-size: 11px;
   line-height: 17px;
@@ -74,7 +76,7 @@
 .input {
   height: 34px;
   padding: 0 12px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 8px;
   background: var(--dsw-alias-bg-layer-3);
   font: inherit;

+ 2 - 1
packages/client/ui-sidebar/src/client/SidebarRoot.module.css

@@ -185,6 +185,7 @@
   height: 28px;
   border: none;
   border-radius: 50%;
+  corner-shape: round;
   padding: 0;
   background: transparent;
   cursor: pointer;
@@ -255,7 +256,7 @@
   padding: 8px 16px;
   margin: 0 2px 8px;
   box-sizing: border-box;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l3);
   border-radius: 12px;
   background: var(--dsw-alias-button-elevated-fill);
   color: var(--dsw-alias-label-primary);

+ 4 - 3
packages/client/ui-skill/src/client/SkillRow.module.css

@@ -118,7 +118,7 @@
   max-height: 260px;
   margin: 4px 0 4px 4px;
   overflow: hidden;
-  border: 1px solid var(--dsw-alias-border-l1);
+  border: 0.5px solid var(--dsw-alias-border-l1);
   border-radius: 12px;
   background: var(--dsw-alias-markdown-code-block);
 }
@@ -126,7 +126,7 @@
 .instructionsHeader {
   flex: none;
   padding: 8px 12px;
-  border-bottom: 1px solid var(--dsw-alias-border-l2);
+  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
   background: var(--dsw-alias-markdown-code-block-banner);
   font-size: 11px;
   font-weight: 500;
@@ -168,8 +168,9 @@
   gap: 4px;
   margin: 4px 0 2px 4px;
   padding: 2px 8px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 999px;
+  corner-shape: round;
   background: var(--dsw-alias-bg-base);
   color: var(--dsw-alias-label-secondary);
   font-size: 11px;

+ 5 - 4
packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css

@@ -103,11 +103,12 @@
   max-height: min(560px, calc(100vh - 140px));
   padding: 4px;
   overflow: auto;
-  border-radius: 12px;
+  border-radius: 20px;
   background: var(--dsw-specific-menu);
   --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
-  box-shadow: var(--dsw-shadow-lv3);
+  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
+  box-shadow: var(--dsw-elevation-prominent);
 }
 
 .node {
@@ -260,7 +261,7 @@
   content: '';
   position: absolute;
   left: 0;
-  border-left: 1px solid var(--dsw-alias-border-l2);
+  border-left: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .children::before {
@@ -289,7 +290,7 @@
   top: 16px;
   left: -4px;
   width: 14px;
-  border-top: 1px solid var(--dsw-alias-border-l2);
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
 }
 
 .notice,

+ 1 - 1
packages/client/ui-subagent/src/client/SubagentReadOnlyComposer.module.css

@@ -6,7 +6,7 @@
   margin: 0 24px 20px;
   min-height: 54px;
   padding: 10px 16px;
-  border: 1px solid var(--dsw-alias-border-l2);
+  border: 0.5px solid var(--dsw-alias-border-l4);
   border-radius: 14px;
   background: var(--dsw-alias-bg-layer-1);
   color: var(--dsw-alias-label-tertiary);

+ 2 - 2
packages/client/ui-theme/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md
-README.md: 8d635f4523fe89c9c6d711f32f8d9cf6e4cfb627
-README.zh.md: dec856fde15aae690096bdb52fc84c00c66336ab
+README.md: c591501e274de0419ea3585feb731802985ac95b
+README.zh.md: e970141963cc592541e77d748c421e693cc9568f

+ 4 - 2
packages/client/ui-theme/README.md

@@ -51,9 +51,11 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou
 
 ### Stylesheets
 
-`src/styles/` holds five sheets imported in order by ui-theme's dynamic client entry: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.
+`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.
 
-`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. It also derives the secondary tier `--dsh-content-font-size-secondary` (setting −1 at ≤14, setting −2 above; 13px at the default) with its own `--dsh-content-font-delta-secondary` for the table variants and the flow rows one step under the body. Dense small and code variants stay fixed. Outside the ladder, the user bubble and composer draft read the body pair directly, and flow-row titles and summaries read the secondary pair.
+`corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet ([corner-smoothing note](../../../.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md)).
+
+`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. It also derives the secondary tier `--dsh-content-font-size-secondary` (setting −1 at ≤14, setting −2 above; 13px at the default) with its own `--dsh-content-font-delta-secondary` for the table variants and the flow rows one step under the body. Dense small and code variants stay fixed. Outside the ladder, the user bubble and composer draft read the body pair directly, and flow-row titles and summaries read the secondary pair. The sheet also owns the shadow scale (`--dsw-shadow-lv*`) and the elevation tokens: `--dsw-elevation-stroke` draws a 0.5px hairline through the rebindable `--dsw-elevation-stroke-color`, and `--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft` (the composer's larger-blur, lower-alpha tier) layer two faint soft shadows over that stroke, so elevated surfaces set `border: 0` and carry no layout-consuming outline; the derived tokens are re-declared per element so a surface's stroke-color rebind takes effect ([elevation note](../../../.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md)).
 
 ### Scrollbar rebinding
 

+ 4 - 2
packages/client/ui-theme/README.zh.md

@@ -51,9 +51,11 @@ kind: "package-reference"
 
 ### 样式表
 
-`src/styles/` 下有五张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR 会随 ui-theme 一同移除。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。
+`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR 会随 ui-theme 一同移除。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。
 
-`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。它同时派生低一档变量 `--dsh-content-font-size-secondary`(设置 ≤14 时为设置值 −1,>14 时为设置值 −2;默认设置下为 13px)及配套的 `--dsh-content-font-delta-secondary`,供表格变体与比正文低一档的流内行使用。紧凑的小号文本与代码变体保持固定字号。阶梯之外,用户气泡与 composer 草稿直接读取正文档变量对,流内行的标题及摘要读取低一档变量对。
+`corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对([圆角平滑笔记](../../../.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.zh.md))。
+
+`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。它同时派生低一档变量 `--dsh-content-font-size-secondary`(设置 ≤14 时为设置值 −1,>14 时为设置值 −2;默认设置下为 13px)及配套的 `--dsh-content-font-delta-secondary`,供表格变体与比正文低一档的流内行使用。紧凑的小号文本与代码变体保持固定字号。阶梯之外,用户气泡与 composer 草稿直接读取正文档变量对,流内行的标题及摘要读取低一档变量对。该表还持有阴影阶(`--dsw-shadow-lv*`)与 elevation token:`--dsw-elevation-stroke` 经可重绑的 `--dsw-elevation-stroke-color` 画 0.5px 发丝描边,`--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft`(输入框专用的更大模糊、更低透明度档)在描边之上叠两层极淡柔光,因此高层级表面设 `border: 0`,不再有占布局的轮廓;派生 token 逐元素重声明,使表面对描边色的重绑真实生效([elevation 笔记](../../../.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.zh.md))。
 
 ### 滚动条重新绑定
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно