diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml index 3f8444bcfa..d4ad622140 100644 --- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml @@ -9,8 +9,8 @@ en: c8b932b02a4e1c80 zh: 96c466be1a521f3f /agent-note-web-capability-seam-stable-tools-over-multiple-providers/decision: - en: b57eb0f268033ff1 - zh: 4143382a50181414 + en: 2e08d801099cbe18 + zh: 8c5ab162ada9cc41 /agent-note-web-capability-seam-stable-tools-over-multiple-providers/package-topology: en: 5ea3ddfae82df181 zh: 2d05d95e51081696 diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md index 91e212ad9f..74e88534ee 100644 --- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md +++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md @@ -32,7 +32,7 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov - A tool is never unregistered merely because its selected provider is missing, misconfigured, missing credentials, ambiguous, or temporarily unavailable. - The provider is resolved at execution time, and a structured `WebError` is returned when the selected capability cannot run. -This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`. +This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. `dsh-tool-web` narrows this per model request: it [withholds `web_search` while the selected provider reports that the request's search cannot authenticate](2026-09-27-web-search-offered-by-provider-readiness.md). If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`. The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes. diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md index 27b7777f23..ac5f935c92 100644 --- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md @@ -32,7 +32,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13 - 工具绝不会仅仅因为其选定的提供方缺失、配置错误、缺少凭证、存在歧义或暂时不可用就被注销。 - 提供方在执行时解析,当选定的能力无法运行时返回结构化的 `WebError`。 -这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。 +这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。发行版的 `dsh-tool-web` 按模型请求收窄了这一点:选中的提供方报告该请求的搜索无法鉴权时,它会[撤下 `web_search`](2026-09-27-web-search-offered-by-provider-readiness.zh.md)。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。 该 seam 刻意不暴露任何观察面——没有注册表变更事件,也没有聚合的能力状态查询。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。 diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml index 2663220c51..c0587791fd 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml @@ -15,5 +15,5 @@ en: b78785f2a603eba8 zh: b8c268228da7e2e7 /agent-note-active-tool-headers-and-stateful-request-projection/consequences: - en: 93c32b795b71a1cc - zh: d42b5c563341c898 + en: 60d7bf28fa5a8fd1 + zh: a17b222e7aca01e2 diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md index 1b505eec4b..a40b85601c 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md @@ -30,7 +30,7 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool ## Consequences -Tool lists and logical update events are capability-independent; only outgoing declarations and messages vary. Calls to removed retained tools fail through the existing `UNKNOWN_TOOL` path. Changed definitions and incomplete auxiliary prefixes sacrifice prefix reuse for coherent declarations; unchanged restoration keeps the cached prefix on `in-history` routes only while the request series continues. Routes supporting tool updates and in-history system prompts preserve earlier messages when tools are added alongside a changed prompt or after an earlier prompt update. Routes without tool-update support still consolidate on tool changes; explicit series starts and surface replacements still consolidate on every route. No persisted fields or Session format revision are added. +Tool lists and logical update events are capability-independent; only outgoing declarations and messages vary. Calls to removed retained tools fail through the existing `UNKNOWN_TOOL` path. Changed definitions and incomplete auxiliary prefixes sacrifice prefix reuse for coherent declarations; unchanged restoration keeps the cached prefix on `in-history` routes only while the request series continues. Routes supporting tool updates and in-history system prompts preserve earlier messages when tools are added alongside a changed prompt or after an earlier prompt update. Routes without tool-update support still start a new series on tool changes; explicit series starts and surface replacements consolidate the prompt only when the effective text differs, and otherwise leave the surviving prompt node in place. No persisted fields or Session format revision are added. The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations. diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md index 6a8fb256c5..372dcb27af 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md @@ -30,7 +30,7 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to ## Consequences -工具列表和逻辑更新事件不依赖能力;只有发出的声明和消息不同。调用已移除但保留声明的工具时,通过现有 `UNKNOWN_TOOL` 路径失败。定义变化和不完整辅助前缀为保证声明一致而放弃前缀复用;仅当请求序列继续时,未变化的恢复才在 `in-history` 路由上保留缓存前缀。同时支持工具更新和历史内系统提示词的路由,在工具添加与提示词变化同时发生或晚于此前提示词更新时,保留此前消息。不支持工具更新的路由仍在工具变化时归并提示词;所有路由仍在显式新序列和 surface 替换时归并。不增加持久字段或 Session 格式版本。 +工具列表和逻辑更新事件不依赖能力;只有发出的声明和消息不同。调用已移除但保留声明的工具时,通过现有 `UNKNOWN_TOOL` 路径失败。定义变化和不完整辅助前缀为保证声明一致而放弃前缀复用;仅当请求序列继续时,未变化的恢复才在 `in-history` 路由上保留缓存前缀。同时支持工具更新和历史内系统提示词的路由,在工具添加与提示词变化同时发生或晚于此前提示词更新时,保留此前消息。不支持工具更新的路由仍在工具变化时开启新序列;显式新序列和 surface 替换只在有效文本不同时归并提示词,否则保留存活的提示词节点。不增加持久字段或 Session 格式版本。 手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。 diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml index 71c61113a6..d8e3f3fcaf 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 2eeb916eb65c66e7 zh: 7bf5c0abb8bb61e4 /agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/decision: - en: 42e800c4cd3b6ebe - zh: 06fabe5a9681fa27 + en: 0125e33ba966c8ac + zh: bf6af389336999af /agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/alternatives-considered: en: 7c7880fdb0cc95f6 zh: 2b8a539192626789 diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md index 75265db1ce..867198fba2 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md @@ -12,8 +12,8 @@ The account route's catalog was deployment configuration: a build advertised the - [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.md) loads the listing lazily for each credential, waits for it once up to `modelsTimeoutMs`, refreshes it in the background after `modelsMaxAgeMs`, keeps the last good listing when a load fails or returns no model, and serves the bundled catalog before any listing loads. The cache lives in memory and is keyed by endpoint and token fingerprint, so per-account rollouts stay separate. - A listed model is available when its `client_requirements["deepseek-harness"].min_version` is at most the current DeepSeek Harness version in semantic-version order. Without that declaration, only ids the built-in catalog knows are available; the others stay hidden, because a client cannot tell which future version supports them. A request for a refused listed model fails before network I/O with `CLIENT_UPGRADE_REQUIRED` or `MODEL_UNSUPPORTED`. -- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge such as tool-update mode and image budgets; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it. -- The shared transport gains only two provider hooks, `connect` and `admit`; listing, gating, and merging stay in the account package. +- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge the listing does not declare, such as image budgets and a protocol mode the listing omits or sets to `null`; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it. +- The shared transport gains only two provider hooks, `connect` and `admit`. Listing, gating, and merging live in the shared transport, which the [official route](2026-10-22-deepseek-official-model-listing.md) also uses. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md index 7199091c73..37487fbc88 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md @@ -12,8 +12,8 @@ Status: implemented - [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.zh.md) 按凭据懒加载列表,首次访问最多等待 `modelsTimeoutMs`,超过 `modelsMaxAgeMs` 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。 - 列出的模型在其 `client_requirements["deepseek-harness"].min_version` 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 `CLIENT_UPGRADE_REQUIRED` 或 `MODEL_UNSUPPORTED` 失败。 -- 列表中的能力逐字段覆盖内置能力;内置条目保留工具更新模式、图片预算等客户端知识;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。 -- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子;列表、门控与合并都留在账号包中。 +- 列表中的能力逐字段覆盖内置能力;内置条目保留列表没有声明的客户端知识,例如图片预算,以及列表没有给出或设为 `null` 的协议模式;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。 +- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子。列表、门控与合并位于共享传输包中,[官方路由](2026-10-22-deepseek-official-model-listing.zh.md)也使用它们。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 46925831d8..8b028995ae 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 52b45f1ac62e65ab - zh: 7f83cd4fb8dd09a6 + en: fdd39b7e8fa89d70 + zh: 4a0dfc81509e5485 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 6ec74e7ac065bd5e - zh: 04d029059dea732b + en: 79d8d66071453d1f + zh: f940f0b6d74148d6 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 607dc68b7a..2cc2c8be43 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -20,6 +20,8 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen, no coercion hook is overridden, and a span is bounded by the exactly representable millisecond range, which is what makes the split exact. The published surface is `fromMilliseconds` and a whole-part `decompose()`, the two entries the migrated consumer calls. +The last copy of the canonical instant profile moves too. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` validated a stored schedule instant with its own regular expression and round trip; the shared `fourDigitYearEpoch` owns that same profile, so the migration now asks it and keeps its own error type and message. Because that migration reads released logs, its accepted set is a compatibility promise: `packages/session/session-format-v0-to-v1/tests/validation.spec.ts` accepts the four-digit-year bounds and refuses each spelling a regular expression alone would admit (`0000`, `2026-02-30`, a four-digit fraction, a numeric offset, surrounding whitespace). + The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why `packages/README.md` and the `util/` line in `AGENTS.md` keep the phrase while `packages/util/README.md` and this package's own description name what it loads. Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's suites assert error codes and a few unrelated message substrings, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. @@ -48,4 +50,4 @@ A value type publishes what its consumers call, so the span exposes the construc The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor, so a reading that is not a finite number fails where it is produced instead of rendering text. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built. -Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. +Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own offset reading for display, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format. Five time sites keep their own code, each for a different reason. `packages/experimental/tool-session-query/src/input.ts` parses a wider profile than the shared entry — seconds optional, any number of fractional digits, and the digits below the millisecond kept so two instants can be compared exactly — so substituting the shared parser would narrow what it accepts, and moving that profile into the library is a design step of its own. The `Date.parse(record.scheduledAt)` reads in `dsh-schedule` parse a value the write path already validated, so a discriminated decode would add a rejection branch that cannot occur in practice and change what a corrupt record reports. The `everySeconds`, `afterSeconds`, and `refreshMarginSeconds` multiplications each sit beside a validation that raises the package's own error, so routing them through a constructor that throws its own type would change the failure a caller sees. The UTC offset a client reports is computed in `packages/client/ui-settings-account/src/client/client-metadata.ts` instead of shared: the client bundle admits a cross-plugin value import only for the packages it lists as inline-safe, `dsh-util-time` is not one of them, and it imports the `Temporal` polyfill at module scope, so inlining it would ship that polyfill to the browser. The Desktop host computes the same offset from the shared library, where no such rule applies. `dsh-workspace` writes its record timestamps with the platform's `toISOString`, which is the canonical rendering for every representable epoch: the extended-year and fractional-truncation cases a shared writer would refuse are unreachable from `Date.now()` and from a `SessionHeader` epoch, so adopting the shared formatter there would add a module and a dependency without changing a byte. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 838f6b9d4b..72d4e6b12b 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -20,6 +20,8 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结,不覆盖任何强制转换钩子,且时间段受可精确表示的毫秒范围约束——这正是拆分精确的前提。公开面就是 `fromMilliseconds` 与整数量拆解 `decompose()`,即已迁移消费方所调用的两个入口。 +最后一份规范时刻剖面副本也一并迁移。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 原本用自己的一段正则与往返比对校验持久记录里的调度时刻;共享的 `fourDigitYearEpoch` 拥有同一剖面,因此该迁移改为询问它,并保留自己的错误类型与文案。由于这个迁移要读取已发布的日志,其接受集是兼容性承诺:`packages/session/session-format-v0-to-v1/tests/validation.spec.ts` 接受四位年份两端,并拒绝只靠正则会放进来的每种拼写(`0000`、`2026-02-30`、四位小数、数字偏移、首尾空白)。 + 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此 `packages/README.md` 与 `AGENTS.md` 里的 `util/` 一行保留该说法,而 `packages/util/README.md` 与本包自己的描述写明它加载了什么。 可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试断言错误码以及少量无关的消息子串,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 @@ -48,4 +50,4 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出,于是非有限的读数在产生它的地方失败,而不是渲染出一段文本。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。 -本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 +本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与用于展示的偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有持久读数字格式。有五处时间代码各自保留实现,原因各不相同。`packages/experimental/tool-session-query/src/input.ts` 解析的剖面比共享入口更宽——秒可省略、小数位不限、并保留下毫秒数字以便精确比较两个时刻——因此换成共享解析器会收窄它接受的输入,而把那条剖面搬进本库是另一项独立的设计。`dsh-schedule` 里的 `Date.parse(record.scheduledAt)` 读的是写入路径已经校验过的值,换成带判别值的解码会在每个读取点新增一个实际不可能发生的拒绝分支,并改变损坏记录的报告方式。`everySeconds`、`afterSeconds` 与 `refreshMarginSeconds` 三处乘法旁边都紧跟一条抛本包自有错误的校验,因此把它们改走会抛自身错误类型的构造函数,会改变调用方看到的失败。客户端上报的 UTC 偏移由 `packages/client/ui-settings-account/src/client/client-metadata.ts` 自行计算而不共享:客户端 bundle 只允许对列入 inline-safe 的包做跨插件值导入,`dsh-util-time` 不在其中,而且它在模块顶层静态导入 `Temporal` polyfill,内联它等于把该 polyfill 打进浏览器;Desktop 的宿主侧则用共享库计算同一个偏移,那里没有这条限制。`dsh-workspace` 用平台的 `toISOString` 写记录时间戳,对每个可表示的纪元那已是规范渲染:共享写入器会拒绝的扩展年份与小数截断两种情形,从 `Date.now()` 与 `SessionHeader` 的纪元出发都不可达,因此在那边采用共享格式化器只会增加一个模块与一条依赖,却不会改变任何字节。 diff --git a/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.i18n.yaml new file mode 100644 index 0000000000..4a2fe2ba9d --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-27-web-search-offered-by-provider-readiness.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.md +/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready: + en: 9163af91ea0e030d + zh: 92e44043b999d32f +/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/problem: + en: 6fac3c73a4415c83 + zh: f65b61cc1705348b +/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/decision: + en: 458416ed7e47f644 + zh: 8b4dd820228e9dfd +/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/alternatives-considered: + en: b731254983be656d + zh: 5dee2b09f7faa537 +/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/consequences: + en: 798faeaa0b406eac + zh: 97bbfdaebc84b1d4 diff --git a/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.md b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.md new file mode 100644 index 0000000000..c3e74990d7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.md @@ -0,0 +1,51 @@ +# Agent Note: Offer web_search and web_fetch only while the selected provider is ready + +Status: implemented + +English | [中文](2026-09-27-web-search-offered-by-provider-readiness.zh.md) + +## Problem + +`dsh-tool-web` registers `web_search` whenever search is enabled, and the [web capability seam decision](2026-06-24-web-capability-seam.md) keeps it visible while the selected provider cannot run. The shipped search provider, `dsh-web-search-deepseek`, authenticates a search with the DeepSeek account token only for a model request on the `deepseek-account` route; every other route needs a configured API key. A Session on an API-key route without a search key was therefore offered a tool whose every call failed with `WEB_PROVIDER_CREDENTIAL_MISSING`. Sessions in one Host can use different routes, a Session can switch routes between requests, and the Web app registers `web_search` once per agent preset, so no registration decision matches every request. The Search and Fetch APIs of `dsh-web-deepseek-api`, which the Web search settings switch selects together, need that credential for both operations, so the same Session was also offered a `web_fetch` whose every call failed. + +## Decision + +Each role of the web capability seam owns one part of a per-step check: + +- **Service Definition.** `WebSearchProvider` and `WebFetchProvider` have an optional `ready(llmProvider)` that answers whether an operation made for a model request on that chat route has a credential to send. It checks configuration only and makes no network calls. `ctx.web.searchReady(llmProvider?)` and `ctx.web.fetchReady(llmProvider?)` resolve the provider with the same rules as `search()` and `fetch()` and return its answer. A provider without `ready()` counts as ready, and a selection failure answers true because the operation reports that failure as a structured `WebError`. +- **Service Provider.** `dsh-web-search-deepseek` answers true when the route is `deepseek-account` and `ctx.deepseekAccount.resolveToken()` returns a token for the configured search endpoint, when the section carries a literal `apiKey`, or when `ctx.credentials.describe()` reports the `apiKeyEnv` reference configured (without that service, when the launching environment carries it). It checks credentials in the order `search()` resolves them, and a failed account-token or credential-store read answers false, because `search()` fails on that read instead of trying the next credential. The Search and Fetch API providers of `dsh-web-deepseek-api` share one credential and answer the same way for their API base; their transport repeats these checks, so neither package depends on the other. The providers name no tool or prompt section. +- **Consumer.** `dsh-tool-web` registers `web_search` and `web_fetch` with [step activation conditions](../../../../packages/core/tools/README.md#conditional-tools) that ask `ctx.web.searchReady()` and `ctx.web.fetchReady()` about the route of the step's model selection. The agent loop resolves conditions before it assembles the step, so the declared tools, the `tool:web_search` and `tool:web_fetch` prompt sections, PTC program bindings, and dispatch all read the same admitted set. + +The route decides only whether the account token is checked before the API key, which happens on the `deepseek-account` route; an API key counts on every route, so a conversation on another provider's model is offered the DeepSeek web tools whenever a DeepSeek key is configured. The providers do not compare the chat route's endpoint with their own: each operation sends the configured key, `DEEPSEEK_API_KEY` by default, to the configured endpoint, even when the official route chats through a gateway. + +Only presence decides: no API key value is read and no endpoint is contacted, so a wrong key or an endpoint that does not serve the native `web_search_20250305` server tool keeps the tool admitted. A step that does not admit a web tool also refuses calls to it as `UNKNOWN_TOOL`, so a model that repeats an earlier call reaches no provider; an operation made outside a model step resolves its credential when it runs and fails with `WEB_PROVIDER_CREDENTIAL_MISSING` when there is none. + +The request header records the assembled tools, so the agent loop announces each change through tool-registry updates. A route switch, a stored key, or a removed key changes the next request; DeepSeek routes load a returning tool as a deferred addition, and routes without tool-update support receive the complete tool list. + +## Alternatives considered + +**Let the DeepSeek provider remove `web_search` in its own assemble listener.** The seam stays unchanged, but the provider then names another package's tool, prompt section, and PTC transport, and it removes the tool even when the deployment selects another search provider, such as Exa, whose searches would succeed. + +**Extend `available()`.** It is synchronous and takes no request, while this check reads an asynchronous credential store and depends on the request's route. A route-dependent `available()` would also make provider selection depend on the conversation. + +**Read the route from the Session's latest `request/context` event instead of passing it.** That event records the previous request, so a new Session's first request and the first request after a model switch would be decided for the wrong route or for none. + +**Register or unregister `web_search` in `dsh-tool-web`.** Registration is shared by every Session of an agent preset, while credentials depend on each request's route. Unregistering would also make `toolOrder` entries and subagent `toolFilter` restrictions that name `web_search` fail. + +**Remove `web_search` from the assembled request.** A `system-prompt/assemble` listener could drop the declaration and its section, but the tool would stay executable, so a model that repeats an earlier call would still run it, and PTC programs would still bind it. Prompt assembly also no longer carries tool declarations. + +**Offer search only on the DeepSeek account route.** Account tokens are issued only for the official endpoint, so a gateway key could never reach DeepSeek. But every shipped profile defaults to the `deepseek-official` route, so search would be off by default, including the official API-key case. + +**Withhold the tools from other providers' conversations unless search has a key of its own.** A search from another model's conversation bills the DeepSeek key, but DeepSeek search is one of the services that key pays for, and a user who configured it expects search to work whatever model chats. The Web search settings page saves its key under `apiKeyEnv`, `DEEPSEEK_API_KEY` by default, which the chat route also reads, so a key entered there could never serve another model; a `routes` setting that opens them would be one more thing a user has to find. + +**Compare the official chat route's endpoint with the search endpoint.** Withholding the tools or the key when the two endpoints differ in origin keeps a gateway's key away from DeepSeek, but the providers would have to find the chat route's configuration themselves. Taking the first active `dsh-llm-deepseek-api-key` Loader row answers for an unrelated instance when a composition isolates several, which disables a correct route or lets a gateway key through; only the route's owner knows which endpoint serves a request, and an endpoint field in `dsh-llm` would reach the browser through the Remote `listProviders()`. The check would also withhold search from users who chat through a proxy or development endpoint with a key the official search endpoint accepts. Without it, a gateway user's search fails at the configured endpoint. + +**Keep the tool offered and fail at execution.** The model spends a call on a search that cannot authenticate, and the failure repeats on every attempt. + +**Probe the key or endpoint before offering the tool.** A network request per model request adds latency and cost; the user controls configuration, not its validity at that moment. + +## Consequences + +`dsh-web` gains public members: the optional `ready()` of both provider interfaces, `WebRuntime.searchReady()`, and `WebRuntime.fetchReady()`. Each readiness method is the smallest query its one consumer needs, which is how the [observation-surface decision](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) lets a status query return; neither reports availability or selection failures. The Exa, Perplexity, and HTTP fetch providers implement no `ready()`, so their behavior is unchanged, and `web_fetch` stays admitted whenever the switch leaves HTTP fetch selected. A step that does not admit a web tool leaves it out of PTC program bindings too. An endpoint that cannot serve the native server tool keeps the tool admitted and fails each call. Removing a tool on an unchanged route changes the declared tools and misses the prompt cache once; a model switch already invalidates it. Searches and fetches from other providers' conversations bill the configured DeepSeek key, and a deployment whose official route chats through a gateway sends the gateway's key to the configured endpoint, the official one by default, where each operation fails unless that endpoint accepts the key. The DeepSeek web packages read no Loader rows and depend on no LLM package. + +Unit tests pin each role: `packages/web/web/tests/web.spec.ts` covers which provider `searchReady()` and `fetchReady()` ask and their answer on selection failure, `packages/web/web-search-deepseek/tests/deepseek.spec.ts` and `packages/web/web-deepseek-api/tests/readiness.spec.ts` cover each credential case of `ready()` on the account route and on other routes, and `packages/web/tool-web/tests/tool-web.spec.ts` resolves step admission of both tools through the real prompt, tool, and executor services, including the `UNKNOWN_TOOL` refusal of a call in a step that withheld the tool. In real compositions, the shipped headless keyless smoke test and the `web-search-unconfigured` recorded-session snapshot, whose search key references are unset, pin the withheld search tool. Snapshot replays supply a placeholder DeepSeek key, so their tool lists do not depend on the runner's environment; with it, the `pi-ai-conversation-updates` conversation on a pi-ai route keeps `web_search`. diff --git a/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.zh.md b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.zh.md new file mode 100644 index 0000000000..9c4b0f6948 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.zh.md @@ -0,0 +1,51 @@ +# Agent Note: 仅在选中的提供方就绪时提供 web_search 与 web_fetch + +Status: implemented + +[English](2026-09-27-web-search-offered-by-provider-readiness.md) | 中文 + +## Problem + +`dsh-tool-web` 在启用搜索时总会注册 `web_search`,而 [web 能力 seam 决策](2026-06-24-web-capability-seam.zh.md)让它在选中的提供方无法运行时仍然可见。发行版的搜索提供方 `dsh-web-search-deepseek` 只对 `deepseek-account` 路由上的模型请求使用 DeepSeek 账号 token 鉴权;其他路由都需要已配置的 API 密钥。因此,API 密钥路由上没有搜索密钥的会话会获得一个每次调用都以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败的工具。同一 Host 中的会话可以使用不同路由,一个会话可以在请求之间切换路由,而 Web 应用按 agent preset 只注册一次 `web_search`,因此任何注册层面的决定都无法与每个请求相符。Web 搜索设置开关会同时选中 `dsh-web-deepseek-api` 的 Search 与 Fetch API,两个操作都需要这份凭据,因此同一会话还会获得一个每次调用都失败的 `web_fetch`。 + +## Decision + +web 能力 seam 的每个角色各负责逐步骤检查的一部分: + +- **Service Definition。** `WebSearchProvider` 与 `WebFetchProvider` 有一个可选的 `ready(llmProvider)`,回答为该对话路由上的模型请求发起的操作是否有可发送的凭据。它只检查配置,不发起网络调用。`ctx.web.searchReady(llmProvider?)` 与 `ctx.web.fetchReady(llmProvider?)` 按与 `search()`、`fetch()` 相同的规则解析提供方并返回其回答。没有 `ready()` 的提供方视为就绪;选择失败时回答 true,因为对应操作会把该失败报告为结构化 `WebError`。 +- **Service Provider。** `dsh-web-search-deepseek` 在以下任一情况下回答 true:路由为 `deepseek-account` 且 `ctx.deepseekAccount.resolveToken()` 为配置的搜索端点返回 token;配置段带有字面 `apiKey`;或 `ctx.credentials.describe()` 报告 `apiKeyEnv` 引用已配置(没有该服务时,启动环境中存在该变量)。它按 `search()` 解析凭据的顺序检查凭据,读取账号 token 或凭据存储失败时回答 false,因为 `search()` 会在该次读取上失败,而不会尝试下一种凭据。`dsh-web-deepseek-api` 的 Search 与 Fetch API 提供方共用一份凭据,并对其 API 基础地址以同样方式回答;它们的传输层重复了这些检查,因此两个包互不依赖。提供方不点名任何工具或提示词段落。 +- **Consumer。** `dsh-tool-web` 注册 `web_search` 与 `web_fetch` 时各带一个[步骤激活条件](../../../../packages/core/tools/README.zh.md#conditional-tools),以该步骤模型选择的路由询问 `ctx.web.searchReady()` 与 `ctx.web.fetchReady()`。agent loop 在组装该步骤之前解析各条件,因此声明的工具、`tool:web_search` 与 `tool:web_fetch` 提示词段落、PTC 程序绑定与派发都读取同一个准入集合。 + +路由只决定是否在 API 密钥之前检查账号 token,这只发生在 `deepseek-account` 路由上;API 密钥在任何路由上都算数,因此只要配置了 DeepSeek 密钥,使用其他提供方模型的对话也会获得 DeepSeek Web 工具。提供方不比较聊天路由的地址与自己的端点:即使官方路由经网关聊天,每次操作也把配置的密钥(默认 `DEEPSEEK_API_KEY`)发往配置的端点。 + +只看凭据是否存在:不读取 API 密钥的值,也不访问任何端点,因此错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,仍会让该工具保持准入。未准入某个 Web 工具的步骤也会以 `UNKNOWN_TOOL` 拒绝对它的调用,因此模型重复先前的调用也到达不了任何提供方;在模型步骤之外发起的操作在运行时解析凭据,没有凭据时以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败。 + +请求头记录组装后的工具,因此 agent loop(智能体循环)通过工具注册表更新宣告每次变化。路由切换、存储密钥或删除密钥都会改变下一个请求;DeepSeek 路由以延迟加载的新增方式载入重新提供的工具,不支持工具更新的路由则接收完整工具列表。 + +## Alternatives considered + +**让 DeepSeek 提供方在自己的 assemble 监听器中移除 `web_search`。** seam 保持不变,但提供方因此要点名另一个包的工具、提示词段落与 PTC 传输工具;而且即使部署选择了另一个搜索成功的提供方(例如 Exa),它也会移除该工具。 + +**扩展 `available()`。** 它是同步的且不接收请求,而这项检查要读取异步凭据存储,并取决于请求的路由。依赖路由的 `available()` 还会让提供方选择取决于对话。 + +**从会话最近的 `request/context` 事件读取路由,而不是传入路由。** 该事件记录的是上一个请求,因此新会话的首个请求,以及模型切换后的首个请求,会按错误的路由判断,或根本没有路由可用。 + +**在 `dsh-tool-web` 中注册或注销 `web_search`。** 注册由同一 agent preset 的所有会话共享,而凭据取决于每个请求的路由。注销还会使点名 `web_search` 的 `toolOrder` 条目和 subagent `toolFilter` 限制失败。 + +**从组装好的请求中移除 `web_search`。** `system-prompt/assemble` 监听器可以删掉该声明及其段落,但工具仍可执行,因此模型重复先前的调用时仍会运行它,PTC 程序也仍能绑定它。提示词组装也已不再携带工具声明。 + +**只在 DeepSeek 账号路由上提供搜索。** 账号 token 只对官方端点签发,因此网关的密钥不可能到达 DeepSeek。但所有发行 profile 的默认路由都是 `deepseek-official`,搜索会因此默认关闭,连官方 API 密钥的情况也一并关闭。 + +**除非搜索有自己的密钥,否则不向其他提供方的对话提供这些工具。** 其他模型对话中的搜索会由 DeepSeek 密钥计费,但 DeepSeek 搜索本就是该密钥付费使用的服务之一,配置了它的用户也期望无论用哪个模型对话,搜索都能用。Web 搜索设置页把密钥存在 `apiKeyEnv` 指向的引用下,默认是聊天路由也在读取的 `DEEPSEEK_API_KEY`,因此在那里输入的密钥永远无法服务其他模型;再加一个开放这些路由的 `routes` 配置项,只会让用户多找一样东西。 + +**比较官方聊天路由的端点与搜索端点。** 两个端点不同源时撤下工具或不发送密钥,能让网关的密钥远离 DeepSeek,但提供方得自己找出聊天路由的配置。取第一条激活的 `dsh-llm-deepseek-api-key` Loader 行时,如果组合隔离了多个实例,回答的可能是无关的实例,从而关掉正确的路由,或放过网关的密钥;只有路由的所有者知道哪个端点服务某个请求,而在 `dsh-llm` 加地址字段会让它经 Remote 方法 `listProviders()` 进入浏览器。这项检查还会让经代理或开发端点对话、而密钥能被官方搜索端点接受的用户失去搜索。不做这项检查时,网关用户的搜索会在配置的端点上失败。 + +**保持工具可见,在执行时失败。** 模型会把一次调用花在无法鉴权的搜索上,并且每次尝试都会重复失败。 + +**提供工具前探测密钥或端点。** 每个模型请求都发起一次网络请求会增加延迟与成本;用户控制的是配置本身,而不是它在那一刻是否有效。 + +## Consequences + +`dsh-web` 增加以下公共成员:两个提供方接口上可选的 `ready()`、`WebRuntime.searchReady()` 与 `WebRuntime.fetchReady()`。每个就绪方法都是其唯一消费方所需的最小查询,这正是[观测接口决策](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)允许状态查询回归的方式;它们既不报告可用性,也不报告选择失败。Exa、Perplexity 与 HTTP 抓取提供方没有实现 `ready()`,其行为不变;只要开关让 HTTP 抓取保持选中,`web_fetch` 就始终准入。未准入某个 Web 工具的步骤,其 PTC 程序绑定中也没有它。无法提供原生服务器工具的端点仍会让工具保持准入,并使每次调用失败。在路由不变时移除工具会改变声明的工具,并使提示词缓存失效一次;模型切换本身已经使缓存失效。其他提供方对话中的搜索与抓取由配置的 DeepSeek 密钥计费;官方路由经网关对话的部署会把网关的密钥发往配置的端点(默认是官方端点),除非该端点接受这个密钥,否则每次操作都会失败。DeepSeek Web 包不读取 Loader 的行,也不依赖任何 LLM 包。 + +单元测试分别固定每个角色:`packages/web/web/tests/web.spec.ts` 覆盖 `searchReady()` 与 `fetchReady()` 询问哪个提供方以及选择失败时的回答,`packages/web/web-search-deepseek/tests/deepseek.spec.ts` 与 `packages/web/web-deepseek-api/tests/readiness.spec.ts` 覆盖 `ready()` 在账号路由与其他路由上的每种凭据情况,`packages/web/tool-web/tests/tool-web.spec.ts` 通过真实的提示词、工具与执行器服务解析两个工具的步骤准入,包括在撤下该工具的步骤中调用它时以 `UNKNOWN_TOOL` 拒绝。在真实组合中,发行版 headless 无密钥冒烟测试与 `web-search-unconfigured` 录制会话快照的搜索密钥引用都未设置,二者固定了搜索工具的撤下行为。快照回放提供占位 DeepSeek 密钥,因此其工具列表不依赖运行环境;借助该密钥,`pi-ai-conversation-updates` 中 pi-ai 路由上的对话保留 `web_search`。 diff --git a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.i18n.yaml index 3ae45a7509..b04f873a24 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.i18n.yaml @@ -12,8 +12,8 @@ en: a15c7c13f167e5ac zh: 63bcc2ef60c2f344 /agent-note-boot-the-preview-worker-from-the-shipped-profile-and-fetch-lazy-chunks-on-demand/decision/boot-the-same-profile-as-the-served-web-deployment: - en: caf1ca77192dcb20 - zh: d663e2cbb05fe4fc + en: 9b957e37f290abaf + zh: 06a4599c8fe449ad /agent-note-boot-the-preview-worker-from-the-shipped-profile-and-fetch-lazy-chunks-on-demand/decision/node-internal-module-interfaces-in-the-worker: en: 37d8e809eaa21d25 zh: 8c7c31bd6f45c823 diff --git a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.md b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.md index 47b2866402..71509500e5 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.md +++ b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.md @@ -23,7 +23,7 @@ Measured: with no bundle enabled, the two compositions differ in only one of 188 The served launch path (apps/cli, app-boot) carries no Preview-specific code; the webworker packages reproduce it: - **packer** - - Package roster = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + the rows named in those bundles' patch files; the bundle packages themselves also enter the image (patches, locale, and icon through the publish view). Both tables are read from app-boot, the same source the Plugins page uses. + - Package roster = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + the rows named in those bundles' patch files; the bundle packages themselves also enter the image (patches, locale, and icon through the publish view). Both tables are read from `@deepseek-ai/dsh-official-bundles`, the same source the Plugins page uses. - The image carries a manifest-only `node_modules/@deepseek-ai/dsh/package.json` (a verbatim copy of `apps/cli/package.json`) as the installation anchor, and the image manifest names it in `installation`. - The image carries no composed configuration and no `config/cordis.yml`; the image manifest records the `profile` the Worker boots. - The image carries a home patch, `home/cordis.patch.yml`: for each row the runtime's `PLATFORM_ENTRY_CONFIG` names (`session-persistence-jsonl`), the config the template's bundle layers compose with the required fields over it (`compression: none`). A patch replaces a row's whole config, so the composed fields stay; the bundle files read at pack time are the ones in the image. diff --git a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.zh.md b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.zh.md index 018992e103..5c9b5bf233 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-14-webworker-preview-profile-parity.zh.md @@ -23,7 +23,7 @@ Status: implemented 正式启动链路(apps/cli、app-boot)不含 Preview 专用代码;由 webworker 包复现这条链路: - **packer** - - 收包清单 = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + 这些 bundle 补丁文件里的行;bundle 包本身也打进镜像(补丁、locale、icon 走 publish view)。两张表从 app-boot 读,与插件页同一来源。 + - 收包清单 = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + 这些 bundle 补丁文件里的行;bundle 包本身也打进镜像(补丁、locale、icon 走 publish view)。两张表从 `@deepseek-ai/dsh-official-bundles` 读,与插件页同一来源。 - 镜像里放一份只含清单的 `node_modules/@deepseek-ai/dsh/package.json`(原样复制 `apps/cli/package.json`),作为安装锚点,镜像 manifest 在 `installation` 里记录它。 - 镜像不带组合好的配置,也没有 `config/cordis.yml`;镜像 manifest 记录 Worker 要启动的 `profile`。 - 镜像带一份 home patch `home/cordis.patch.yml`:对运行时 `PLATFORM_ENTRY_CONFIG` 列出的每一行(`session-persistence-jsonl`),写入模板各 bundle 层合成出的配置,并在其上设置所需字段(`compression: none`)。补丁会整体替换一行的配置,所以保留合成出的字段;打包时读取的 bundle 文件就是镜像里的那些。 diff --git a/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.i18n.yaml new file mode 100644 index 0000000000..59918ce6a1 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-21-replaceable-icon-sprite.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.md +/agent-note-replaceable-product-icons-through-an-svg-sprite: + en: 2b527e550e071451 + zh: 7de256912270cae0 +/agent-note-replaceable-product-icons-through-an-svg-sprite/problem: + en: d55557fe41ab87c8 + zh: eea3f7d0c82561bf +/agent-note-replaceable-product-icons-through-an-svg-sprite/decision: + en: 6ddd80eae56693dd + zh: 4d654a76eb64c50a +/agent-note-replaceable-product-icons-through-an-svg-sprite/alternatives-considered: + en: 2e25ece88bccdb22 + zh: bb24a28bef7ea7ce +/agent-note-replaceable-product-icons-through-an-svg-sprite/consequences: + en: 0ad6db37bbb76ddb + zh: 0ca09d55a2167970 diff --git a/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.md b/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.md new file mode 100644 index 0000000000..f4e228ee02 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.md @@ -0,0 +1,29 @@ +# Agent Note: Replaceable product icons through an SVG sprite + +Status: implemented + +English | [中文](2026-10-21-replaceable-icon-sprite.zh.md) + +## Problem + +Long-term skin customization needs themes to replace icons, not only colors. Product icons were React components that drew inline SVG paths, so a theme registered through `ctx.theme` could recolor them through `currentColor` but could not change their artwork. About 150 Client files import these components. + +## Decision + +Each product glyph weight is one sprite symbol, `dsw-icon--`. ui-primitives keeps the default SVG documents as `ICON_SYMBOLS`; an icon component keeps its export name, `size`, and `className`, and renders an `` box with one `` of its symbol. ui-theme keeps one hidden sprite in the document whose `` children come from the active theme's `ThemeDefinition.icons` replacements or the defaults. Artwork reached through `` inherits `currentColor` and CSS custom properties from the use site, so a replacement can be multi-color, can paint with `--dsw-*` color tokens, and follows light and dark palettes without separate values. + +## Alternatives considered + +- **Icons as CSS image tokens painted through `mask-image`.** Rejected after design review: a mask keeps only the image's alpha, so every icon stays one color, and a `url()` image cannot read CSS variables, so multi-color artwork could not follow color tokens. It would have reused the token override layer without a new theme field. +- **A runtime icon registry service with React components as values.** Rejected: components cannot read services or React context, so every icon import site would need a new prop or slot. +- **A slot per icon.** Rejected for the same caller cost; slots carry composition, not artwork. + +## Consequences + +- Icon DOM contains a `` reference instead of paths; tests identify glyphs through `data-icon` and inspect artwork through `ICON_SYMBOLS`. +- Without ui-theme, the sprite is absent and icons render nothing. +- Every symbol shares the document id space, so `register` requires element ids inside a replacement to start with `-`; `` in another document, such as an iframe, cannot reach the sprite. +- Replacement markup is trusted like the registering plugin's code; `register` rejects `
Optional action cards"] + pkg_creation_recommendations["creation-recommendations"] + pkg_client_ui_action_cards["client-ui-action-cards"] + pkg_plugin_recommendations["plugin-recommendations"] + svc_pluginRecommendations["ctx.pluginRecommendations
Explicit-need plugin recommendations"] + pkg_client_ui_plugin_recommendations["client-ui-plugin-recommendations"] pkg_headless["headless"] svc_headlessStartup["ctx.headlessStartup
Headless invocation options"] pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] @@ -298,6 +305,7 @@ flowchart LR pkg_cordis_host_runner["cordis-host-runner"] svc_dynamicCordisRunner["ctx.dynamicCordisRunner
Dynamic Cordis package host runner"] svc_cordisInspect["ctx.cordisInspect
Dynamic Cordis inspect registry"] + pkg_action_cards --> svc_actionCards pkg_agent --> svc_agents pkg_agent_default_model --> svc_agentDefaultModel pkg_agent_loop --> svc_agentLoop @@ -388,6 +396,7 @@ flowchart LR pkg_plugin_manager --> svc_pluginManager pkg_plugin_market --> svc_pluginMarket pkg_plugin_package_inventory_deepseek --> svc_deepseekLlmApiExtensions + pkg_plugin_recommendations --> svc_pluginRecommendations pkg_ptc_runtime --> svc_ptcRuntime pkg_ptc_runtime_node --> svc_ptcRuntime pkg_pwsh_local --> svc_shell @@ -457,6 +466,8 @@ flowchart LR pkg_working_directory --> svc_workingDirectory pkg_workspace --> svc_workspaceRegistry pkg_workspace_changes --> svc_workspaceChanges + svc_actionCards --> pkg_client_ui_action_cards + svc_actionCards --> pkg_creation_recommendations svc_agentDefaultModel --> pkg_api_session_controller svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base @@ -515,6 +526,7 @@ flowchart LR svc_pluginManager --> pkg_ui_settings_plugin_inventory svc_pluginMarket --> pkg_client_ui_plugin_market svc_pluginMarket --> pkg_plugin_market + svc_pluginRecommendations --> pkg_client_ui_plugin_recommendations svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager svc_profileContext --> pkg_plugin_manager svc_ptcRuntime --> pkg_tool_executor_ptc @@ -619,6 +631,8 @@ flowchart LR | ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note | | --- | --- | --- | --- | --- | --- | --- | +| `ctx.actionCards` | `service` | [`action-cards`](../packages/experimental/action-cards) | - | [`creation-recommendations`](../packages/experimental/creation-recommendations), [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | Owns the card tool, card state, user actions and status notices; card kinds register their actions and handlers. | +| `ctx.pluginRecommendations` | `service` | [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | - | [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | - | Reads the current catalog text of an official plugin card and enables the plugin through the profile plugin manager after a click. | | `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | Supplies parsed task and run options to the headless runner and its sandbox-policy configuration. | | `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | Serves reasoning translations, reuses saved results, and joins ordinary Session activation for uncached inactive Sessions. | | `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | Resolves Google or Bing requests and retains shared experimental records through the active Session writer. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 1b10ace20d..8a2963f3cc 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -9,6 +9,13 @@ ```mermaid flowchart LR + pkg_action_cards["action-cards"] + svc_actionCards["ctx.actionCards
Optional action cards"] + pkg_creation_recommendations["creation-recommendations"] + pkg_client_ui_action_cards["client-ui-action-cards"] + pkg_plugin_recommendations["plugin-recommendations"] + svc_pluginRecommendations["ctx.pluginRecommendations
Explicit-need plugin recommendations"] + pkg_client_ui_plugin_recommendations["client-ui-plugin-recommendations"] pkg_headless["headless"] svc_headlessStartup["ctx.headlessStartup
Headless invocation options"] pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] @@ -300,6 +307,7 @@ flowchart LR pkg_cordis_host_runner["cordis-host-runner"] svc_dynamicCordisRunner["ctx.dynamicCordisRunner
Dynamic Cordis package host runner"] svc_cordisInspect["ctx.cordisInspect
Dynamic Cordis inspect registry"] + pkg_action_cards --> svc_actionCards pkg_agent --> svc_agents pkg_agent_default_model --> svc_agentDefaultModel pkg_agent_loop --> svc_agentLoop @@ -390,6 +398,7 @@ flowchart LR pkg_plugin_manager --> svc_pluginManager pkg_plugin_market --> svc_pluginMarket pkg_plugin_package_inventory_deepseek --> svc_deepseekLlmApiExtensions + pkg_plugin_recommendations --> svc_pluginRecommendations pkg_ptc_runtime --> svc_ptcRuntime pkg_ptc_runtime_node --> svc_ptcRuntime pkg_pwsh_local --> svc_shell @@ -459,6 +468,8 @@ flowchart LR pkg_working_directory --> svc_workingDirectory pkg_workspace --> svc_workspaceRegistry pkg_workspace_changes --> svc_workspaceChanges + svc_actionCards --> pkg_client_ui_action_cards + svc_actionCards --> pkg_creation_recommendations svc_agentDefaultModel --> pkg_api_session_controller svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base @@ -517,6 +528,7 @@ flowchart LR svc_pluginManager --> pkg_ui_settings_plugin_inventory svc_pluginMarket --> pkg_client_ui_plugin_market svc_pluginMarket --> pkg_plugin_market + svc_pluginRecommendations --> pkg_client_ui_plugin_recommendations svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager svc_profileContext --> pkg_plugin_manager svc_ptcRuntime --> pkg_tool_executor_ptc @@ -621,6 +633,8 @@ flowchart LR | ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 | | --- | --- | --- | --- | --- | --- | --- | +| `ctx.actionCards` | `service` | [`action-cards`](../packages/experimental/action-cards) | - | [`creation-recommendations`](../packages/experimental/creation-recommendations), [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | - | 负责卡片工具、卡片状态、用户操作和状态通知;各卡片种类注册自己的动作和处理器。 | +| `ctx.pluginRecommendations` | `service` | [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | - | [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | - | 读取官方插件卡片的当前目录文本,并在用户点击后通过配置插件管理器启用该插件。 | | `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | 将解析后的任务和运行选项提供给无界面运行器及其 sandbox-policy 配置。 | | `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | 为展开的思考内容提供有界的 Session 翻译请求,不改变模型可见历史。 | | `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | 解析 Google 或 Bing 请求,并可选保留统一的实验性 Session 记录。 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 3c5fc354c1..867ec60183 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -33,6 +33,21 @@ export interface AcpConfig { ``` + + + +## `@deepseek-ai/dsh-action-cards` + +- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess` +- `refs`: `z` (`zod`) +- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts) + +```ts config-catalog +/** Validated card limits after schema defaults. */ +export type Config = z.infer +``` + + @@ -1623,7 +1638,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-frontend-static` - `inject`: `webServer` · `connection` -- `source`: [`packages/host/frontend-static/src/index.ts:30`](../packages/host/frontend-static/src/index.ts) +- `source`: [`packages/host/frontend-static/src/index.ts:34`](../packages/host/frontend-static/src/index.ts) ```ts config-catalog /** Plugin config: the dist anchor. */ @@ -1708,7 +1723,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-webserver` -- `source`: [`packages/host/webserver/src/index.ts:79`](../packages/host/webserver/src/index.ts) +- `source`: [`packages/host/webserver/src/index.ts:97`](../packages/host/webserver/src/index.ts) ```ts config-catalog /** Web server listen, TLS, and response-compression config. */ @@ -1791,25 +1806,15 @@ export interface Config { ## `@deepseek-ai/dsh-llm-deepseek-account` - `inject`: `llm` -- `refs`: [`DeepSeekCatalogModel`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:19`](../packages/llm/llm-deepseek-account/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ModelOverride`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:15`](../packages/llm/llm-deepseek-account/src/config.ts) ```ts config-catalog /** Account route configuration; authentication comes exclusively from the account service. */ -export interface Config extends Omit { - /** Deadline of one listing request, which also bounds a first-use wait (default five seconds). */ - modelsTimeoutMs: Volatile - /** - * Listing age after which the next access refreshes it in the background; - * a failed load retries after the same age (default ten minutes). - */ - modelsMaxAgeMs: Volatile +export interface Config extends Omit, ModelListingConfig { /** Field overrides by model id, layered over the account listing and the built-in catalog. */ modelOverrides: Volatile> } - -/** Field overrides for one model; unset fields and empty lists inherit the underlying entry. */ -export type ModelOverride = Omit ``` @@ -1819,12 +1824,12 @@ export type ModelOverride = Omit ## `@deepseek-ai/dsh-llm-deepseek-api-key` - `inject`: `llm` -- `refs`: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:18`](../packages/llm/llm-deepseek-api-key/src/config.ts) ```ts config-catalog /** Messages configuration with a per-request API-key reference. */ -export interface Config extends ProtocolConfig { +export interface Config extends ProtocolConfig, ModelListingConfig { /** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */ apiKeyEnv: Volatile } @@ -2485,7 +2490,7 @@ export interface PlanModeConfig { - `inject`: `loader` · `profileContext` - `refs`: [`NpmFileSource`](subsystems/boot.md) -- `source`: [`packages/boot/plugin-manager/src/index.ts:50`](../packages/boot/plugin-manager/src/index.ts) +- `source`: [`packages/boot/plugin-manager/src/index.ts:52`](../packages/boot/plugin-manager/src/index.ts) ```ts config-catalog /** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ @@ -2500,6 +2505,8 @@ export interface Config { inspectTimeoutMs?: number /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ githubConnectionTimeoutMs?: number + /** Maximum duration of each HTTP(S) and SSH access probe of a self-hosted Git repository before installation, in milliseconds. */ + gitTransportTimeoutMs?: number /** Maximum time one captured package run may print nothing before the manager terminates it, in milliseconds. */ idleTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ @@ -3735,7 +3742,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:47`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:52`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3834,7 +3841,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` - `inject`: `tools` · `terminals` · `workingDirectory` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ @@ -3958,7 +3965,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-jobs` - `inject`: `tools` · `jobs` · `systemPrompt` -- `source`: [`packages/jobs/tool-jobs/src/index.ts:41`](../packages/jobs/tool-jobs/src/index.ts) +- `source`: [`packages/jobs/tool-jobs/src/index.ts:43`](../packages/jobs/tool-jobs/src/index.ts) ```ts config-catalog /** Configures bounded `job_output` waits and completion-notice delivery. */ @@ -3978,6 +3985,13 @@ export interface Config { * the next user input. */ maxConsecutiveWakes?: number + /** + * Largest complete completion notice, in UTF-8 bytes, that carries the + * job's output and final result inline (default 2048); a larger one, or one + * over the producer's `outputLimitBytes`, leaves them for `job_output`. 0 + * never inlines. + */ + inlineOutputBytes?: number } /** @@ -4264,7 +4278,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-web` - `inject`: `tools` · `web` · `systemPrompt` -- `source`: [`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts) +- `source`: [`packages/web/tool-web/src/index.ts:39`](../packages/web/tool-web/src/index.ts) ```ts config-catalog /** Plugin config: which web tools to register, search bounds, per-tool budgets, and the fetch output cap. */ @@ -4361,7 +4375,8 @@ export interface Config { ## `@deepseek-ai/dsh-user-approval` -- `source`: [`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts) +- `inject`: `sessionProjections` +- `source`: [`packages/interaction/user-approval/src/index.ts:152`](../packages/interaction/user-approval/src/index.ts) ```ts config-catalog /** Plugin config. All optional — `static Config` supplies the defaults. */ @@ -4454,7 +4469,7 @@ export interface Config { - `inject`: `web` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/web/web-deepseek-api/src/index.ts:34`](../packages/web/web-deepseek-api/src/index.ts) +- `source`: [`packages/web/web-deepseek-api/src/index.ts:36`](../packages/web/web-deepseek-api/src/index.ts) ```ts config-catalog /** Settings read by the next search or fetch without re-registering either provider. */ @@ -4505,7 +4520,7 @@ export interface Config { - `inject`: `web` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts) +- `source`: [`packages/web/web-search-deepseek/src/index.ts:51`](../packages/web/web-search-deepseek/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `apply` fills env-var and constant defaults). */ @@ -4689,6 +4704,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-locale` | — | [`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts) | | `@deepseek-ai/dsh-client-modules` | `loader` | [`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts) | | `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) | | `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) | | `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) | | `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) | @@ -4697,6 +4713,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | | `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) | @@ -4713,6 +4730,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-permission-presets` | — | [`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts) | | `@deepseek-ai/dsh-client-ui-plan` | — | [`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts) | | `@deepseek-ai/dsh-client-ui-plugin-market` | — | [`packages/client/ui-plugin-market/src/index.ts`](../packages/client/ui-plugin-market/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-plugin-recommendations` | — | [`packages/experimental/client-ui-plugin-recommendations/src/index.ts`](../packages/experimental/client-ui-plugin-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-reference` | — | [`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts) | | `@deepseek-ai/dsh-client-ui-renderer` | — | [`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts) | | `@deepseek-ai/dsh-client-ui-schedule` | — | [`packages/client/ui-schedule/src/index.ts`](../packages/client/ui-schedule/src/index.ts) | @@ -4748,6 +4766,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-computer-use` | — | [`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts) | | `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) | | `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) | +| `@deepseek-ai/dsh-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) | | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | | `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) | @@ -4770,6 +4789,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) | | `@deepseek-ai/dsh-message-quotes` | — | [`packages/session/message-quotes/src/index.ts`](../packages/session/message-quotes/src/index.ts) | | `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) | +| `@deepseek-ai/dsh-plugin-recommendations` | `pluginManager` · `agents` · `skills` · `actionCards` | [`packages/experimental/plugin-recommendations/src/index.ts`](../packages/experimental/plugin-recommendations/src/index.ts) | | `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) | | `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) | | `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) | @@ -4847,6 +4867,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-liveshare-protocol` | — | [`packages/experimental/liveshare-protocol/src/index.ts`](../packages/experimental/liveshare-protocol/src/index.ts) | | `@deepseek-ai/dsh-experimental-plugin-market-bundle` | — | [`packages/experimental/plugin-market-bundle/src/index.ts`](../packages/experimental/plugin-market-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-session-search` | — | [`packages/experimental/session-search/src/index.ts`](../packages/experimental/session-search/src/index.ts) | @@ -4864,8 +4885,10 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-llm-mock-server` | — | [`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts) | | `@deepseek-ai/dsh-loader-smoke` | — | [`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts) | | `@deepseek-ai/dsh-native-command` | — | [`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts) | +| `@deepseek-ai/dsh-official-bundles` | — | [`packages/util/official-bundles/src/index.ts`](../packages/util/official-bundles/src/index.ts) | | `@deepseek-ai/dsh-output-retention` | — | [`packages/util/output-retention/src/index.ts`](../packages/util/output-retention/src/index.ts) | | `@deepseek-ai/dsh-package-manifest` | — | [`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts) | +| `@deepseek-ai/dsh-personalized-recommendations` | — | [`packages/experimental/personalized-recommendations/src/index.ts`](../packages/experimental/personalized-recommendations/src/index.ts) | | `@deepseek-ai/dsh-plugin-detail` | — | [`packages/util/plugin-detail/src/index.ts`](../packages/util/plugin-detail/src/index.ts) | | `@deepseek-ai/dsh-plugin-market-index` | — | [`packages/util/plugin-market-index/src/index.ts`](../packages/util/plugin-market-index/src/index.ts) | | `@deepseek-ai/dsh-remote-mock` | — | [`packages/test-support/remote-mock/src/index.ts`](../packages/test-support/remote-mock/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 9ecd1cedf9..4161e0dd06 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -35,6 +35,21 @@ export interface AcpConfig { ``` + + + +## `@deepseek-ai/dsh-action-cards` + +- `inject`: `tools` · `agents` · `sessionProjections` · `remoteAccess` +- `refs`: `z` (`zod`) +- `source`: [`packages/experimental/action-cards/src/index.ts:23`](../packages/experimental/action-cards/src/index.ts) + +```ts config-catalog +/** Validated card limits after schema defaults. */ +export type Config = z.infer +``` + + @@ -1625,7 +1640,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-frontend-static` - `inject`: `webServer` · `connection` -- `source`: [`packages/host/frontend-static/src/index.ts:30`](../packages/host/frontend-static/src/index.ts) +- `source`: [`packages/host/frontend-static/src/index.ts:34`](../packages/host/frontend-static/src/index.ts) ```ts config-catalog /** Plugin config: the dist anchor. */ @@ -1710,7 +1725,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-webserver` -- `source`: [`packages/host/webserver/src/index.ts:79`](../packages/host/webserver/src/index.ts) +- `source`: [`packages/host/webserver/src/index.ts:97`](../packages/host/webserver/src/index.ts) ```ts config-catalog /** Web server listen, TLS, and response-compression config. */ @@ -1793,25 +1808,15 @@ export interface Config { ## `@deepseek-ai/dsh-llm-deepseek-account` - `inject`: `llm` -- `refs`: [`DeepSeekCatalogModel`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:19`](../packages/llm/llm-deepseek-account/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ModelOverride`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:15`](../packages/llm/llm-deepseek-account/src/config.ts) ```ts config-catalog /** Account route configuration; authentication comes exclusively from the account service. */ -export interface Config extends Omit { - /** Deadline of one listing request, which also bounds a first-use wait (default five seconds). */ - modelsTimeoutMs: Volatile - /** - * Listing age after which the next access refreshes it in the background; - * a failed load retries after the same age (default ten minutes). - */ - modelsMaxAgeMs: Volatile +export interface Config extends Omit, ModelListingConfig { /** Field overrides by model id, layered over the account listing and the built-in catalog. */ modelOverrides: Volatile> } - -/** Field overrides for one model; unset fields and empty lists inherit the underlying entry. */ -export type ModelOverride = Omit ``` @@ -1821,12 +1826,12 @@ export type ModelOverride = Omit ## `@deepseek-ai/dsh-llm-deepseek-api-key` - `inject`: `llm` -- `refs`: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:18`](../packages/llm/llm-deepseek-api-key/src/config.ts) ```ts config-catalog /** Messages configuration with a per-request API-key reference. */ -export interface Config extends ProtocolConfig { +export interface Config extends ProtocolConfig, ModelListingConfig { /** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */ apiKeyEnv: Volatile } @@ -2487,7 +2492,7 @@ export interface PlanModeConfig { - `inject`: `loader` · `profileContext` - `refs`: [`NpmFileSource`](subsystems/boot.zh.md) -- `source`: [`packages/boot/plugin-manager/src/index.ts:50`](../packages/boot/plugin-manager/src/index.ts) +- `source`: [`packages/boot/plugin-manager/src/index.ts:52`](../packages/boot/plugin-manager/src/index.ts) ```ts config-catalog /** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ @@ -2502,6 +2507,8 @@ export interface Config { inspectTimeoutMs?: number /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ githubConnectionTimeoutMs?: number + /** Maximum duration of each HTTP(S) and SSH access probe of a self-hosted Git repository before installation, in milliseconds. */ + gitTransportTimeoutMs?: number /** Maximum time one captured package run may print nothing before the manager terminates it, in milliseconds. */ idleTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ @@ -3737,7 +3744,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:47`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:52`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3836,7 +3843,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` - `inject`: `tools` · `terminals` · `workingDirectory` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ @@ -3960,7 +3967,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-jobs` - `inject`: `tools` · `jobs` · `systemPrompt` -- `source`: [`packages/jobs/tool-jobs/src/index.ts:41`](../packages/jobs/tool-jobs/src/index.ts) +- `source`: [`packages/jobs/tool-jobs/src/index.ts:43`](../packages/jobs/tool-jobs/src/index.ts) ```ts config-catalog /** Configures bounded `job_output` waits and completion-notice delivery. */ @@ -3980,6 +3987,13 @@ export interface Config { * the next user input. */ maxConsecutiveWakes?: number + /** + * Largest complete completion notice, in UTF-8 bytes, that carries the + * job's output and final result inline (default 2048); a larger one, or one + * over the producer's `outputLimitBytes`, leaves them for `job_output`. 0 + * never inlines. + */ + inlineOutputBytes?: number } /** @@ -4266,7 +4280,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-web` - `inject`: `tools` · `web` · `systemPrompt` -- `source`: [`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts) +- `source`: [`packages/web/tool-web/src/index.ts:39`](../packages/web/tool-web/src/index.ts) ```ts config-catalog /** Plugin config: which web tools to register, search bounds, per-tool budgets, and the fetch output cap. */ @@ -4363,7 +4377,8 @@ export interface Config { ## `@deepseek-ai/dsh-user-approval` -- `source`: [`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts) +- `inject`: `sessionProjections` +- `source`: [`packages/interaction/user-approval/src/index.ts:152`](../packages/interaction/user-approval/src/index.ts) ```ts config-catalog /** Plugin config. All optional — `static Config` supplies the defaults. */ @@ -4456,7 +4471,7 @@ export interface Config { - `inject`: `web` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/web/web-deepseek-api/src/index.ts:34`](../packages/web/web-deepseek-api/src/index.ts) +- `source`: [`packages/web/web-deepseek-api/src/index.ts:36`](../packages/web/web-deepseek-api/src/index.ts) ```ts config-catalog /** Settings read by the next search or fetch without re-registering either provider. */ @@ -4507,7 +4522,7 @@ export interface Config { - `inject`: `web` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts) +- `source`: [`packages/web/web-search-deepseek/src/index.ts:51`](../packages/web/web-search-deepseek/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `apply` fills env-var and constant defaults). */ @@ -4691,6 +4706,7 @@ export interface Config { | `@deepseek-ai/dsh-client-locale` | — | [`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts) | | `@deepseek-ai/dsh-client-modules` | `loader` | [`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts) | | `@deepseek-ai/dsh-client-resources` | — | [`packages/client/resources/src/index.ts`](../packages/client/resources/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-action-cards` | — | [`packages/experimental/client-ui-action-cards/src/index.ts`](../packages/experimental/client-ui-action-cards/src/index.ts) | | `@deepseek-ai/dsh-client-ui-agent-preset` | — | [`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts) | | `@deepseek-ai/dsh-client-ui-approval` | — | [`packages/client/ui-approval/src/index.ts`](../packages/client/ui-approval/src/index.ts) | | `@deepseek-ai/dsh-client-ui-attachment` | — | [`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts) | @@ -4699,6 +4715,7 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | | `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) | @@ -4715,6 +4732,7 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-permission-presets` | — | [`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts) | | `@deepseek-ai/dsh-client-ui-plan` | — | [`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts) | | `@deepseek-ai/dsh-client-ui-plugin-market` | — | [`packages/client/ui-plugin-market/src/index.ts`](../packages/client/ui-plugin-market/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-plugin-recommendations` | — | [`packages/experimental/client-ui-plugin-recommendations/src/index.ts`](../packages/experimental/client-ui-plugin-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-reference` | — | [`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts) | | `@deepseek-ai/dsh-client-ui-renderer` | — | [`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts) | | `@deepseek-ai/dsh-client-ui-schedule` | — | [`packages/client/ui-schedule/src/index.ts`](../packages/client/ui-schedule/src/index.ts) | @@ -4750,6 +4768,7 @@ export interface Config { | `@deepseek-ai/dsh-computer-use` | — | [`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts) | | `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) | | `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) | +| `@deepseek-ai/dsh-creation-recommendations` | `tools` · `pluginManager` · `agents` · `subagents` · `profileContext` · `skills` · `actionCards` | [`packages/experimental/creation-recommendations/src/index.ts`](../packages/experimental/creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) | | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | | `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) | @@ -4772,6 +4791,7 @@ export interface Config { | `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) | | `@deepseek-ai/dsh-message-quotes` | — | [`packages/session/message-quotes/src/index.ts`](../packages/session/message-quotes/src/index.ts) | | `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) | +| `@deepseek-ai/dsh-plugin-recommendations` | `pluginManager` · `agents` · `skills` · `actionCards` | [`packages/experimental/plugin-recommendations/src/index.ts`](../packages/experimental/plugin-recommendations/src/index.ts) | | `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) | | `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) | | `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) | @@ -4849,6 +4869,7 @@ export interface Config { | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-liveshare-protocol` | — | [`packages/experimental/liveshare-protocol/src/index.ts`](../packages/experimental/liveshare-protocol/src/index.ts) | | `@deepseek-ai/dsh-experimental-plugin-market-bundle` | — | [`packages/experimental/plugin-market-bundle/src/index.ts`](../packages/experimental/plugin-market-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-session-search` | — | [`packages/experimental/session-search/src/index.ts`](../packages/experimental/session-search/src/index.ts) | @@ -4866,8 +4887,10 @@ export interface Config { | `@deepseek-ai/dsh-llm-mock-server` | — | [`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts) | | `@deepseek-ai/dsh-loader-smoke` | — | [`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts) | | `@deepseek-ai/dsh-native-command` | — | [`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts) | +| `@deepseek-ai/dsh-official-bundles` | — | [`packages/util/official-bundles/src/index.ts`](../packages/util/official-bundles/src/index.ts) | | `@deepseek-ai/dsh-output-retention` | — | [`packages/util/output-retention/src/index.ts`](../packages/util/output-retention/src/index.ts) | | `@deepseek-ai/dsh-package-manifest` | — | [`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts) | +| `@deepseek-ai/dsh-personalized-recommendations` | — | [`packages/experimental/personalized-recommendations/src/index.ts`](../packages/experimental/personalized-recommendations/src/index.ts) | | `@deepseek-ai/dsh-plugin-detail` | — | [`packages/util/plugin-detail/src/index.ts`](../packages/util/plugin-detail/src/index.ts) | | `@deepseek-ai/dsh-plugin-market-index` | — | [`packages/util/plugin-market-index/src/index.ts`](../packages/util/plugin-market-index/src/index.ts) | | `@deepseek-ai/dsh-remote-mock` | — | [`packages/test-support/remote-mock/src/index.ts`](../packages/test-support/remote-mock/src/index.ts) | diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml index 2f95205489..28ee60cb3d 100644 --- a/docs/cookbook/adding-a-package.i18n.yaml +++ b/docs/cookbook/adding-a-package.i18n.yaml @@ -21,8 +21,8 @@ en: 0894bd9f35430753 zh: 2d28090e71134edd /cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata: - en: 1238eaea376391d1 - zh: c7e606f95d8980bf + en: ee71fb0a70f02550 + zh: 1f0d6936d6c2acad /cookbook-adding-a-workspace-package/6-verify: en: 80c7fb90b1175aae zh: 2ba37962dbc57299 diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 2950932479..16fe3e564f 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -136,7 +136,7 @@ Merge these entries into `package.json`, retaining existing runtime exports and } ``` -Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). At runtime, missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic. First-party packages and plugin templates must provide `ja.json` covering every field present in `en.json` or optional `zh.json`, preserving placeholders; detail paths may point to the existing English document. Subpath resources under `locale//` follow the same checks. English descriptions explain user-visible behavior and end with sentence punctuation. Run `pnpm run check:i18n` to verify JSON metadata and UI dictionary completeness; the JSON checks also run in `check:all`, CI static checks, and hygiene. +Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). At runtime, missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic. First-party packages and plugin templates must provide `ja.json`, `es.json`, `fr.json`, `de.json`, `ru.json`, and `pt.json` covering every field present in `en.json` or optional `zh.json`, preserving placeholders, slash commands, and API field names; detail paths may point to the existing English document. Subpath resources under `locale//` follow the same checks. English descriptions explain user-visible behavior and end with sentence punctuation. Run `pnpm run check:i18n` to verify JSON metadata and UI dictionary completeness; the JSON checks also run in `check:all`, CI static checks, and hygiene. Fields fall back independently before view-specific name formatting, using the existing locale language chain first: @@ -147,7 +147,7 @@ Export `/locale/en.json` for locale lookup; expose ` For an image on bundle cards, details, and component rows, a package-root plugin sets top-level `"icon": "./icon.svg"` in its exported manifest, or exports `./icon` when that field is omitted. A subpath plugin exports its own icon, such as `"./search/icon": "./search.svg"`. Include each image in `files`. A manifest `icon` is relative to the manifest's directory; absolute paths, URLs, paths outside that directory, and symlinks resolving outside it are rejected. An exported icon resolves through Node exports like locale resources and must remain inside its package. SVG, PNG, JPEG (`.jpg`/`.jpeg`), and WebP files are supported up to 256 KiB. Images must be self-contained; SVG is rendered as an image, not inline HTML. The Host returns a data URL without activating the plugin. Invalid declarations or unreadable files produce a metadata diagnostic while retaining valid text, without trying the export; missing or undecodable images use the panel's default artwork. -A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. +A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its index entry page when the [custom plugin indexes bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and an index entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. Installed bundle cards and details, component lists and configuration details, and Settings' plugin inventory display this metadata, including disabled and preset plugins. Reads do not activate plugins. diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md index 6ab95ce3b3..0d74f645dc 100644 --- a/docs/cookbook/adding-a-package.zh.md +++ b/docs/cookbook/adding-a-package.zh.md @@ -136,7 +136,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex } ``` -语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。运行时文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。仓库内的包和插件模板必须提供 `ja.json`,覆盖 `en.json` 或可选 `zh.json` 中的全部字段并保留占位符;详情路径可指向现有英文文档。`locale//` 下的子路径资源使用相同检查。英文描述面向产品用户说明可见行为,并以句末标点结束。运行 `pnpm run check:i18n` 检查 JSON 元数据和 UI 字典完整性;JSON 检查也接入 `check:all`、CI 静态检查和 hygiene。 +语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。运行时文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。仓库内的包和插件模板必须提供 `ja.json`、`es.json`、`fr.json`、`de.json`、`ru.json` 和 `pt.json`,覆盖 `en.json` 或可选 `zh.json` 中的全部字段并保留占位符、斜杠命令和 API 字段名;详情路径可指向现有英文文档。`locale//` 下的子路径资源使用相同检查。英文描述面向产品用户说明可见行为,并以句末标点结束。运行 `pnpm run check:i18n` 检查 JSON 元数据和 UI 字典完整性;JSON 检查也接入 `check:all`、CI 静态检查和 hygiene。 各字段先独立回退,再按页面规则格式化技术名称;回退优先经过现有 locale 语言链: @@ -147,7 +147,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex 要在组合包卡片、详情和组件行显示图片,包根插件在导出清单顶层设置 `"icon": "./icon.svg"`,或在省略该字段时导出 `./icon`。子路径插件导出自己的图标,例如 `"./search/icon": "./search.svg"`。将每张图片加入 `files`。清单 `icon` 相对于清单所在目录;绝对路径、URL、目录外路径,以及解析到目录外的符号链接均被拒绝。导出的图标像 locale 资源一样经 Node exports 解析,且必须位于所属包内。支持不超过 256 KiB 的 SVG、PNG、JPEG(`.jpg`/`.jpeg`)和 WebP 文件。图片必须自包含;SVG 作为图片渲染,不作为内联 HTML。Host 返回 data URL,不激活插件。声明无效或文件不可读时,显示元信息诊断并保留有效文本,不再尝试导出的图标;图片缺失或无法解码时使用面板的默认插画。 -组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 +组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件索引组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其索引条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;索引条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 已安装 bundle 的卡片和详情、组件列表与配置详情、设置中的插件清单都展示这些元信息,包括禁用插件和预设内插件。读取时不激活插件。 diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index a97c7d4984..8f83b554e1 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -21,8 +21,8 @@ en: 3221d1076554001f zh: 29f166be6a5c04d9 /development-guide/contributor-reference/typescript-project-layout: - en: 51ec519fdaac9ae7 - zh: ed0c6a8d655c350e + en: 8c004ba883c40653 + zh: f1964048560dc34b /development-guide/contributor-reference/environment-variables: en: dba2c72ee6ffa930 zh: b65c27fbb9129ec8 diff --git a/docs/development.md b/docs/development.md index bd72547a25..04af67a5e0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -72,7 +72,7 @@ Host and Client stay two aggregate programs because both sides declaration-merge - A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges. - A new package is registered in exactly one aggregate; only the split packages above carry both leaf configs, and the shared leaves are registered in both aggregates because each side must type-check the same source. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase. -Six packages split Host and Client tsconfigs: `api/remotes`, `api/gateway`, `api/session-controller`, `api/workspace-controller`, `client/connection`, and `session-query/session-log-export`. `api/remotes`' Host entry participates in the Host Typert graph while its Client entry imports generated `/remote` declarations; `session-log-export` keeps Node archive production out of its browser controller. Each split package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf; it discovers split packages from the presence of both leaf configs, so a new split joins the gate automatically. The [`api-remotes` README](../packages/api/remotes/README.md) and [`session-log-export` README](../packages/session-query/session-log-export/README.md) explain their splits. +Seven packages split Host and Client tsconfigs: `api/remotes`, `api/gateway`, `api/session-controller`, `api/workspace-controller`, `client/connection`, `session-query/session-log-export`, and `experimental/liveshare-protocol`. `api/remotes`' Host entry participates in the Host Typert graph while its Client entry imports generated `/remote` declarations; `session-log-export` keeps Node archive production out of its browser controller; `liveshare-protocol` keeps the Node wire codecs on its Host leaf and the browser viewer carrier on its Client leaf. Each split package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf; it discovers split packages from the presence of both leaf configs, so a new split joins the gate automatically. The [`api-remotes` README](../packages/api/remotes/README.md) and [`session-log-export` README](../packages/session-query/session-log-export/README.md) explain their splits. `pnpm run build:artifacts` emits every artifact, checking and compiling only the package references of each compiler face. Client compilation waits for Host and Desktop bundling to finish: split compiler faces rewrite shared `lib/types` files read by those bundles. Client bundling follows Client compilation. The artifact order is: diff --git a/docs/development.zh.md b/docs/development.zh.md index 04133a4dd8..20fcc68413 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -76,7 +76,7 @@ Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下 - 构造全仓 `ts.Program` 的脚本显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子——根 solution 永不作为种子,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。 - 新包只登记进一个 aggregate;只有上述拆分包同时携带两个 leaf 配置,共享 leaf 因两侧需要对同一份源码做类型检查而登记进两个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client 插件的两份运行时产物都在 Client 构建阶段生成。 -拆分 Host/Client tsconfig 的包有六个:`api/remotes`、`api/gateway`、`api/session-controller`、`api/workspace-controller`、`client/connection` 与 `session-query/session-log-export`。`api/remotes` 的 Host 入口进入 Host Typert 图,而 Client 入口导入生成的 `/remote` 声明;`session-log-export` 则让 Node archive 生产代码不进入浏览器 controller。每个拆分包根 `tsconfig.json` 因此只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf;该门禁按「两个 leaf 配置同时存在」自动发现拆分包,所以新拆分的包会自动纳入管辖。[`api-remotes` README](../packages/api/remotes/README.zh.md) 与 [`session-log-export` README](../packages/session-query/session-log-export/README.zh.md)分别说明其拆分。 +拆分 Host/Client tsconfig 的包有七个:`api/remotes`、`api/gateway`、`api/session-controller`、`api/workspace-controller`、`client/connection`、`session-query/session-log-export` 与 `experimental/liveshare-protocol`。`api/remotes` 的 Host 入口进入 Host Typert 图,而 Client 入口导入生成的 `/remote` 声明;`session-log-export` 则让 Node archive 生产代码不进入浏览器 controller;`liveshare-protocol` 把 Node 线路编解码放在 Host leaf、浏览器查看页载体放在 Client leaf。每个拆分包根 `tsconfig.json` 因此只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf;该门禁按「两个 leaf 配置同时存在」自动发现拆分包,所以新拆分的包会自动纳入管辖。[`api-remotes` README](../packages/api/remotes/README.zh.md) 与 [`session-log-export` README](../packages/session-query/session-log-export/README.zh.md)分别说明其拆分。 `pnpm run build:artifacts` 产出全部产物,只检查并编译各 compiler face 的包 reference。Client 编译等待 Host 和 Desktop 打包完成:拆分 compiler face 会重写这些打包步骤读取的共享 `lib/types` 文件。Client 打包在 Client 编译后执行。产物步骤顺序为: diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 014495e428..47056513f2 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -11,13 +11,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | @@ -26,7 +26,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | +| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:53`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -60,12 +60,12 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:407`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:414`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | -| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | +| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | @@ -80,9 +80,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` | -| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:42`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | +| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | | `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | | `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - | @@ -105,7 +105,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `internal/service` | - | `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | -| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | +| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 6a3aae2972..dc555041ff 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -13,13 +13,13 @@ | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | @@ -28,7 +28,7 @@ | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | +| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:53`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -62,12 +62,12 @@ | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:407`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:414`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | -| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | +| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | @@ -82,9 +82,9 @@ | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` | -| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:42`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | +| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | | `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | | `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - | @@ -107,7 +107,7 @@ | `internal/service` | - | `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | -| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | +| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/experimental-persistence-catalog.i18n.yaml b/docs/experimental-persistence-catalog.i18n.yaml index a65825ab71..7dd24c2aa4 100644 --- a/docs/experimental-persistence-catalog.i18n.yaml +++ b/docs/experimental-persistence-catalog.i18n.yaml @@ -6,5 +6,5 @@ en: a11f106978c9dce7 zh: a11f106978c9dce7 /experimental-plugin-record-catalog: - en: 84dbc9c06ab92877 - zh: b82ef2a46292ab0e + en: cb931db3b4bcbe33 + zh: aca9cfecfdee34c0 diff --git a/docs/experimental-persistence-catalog.md b/docs/experimental-persistence-catalog.md index 0868849c51..b6d3a73de4 100644 --- a/docs/experimental-persistence-catalog.md +++ b/docs/experimental-persistence-catalog.md @@ -13,5 +13,6 @@ Shared Session event declarations and resolved schemas appear in the [Session pe | Record | Owner | Purpose | Payload and source | |---|---|---|---| +| `plugin:action-cards/decision` | `@deepseek-ai/dsh-action-cards` | Dismissals and kind-defined progress of action cards. | ActionCardDecision · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) | | `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | TranslationRequestRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | | `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | TranslationResultRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | diff --git a/docs/experimental-persistence-catalog.zh.md b/docs/experimental-persistence-catalog.zh.md index 32cb1d0533..c32cdcabd0 100644 --- a/docs/experimental-persistence-catalog.zh.md +++ b/docs/experimental-persistence-catalog.zh.md @@ -13,5 +13,6 @@ | 记录 | 所属包 | 用途 | 载荷与源码 | |---|---|---|---| +| `plugin:action-cards/decision` | `@deepseek-ai/dsh-action-cards` | Dismissals and kind-defined progress of action cards. | ActionCardDecision · [`packages/experimental/action-cards/src/types.ts`](../packages/experimental/action-cards/src/types.ts) | | `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | TranslationRequestRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | | `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | TranslationResultRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | diff --git a/docs/module-graph.md b/docs/module-graph.md index 6dea2c0abb..f17471eb34 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -17,6 +17,7 @@ flowchart TD pkg_launch_environment["launch-environment"] pkg_lazy_require["lazy-require"] pkg_native_command["native-command"] + pkg_official_bundles["official-bundles"] pkg_output_retention["output-retention"] pkg_package_manifest["package-manifest"] pkg_plugin_detail["plugin-detail"] @@ -253,6 +254,11 @@ flowchart TD pkg_office_to_pdf["office-to-pdf"] end subgraph group_experimental["packages/experimental"] + pkg_action_cards["action-cards"] + pkg_client_ui_action_cards["client-ui-action-cards"] + pkg_client_ui_creation_recommendations["client-ui-creation-recommendations"] + pkg_client_ui_plugin_recommendations["client-ui-plugin-recommendations"] + pkg_creation_recommendations["creation-recommendations"] pkg_cua["cua"] pkg_cua_bundle["cua-bundle"] pkg_experimental_agent_team["experimental-agent-team"] @@ -275,6 +281,7 @@ flowchart TD pkg_experimental_cua_macos["experimental-cua-macos"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] + pkg_experimental_liveshare_protocol["experimental-liveshare-protocol"] pkg_experimental_plugin_market_bundle["experimental-plugin-market-bundle"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] pkg_experimental_ralph_bundle["experimental-ralph-bundle"] @@ -296,6 +303,8 @@ flowchart TD pkg_hook_protocol["hook-protocol"] pkg_hooks_claude_code["hooks-claude-code"] pkg_hooks_codex["hooks-codex"] + pkg_personalized_recommendations["personalized-recommendations"] + pkg_plugin_recommendations["plugin-recommendations"] pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] pkg_skill_badge["skill-badge"] pkg_tool_ralph["tool-ralph"] @@ -514,6 +523,9 @@ flowchart TD pkg_experimental_webworker_runtime --> pkg_client_connection pkg_experimental_webworker_runtime --> pkg_client_modules pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_plugin_recommendations --> pkg_llm + pkg_plugin_recommendations --> pkg_skill + pkg_plugin_recommendations --> pkg_typert_protocol pkg_skill_badge --> pkg_skill pkg_host_directory_picker_auto --> pkg_client_connection pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse @@ -685,6 +697,7 @@ flowchart TD pkg_user_approval --> pkg_llm pkg_user_approval --> pkg_scope pkg_user_approval --> pkg_session + pkg_user_approval --> pkg_session_projection pkg_user_approval --> pkg_system_prompt pkg_user_questions --> pkg_agent pkg_user_questions --> pkg_llm @@ -728,7 +741,6 @@ flowchart TD pkg_llm_deepseek_account --> pkg_launch_environment pkg_llm_deepseek_account --> pkg_llm pkg_llm_deepseek_account --> pkg_llm_deepseek - pkg_llm_deepseek_account --> pkg_timeout pkg_tools --> pkg_agent pkg_tools --> pkg_llm pkg_tools --> pkg_scope @@ -923,6 +935,14 @@ flowchart TD pkg_workspace_changes --> pkg_session pkg_workspace_changes --> pkg_subprocess pkg_workspace_changes --> pkg_tools + pkg_action_cards --> pkg_llm + pkg_action_cards --> pkg_session + pkg_action_cards --> pkg_tools + pkg_action_cards --> pkg_typert_protocol + pkg_creation_recommendations --> pkg_llm + pkg_creation_recommendations --> pkg_session + pkg_creation_recommendations --> pkg_skill + pkg_creation_recommendations --> pkg_tools pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt @@ -1466,6 +1486,7 @@ flowchart TD | [`launch-environment`](../packages/util/launch-environment) | `util` | — | | [`lazy-require`](../packages/util/lazy-require) | `util` | — | | [`native-command`](../packages/util/native-command) | `util` | — | +| [`official-bundles`](../packages/util/official-bundles) | `util` | — | | [`output-retention`](../packages/util/output-retention) | `util` | — | | [`package-manifest`](../packages/util/package-manifest) | `util` | — | | [`plugin-detail`](../packages/util/plugin-detail) | `util` | — | @@ -1558,6 +1579,9 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`credentials`](../packages/credentials/credentials) | `credentials` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | +| [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — | +| [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — | +| [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — | | [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | | [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | @@ -1567,6 +1591,7 @@ flowchart TD | [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | — | | [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — | +| [`experimental-liveshare-protocol`](../packages/experimental/liveshare-protocol) | `experimental` | — | | [`experimental-plugin-market-bundle`](../packages/experimental/plugin-market-bundle) | `experimental` | — | | [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — | | [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — | @@ -1575,6 +1600,7 @@ flowchart TD | [`experimental-terminal-bundle`](../packages/experimental/terminal-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | +| [`personalized-recommendations`](../packages/experimental/personalized-recommendations) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | — | | [`host-directory-picker`](../packages/host/directory-picker) | `host` | — | @@ -1626,6 +1652,7 @@ flowchart TD | [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) | | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) | | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | +| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) | | [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) | | [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) | | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) | @@ -1679,7 +1706,7 @@ flowchart TD | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) | | [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`util-values`](../packages/util/values) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | +| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) | | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) | | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | @@ -1689,7 +1716,7 @@ flowchart TD | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | @@ -1728,6 +1755,8 @@ flowchart TD | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | +| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | +| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 57fecbbf33..baf721dc22 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -19,6 +19,7 @@ flowchart TD pkg_launch_environment["launch-environment"] pkg_lazy_require["lazy-require"] pkg_native_command["native-command"] + pkg_official_bundles["official-bundles"] pkg_output_retention["output-retention"] pkg_package_manifest["package-manifest"] pkg_plugin_detail["plugin-detail"] @@ -255,6 +256,11 @@ flowchart TD pkg_office_to_pdf["office-to-pdf"] end subgraph group_experimental["packages/experimental"] + pkg_action_cards["action-cards"] + pkg_client_ui_action_cards["client-ui-action-cards"] + pkg_client_ui_creation_recommendations["client-ui-creation-recommendations"] + pkg_client_ui_plugin_recommendations["client-ui-plugin-recommendations"] + pkg_creation_recommendations["creation-recommendations"] pkg_cua["cua"] pkg_cua_bundle["cua-bundle"] pkg_experimental_agent_team["experimental-agent-team"] @@ -277,6 +283,7 @@ flowchart TD pkg_experimental_cua_macos["experimental-cua-macos"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] + pkg_experimental_liveshare_protocol["experimental-liveshare-protocol"] pkg_experimental_plugin_market_bundle["experimental-plugin-market-bundle"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] pkg_experimental_ralph_bundle["experimental-ralph-bundle"] @@ -298,6 +305,8 @@ flowchart TD pkg_hook_protocol["hook-protocol"] pkg_hooks_claude_code["hooks-claude-code"] pkg_hooks_codex["hooks-codex"] + pkg_personalized_recommendations["personalized-recommendations"] + pkg_plugin_recommendations["plugin-recommendations"] pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] pkg_skill_badge["skill-badge"] pkg_tool_ralph["tool-ralph"] @@ -516,6 +525,9 @@ flowchart TD pkg_experimental_webworker_runtime --> pkg_client_connection pkg_experimental_webworker_runtime --> pkg_client_modules pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_plugin_recommendations --> pkg_llm + pkg_plugin_recommendations --> pkg_skill + pkg_plugin_recommendations --> pkg_typert_protocol pkg_skill_badge --> pkg_skill pkg_host_directory_picker_auto --> pkg_client_connection pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse @@ -687,6 +699,7 @@ flowchart TD pkg_user_approval --> pkg_llm pkg_user_approval --> pkg_scope pkg_user_approval --> pkg_session + pkg_user_approval --> pkg_session_projection pkg_user_approval --> pkg_system_prompt pkg_user_questions --> pkg_agent pkg_user_questions --> pkg_llm @@ -730,7 +743,6 @@ flowchart TD pkg_llm_deepseek_account --> pkg_launch_environment pkg_llm_deepseek_account --> pkg_llm pkg_llm_deepseek_account --> pkg_llm_deepseek - pkg_llm_deepseek_account --> pkg_timeout pkg_tools --> pkg_agent pkg_tools --> pkg_llm pkg_tools --> pkg_scope @@ -925,6 +937,14 @@ flowchart TD pkg_workspace_changes --> pkg_session pkg_workspace_changes --> pkg_subprocess pkg_workspace_changes --> pkg_tools + pkg_action_cards --> pkg_llm + pkg_action_cards --> pkg_session + pkg_action_cards --> pkg_tools + pkg_action_cards --> pkg_typert_protocol + pkg_creation_recommendations --> pkg_llm + pkg_creation_recommendations --> pkg_session + pkg_creation_recommendations --> pkg_skill + pkg_creation_recommendations --> pkg_tools pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt @@ -1468,6 +1488,7 @@ flowchart TD | [`launch-environment`](../packages/util/launch-environment) | `util` | — | | [`lazy-require`](../packages/util/lazy-require) | `util` | — | | [`native-command`](../packages/util/native-command) | `util` | — | +| [`official-bundles`](../packages/util/official-bundles) | `util` | — | | [`output-retention`](../packages/util/output-retention) | `util` | — | | [`package-manifest`](../packages/util/package-manifest) | `util` | — | | [`plugin-detail`](../packages/util/plugin-detail) | `util` | — | @@ -1560,6 +1581,9 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`credentials`](../packages/credentials/credentials) | `credentials` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | +| [`client-ui-action-cards`](../packages/experimental/client-ui-action-cards) | `experimental` | — | +| [`client-ui-creation-recommendations`](../packages/experimental/client-ui-creation-recommendations) | `experimental` | — | +| [`client-ui-plugin-recommendations`](../packages/experimental/client-ui-plugin-recommendations) | `experimental` | — | | [`cua-bundle`](../packages/experimental/cua-bundle) | `experimental` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | | [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | @@ -1569,6 +1593,7 @@ flowchart TD | [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | — | | [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — | +| [`experimental-liveshare-protocol`](../packages/experimental/liveshare-protocol) | `experimental` | — | | [`experimental-plugin-market-bundle`](../packages/experimental/plugin-market-bundle) | `experimental` | — | | [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — | | [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — | @@ -1577,6 +1602,7 @@ flowchart TD | [`experimental-terminal-bundle`](../packages/experimental/terminal-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | +| [`personalized-recommendations`](../packages/experimental/personalized-recommendations) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | — | | [`host-directory-picker`](../packages/host/directory-picker) | `host` | — | @@ -1628,6 +1654,7 @@ flowchart TD | [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) | | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) | | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | +| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) | | [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) | | [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) | | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) | @@ -1681,7 +1708,7 @@ flowchart TD | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) | | [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`util-values`](../packages/util/values) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | +| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) | | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) | | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | @@ -1691,7 +1718,7 @@ flowchart TD | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | @@ -1730,6 +1757,8 @@ flowchart TD | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | +| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | +| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 291a298c66..0fecbb8961 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -9,11 +9,11 @@ en: 1387c4505dde2faf zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: bcff48893d904af8 - zh: f82e75c91c39adb1 + en: 3e24343c4745793d + zh: 90f808fd6b79c5a3 /session-persistence-event-catalog/event-envelope: - en: 981db6b1928eda50 - zh: d77bc66cf678099d + en: 56414ffd00cdc32a + zh: 6dc11eb013e95e41 /session-persistence-event-catalog/events: en: f4776ba3d0fd53ce zh: e86769f50a69055a @@ -39,17 +39,17 @@ en: 1e65aa4c24a595ee zh: fe7f527c5e45d6a6 /session-persistence-event-catalog/events/approval/approval-policy-log-only: - en: d520080fbb669b54 - zh: a4459a534c1c96c5 + en: 13ecb3c4d92a39e2 + zh: 98ca36fea1b799ec /session-persistence-event-catalog/events/assistant: en: 44426b6cd0dc01cf zh: 44426b6cd0dc01cf /session-persistence-event-catalog/events/assistant/assistant-attempt-log-only: - en: 4e684124f71bd73b - zh: 445ad50a3a4fb635 + en: f330b2521ca12383 + zh: cdc6912a9244677d /session-persistence-event-catalog/events/assistant/assistant-message-surface: - en: 87d9bb62e2aea4b9 - zh: 68ef3b05d4aad7d3 + en: 95916897e677eee6 + zh: e1ea8b17e5af9f35 /session-persistence-event-catalog/events/command: en: 2cecb5b0ee955732 zh: 2cecb5b0ee955732 @@ -84,8 +84,8 @@ en: 3344204b1858edb1 zh: 3344204b1858edb1 /session-persistence-event-catalog/events/developer/developer-message-surface: - en: 927f25c5dbd69c82 - zh: 9c0331f6e61d7e2b + en: fd716cfc669a05a0 + zh: 8693a334958ab3cf /session-persistence-event-catalog/events/feedback: en: f50dbac6867fa64d zh: f50dbac6867fa64d @@ -159,11 +159,11 @@ en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 /session-persistence-event-catalog/events/request/request-context-log-only: - en: 9167d83823675b3e - zh: dfe9095419cf76f4 + en: 22abf9a3f69d498f + zh: 94055a136b641207 /session-persistence-event-catalog/events/request/request-header-log-only: - en: 6d9e26081d1e3aec - zh: 36d30d0bff197d1e + en: 6c506b420662b60e + zh: 823513802c2cc394 /session-persistence-event-catalog/events/sandbox: en: ec841ab8439335b0 zh: ec841ab8439335b0 @@ -174,14 +174,14 @@ en: e5b55405bd2b69f5 zh: e5b55405bd2b69f5 /session-persistence-event-catalog/events/schedule/schedule-change-log-only: - en: 162584304e006c72 - zh: d8a6bacf60777aa9 + en: e73c202fb99159b6 + zh: b1f43bbd116f83da /session-persistence-event-catalog/events/session: en: a1ac8f2660a4f61e zh: a1ac8f2660a4f61e /session-persistence-event-catalog/events/session/session-end-seed-log-only: - en: f285c670d1bf0e71 - zh: 14ec5689a5943156 + en: 20645a60e921f012 + zh: 7141cafe3a0e71d7 /session-persistence-event-catalog/events/session/session-title-log-only: en: 6c6a27578dff5828 zh: 9a5add2bc0df2d0a @@ -198,11 +198,11 @@ en: bd0dcbbc352b771a zh: bd0dcbbc352b771a /session-persistence-event-catalog/events/step/step-end-log-only: - en: 24bc99ca25809609 - zh: 420384e753feba64 + en: b30d8571ba611386 + zh: 42e45b49142c306c /session-persistence-event-catalog/events/step/step-start-log-only: - en: 269a493d00348184 - zh: 80feab63a0431c60 + en: 161a661514649037 + zh: 5a2e3ec5af378746 /session-persistence-event-catalog/events/subagent: en: 9630752e97ddbfbc zh: 9630752e97ddbfbc @@ -219,8 +219,8 @@ en: 8cac41d2fb575975 zh: 8cac41d2fb575975 /session-persistence-event-catalog/events/system/system-message-surface: - en: 23aedcadb0c95a51 - zh: f58e5195075ddb21 + en: f08085060f967358 + zh: 4d77a8636d9bfef5 /session-persistence-event-catalog/events/team: en: a640800a38b3c874 zh: a640800a38b3c874 @@ -246,8 +246,8 @@ en: 28a24430609494b2 zh: 28a24430609494b2 /session-persistence-event-catalog/events/tool/tool-call-log-only: - en: b5dfd3b4c54281c3 - zh: 3ac01ceab4a36d06 + en: 2edbe249bcd484fc + zh: fa838ad8325507b4 /session-persistence-event-catalog/events/tool/tool-ptc-dispatch-log-only: en: a62b38c00afed655 zh: 793bef93e856eafa @@ -255,8 +255,8 @@ en: da841b1ed3b22c87 zh: 4c9a4d4bc5bd76b6 /session-persistence-event-catalog/events/tool/tool-result-surface: - en: 722a3bf051b384eb - zh: 01e1258be37a82ce + en: e5fdf9592ffa86b4 + zh: 7455727a8f208a1c /session-persistence-event-catalog/events/tool-workflow: en: 1eac7deac6af15f3 zh: 1eac7deac6af15f3 @@ -276,17 +276,17 @@ en: d7e8c999a0851a0c zh: d7e8c999a0851a0c /session-persistence-event-catalog/events/turn/turn-end-log-only: - en: 38d2280f449d3112 - zh: 0f7f05475ea514a3 + en: 7d5bacee7392ed18 + zh: 65b8eb8ab9b10ccd /session-persistence-event-catalog/events/turn/turn-start-log-only: - en: 853b7f23d092c48a - zh: 53e312793b609198 + en: 1b5d3369f6178575 + zh: 34550bedca73336a /session-persistence-event-catalog/events/user: en: d864a88c03e8971c zh: d864a88c03e8971c /session-persistence-event-catalog/events/user/user-message-surface: - en: ba681b5ec685d6e8 - zh: e4b14b585d2b5804 + en: 836d2e0de5f33816 + zh: 67055e83e820f5f5 /session-persistence-event-catalog/events/web: en: d278748103cf1640 zh: d278748103cf1640 @@ -309,8 +309,11 @@ en: 3493ecf0993e5397 zh: ec95688f593a4ed4 /session-persistence-event-catalog/resolved-persistence-types/aborted: - en: d716fb9ee9baa564 - zh: d716fb9ee9baa564 + en: 40049fd36471e4b2 + zh: 40049fd36471e4b2 +/session-persistence-event-catalog/resolved-persistence-types/action-cards: + en: 629f821b50a38bc6 + zh: 629f821b50a38bc6 /session-persistence-event-catalog/resolved-persistence-types/active: en: 6b343e390ae7d7a7 zh: 6b343e390ae7d7a7 @@ -969,8 +972,8 @@ en: bb7b3652609ac651 zh: bdde4e652413c5eb /session-persistence-event-catalog/resolved-persistence-types/approvalpolicy: - en: 670f484e05d50fa7 - zh: 7c107f338ea6cf81 + en: c78bdd273133a02f + zh: 2cbbc4d9c7e0a726 /session-persistence-event-catalog/resolved-persistence-types/assistantmessage: en: 2a8bc13a30855b26 zh: 87f1f5fd2882a411 @@ -981,8 +984,8 @@ en: e393deed01de3665 zh: 0fa98ad93c22ce79 /session-persistence-event-catalog/resolved-persistence-types/commandsource: - en: 53feeeb70547e7df - zh: 99d9e901d9b4afe5 + en: 459a6f8c8800d3e5 + zh: f1b2626c08c62b4d /session-persistence-event-catalog/resolved-persistence-types/compactioncheckpointsource: en: 87f2aaf2b23a5249 zh: 3d6c5ed6603bee31 @@ -1005,14 +1008,14 @@ en: a6a2e38fdd6d127a zh: 1fe82356d89f5769 /session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest: - en: 128c0ecc1897eb68 - zh: e4c67c92752bcc2a + en: 0e286bae74e78505 + zh: b1c9da5aa56d5a81 /session-persistence-event-catalog/resolved-persistence-types/developermessage: - en: 997b5db27df00446 - zh: 07e5197ddd7bd59b + en: 3d8a393be23efcde + zh: f95f337d816dbbe7 /session-persistence-event-catalog/resolved-persistence-types/epochheader: - en: c804cb9e2e31c5ec - zh: 3ed7ac59cd1441b7 + en: 3bdc8ecd0bdf0c9c + zh: 0fe5b75c174ebab4 /session-persistence-event-catalog/resolved-persistence-types/everyscheduledispatchchange: en: 54c487394ce0db1d zh: 48d7c9affbe841cb @@ -1119,11 +1122,11 @@ en: 6dbe099c62f7c226 zh: 6fd788f63c038c91 /session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata: - en: c2d9edd8d9f492b7 - zh: f59c687ac691f7ce + en: 3d2f7c88259b6446 + zh: 58bee68c335d6d4f /session-persistence-event-catalog/resolved-persistence-types/message: - en: 594707e4894819a8 - zh: 268c4bfd5c4c28f2 + en: 76d13d39fe22f7bd + zh: b1e19cfe90a6d6b6 /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete: en: c582953270d4ca44 zh: 966ccfe2e7660a2a @@ -1134,14 +1137,14 @@ en: f58575a1fc96df42 zh: 9f0c6ed9033e5c9d /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating: - en: 674296d54c1914d0 - zh: 3d71ab319514be25 + en: 887549d6401dd116 + zh: 30198672b67dac6b /session-persistence-event-catalog/resolved-persistence-types/messagesource: - en: 3fab179fe8d58c31 - zh: 5328c9161ff908a9 + en: 2f21414632f8d184 + zh: d85f66e001fd814c /session-persistence-event-catalog/resolved-persistence-types/message~2: - en: 8bd12543fcdcda5f - zh: d4c6b17083948856 + en: 87e95476b4985e5e + zh: 0e0322e005b3e995 /session-persistence-event-catalog/resolved-persistence-types/modelmessagesource: en: 5f9154ca34235ab9 zh: a833105910ebfa82 @@ -1179,11 +1182,11 @@ en: 488050ba0b6438b4 zh: 2bf607b5ad51e39d /session-persistence-event-catalog/resolved-persistence-types/requestcontext: - en: 00058b6c2220cdcb - zh: a3426a8c69123e2e + en: 544039047a28b6bb + zh: c691f0bb1e23db08 /session-persistence-event-catalog/resolved-persistence-types/requestheaderreason: - en: 8e563a11c123255f - zh: ebf891f6a3cb93d3 + en: 1f691c223f54ab2c + zh: f88fef3ee9745c91 /session-persistence-event-catalog/resolved-persistence-types/sandboxmode: en: c9fa5343ea99733b zh: d6de41351c57a73f @@ -1203,11 +1206,11 @@ en: 9a54c4478ad3b620 zh: 02c4c1fd7e252770 /session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata: - en: bf7325029c9338d6 - zh: e5f98271511e5318 + en: c0104327b481ae54 + zh: b4624d112bee37f1 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 89dcb9a9b957c126 - zh: 132aeb7dfd435290 + en: 2e40681d575da503 + zh: 57cbccb16548e2da /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1236,8 +1239,8 @@ en: d34acab659e74957 zh: 25cabc6c9fb44a7e /session-persistence-event-catalog/resolved-persistence-types/surfaceop: - en: a62be7c2cb8a4af7 - zh: 616fd1ac4bd3995a + en: 737806b6ec54263d + zh: b4e44293950ddf8c /session-persistence-event-catalog/resolved-persistence-types/systemmessage: en: 94876079d6696df9 zh: d25c7ad9f5054eb7 @@ -1311,17 +1314,17 @@ en: 54090e169899c573 zh: 530ea8d0d0f8cf68 /session-persistence-event-catalog/resolved-persistence-types/turnendcancelcause: - en: 4dc89c072fe5d0d1 - zh: 4ed2c9e9e8b4f7e2 + en: 778debd61e0284a2 + zh: 2cc82ad9e80f18b1 /session-persistence-event-catalog/resolved-persistence-types/turnendreason: - en: 344a7207a4199a32 - zh: d500af39e83289cf + en: 933770adc9ee2780 + zh: 8d8462c33ed6029e /session-persistence-event-catalog/resolved-persistence-types/usermessage: - en: bd3aedd570ded4d2 - zh: f020d0816d3a6c1a + en: 79b3ac6c731859aa + zh: 81058fe7edf61bdb /session-persistence-event-catalog/resolved-persistence-types/usermessage~2: - en: b99d66ffe277f0bc - zh: eacc9c1f62ae66c1 + en: 8e699c0217638bbe + zh: a2047db24c25217b /session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome: en: d0a3f9aab18c3de5 zh: 76ac69a835ca4e29 @@ -1410,8 +1413,8 @@ en: a117573a5d58cb3c zh: d5ad59e17a3a815d /session-persistence-event-catalog/resolved-persistence-types/arguments-callid-name-step: - en: 036101ee99efbf72 - zh: a324d7f9f671bab0 + en: 1cd530a4f45fcb4f + zh: 0bf7c0f53d10a5ec /session-persistence-event-catalog/resolved-persistence-types/callid-files-turn: en: 5f4d3f66ed86a14d zh: 7c729360fa1279e6 @@ -1443,8 +1446,8 @@ en: a1f188c9058cf621 zh: eb43c0f045fb9afb /session-persistence-event-catalog/resolved-persistence-types/code-name-reason: - en: 17649d71411cb827 - zh: abb3138d38c76459 + en: 25df2af19bc9c46d + zh: ebb6ecbeee25c9be /session-persistence-event-catalog/resolved-persistence-types/commandid-kind-sourceeventseq-text: en: 87b03e1c73d2a326 zh: d7b9f06863cad891 @@ -1485,14 +1488,14 @@ en: 4b7a8793663d3fef zh: 249459c825d330c3 /session-persistence-event-catalog/resolved-persistence-types/endseq-op-startseq: - en: 1a533265eb56a8b8 - zh: 57480f4b53713570 + en: 766772dfbbb2e164 + zh: 09973d26e68004cb /session-persistence-event-catalog/resolved-persistence-types/error-kind-error: - en: 8d0448ef77e0da08 - zh: 25770329c9db129a + en: e0833e1b1cdb29d0 + zh: 6244880c79509016 /session-persistence-event-catalog/resolved-persistence-types/error-message-meta-step: - en: 9ffa836bf88fd7b4 - zh: 7ca8bc7ebefeadb8 + en: d019b2909673d2a7 + zh: cc16e9c055ea603d /session-persistence-event-catalog/resolved-persistence-types/failure-kind-aborted: en: 349cd90f2598fd12 zh: 1eda02d520ad5d76 @@ -1500,11 +1503,11 @@ en: 51af619ed8532a43 zh: a5ad878c63d0e046 /session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries: - en: 0b1ca5c765d716d1 - zh: dd939095c250c4eb + en: e4a915fcd4cca68b + zh: 04a70157c20a2f80 /session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn: - en: 3d16abb7b9581429 - zh: dc959157864261d8 + en: 0f367699daf0d4de + zh: 58b980af0f365374 /session-persistence-event-catalog/resolved-persistence-types/height-width: en: a5df50710083ac25 zh: aad3ccf6f6de70e4 @@ -1521,29 +1524,50 @@ en: 41f73c58a77dfa28 zh: af77f4aee3cb4a40 /session-persistence-event-catalog/resolved-persistence-types/inherited: - en: 6ce6ddac6d815805 - zh: f7196176e902c965 + en: 0e761849881f1b90 + zh: 62f72209fb941192 /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: 346d884a7a74e48d - zh: 646ac1021f9ca920 + en: 1750502af87d698c + zh: 382a3356f0a61546 /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: - en: 89fb3f919d181c86 - zh: 4380624236500687 + en: 8e86cd838e3e4a49 + zh: f5fec2a8adf1f3cc /session-persistence-event-catalog/resolved-persistence-types/kind-aborted-reason: - en: 48424cbed3fbc269 - zh: 2ce88c04ad002104 + en: d2b0ef9a4e2b9be5 + zh: 44ad13fbbf836ac7 +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards: + en: 8d057f0bd0b88fc6 + zh: 500d1e3d54e6d7c4 +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-catalog: + en: 14e9dd01c49f91e0 + zh: 4a7e4e4aeeb15982 +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-instructions: + en: f59ec0c37dc8f9b6 + zh: debc915ed98581fb +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-notice: + en: ef0a456530698cca + zh: 23f2ace5345eb847 +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-recall: + en: 0b607f9577e3a3c5 + zh: 237ed72b7a4154df +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-relay: + en: 712bc9ad7c51a8fe + zh: f230dc9c09bdbf3c +/session-persistence-event-catalog/resolved-persistence-types/kind-action-cards-form-snapshot: + en: 4e030b06ef900819 + zh: 27532bbabe118206 /session-persistence-event-catalog/resolved-persistence-types/kind-blocked: - en: 2fad818b19823c48 - zh: a6b33dee21c3fdc9 + en: 9db831e991ef17ce + zh: 332002f65d191b68 /session-persistence-event-catalog/resolved-persistence-types/kind-completed: - en: e8f76b61a39ebeb0 - zh: e61a304be00b4490 + en: 9affe5d5dac75c57 + zh: 8db62dcb41e804c8 /session-persistence-event-catalog/resolved-persistence-types/kind-cordis-host-runner: en: fb7b8207a6ef3b81 zh: 9d6f112181d90701 /session-persistence-event-catalog/resolved-persistence-types/kind-disposed: - en: e3c9527803bc6de3 - zh: 375c82ce2ac5282b + en: 9a3ce668c21d6c90 + zh: f661c7ce62c44a60 /session-persistence-event-catalog/resolved-persistence-types/kind-dsh-session-title-llm: en: 428529e5dedc761d zh: 3ca0b54c9c521ea0 @@ -1569,11 +1593,11 @@ en: 78e34e6d0d70339e zh: 072127e40cfb9686 /session-persistence-event-catalog/resolved-persistence-types/kind-forked: - en: e2588043ee1a8d50 - zh: d87a5bff12b9190f + en: 4663f6c1a2634822 + zh: 3a2e4f61195759a0 /session-persistence-event-catalog/resolved-persistence-types/kind-hook: - en: 78278a869370695f - zh: 6cdc47f1876f3b23 + en: 669685ab52bb7b68 + zh: 94325f99ba96dec5 /session-persistence-event-catalog/resolved-persistence-types/kind-hooks-claude-code: en: 5eb383a67f11ac4d zh: 1943747cb4995dbc @@ -1617,14 +1641,14 @@ en: a8a338be3b91008f zh: c7632c42c869c2f6 /session-persistence-event-catalog/resolved-persistence-types/kind-interrupted: - en: 452b4a96d072a9b9 - zh: d63c8ab84bd37dbb + en: 8f5585c1e90d6ca3 + zh: 78329cf995882344 /session-persistence-event-catalog/resolved-persistence-types/kind-legacy: - en: c6f68741f8631d4f - zh: 090b58f2b86c02d0 + en: 673110c18ee48238 + zh: e8dd81d68e1a4ee0 /session-persistence-event-catalog/resolved-persistence-types/kind-max-tokens: - en: f85b7a64a6a5f1f7 - zh: 3f4ad5183e1020fd + en: 337ebe760096729a + zh: bd35a401103a3071 /session-persistence-event-catalog/resolved-persistence-types/kind-model-selection: en: a5165486dca91116 zh: b7051ca8167806ef @@ -1647,8 +1671,8 @@ en: 509f9a91aedb2da1 zh: db1071678bfb98d1 /session-persistence-event-catalog/resolved-persistence-types/kind-parent: - en: 7a3ac2238405913d - zh: b249a4d6d0508d40 + en: 5ab43c283829de53 + zh: 5ed4e90922a3a88b /session-persistence-event-catalog/resolved-persistence-types/kind-plan-mode: en: 9758304800b717df zh: dccf14f3a5d0d861 @@ -1872,8 +1896,8 @@ en: 82b0989dad6bbfc0 zh: 0789b63f1d59b2ed /session-persistence-event-catalog/resolved-persistence-types/message-step-turn: - en: 762219ab10a2d9b1 - zh: ce31f3b385595b96 + en: 86fe91fe83eeb08a + zh: 2ebf2f1904829753 /session-persistence-event-catalog/resolved-persistence-types/message-teamid-version: en: f778769acb264dd5 zh: eba9b81d04e9f57b @@ -1884,14 +1908,14 @@ en: 3028b222670ce5ad zh: cae4c9437e560d61 /session-persistence-event-catalog/resolved-persistence-types/policy-source: - en: 00f1d435f82d2753 - zh: c2ab108ad2748a6c + en: 88fd92dff7aa505f + zh: b400712b3ef749aa /session-persistence-event-catalog/resolved-persistence-types/preset: en: 8ebd37343b638773 zh: bf7ffa9a60ca42a8 /session-persistence-event-catalog/resolved-persistence-types/reason-turn: - en: 2d45d5c5e3959d12 - zh: df4d0d7fca4e26bc + en: b8d80b4b1e6441ad + zh: d5c52e1bfe560297 /session-persistence-event-catalog/resolved-persistence-types/role-user: en: 04b34b8ffe7e7764 zh: a792d5371516fd9d @@ -1902,11 +1926,11 @@ en: 7a2ab7b62244f43b zh: 1084e3f8876feebc /session-persistence-event-catalog/resolved-persistence-types/step-stream-turn: - en: d911fd8591d66386 - zh: cba2966a909b9293 + en: 59f11c6d124335cb + zh: 5bbdd6c88612fcff /session-persistence-event-catalog/resolved-persistence-types/step-turn: - en: 4bbf63488cce88c9 - zh: 46b40433a749c133 + en: 8f9e774f89087efa + zh: 72474e7e556e2469 /session-persistence-event-catalog/resolved-persistence-types/targets: en: 5360af614fe753e3 zh: 96427c489cc71de6 @@ -1917,14 +1941,14 @@ en: 8ab873d30e062fd5 zh: a138832c6e299ebb /session-persistence-event-catalog/resolved-persistence-types/turn: - en: ea32b4165dbfb182 - zh: 57f48af06cfa201c + en: 374b8adee8d571fa + zh: b21057efc58e6490 /session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected: - en: c8c731ee23bed85a - zh: f4ffc80f7828701d + en: 6a59f1cd472759e9 + zh: 1db67f4f97207efc /session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced: - en: 62dfe9447efaceb1 - zh: 076454de19b5d7d7 + en: 6c276128e62a5432 + zh: 4ff5e93d255caece /session-persistence-event-catalog/resolved-persistence-types/type-approval-asked: en: 9c77f18b0dad22f0 zh: 8894cf2878fe90db @@ -1968,11 +1992,11 @@ en: ba73fb10b7e2ffc9 zh: 8d5c97e63e5f16da /session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented: - en: a4d9eab2928e9b30 - zh: b36702c866518273 + en: 10d95ce64c285e64 + zh: 0ddb26f1fa16629c /session-persistence-event-catalog/resolved-persistence-types/type-developer-message: - en: a9e6e24b98390923 - zh: 65addae898d8aaec + en: 78d421c83506ebe9 + zh: 2e9399ba2d360627 /session-persistence-event-catalog/resolved-persistence-types/type-disabled: en: 585627a50ece4e5f zh: 157837aee3da0ac3 @@ -2046,11 +2070,11 @@ en: acf7e21e4d21ebff zh: fb83a853634d204a /session-persistence-event-catalog/resolved-persistence-types/type-session-title: - en: b99fbacda0db1abd - zh: 9e75ce6b1d174d48 + en: 26d99fabe31f7fa3 + zh: 0a44b5c8f7f6488c /session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request: - en: 7ba30b98d8e3f653 - zh: ca68b01ab4b6ba3d + en: bbbb345651600abf + zh: e3b29e05fecb136a /session-persistence-event-catalog/resolved-persistence-types/type-step-end: en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 @@ -2127,11 +2151,11 @@ en: 1b045de96172be4b zh: 8a15c6f35b576b37 /session-persistence-event-catalog/resolved-persistence-types/type-usage: - en: 8eec3bb6eee5085b - zh: 04bb3281fd7bd95c + en: ebb801c4444061fb + zh: f2d6531de88ee16e /session-persistence-event-catalog/resolved-persistence-types/type-user-message: - en: d6d9401f19a66ee8 - zh: 569fdddf887660a2 + en: 536f875df13987d0 + zh: 857a381433a4217f /session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request: en: 99c8837344178a47 zh: bf746f5678346596 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index e98ae23f5c..74a7956486 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -23,7 +23,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034) | +| `event:agent/inbox/spliced` | event | `b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -36,7 +36,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c` | [`{ type: "developer/message" }`](#persistence-type-sha256-97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c) | +| `event:developer/message` | event | `5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be` | [`{ type: "developer/message" }`](#persistence-type-sha256-5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -58,7 +58,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056) | +| `event:session/title-llm-request` | event | `efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -80,7 +80,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` | [`{ type: "user/message" }`](#persistence-type-sha256-2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8) | +| `event:user/message` | event | `58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019` | [`{ type: "user/message" }`](#persistence-type-sha256-58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -160,7 +160,7 @@ export type SessionEvent = { }[T] ``` -Sources: [`packages/core/session/src/types.ts:433`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:441`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:464`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:495`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:501`](../packages/core/session/src/types.ts) ## Events @@ -273,7 +273,7 @@ Source: [`packages/interaction/user-approval/src/types.ts:55`](../packages/inter } ``` -Source: [`packages/interaction/user-approval/src/index.ts:40`](../packages/interaction/user-approval/src/index.ts) +Source: [`packages/interaction/user-approval/src/index.ts:41`](../packages/interaction/user-approval/src/index.ts) ### `assistant/*` @@ -290,7 +290,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:40`](../packages/inter 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] } ``` -Source: [`packages/core/session/src/types.ts:357`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) @@ -320,7 +320,7 @@ Source: [`packages/core/session/src/types.ts:357`](../packages/core/session/src/ Types: [TokenUsage](subsystems/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:349`](../packages/core/session/src/types.ts) ### `command/*` @@ -508,7 +508,7 @@ Source: [`packages/deliverables/tool-present/src/types.ts:15`](../packages/deliv } ``` -Source: [`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) ### `feedback/*` @@ -753,7 +753,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s 'request/context': RequestContext ``` -Source: [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:410`](../packages/core/session/src/types.ts) @@ -772,7 +772,7 @@ Source: [`packages/core/session/src/types.ts:404`](../packages/core/session/src/ } ``` -Source: [`packages/core/session/src/types.ts:392`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) ### `sandbox/*` @@ -813,7 +813,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s Types: [ScheduleChange](subsystems/schedule.md) -Source: [`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts) +Source: [`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts) ### `session/*` @@ -849,7 +849,7 @@ Source: [`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/sch 'session/end-seed': { inherited?: true } ``` -Source: [`packages/core/session/src/types.ts:429`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:435`](../packages/core/session/src/types.ts) @@ -911,7 +911,7 @@ Source: [`packages/session/session-log-deepseek/src/types.ts:81`](../packages/se 'step/end': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) @@ -922,7 +922,7 @@ Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/ 'step/start': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) ### `subagent/*` @@ -987,8 +987,10 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../p * The rendered system prompt on the model-visible surface. The loop appends * the first one as surface node 0 before the step's first `user/message`. * A prepared in-history route can append nonempty changes in a continuing - * series. An incapable route or new series normalizes text to the first system - * node. Normalization empties nonempty later nodes, then rewrites the head if + * series. An incapable route, or a new series whose effective text differs, + * normalizes text to the first system node; a series start whose latest + * surviving node already holds the rendering leaves it in place. + * Normalization empties nonempty later nodes, then rewrites the head if * needed, through logged per-node replacements. An empty rendering always * clears all active system nodes, leaving no older instructions model-visible. * Empty later nodes are dormant and project to no message; an empty head with @@ -998,7 +1000,7 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../p 'system/message': { turn: number; step: number; message: SystemMessage } ``` -Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) ### `team/*` @@ -1091,7 +1093,7 @@ Source: [`packages/todo/tool-todo/src/types.ts:33`](../packages/todo/tool-todo/s Types: [ToolCallId](subsystems/core.md) -Source: [`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:369`](../packages/core/session/src/types.ts) @@ -1172,7 +1174,7 @@ Source: [`packages/core/tool-executor-ptc/src/types.ts:45`](../packages/core/too } ``` -Source: [`packages/core/session/src/types.ts:377`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) ### `tool-workflow/*` @@ -1252,7 +1254,7 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow Types: [TurnEndReason](subsystems/session.md) -Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) @@ -1268,7 +1270,7 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/ 'turn/start': { turn: number } ``` -Source: [`packages/core/session/src/types.ts:290`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts) ### `user/*` @@ -1287,7 +1289,7 @@ Source: [`packages/core/session/src/types.ts:290`](../packages/core/session/src/ 'user/message': UserMessage ``` -Source: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) ### `web/*` @@ -1347,6 +1349,14 @@ SHA-256: `1e0b0b66b6f0c53fc72e52289d06b4708d776f1ca9df4c86a22bff621d403cf0` `"aborted"` + + +### `"action-cards"` + +SHA-256: `05205074b24d8340cfd7995f6cb64779c7f1241490954700115601e264178877` + +`"action-cards"` + ### `"active"` @@ -3159,7 +3169,7 @@ One of: SHA-256: `05d2ae1e0c7f8770f83f9a09047afb39a335d8ac986df3193659aa1e581472cf` -Sources: [`packages/interaction/user-approval/src/index.ts:67`](../packages/interaction/user-approval/src/index.ts) +Sources: [`packages/interaction/user-approval/src/index.ts:68`](../packages/interaction/user-approval/src/index.ts) One of: @@ -3222,7 +3232,7 @@ Array of [`AssistantStreamRecord`](#persistence-type-sha256-33341aac09312300de5a SHA-256: `ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05` -Sources: [`packages/core/session/src/types.ts:191`](../packages/core/session/src/types.ts) · [`packages/interaction/commands/src/types.ts:75`](../packages/interaction/commands/src/types.ts) · [`packages/interaction/commands/src/types.ts:79`](../packages/interaction/commands/src/types.ts) · [`packages/llm/llm/src/message.ts:111`](../packages/llm/llm/src/message.ts) · [`packages/session/session-title/src/types.ts:35`](../packages/session/session-title/src/types.ts) +Sources: [`packages/core/session/src/types.ts:195`](../packages/core/session/src/types.ts) · [`packages/interaction/commands/src/types.ts:75`](../packages/interaction/commands/src/types.ts) · [`packages/interaction/commands/src/types.ts:79`](../packages/interaction/commands/src/types.ts) · [`packages/llm/llm/src/message.ts:111`](../packages/llm/llm/src/message.ts) · [`packages/session/session-title/src/types.ts:35`](../packages/session/session-title/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -3365,7 +3375,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web | `body` | required | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | required | `string` | - + @@ -3373,7 +3383,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web ### `DeveloperMessage` -SHA-256: `0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7` +SHA-256: `7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616` Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3382,11 +3392,11 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"developer"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690) | Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. -Attribution-only additions: `tmux-context`, `user-question-reply`, `working-directory`. +Attribution-only additions: `action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. @@ -3398,7 +3408,7 @@ Attribution-only additions: `tmux-context`, `user-question-reply`, `working-dire SHA-256: `9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7` -Sources: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4081,7 +4091,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr | `step` | required | `number` | | `turn` | required | `number` | - + @@ -4089,17 +4099,17 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `Message` -SHA-256: `040cd65423bd64264d225504f1c572d239eeb1793d0c2856ad8e42bb6e1e148b` +SHA-256: `cda79b839d8cc0c7a7bd09dd067de04925f6e9037080d878b65aaa5a383f117c` Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) One of: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7) -- [`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711) -- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) +- [`DeveloperMessage`](#persistence-type-sha256-7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616) +- [`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) +- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4174,7 +4184,7 @@ One of: - `"negative"` - `"positive"` - + @@ -4182,12 +4192,19 @@ One of: ### `MessageSource` -SHA-256: `5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8` +SHA-256: `7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690` Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) | kind | Form property | Other required fields | Full definition | |---|---|---|---| +| `action-cards` | `"catalog"` | `through`: `number` | [`{ kind: "action-cards", form: "catalog" }`](#persistence-type-sha256-dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a) | +| `action-cards` | `"instructions"` | `through`: `number` | [`{ kind: "action-cards", form: "instructions" }`](#persistence-type-sha256-ba2a1a8f194b1bb21f270713398f6643e206e8ff5f7f26f6d36a88e3f0f0ab10) | +| `action-cards` | `"notice"` | `summary`: `string`; `through`: `number` | [`{ kind: "action-cards", form: "notice" }`](#persistence-type-sha256-e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864) | +| `action-cards` | `"recall"` | `through`: `number` | [`{ kind: "action-cards", form: "recall" }`](#persistence-type-sha256-b613f83572fe2fbf7259c72308cafd008f4fceea897749128a48bcd158f290aa) | +| `action-cards` | `"relay"` | `through`: `number` | [`{ kind: "action-cards", form: "relay" }`](#persistence-type-sha256-45f005e6bbb7645248be792b715a74bc7fdf5a67f5381655d3a75dcc7aa2112e) | +| `action-cards` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262); `through`: `number` | [`{ kind: "action-cards", form: "snapshot" }`](#persistence-type-sha256-3247e566f4340029325b70e4366b56fa9719977f1e39033b769a2ec221ab22a9) | +| `action-cards` | not declared | `through`: `number` | [`{ kind: "action-cards" }`](#persistence-type-sha256-dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0) | | `agent-instructions` | `"instructions"` | `changes`: [`AgentInstructionChange[]`](#persistence-type-sha256-2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee) | [`AgentInstructionSource`](#persistence-type-sha256-6f22a9d02b9a1f1157f3b0e7bdccd7e7b12b085c4efdb709dfc74a465cd4e590) | | `agent-message` | `"relay"` | `senderSessionId`: `string` | [`AgentMessageSource`](#persistence-type-sha256-4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86) | | `compact-checkpoint` | not declared | `compactionId`: `string` | [`CompactionCheckpointSource`](#persistence-type-sha256-31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648) | @@ -4300,13 +4317,13 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | not declared | none | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `a927d444c928ac0efd9dd8b2e7d7d04d711c264cb71578d81e0e2fd143ff669f` +SHA-256: `78b9204976ab9faaf133c3488b73d1dfb6f1a4bb6c0903e3473cd41a5e1e39e1` -Array of [`Message`](#persistence-type-sha256-040cd65423bd64264d225504f1c572d239eeb1793d0c2856ad8e42bb6e1e148b). +Array of [`Message`](#persistence-type-sha256-cda79b839d8cc0c7a7bd09dd067de04925f6e9037080d878b65aaa5a383f117c). @@ -4528,7 +4545,7 @@ Sources: [`packages/llm/llm/src/types.ts:448`](../packages/llm/llm/src/types.ts) SHA-256: `b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081` -Sources: [`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:260`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4547,7 +4564,7 @@ Sources: [`packages/core/session/src/types.ts:256`](../packages/core/session/src SHA-256: `530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6` -Sources: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) One of: @@ -4690,7 +4707,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | required | `string` | - + @@ -4698,7 +4715,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f` +SHA-256: `a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf` Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) @@ -4706,7 +4723,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/sess |---|---|---| | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | required | [`Message[]`](#persistence-type-sha256-a927d444c928ac0efd9dd8b2e7d7d04d711c264cb71578d81e0e2fd143ff669f) | +| `messages` | required | [`Message[]`](#persistence-type-sha256-78b9204976ab9faaf133c3488b73d1dfb6f1a4bb6c0903e3473cd41a5e1e39e1) | | `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | @@ -4885,7 +4902,7 @@ Sources: [`packages/subagent/subagent/src/continuation-messages.ts:29`](../packa SHA-256: `335e242de1fcc17b6ca920fc420d71bec2d76e53e37955c00948b65ab77f05c5` -Sources: [`packages/core/session/src/types.ts:464`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) One of: @@ -5330,7 +5347,7 @@ Sources: [`packages/workflow/tool-workflow/src/types.ts:14`](../packages/workflo SHA-256: `1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0` -Sources: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts) | kind | Form property | Other required fields | Full definition | |---|---|---|---| @@ -5350,7 +5367,7 @@ Sources: [`packages/core/session/src/types.ts:197`](../packages/core/session/src SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` -Sources: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts) | kind | Form property | Other required fields | Full definition | |---|---|---|---| @@ -5362,7 +5379,7 @@ Sources: [`packages/core/session/src/types.ts:234`](../packages/core/session/src | `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5370,7 +5387,7 @@ Sources: [`packages/core/session/src/types.ts:234`](../packages/core/session/src ### `UserMessage` -SHA-256: `2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711` +SHA-256: `b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69` Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5379,19 +5396,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"user"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690) | Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. -Attribution-only additions: `tmux-context`, `user-question-reply`, `working-directory`. +Attribution-only additions: `action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `454a0fa879e38076bdcdbd87ef561cd67d7e43e58a287cf7dbaa7b6cf7847020` +SHA-256: `5d00abf938272f22eb47199e9d40801e8d8ec4c88e2b138b15aa2a5a5bf87520` -Array of [`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711). +Array of [`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69). @@ -5820,7 +5837,7 @@ Sources: [`packages/interaction/commands/src/types.ts:105`](../packages/interact SHA-256: `e4c18e294232c3ba6c9f1999f168263cc55956147cfe4121720899be8e52edd1` -Sources: [`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:369`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5979,7 +5996,7 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735` -Sources: [`packages/core/session/src/types.ts:385`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts) +Sources: [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6199,7 +6216,7 @@ Sources: [`packages/compaction/compaction/src/types.ts:38`](../packages/compacti SHA-256: `bcf0caf62d964b2fcf5404bd5c909cfa9a21c3f3c96e6b7e36d9e33565223825` -Sources: [`packages/core/session/src/types.ts:466`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:472`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6213,7 +6230,7 @@ Sources: [`packages/core/session/src/types.ts:466`](../packages/core/session/src SHA-256: `79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985` -Sources: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:218`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6226,7 +6243,7 @@ Sources: [`packages/core/session/src/types.ts:214`](../packages/core/session/src SHA-256: `5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777` -Sources: [`packages/core/session/src/types.ts:377`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6268,7 +6285,7 @@ Sources: [`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts) SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` -Sources: [`packages/core/session/src/types.ts:392`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6276,18 +6293,18 @@ Sources: [`packages/core/session/src/types.ts:392`](../packages/core/session/src | `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | optional | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `f4ed4717b4c259e933b550a8dcd90dc93628cda8a3c7affcaa53b9c13c1d53eb` +SHA-256: `7404c6123fdaa591474a37979a5a8ae70d1387af7c9f09bfe568b218935a974d` -Sources: [`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `headerSeq` | optional | `number` | -| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7) | +| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616) | | `step` | required | `number` | | `turn` | required | `number` | @@ -6365,23 +6382,23 @@ SHA-256: `e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f` SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` -Sources: [`packages/core/session/src/types.ts:429`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:435`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `inherited` | optional | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42` +SHA-256: `b2a948a5340a9341fa3bab99507c7090c5b0456de72ec28b5b157b8df3f1d640` Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| -| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-454a0fa879e38076bdcdbd87ef561cd67d7e43e58a287cf7dbaa7b6cf7847020) | +| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-5d00abf938272f22eb47199e9d40801e8d8ec4c88e2b138b15aa2a5a5bf87520) | | `outcome` | optional | `"canceled"` | | `removedCount` | optional | `number` | | `start` | required | `number` | @@ -6393,7 +6410,7 @@ Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/typ SHA-256: `1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d` -Sources: [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:349`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6410,20 +6427,105 @@ Sources: [`packages/core/session/src/types.ts:343`](../packages/core/session/src SHA-256: `0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3` -Sources: [`packages/core/session/src/types.ts:205`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:209`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `kind` | required | `"aborted"` | | `reason` | required | [`TurnEndCancelCause`](#persistence-type-sha256-1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0) | + + +### `{ kind: "action-cards" }` + +SHA-256: `dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0` + +| Property | Presence | Type | +|---|---|---| +| `kind` | required | `"action-cards"` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "catalog" }` + +SHA-256: `dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"catalog"` | +| `kind` | required | `"action-cards"` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "instructions" }` + +SHA-256: `ba2a1a8f194b1bb21f270713398f6643e206e8ff5f7f26f6d36a88e3f0f0ab10` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"instructions"` | +| `kind` | required | `"action-cards"` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "notice" }` + +SHA-256: `e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"notice"` | +| `kind` | required | `"action-cards"` | +| `summary` | required | `string` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "recall" }` + +SHA-256: `b613f83572fe2fbf7259c72308cafd008f4fceea897749128a48bcd158f290aa` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"recall"` | +| `kind` | required | `"action-cards"` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "relay" }` + +SHA-256: `45f005e6bbb7645248be792b715a74bc7fdf5a67f5381655d3a75dcc7aa2112e` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"relay"` | +| `kind` | required | `"action-cards"` | +| `through` | required | `number` | + + + +### `{ kind: "action-cards", form: "snapshot" }` + +SHA-256: `3247e566f4340029325b70e4366b56fa9719977f1e39033b769a2ec221ab22a9` + +| Property | Presence | Type | +|---|---|---| +| `form` | required | `"snapshot"` | +| `kind` | required | `"action-cards"` | +| `sections` | required | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | +| `through` | required | `number` | + ### `{ kind: "blocked" }` SHA-256: `023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4` -Sources: [`packages/core/session/src/types.ts:207`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:211`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6435,7 +6537,7 @@ Sources: [`packages/core/session/src/types.ts:207`](../packages/core/session/src SHA-256: `b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0` -Sources: [`packages/core/session/src/types.ts:203`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:207`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6459,7 +6561,7 @@ Sources: [`packages/extensions/cordis-host-runner/src/index.ts:14`](../packages/ SHA-256: `4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db` -Sources: [`packages/core/session/src/types.ts:194`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:198`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6561,7 +6663,7 @@ Sources: [`packages/session/session-title/src/types.ts:29`](../packages/session/ SHA-256: `ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7` -Sources: [`packages/core/session/src/types.ts:230`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6573,7 +6675,7 @@ Sources: [`packages/core/session/src/types.ts:230`](../packages/core/session/src SHA-256: `6c5a37d42ebdb12db4410699259335f1e149e3a0260425684c00b153afc8990d` -Sources: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6742,7 +6844,7 @@ SHA-256: `88841202942c7683b3b7e5ea6a796047ec326b6701ac23e58cc236d85613b9fa` SHA-256: `56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23` -Sources: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6754,7 +6856,7 @@ Sources: [`packages/core/session/src/types.ts:223`](../packages/core/session/src SHA-256: `eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c` -Sources: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6766,7 +6868,7 @@ Sources: [`packages/core/session/src/types.ts:197`](../packages/core/session/src SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -Sources: [`packages/core/session/src/types.ts:216`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) +Sources: [`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6856,7 +6958,7 @@ SHA-256: `52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734` SHA-256: `6c03546143cdf6ae88f1745ac98c5139efa1588d1415249bbc5f3104d32ab800` -Sources: [`packages/core/session/src/types.ts:192`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:196`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7721,7 +7823,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:230`](../packages/exper SHA-256: `6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5` -Sources: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7777,7 +7879,7 @@ Sources: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/ SHA-256: `11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912` -Sources: [`packages/interaction/user-approval/src/index.ts:40`](../packages/interaction/user-approval/src/index.ts) +Sources: [`packages/interaction/user-approval/src/index.ts:41`](../packages/interaction/user-approval/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -7802,7 +7904,7 @@ Sources: [`packages/interaction/permission-presets/src/index.ts:53`](../packages SHA-256: `e481c46af3813e0010558e93f578b2986acd658404cec2d1679e3ac2a6c48654` -Sources: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7856,7 +7958,7 @@ Sources: [`packages/compaction/compaction/src/types.ts:82`](../packages/compacti SHA-256: `54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0` -Sources: [`packages/core/session/src/types.ts:357`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7870,7 +7972,7 @@ Sources: [`packages/core/session/src/types.ts:357`](../packages/core/session/src SHA-256: `ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545` -Sources: [`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7921,7 +8023,7 @@ Sources: [`packages/todo/tool-todo/src/types.ts:33`](../packages/todo/tool-todo/ SHA-256: `f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d` -Sources: [`packages/core/session/src/types.ts:290`](../packages/core/session/src/types.ts) · [`packages/deliverables/workspace-changes/src/types.ts:106`](../packages/deliverables/workspace-changes/src/types.ts) +Sources: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts) · [`packages/deliverables/workspace-changes/src/types.ts:106`](../packages/deliverables/workspace-changes/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7943,17 +8045,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | required | `number` | | `type` | required | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034` +SHA-256: `b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42) | +| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-b2a948a5340a9341fa3bab99507c7090c5b0456de72ec28b5b157b8df3f1d640) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8194,17 +8296,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | required | `number` | | `type` | required | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c` +SHA-256: `5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-f4ed4717b4c259e933b550a8dcd90dc93628cda8a3c7affcaa53b9c13c1d53eb) | +| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-7404c6123fdaa591474a37979a5a8ae70d1387af7c9f09bfe568b218935a974d) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8604,17 +8706,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056` +SHA-256: `efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -9037,17 +9139,17 @@ Sources: [`packages/llm/llm/src/types.ts:476`](../packages/llm/llm/src/types.ts) | `type` | required | `"usage"` | | `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` +SHA-256: `58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019` | Property | Presence | Type | |---|---|---| -| `data` | required | [`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711) | +| `data` | required | [`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 9004bc4b21..7b4f09039e 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -25,7 +25,7 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034) | +| `event:agent/inbox/spliced` | event | `b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -38,7 +38,7 @@ | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c` | [`{ type: "developer/message" }`](#persistence-type-sha256-97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c) | +| `event:developer/message` | event | `5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be` | [`{ type: "developer/message" }`](#persistence-type-sha256-5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -60,7 +60,7 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056) | +| `event:session/title-llm-request` | event | `efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -82,7 +82,7 @@ | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` | [`{ type: "user/message" }`](#persistence-type-sha256-2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8) | +| `event:user/message` | event | `58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019` | [`{ type: "user/message" }`](#persistence-type-sha256-58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -162,7 +162,7 @@ export type SessionEvent = { }[T] ``` -来源:[`packages/core/session/src/types.ts:433`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:441`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:464`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:495`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:501`](../packages/core/session/src/types.ts) ## 事件 @@ -275,7 +275,7 @@ export type SessionEvent = { } ``` -来源:[`packages/interaction/user-approval/src/index.ts:40`](../packages/interaction/user-approval/src/index.ts) +来源:[`packages/interaction/user-approval/src/index.ts:41`](../packages/interaction/user-approval/src/index.ts) ### `assistant/*` @@ -292,7 +292,7 @@ export type SessionEvent = { 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] } ``` -来源:[`packages/core/session/src/types.ts:357`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) @@ -322,7 +322,7 @@ export type SessionEvent = { 类型:[TokenUsage](subsystems/llm-streaming.zh.md) -来源:[`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:349`](../packages/core/session/src/types.ts) ### `command/*` @@ -510,7 +510,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) ### `feedback/*` @@ -755,7 +755,7 @@ export type SessionEvent = { 'request/context': RequestContext ``` -来源:[`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:410`](../packages/core/session/src/types.ts) @@ -774,7 +774,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/session/src/types.ts:392`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) ### `sandbox/*` @@ -815,7 +815,7 @@ export type SessionEvent = { 类型:[ScheduleChange](subsystems/schedule.zh.md) -来源:[`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts) +来源:[`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts) ### `session/*` @@ -851,7 +851,7 @@ export type SessionEvent = { 'session/end-seed': { inherited?: true } ``` -来源:[`packages/core/session/src/types.ts:429`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:435`](../packages/core/session/src/types.ts) @@ -913,7 +913,7 @@ export type SessionEvent = { 'step/end': { turn: number; step: number } ``` -来源:[`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) @@ -924,7 +924,7 @@ export type SessionEvent = { 'step/start': { turn: number; step: number } ``` -来源:[`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) ### `subagent/*` @@ -989,8 +989,10 @@ export type SessionEvent = { * The rendered system prompt on the model-visible surface. The loop appends * the first one as surface node 0 before the step's first `user/message`. * A prepared in-history route can append nonempty changes in a continuing - * series. An incapable route or new series normalizes text to the first system - * node. Normalization empties nonempty later nodes, then rewrites the head if + * series. An incapable route, or a new series whose effective text differs, + * normalizes text to the first system node; a series start whose latest + * surviving node already holds the rendering leaves it in place. + * Normalization empties nonempty later nodes, then rewrites the head if * needed, through logged per-node replacements. An empty rendering always * clears all active system nodes, leaving no older instructions model-visible. * Empty later nodes are dormant and project to no message; an empty head with @@ -1000,7 +1002,7 @@ export type SessionEvent = { 'system/message': { turn: number; step: number; message: SystemMessage } ``` -来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) ### `team/*` @@ -1093,7 +1095,7 @@ export type SessionEvent = { 类型:[ToolCallId](subsystems/core.zh.md) -来源:[`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:369`](../packages/core/session/src/types.ts) @@ -1174,7 +1176,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/session/src/types.ts:377`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) ### `tool-workflow/*` @@ -1254,7 +1256,7 @@ export type SessionEvent = { 类型:[TurnEndReason](subsystems/session.zh.md) -来源:[`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) @@ -1270,7 +1272,7 @@ export type SessionEvent = { 'turn/start': { turn: number } ``` -来源:[`packages/core/session/src/types.ts:290`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts) ### `user/*` @@ -1289,7 +1291,7 @@ export type SessionEvent = { 'user/message': UserMessage ``` -来源:[`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) ### `web/*` @@ -1349,6 +1351,14 @@ SHA-256: `1e0b0b66b6f0c53fc72e52289d06b4708d776f1ca9df4c86a22bff621d403cf0` `"aborted"` + + +### `"action-cards"` + +SHA-256: `05205074b24d8340cfd7995f6cb64779c7f1241490954700115601e264178877` + +`"action-cards"` + ### `"active"` @@ -3161,7 +3171,7 @@ SHA-256: `930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7` SHA-256: `05d2ae1e0c7f8770f83f9a09047afb39a335d8ac986df3193659aa1e581472cf` -来源:[`packages/interaction/user-approval/src/index.ts:67`](../packages/interaction/user-approval/src/index.ts) +来源:[`packages/interaction/user-approval/src/index.ts:68`](../packages/interaction/user-approval/src/index.ts) 以下类型之一: @@ -3224,7 +3234,7 @@ SHA-256: `84bbfdce7d2eab5b0c53e72ff7406db856c8bfd621b965b72fd86598ba493bf7` SHA-256: `ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05` -来源:[`packages/core/session/src/types.ts:191`](../packages/core/session/src/types.ts) · [`packages/interaction/commands/src/types.ts:75`](../packages/interaction/commands/src/types.ts) · [`packages/interaction/commands/src/types.ts:79`](../packages/interaction/commands/src/types.ts) · [`packages/llm/llm/src/message.ts:111`](../packages/llm/llm/src/message.ts) · [`packages/session/session-title/src/types.ts:35`](../packages/session/session-title/src/types.ts) +来源:[`packages/core/session/src/types.ts:195`](../packages/core/session/src/types.ts) · [`packages/interaction/commands/src/types.ts:75`](../packages/interaction/commands/src/types.ts) · [`packages/interaction/commands/src/types.ts:79`](../packages/interaction/commands/src/types.ts) · [`packages/llm/llm/src/message.ts:111`](../packages/llm/llm/src/message.ts) · [`packages/session/session-title/src/types.ts:35`](../packages/session/session-title/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3367,7 +3377,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` | `body` | 必需 | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | 必需 | `string` | - + @@ -3375,7 +3385,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` ### `DeveloperMessage` -SHA-256: `0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7` +SHA-256: `7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616` 来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3384,11 +3394,11 @@ SHA-256: `0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"developer"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. -仅表示归属的新增 kind:`tmux-context`, `user-question-reply`, `working-directory`. +仅表示归属的新增 kind:`action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. @@ -3400,7 +3410,7 @@ SHA-256: `0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7` SHA-256: `9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7` -来源:[`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4083,7 +4093,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` | `step` | 必需 | `number` | | `turn` | 必需 | `number` | - + @@ -4091,17 +4101,17 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `Message` -SHA-256: `040cd65423bd64264d225504f1c572d239eeb1793d0c2856ad8e42bb6e1e148b` +SHA-256: `cda79b839d8cc0c7a7bd09dd067de04925f6e9037080d878b65aaa5a383f117c` 来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) 以下类型之一: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7) -- [`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711) -- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) +- [`DeveloperMessage`](#persistence-type-sha256-7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616) +- [`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) +- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4176,7 +4186,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` - `"negative"` - `"positive"` - + @@ -4184,12 +4194,19 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` ### `MessageSource` -SHA-256: `5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8` +SHA-256: `7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690` 来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) | kind | form 属性 | 其他必需字段 | 完整定义 | |---|---|---|---| +| `action-cards` | `"catalog"` | `through`: `number` | [`{ kind: "action-cards", form: "catalog" }`](#persistence-type-sha256-dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a) | +| `action-cards` | `"instructions"` | `through`: `number` | [`{ kind: "action-cards", form: "instructions" }`](#persistence-type-sha256-ba2a1a8f194b1bb21f270713398f6643e206e8ff5f7f26f6d36a88e3f0f0ab10) | +| `action-cards` | `"notice"` | `summary`: `string`; `through`: `number` | [`{ kind: "action-cards", form: "notice" }`](#persistence-type-sha256-e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864) | +| `action-cards` | `"recall"` | `through`: `number` | [`{ kind: "action-cards", form: "recall" }`](#persistence-type-sha256-b613f83572fe2fbf7259c72308cafd008f4fceea897749128a48bcd158f290aa) | +| `action-cards` | `"relay"` | `through`: `number` | [`{ kind: "action-cards", form: "relay" }`](#persistence-type-sha256-45f005e6bbb7645248be792b715a74bc7fdf5a67f5381655d3a75dcc7aa2112e) | +| `action-cards` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262); `through`: `number` | [`{ kind: "action-cards", form: "snapshot" }`](#persistence-type-sha256-3247e566f4340029325b70e4366b56fa9719977f1e39033b769a2ec221ab22a9) | +| `action-cards` | 未声明 | `through`: `number` | [`{ kind: "action-cards" }`](#persistence-type-sha256-dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0) | | `agent-instructions` | `"instructions"` | `changes`: [`AgentInstructionChange[]`](#persistence-type-sha256-2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee) | [`AgentInstructionSource`](#persistence-type-sha256-6f22a9d02b9a1f1157f3b0e7bdccd7e7b12b085c4efdb709dfc74a465cd4e590) | | `agent-message` | `"relay"` | `senderSessionId`: `string` | [`AgentMessageSource`](#persistence-type-sha256-4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86) | | `compact-checkpoint` | 未声明 | `compactionId`: `string` | [`CompactionCheckpointSource`](#persistence-type-sha256-31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648) | @@ -4302,13 +4319,13 @@ SHA-256: `5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8` | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | 未声明 | 无 | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `a927d444c928ac0efd9dd8b2e7d7d04d711c264cb71578d81e0e2fd143ff669f` +SHA-256: `78b9204976ab9faaf133c3488b73d1dfb6f1a4bb6c0903e3473cd41a5e1e39e1` -[`Message`](#persistence-type-sha256-040cd65423bd64264d225504f1c572d239eeb1793d0c2856ad8e42bb6e1e148b) 的数组。 +[`Message`](#persistence-type-sha256-cda79b839d8cc0c7a7bd09dd067de04925f6e9037080d878b65aaa5a383f117c) 的数组。 @@ -4530,7 +4547,7 @@ SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` SHA-256: `b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081` -来源:[`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:260`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4549,7 +4566,7 @@ SHA-256: `b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081` SHA-256: `530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6` -来源:[`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) 以下类型之一: @@ -4692,7 +4709,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4700,7 +4717,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f` +SHA-256: `a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf` 来源:[`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) @@ -4708,7 +4725,7 @@ SHA-256: `00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f` |---|---|---| | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-a927d444c928ac0efd9dd8b2e7d7d04d711c264cb71578d81e0e2fd143ff669f) | +| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-78b9204976ab9faaf133c3488b73d1dfb6f1a4bb6c0903e3473cd41a5e1e39e1) | | `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | @@ -4887,7 +4904,7 @@ SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` SHA-256: `335e242de1fcc17b6ca920fc420d71bec2d76e53e37955c00948b65ab77f05c5` -来源:[`packages/core/session/src/types.ts:464`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) 以下类型之一: @@ -5332,7 +5349,7 @@ SHA-256: `a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c` SHA-256: `1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0` -来源:[`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts) | kind | form 属性 | 其他必需字段 | 完整定义 | |---|---|---|---| @@ -5352,7 +5369,7 @@ SHA-256: `1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0` SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` -来源:[`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts) | kind | form 属性 | 其他必需字段 | 完整定义 | |---|---|---|---| @@ -5364,7 +5381,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` | `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5372,7 +5389,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` ### `UserMessage` -SHA-256: `2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711` +SHA-256: `b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69` 来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5381,19 +5398,19 @@ SHA-256: `2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"user"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. -仅表示归属的新增 kind:`tmux-context`, `user-question-reply`, `working-directory`. +仅表示归属的新增 kind:`action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `454a0fa879e38076bdcdbd87ef561cd67d7e43e58a287cf7dbaa7b6cf7847020` +SHA-256: `5d00abf938272f22eb47199e9d40801e8d8ec4c88e2b138b15aa2a5a5bf87520` -[`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711) 的数组。 +[`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69) 的数组。 @@ -5822,7 +5839,7 @@ SHA-256: `a853902cfab417b8f08aa51ad6855fea46f34ed4588a74e47403035111d0fb86` SHA-256: `e4c18e294232c3ba6c9f1999f168263cc55956147cfe4121720899be8e52edd1` -来源:[`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:369`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5981,7 +5998,7 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735` -来源:[`packages/core/session/src/types.ts:385`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts) +来源:[`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6201,7 +6218,7 @@ SHA-256: `9e41386b3a0c9572b0d63078492ebb3997da7d3a830d44e0259418bd02f4bcb2` SHA-256: `bcf0caf62d964b2fcf5404bd5c909cfa9a21c3f3c96e6b7e36d9e33565223825` -来源:[`packages/core/session/src/types.ts:466`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:472`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6215,7 +6232,7 @@ SHA-256: `bcf0caf62d964b2fcf5404bd5c909cfa9a21c3f3c96e6b7e36d9e33565223825` SHA-256: `79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985` -来源:[`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:218`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6228,7 +6245,7 @@ SHA-256: `79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985` SHA-256: `5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777` -来源:[`packages/core/session/src/types.ts:377`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6270,7 +6287,7 @@ SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057` SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` -来源:[`packages/core/session/src/types.ts:392`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6278,18 +6295,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` | `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | 可选 | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `f4ed4717b4c259e933b550a8dcd90dc93628cda8a3c7affcaa53b9c13c1d53eb` +SHA-256: `7404c6123fdaa591474a37979a5a8ae70d1387af7c9f09bfe568b218935a974d` -来源:[`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `headerSeq` | 可选 | `number` | -| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7) | +| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616) | | `step` | 必需 | `number` | | `turn` | 必需 | `number` | @@ -6367,23 +6384,23 @@ SHA-256: `e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f` SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` -来源:[`packages/core/session/src/types.ts:429`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:435`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `inherited` | 可选 | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42` +SHA-256: `b2a948a5340a9341fa3bab99507c7090c5b0456de72ec28b5b157b8df3f1d640` 来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| -| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-454a0fa879e38076bdcdbd87ef561cd67d7e43e58a287cf7dbaa7b6cf7847020) | +| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-5d00abf938272f22eb47199e9d40801e8d8ec4c88e2b138b15aa2a5a5bf87520) | | `outcome` | 可选 | `"canceled"` | | `removedCount` | 可选 | `number` | | `start` | 必需 | `number` | @@ -6395,7 +6412,7 @@ SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42` SHA-256: `1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d` -来源:[`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:349`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6412,20 +6429,105 @@ SHA-256: `1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d` SHA-256: `0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3` -来源:[`packages/core/session/src/types.ts:205`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:209`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `kind` | 必需 | `"aborted"` | | `reason` | 必需 | [`TurnEndCancelCause`](#persistence-type-sha256-1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0) | + + +### `{ kind: "action-cards" }` + +SHA-256: `dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `kind` | 必需 | `"action-cards"` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "catalog" }` + +SHA-256: `dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"catalog"` | +| `kind` | 必需 | `"action-cards"` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "instructions" }` + +SHA-256: `ba2a1a8f194b1bb21f270713398f6643e206e8ff5f7f26f6d36a88e3f0f0ab10` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"instructions"` | +| `kind` | 必需 | `"action-cards"` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "notice" }` + +SHA-256: `e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"notice"` | +| `kind` | 必需 | `"action-cards"` | +| `summary` | 必需 | `string` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "recall" }` + +SHA-256: `b613f83572fe2fbf7259c72308cafd008f4fceea897749128a48bcd158f290aa` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"recall"` | +| `kind` | 必需 | `"action-cards"` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "relay" }` + +SHA-256: `45f005e6bbb7645248be792b715a74bc7fdf5a67f5381655d3a75dcc7aa2112e` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"relay"` | +| `kind` | 必需 | `"action-cards"` | +| `through` | 必需 | `number` | + + + +### `{ kind: "action-cards", form: "snapshot" }` + +SHA-256: `3247e566f4340029325b70e4366b56fa9719977f1e39033b769a2ec221ab22a9` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `form` | 必需 | `"snapshot"` | +| `kind` | 必需 | `"action-cards"` | +| `sections` | 必需 | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | +| `through` | 必需 | `number` | + ### `{ kind: "blocked" }` SHA-256: `023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4` -来源:[`packages/core/session/src/types.ts:207`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:211`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6437,7 +6539,7 @@ SHA-256: `023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4` SHA-256: `b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0` -来源:[`packages/core/session/src/types.ts:203`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:207`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6461,7 +6563,7 @@ SHA-256: `02b2c23f2d94b028c9588a8faace3fce1217405db346da3389368b495457bf79` SHA-256: `4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db` -来源:[`packages/core/session/src/types.ts:194`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:198`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6563,7 +6665,7 @@ SHA-256: `1b5649238876e28811e80f10ec67a00c197ad3babd4ecc61a22b09064b88c597` SHA-256: `ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7` -来源:[`packages/core/session/src/types.ts:230`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6575,7 +6677,7 @@ SHA-256: `ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7` SHA-256: `6c5a37d42ebdb12db4410699259335f1e149e3a0260425684c00b153afc8990d` -来源:[`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6744,7 +6846,7 @@ SHA-256: `88841202942c7683b3b7e5ea6a796047ec326b6701ac23e58cc236d85613b9fa` SHA-256: `56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23` -来源:[`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6756,7 +6858,7 @@ SHA-256: `56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23` SHA-256: `eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c` -来源:[`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6768,7 +6870,7 @@ SHA-256: `eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c` SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -来源:[`packages/core/session/src/types.ts:216`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) +来源:[`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6858,7 +6960,7 @@ SHA-256: `52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734` SHA-256: `6c03546143cdf6ae88f1745ac98c5139efa1588d1415249bbc5f3104d32ab800` -来源:[`packages/core/session/src/types.ts:192`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:196`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7723,7 +7825,7 @@ SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` SHA-256: `6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5` -来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7779,7 +7881,7 @@ SHA-256: `9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e` SHA-256: `11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912` -来源:[`packages/interaction/user-approval/src/index.ts:40`](../packages/interaction/user-approval/src/index.ts) +来源:[`packages/interaction/user-approval/src/index.ts:41`](../packages/interaction/user-approval/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7804,7 +7906,7 @@ SHA-256: `8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad` SHA-256: `e481c46af3813e0010558e93f578b2986acd658404cec2d1679e3ac2a6c48654` -来源:[`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7858,7 +7960,7 @@ SHA-256: `5dd766ccd1702a1142ad8b4bd44f2242f8e8a6752669ad3de0a5d224329b1438` SHA-256: `54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0` -来源:[`packages/core/session/src/types.ts:357`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:363`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7872,7 +7974,7 @@ SHA-256: `54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0` SHA-256: `ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545` -来源:[`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts) +来源:[`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7923,7 +8025,7 @@ SHA-256: `79ddc907312f97cf9553d4841e8b3fc937097bd8d97900a790efad3c05255e99` SHA-256: `f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d` -来源:[`packages/core/session/src/types.ts:290`](../packages/core/session/src/types.ts) · [`packages/deliverables/workspace-changes/src/types.ts:106`](../packages/deliverables/workspace-changes/src/types.ts) +来源:[`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts) · [`packages/deliverables/workspace-changes/src/types.ts:106`](../packages/deliverables/workspace-changes/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7945,17 +8047,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | 必需 | `number` | | `type` | 必需 | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034` +SHA-256: `b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42) | +| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-b2a948a5340a9341fa3bab99507c7090c5b0456de72ec28b5b157b8df3f1d640) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8196,17 +8298,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | 必需 | `number` | | `type` | 必需 | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c` +SHA-256: `5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-f4ed4717b4c259e933b550a8dcd90dc93628cda8a3c7affcaa53b9c13c1d53eb) | +| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-7404c6123fdaa591474a37979a5a8ae70d1387af7c9f09bfe568b218935a974d) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8606,17 +8708,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056` +SHA-256: `efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -9039,17 +9141,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` | `type` | 必需 | `"usage"` | | `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` +SHA-256: `58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711) | +| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-changes/2026-10-22-action-card-status.i18n.yaml b/docs/persistence-changes/2026-10-22-action-card-status.i18n.yaml new file mode 100644 index 0000000000..80b7c81132 --- /dev/null +++ b/docs/persistence-changes/2026-10-22-action-card-status.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-22-action-card-status.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-22-action-card-status.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-22-action-card-status: + en: f580a2fb64e941db + zh: 0e34217155210da6 +/2026-10-22-action-card-status/summary: + en: 3da2594bf2891e0e + zh: 5bc7d279d99400aa +/2026-10-22-action-card-status/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-22-action-card-status/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-22-action-card-status/compatibility: + en: fbf4d1ada55f35fe + zh: bd5ce9a23c55fac6 +/2026-10-22-action-card-status/verification: + en: 91043c4f0b19e7bc + zh: ee1c541133475bb8 +/2026-10-22-action-card-status/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-22-action-card-status.md b/docs/persistence-changes/2026-10-22-action-card-status.md new file mode 100644 index 0000000000..4c62efb5ce --- /dev/null +++ b/docs/persistence-changes/2026-10-22-action-card-status.md @@ -0,0 +1,60 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-22-action-card-status + +English | [中文](2026-10-22-action-card-status.zh.md) + +## Summary + +Adds action-card status attribution to logged model context. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-22-action-card-status +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-13-user-rpc-principal" + after: "b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-13-user-rpc-principal" + after: "5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-13-user-rpc-principal" + after: "efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc" + decision: same-version + - root: "event:user/message" + previous: "2026-10-13-user-rpc-principal" + after: "58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019" + decision: same-version +``` + + +## Compatibility + +Existing messages and records are unchanged. Readers without the action-card plugin preserve the new action-cards source kind and its JSON metadata and derive the recorded text as ordinary context. Only the producer uses the through sequence, to acknowledge delivered decisions; it grants no authority and does not change replay interpretation. + + +## Verification + +pnpm exec vitest run on the action-cards, creation-recommendations, client-ui-action-cards and client-ui-creation-recommendations tests: 120 tests passed with 100% coverage of their sources. The Web creation-recommendations replay logs the status notice of a dismissed card before the next answer. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-22-action-card-status.schema.json b/docs/persistence-changes/2026-10-22-action-card-status.schema.json new file mode 100644 index 0000000000..9aa642699a --- /dev/null +++ b/docs/persistence-changes/2026-10-22-action-card-status.schema.json @@ -0,0 +1,249 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019"} + ], + "types": [], + "nodes": { + "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, + "05205074b24d8340": {"kind":"literal","value":"action-cards"}, + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "07e1a58c58b593f5": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "09655b34d7f3cf1e": {"kind":"literal","value":"system-prompt"}, + "09f3f8dc417f2782": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"8af841fc9b265ade","optional":false},{"name":"source","type":"65a8400e451e667e","optional":false}],"indices":[]}, + "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, + "11638dc58a31496f": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, + "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, + "16d0507cb94b4cfc": {"kind":"literal","value":"relay"}, + "1922cd79a70806c1": {"kind":"literal","value":"skill-invocation"}, + "1938c8c20f1cba7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1e709346005826eb": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1f0133217eeeffd8": {"kind":"literal","value":"tool-call"}, + "1f1385dbc3e875c0": {"kind":"literal","value":"repeat-tool-reminder"}, + "1fdad624334d3548": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "2152caf1a71e588e": {"kind":"object","properties":[{"name":"kind","type":"2e4debca12f2c449","optional":false},{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "2235d957bf4b7c6c": {"kind":"object","properties":[{"name":"kind","type":"7b1ce60193fa8b69","optional":false}],"indices":[]}, + "224aa11d4d5f88c4": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "23af3759678a30de": {"kind":"literal","value":"dsh-session-title-llm"}, + "24b10f7cdae64928": {"kind":"literal","value":"runtime-context"}, + "24ce31aa528272e5": {"kind":"object","properties":[{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "260523b412c19ad8": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "2765e9119edf5816": {"kind":"literal","value":"model"}, + "277b61f9c6f81704": {"kind":"literal","value":"file"}, + "28936c614bd4e7e2": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "290f32bfd81ff85a": {"kind":"object","properties":[{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "297fbd8067d7815a": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, + "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, + "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, + "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, + "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "3247e566f4340029": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "335e242de1fcc17b": {"kind":"union","types":["bcf0caf62d964b2f","e438a9d4fb883b3e"]}, + "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, + "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "3b4f2c9ef2c1217e": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "3bd652ebfa8726b3": {"kind":"union","types":["0657d3ca5ac4e7bf","938f7c7357bc403e"]}, + "3bdb8dc6989655b0": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "3c2535ca0a2062f1": {"kind":"literal","value":"reasoning"}, + "3e2ece4967cfebe9": {"kind":"literal","value":"next-step"}, + "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, + "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "43e311235aa537df": {"kind":"object","properties":[{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"83d6916021eede8f","optional":false}],"indices":[]}, + "45f005e6bbb76452": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "4991dfc415c9efeb": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "49dab019d00e8c26": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "4a9bef9f27a94252": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"82470020b5bfcb1a","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4b259f7f8f0a44ad": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"isError","type":"aa93010d0e521ad7","optional":true},{"name":"role","type":"a13d8f379222ece8","optional":false},{"name":"source","type":"d9c462b9f62fdd3e","optional":false},{"name":"toolCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "4e44c9118152f682": {"kind":"literal","value":"text"}, + "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, + "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, + "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, + "52c6113cb0e53f34": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "58015da1c1950a66": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "587bb11da730e06a": {"kind":"literal","value":"session/title-llm-request"}, + "589d0d91239681f8": {"kind":"literal","value":"developer"}, + "58b122df513aeaf7": {"kind":"object","properties":[{"name":"data","type":"b782058cef83caf3","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, + "58f94390b982f8a5": {"kind":"literal","value":"image/png"}, + "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "5cdbf342b7748d15": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "5d00abf938272f22": {"kind":"array","element":"b782058cef83caf3"}, + "5d03ba38734809bc": {"kind":"array","element":"0657d3ca5ac4e7bf"}, + "5d7a64ed6aac5551": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "5ef58f2a1bdc576c": {"kind":"object","properties":[{"name":"data","type":"7404c6123fdaa591","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, + "6085488d223aeffd": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, + "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, + "672c4aa2a9b736e3": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1922cd79a70806c1","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "68f44f59e91c4139": {"kind":"literal","value":"user"}, + "6b23ed9ce1b397e6": {"kind":"literal","value":"compact-checkpoint"}, + "6ccb6bc423fb3952": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6dacb1584f2bfbe2": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"deebfc7c62177ea8","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6ea608bdc8a7a63d": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "6ebcc2dbec9f2dab": {"kind":"literal","value":"skill-catalog"}, + "6f22a9d02b9a1f11": {"kind":"object","properties":[{"name":"baseline","type":"dacf3d5f3e4ff619","optional":true},{"name":"baselineIdentity","type":"4cd48f3e5108bd6e","optional":true},{"name":"changes","type":"2ab12a0e345658f9","optional":false},{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"fbd2c74b346a1595","optional":false}],"indices":[]}, + "6f44f32be9bcd9a8": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, + "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, + "7382c4fc95fe8c5d": {"kind":"literal","value":"remove"}, + "7404c6123fdaa591": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"7580e7d96dc8475f","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "7554565be416b034": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "756b5e23a5cb2c7f": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "7580e7d96dc8475f": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"7a5b68bd1a5118c8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "78a9be340090b735": {"kind":"literal","value":"tmux-context"}, + "78b9204976ab9faa": {"kind":"array","element":"cda79b839d8cc0c7"}, + "7a5b68bd1a5118c8": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fc2ae6962f9f8801","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, + "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, + "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, + "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, + "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, + "814d9434e1ddf907": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"3c2535ca0a2062f1","optional":false}],"indices":[]}, + "82470020b5bfcb1a": {"kind":"literal","value":"agent-message"}, + "825ca23416b1ea9d": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "829a2fb3be6efd4b": {"kind":"literal","value":"agent/inbox/spliced"}, + "83079c8a3f733ac3": {"kind":"object","properties":[{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8338ee303e2bd448": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "83d6916021eede8f": {"kind":"literal","value":"tool-removal"}, + "83da72c6857601c7": {"kind":"object","properties":[{"name":"action","type":"904bf499d98be4a3","optional":false},{"name":"digest","type":"4cd48f3e5108bd6e","optional":true},{"name":"path","type":"4cd48f3e5108bd6e","optional":false},{"name":"scope","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "854db3efdfb0cd39": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "85d6d96ff8842974": {"kind":"literal","value":"answered"}, + "85f9b505b7bdff1f": {"kind":"literal","value":"tool-addition"}, + "85fe96bf15d5c755": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "8620189c8e335827": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "86e1e384495ee09a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "87b1a33564b9a4d6": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "88841202942c7683": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8a046b52331959aa": {"kind":"object","properties":[{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "8af841fc9b265ade": {"kind":"literal","value":"system"}, + "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8e611bf46821e500": {"kind":"object","properties":[{"name":"tool","type":"fcf427290c6213c2","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"85f9b505b7bdff1f","optional":false}],"indices":[]}, + "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, + "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "938f7c7357bc403e": {"kind":"primitive","type":"null"}, + "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, + "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, + "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, + "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, + "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "99e7e7d7fed55ae6": {"kind":"literal","value":"tool-jobs"}, + "99f0e98184709ad1": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "9ac52f0f84fec2eb": {"kind":"literal","value":"model-selection"}, + "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "a13d8f379222ece8": {"kind":"literal","value":"tool"}, + "a15a19ef63cf5d1b": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "a16e567c8cd2c55c": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "a2066bd52f61218b": {"kind":"object","properties":[{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a3e299483348f27a": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"78b9204976ab9faa","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a720757f704a2512": {"kind":"literal","value":"tool-registry"}, + "a9059a81430b3207": {"kind":"literal","value":"hooks-claude-code"}, + "aa2bf9e82b9fdeb9": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, + "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, + "ada310bf0bdb8fed": {"kind":"object","properties":[{"name":"kind","type":"68f44f59e91c4139","optional":false}],"indices":[]}, + "ade057f4fdc387ae": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "aec150797b655c5f": {"kind":"literal","value":"catalog"}, + "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b2a5c8c008a31e3d": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "b2a948a5340a9341": {"kind":"object","properties":[{"name":"inserted","type":"5d00abf938272f22","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, + "b3681c026a0b98e9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "b36fd842cd004b53": {"kind":"object","properties":[{"name":"data","type":"b2a948a5340a9341","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, + "b4ec0af39fd52ed9": {"kind":"object","properties":[{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b52f215a6cd69105": {"kind":"array","element":"8064c03c87c357fd"}, + "b613f83572fe2fbf": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b6f94c17dd9bbd7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b70f3e81fe11b7a5": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "b765ee31033cea46": {"kind":"literal","value":"set"}, + "b782058cef83caf3": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"7a5b68bd1a5118c8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "b79641a9d25e7566": {"kind":"object","properties":[{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "b815d7b91cdd9fc6": {"kind":"literal","value":"goal"}, + "ba2a1a8f194b1bb2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "ba4c40b4381e0dcd": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "baccc97ec34be6c4": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "bb94f3217aa56c88": {"kind":"literal","value":"hooks-codex"}, + "bbc5696e3f13c758": {"kind":"opaque","reason":"unknown"}, + "bcf0caf62d964b2f": {"kind":"object","properties":[{"name":"endSeq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"op","type":"f6cb462f808309e3","optional":false},{"name":"startSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "bf16d2e98a7f1389": {"kind":"literal","value":"subagent-settled"}, + "c1f0f17b6493b85d": {"kind":"object","properties":[{"name":"arguments","type":"4cd48f3e5108bd6e","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"1f0133217eeeffd8","optional":false}],"indices":[]}, + "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, + "c5687b1313206a6f": {"kind":"literal","value":"cordis-host-runner"}, + "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, + "cb1c8959e1726f82": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "cda79b839d8cc0c7": {"kind":"union","types":["09f3f8dc417f2782","4b259f7f8f0a44ad","7580e7d96dc8475f","b782058cef83caf3","fa87537f4a9c1e25"]}, + "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, + "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, + "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true}, + "db06ef35b27a3842": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "db6c5246911d47a7": {"kind":"literal","value":"webhook"}, + "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, + "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "de25352d2dc41963": {"kind":"literal","value":"user/message"}, + "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, + "deaae5cd30987d4f": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "deebfc7c62177ea8": {"kind":"object","properties":[{"name":"kind","type":"95a3ffccbf3c8afc","optional":false}],"indices":[]}, + "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, + "e438a9d4fb883b3e": {"kind":"literal","value":"append"}, + "e7f4cbb3a57f4cbd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "e8de0f457fed5b9f": {"kind":"literal","value":"notice"}, + "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, + "edbbfff1128099fd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "ee577a0cc18664c9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "efa1a091017ce533": {"kind":"object","properties":[{"name":"data","type":"a3e299483348f27a","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, + "f19bee84bf3d2090": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f230c0b583e23b25": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "f29d63336e8266d8": {"kind":"object","properties":[{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "f2da0f27c1e6add5": {"kind":"object","properties":[{"name":"attachment","type":"f19bee84bf3d2090","optional":false},{"name":"type","type":"277b61f9c6f81704","optional":false}],"indices":[]}, + "f4a66f654996c093": {"kind":"object","properties":[{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f56c5b59ca016131": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"7f13860ee89a2d35","optional":false},{"name":"references","type":"df843efd87c78cf8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "f6329fb30c7ea801": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f6605d6c57933aea": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f68c335306535a00": {"kind":"literal","value":"user-approval"}, + "f6afe41e3926f422": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "f6cb462f808309e3": {"kind":"literal","value":"replace"}, + "f7283223aacdc39b": {"kind":"object","properties":[{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f88f1b7b6bb733f6": {"kind":"literal","value":1}, + "f97f47b0eb87905e": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, + "fb8b6a2e53cb9cf4": {"kind":"literal","value":"image"}, + "fbd2c74b346a1595": {"kind":"literal","value":"agent-instructions"}, + "fc2ae6962f9f8801": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, + "fcf427290c6213c2": {"kind":"primitive","type":"never"}, + "fcf81d3c58c13593": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"} + } +} diff --git a/docs/persistence-changes/2026-10-22-action-card-status.zh.md b/docs/persistence-changes/2026-10-22-action-card-status.zh.md new file mode 100644 index 0000000000..4257655568 --- /dev/null +++ b/docs/persistence-changes/2026-10-22-action-card-status.zh.md @@ -0,0 +1,60 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-22-action-card-status + +[English](2026-10-22-action-card-status.md) | 中文 + +## 概述 + +为写入日志的模型上下文新增操作卡片状态的来源标识。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-22-action-card-status +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-13-user-rpc-principal" + after: "b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-13-user-rpc-principal" + after: "5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-13-user-rpc-principal" + after: "efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc" + decision: same-version + - root: "event:user/message" + previous: "2026-10-13-user-rpc-principal" + after: "58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019" + decision: same-version +``` + + +## 兼容性 + +已有消息和记录不变。没有操作卡片插件的读取方保留新的 action-cards 来源类型及其 JSON 元数据,并把记录的文本当作普通上下文。只有写入方使用 through 序号确认已送达的决定;它不授予任何权限,也不改变回放的解释。 + + +## 验证 + +对 action-cards、creation-recommendations、client-ui-action-cards 和 client-ui-creation-recommendations 的测试运行 pnpm exec vitest run:120 个测试通过,源码覆盖率 100%。Web creation-recommendations 回放在下一次回答之前记录了已忽略卡片的状态通知。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index 5339de2d2b..40d842e063 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: 17ef09dfc5a89073 - zh: 59b8d60acb66c2dc + en: e3511a4ac3bc4457 + zh: 828f3c39f80d5370 /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index 174c007423..74c575b300 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 614 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 622 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 055e07f954..12eb600771 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 614 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 622 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index c668fb96cf..247fb463f6 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -5,7 +5,7 @@ {"key":"JsonlHeaderLine","kind":"header","digest":"18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f"}, {"key":"SessionEventEnvelope","kind":"envelope","digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc"}, {"key":"event:agent-preset/selected","kind":"event","event":"agent-preset/selected","surface":false,"digest":"a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b"}, - {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034"}, + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e"}, {"key":"event:approval/asked","kind":"event","event":"approval/asked","surface":false,"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1"}, {"key":"event:approval/decided","kind":"event","event":"approval/decided","surface":false,"digest":"bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3"}, {"key":"event:approval/policy","kind":"event","event":"approval/policy","surface":false,"digest":"26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78"}, @@ -18,7 +18,7 @@ {"key":"event:compaction/start","kind":"event","event":"compaction/start","surface":false,"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965"}, {"key":"event:compaction/summary","kind":"event","event":"compaction/summary","surface":false,"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602"}, {"key":"event:deliverables/presented","kind":"event","event":"deliverables/presented","surface":false,"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176"}, - {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be"}, {"key":"event:feedback/message-delete","kind":"event","event":"feedback/message-delete","surface":false,"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"}, {"key":"event:feedback/message-put","kind":"event","event":"feedback/message-put","surface":false,"digest":"b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf"}, {"key":"event:feedback/record","kind":"event","event":"feedback/record","surface":false,"digest":"b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8"}, @@ -40,7 +40,7 @@ {"key":"event:session-log-deepseek/delivery-accepted","kind":"event","event":"session-log-deepseek/delivery-accepted","surface":false,"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9"}, {"key":"event:session/end-seed","kind":"event","event":"session/end-seed","surface":false,"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e"}, {"key":"event:session/title","kind":"event","event":"session/title","surface":false,"digest":"1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5"}, - {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc"}, {"key":"event:step/end","kind":"event","event":"step/end","surface":false,"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05"}, {"key":"event:step/start","kind":"event","event":"step/start","surface":false,"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986"}, {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac"}, @@ -62,29 +62,27 @@ {"key":"event:tool/result","kind":"event","event":"tool/result","surface":true,"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397"}, {"key":"event:turn/end","kind":"event","event":"turn/end","surface":false,"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce"}, {"key":"event:turn/start","kind":"event","event":"turn/start","surface":false,"digest":"aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8"}, - {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019"}, {"key":"event:web/deepseek-search-llm-request","kind":"event","event":"web/deepseek-search-llm-request","surface":false,"digest":"0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"}, {"key":"event:working-directory/change","kind":"event","event":"working-directory/change","surface":false,"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3"}, {"key":"event:workspace/changes","kind":"event","event":"workspace/changes","surface":false,"digest":"e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72"} ], "types": [ - {"digest":"00c054d3018c198d92f459563331edd1385e58345c677bb499791df2da0ade9f","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, {"digest":"0116b1f89c05221d56b0ba35c83d302bf4857bc7277d247e9bbaf74b27d2c66e","names":[],"sources":[]}, {"digest":"0203243b72fc867afc81aff5f4cf2167e564ed342534eb612fc92f295cf07789","names":[],"sources":[]}, {"digest":"022623b9ef98ec47ec3c3bae0c51027c70d2601141c5e92e26a0d83d1b4cef06","names":[],"sources":[]}, - {"digest":"023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4","names":[],"sources":["packages/core/session/src/types.ts:207"]}, + {"digest":"023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4","names":[],"sources":["packages/core/session/src/types.ts:211"]}, {"digest":"02b2c23f2d94b028c9588a8faace3fce1217405db346da3389368b495457bf79","names":[],"sources":["packages/extensions/cordis-host-runner/src/index.ts:14"]}, {"digest":"0341d4c3dfcd53253cdc1e789d9130d3cd848b6f8b0706bf92f313a68b39ff23","names":[],"sources":[]}, - {"digest":"040cd65423bd64264d225504f1c572d239eeb1793d0c2856ad8e42bb6e1e148b","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, + {"digest":"05205074b24d8340cfd7995f6cb64779c7f1241490954700115601e264178877","names":[],"sources":[]}, {"digest":"05a2796621b1ee6c03ef7abe560b25116bb4da4d592312117242a78415a32b04","names":[],"sources":[]}, - {"digest":"05d2ae1e0c7f8770f83f9a09047afb39a335d8ac986df3193659aa1e581472cf","names":["packages/interaction/user-approval/src/index.ts#ApprovalPolicy"],"sources":["packages/interaction/user-approval/src/index.ts:67"]}, + {"digest":"05d2ae1e0c7f8770f83f9a09047afb39a335d8ac986df3193659aa1e581472cf","names":["packages/interaction/user-approval/src/index.ts#ApprovalPolicy"],"sources":["packages/interaction/user-approval/src/index.ts:68"]}, {"digest":"0657d3ca5ac4e7bf79b00bcf860b1c7159d01a9da137497221ec2f1776e365f7","names":["packages/core/session/src/types.ts#SessionSeq"],"sources":["packages/core/session/src/types.ts:33"]}, - {"digest":"0755b7d1128319ba2e389619cbbf2c961ad648882b4e91da7f3e9582cc8a2034","names":[],"sources":[]}, {"digest":"07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235","names":["packages/session/session-title/src/types.ts#SessionTitleModelIdentity","packages/subagent/tool-subagent/src/model-selection.ts#AllowedModelRoute"],"sources":["packages/session/session-title/src/types.ts:20","packages/subagent/tool-subagent/src/model-selection.ts:9"]}, {"digest":"07eccc9ea2f9b5439d76cc77d5e062936677d43f07a23da749819b0a22ee36b7","names":[],"sources":[]}, {"digest":"09655b34d7f3cf1e9005974de2357de2d4a60deea165d16dac1eadaa6ec539a8","names":[],"sources":[]}, {"digest":"09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363","names":["packages/llm/llm/src/message.ts#SystemMessage"],"sources":["packages/llm/llm/src/message.ts:151"]}, - {"digest":"0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3","names":[],"sources":["packages/core/session/src/types.ts:205"]}, + {"digest":"0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3","names":[],"sources":["packages/core/session/src/types.ts:209"]}, {"digest":"0a8a062bc9c3c0d97fd97742262f2ea53dd32297f834e81114870d38ac3dc6da","names":[],"sources":[]}, {"digest":"0aa62ab51b36c0e64b134bd03ba8725d758744180a1a447da49beaad194f219f","names":[],"sources":[]}, {"digest":"0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555","names":["packages/llm/llm/src/types.ts#ReplayEnvelope"],"sources":["packages/llm/llm/src/types.ts:448"]}, @@ -101,14 +99,13 @@ {"digest":"0ed8d1108fe7a5e1c3078ef1faea12199a56b6b1216ccaceb0f6103b0c9a7691","names":[],"sources":[]}, {"digest":"0eeb17e1df89b1ebc2902116fe7145e0d321ef0db26e6bdebc802cb8513a01c1","names":[],"sources":[]}, {"digest":"0f46153645c297846a3fad3911636659ca4717a5e7d71c9f50bffacd312115ef","names":[],"sources":["packages/attachment/attachment/src/types.ts:28"]}, - {"digest":"0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce","names":[],"sources":[]}, {"digest":"0fe52387f65052702ed9d7cd59db826468936595d1b3eec207e7a823adc719db","names":[],"sources":[]}, {"digest":"1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625","names":[],"sources":[]}, {"digest":"10e386801903f9d424602c31d3ff837a94aef623c8b391376d8d9e253525fabe","names":[],"sources":[]}, {"digest":"11638dc58a31496fb6706ced3e23e3a2db1f3b2a8845968c810827a72b5fc811","names":[],"sources":[]}, {"digest":"119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25","names":[],"sources":["packages/experimental/hook-protocol/src/types.ts:19"]}, - {"digest":"11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912","names":[],"sources":["packages/interaction/user-approval/src/index.ts:40"]}, + {"digest":"11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912","names":[],"sources":["packages/interaction/user-approval/src/index.ts:41"]}, {"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3","names":[],"sources":[]}, {"digest":"12a0cfa619ac4a5fc6c3e8e359c8dae15769edc61497a575935c78d5842f8e42","names":[],"sources":[]}, {"digest":"12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7","names":["packages/llm/llm/src/types.ts#ImageBlock"],"sources":["packages/llm/llm/src/types.ts:79"]}, @@ -123,7 +120,7 @@ {"digest":"15d5dfdd822aa35e115afd74a8982825a493880457774e6850bc1520b50875e4","names":[],"sources":[]}, {"digest":"16d0507cb94b4cfc84a87446b1ba7652d6ec1d65ee1684789e747e30d542800e","names":[],"sources":[]}, {"digest":"17aae616ef020b8b9f011ec398ec935450fb06d3e2a0722c456f63aeaf5ff728","names":[],"sources":[]}, - {"digest":"17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3","names":[],"sources":["packages/core/session/src/types.ts:429"]}, + {"digest":"17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3","names":[],"sources":["packages/core/session/src/types.ts:435"]}, {"digest":"18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0","names":["packages/schedule/schedule/src/types.ts#ScheduleDeleteChange"],"sources":["packages/schedule/schedule/src/types.ts:230"]}, {"digest":"18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f","names":["packages/session/session-persistence-jsonl/src/format.ts#HeaderLine"],"sources":["packages/session/session-persistence-jsonl/src/format.ts:83"]}, {"digest":"18fcdd273a2f2f9d813a3be5e71df5903a963a5c88e46b90b14de6e2db890da0","names":[],"sources":[]}, @@ -138,7 +135,7 @@ {"digest":"1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5","names":[],"sources":[]}, {"digest":"1c2686c0682e7bc51d61117b2652bd4a80707e331aa276d6ef915ae32534a330","names":[],"sources":[]}, {"digest":"1c5a58f4599bd4a9711712551ffe2442a5e5fcd0758bafe630cc4ead850bdaa7","names":[],"sources":[]}, - {"digest":"1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d","names":[],"sources":["packages/core/session/src/types.ts:343"]}, + {"digest":"1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d","names":[],"sources":["packages/core/session/src/types.ts:349"]}, {"digest":"1cde1bedde79c5e8a56aee9c05c8e0a579cf5bc611deb85b93fa331a16ca978f","names":["packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget"],"sources":["packages/compaction/compaction-image-offload/src/projection.ts:9"]}, {"digest":"1d5f04a03ea75fc8eeece544129eba0f49f0c82046e9c404461920390135c7b2","names":[],"sources":[]}, {"digest":"1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f","names":[],"sources":["packages/llm/llm/src/types.ts:157"]}, @@ -151,7 +148,7 @@ {"digest":"1ebc6f9ff3aa9e3a38f8695f49bacdab1f8b7a3832741853ca46f40af56ccd01","names":[],"sources":[]}, {"digest":"1f0133217eeeffd8d387baf1ec8ed9856e719bcd999dc08e48624cba96d75fd7","names":[],"sources":[]}, {"digest":"1f1385dbc3e875c021e387c7096d671d606f22daec51b25c80348735b6bbfa10","names":[],"sources":[]}, - {"digest":"1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0","names":["packages/core/session/src/types.ts#TurnEndCancelCause"],"sources":["packages/core/session/src/types.ts:197"]}, + {"digest":"1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0","names":["packages/core/session/src/types.ts#TurnEndCancelCause"],"sources":["packages/core/session/src/types.ts:201"]}, {"digest":"1fdad624334d354820dfb3a26e28d0bc9fc1b62a33f87bab63f715b3f4391bfd","names":[],"sources":[]}, {"digest":"20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5","names":[],"sources":["packages/plan/plan-mode/src/index.ts:53"]}, {"digest":"2142e391b9ccb60d487afcf373ce74db98d952f69e8289eddb352a7e16e996c5","names":[],"sources":[]}, @@ -178,11 +175,9 @@ {"digest":"2aad1179f957631477b582e57cd8f538cb82618fc3fb816bde1171154235b065","names":[],"sources":[]}, {"digest":"2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee","names":[],"sources":[]}, {"digest":"2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095","names":[],"sources":["packages/llm/llm/src/types.ts:473"]}, - {"digest":"2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8","names":[],"sources":[]}, {"digest":"2aed07493b1c65b6d00de5586da1e743cf501684b9ad43a1765fb4007d4ea456","names":[],"sources":[]}, {"digest":"2c4c21f19f426a58f2dac914e34313aa43f8408972de857a486d1f086d5adb6c","names":[],"sources":[]}, {"digest":"2c64d4bc7264fd3f8bed47e96ab8a593bce05a1e4e1c436dcd244a3b7e0c2a9d","names":[],"sources":[]}, - {"digest":"2cf075b013d363ad743b5a4982900678d108499c721bc11255137402003db711","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"2d11ca7b0d4493e244b74eba093227866b33249841e59a1e9bf56afed38604c3","names":[],"sources":["packages/web/web-search-deepseek/src/provider.ts:71"]}, {"digest":"2da725e554cd89301d829ed9dc45a572c3cd1b93b5e91d0d71f007976d8a0863","names":[],"sources":[]}, {"digest":"2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f","names":["packages/schedule/schedule/src/types.ts#ScheduleChange"],"sources":["packages/schedule/schedule/src/types.ts:256"]}, @@ -194,10 +189,11 @@ {"digest":"30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f","names":["packages/session/session-title/src/types.ts#SessionTitleSource"],"sources":["packages/session/session-title/src/types.ts:28"]}, {"digest":"312aee2f0c42aabbae6cc60d92d596d8453c0aeb061c5868d1755a69ef49e69a","names":[],"sources":[]}, {"digest":"31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648","names":["packages/compaction/compaction/src/checkpoint.ts#CompactionCheckpointSource"],"sources":["packages/compaction/compaction/src/checkpoint.ts:22"]}, + {"digest":"3247e566f4340029325b70e4366b56fa9719977f1e39033b769a2ec221ab22a9","names":[],"sources":[]}, {"digest":"326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693","names":["packages/llm/llm/src/call-config.ts#LlmCallConfigAdapterDefaults"],"sources":["packages/llm/llm/src/call-config.ts:54"]}, {"digest":"3305a4bb6b6c29b482ba91d59c60b47e8f98282d3dacc8c29e4a4c2712209bdd","names":[],"sources":[]}, {"digest":"33341aac09312300de5a178e6cc6f6544a4876f7f710cfea55eda473c8d329ef","names":["packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord"],"sources":["packages/llm/llm/src/assistant-stream.ts:20"]}, - {"digest":"335e242de1fcc17b6ca920fc420d71bec2d76e53e37955c00948b65ab77f05c5","names":["packages/core/session/src/types.ts#SurfaceOp"],"sources":["packages/core/session/src/types.ts:464"]}, + {"digest":"335e242de1fcc17b6ca920fc420d71bec2d76e53e37955c00948b65ab77f05c5","names":["packages/core/session/src/types.ts#SurfaceOp"],"sources":["packages/core/session/src/types.ts:470"]}, {"digest":"347de3380f2da308d0c74e08821f9e85559f55af4c5010795b110d00f7fc9dbd","names":[],"sources":[]}, {"digest":"34d1ab5c5df378186d6054b1a1beea9a41e41965d6f336f0bc0025c441a82e66","names":[],"sources":[]}, {"digest":"35203ba7ad5ef6f97d556b85df20ae98f04f09c65748cecdf8eefdb8b6405ffc","names":[],"sources":[]}, @@ -236,7 +232,7 @@ {"digest":"44818000e56acb97afdb5045ce235eb55e3fb1ccabc9fc7d0750bb38f36a7f8e","names":[],"sources":[]}, {"digest":"44c8c77aa9aa100b0d74d2deee85474b29faa6eee0ac0f662f7abe9931ddd7f2","names":[],"sources":[]}, {"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986","names":[],"sources":[]}, - {"digest":"454a0fa879e38076bdcdbd87ef561cd67d7e43e58a287cf7dbaa7b6cf7847020","names":[],"sources":[]}, + {"digest":"45f005e6bbb7645248be792b715a74bc7fdf5a67f5381655d3a75dcc7aa2112e","names":[],"sources":[]}, {"digest":"45f2a07ea94573123b6ae14db787a40062dec3834f36d8be92c523d40ba28448","names":[],"sources":["packages/interaction/user-approval/src/types.ts:44"]}, {"digest":"477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028","names":[],"sources":["packages/compaction/compaction-image-offload/src/projection.ts:25"]}, {"digest":"47e5d84f1a93450a78c7f3954b43990d0f629af7c034a56313e5deb63e772c1b","names":[],"sources":[]}, @@ -252,7 +248,7 @@ {"digest":"4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd","names":["packages/schedule/schedule/src/types.ts#LegacyScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:185"]}, {"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]}, {"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]}, - {"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:194"]}, + {"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:198"]}, {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:380","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, {"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]}, {"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]}, @@ -269,14 +265,14 @@ {"digest":"525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336","names":[],"sources":[]}, {"digest":"529e0ccd0e34079ab4ff21ed87f810cee0388246263bf22e8aba5986b56fae05","names":[],"sources":["packages/util/values/src/index.ts:4"]}, {"digest":"52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734","names":[],"sources":[]}, - {"digest":"5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777","names":[],"sources":["packages/core/session/src/types.ts:377"]}, - {"digest":"530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6","names":["packages/core/session/src/types.ts#RequestHeaderReason"],"sources":["packages/core/session/src/types.ts:275"]}, + {"digest":"5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777","names":[],"sources":["packages/core/session/src/types.ts:383"]}, + {"digest":"530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6","names":["packages/core/session/src/types.ts#RequestHeaderReason"],"sources":["packages/core/session/src/types.ts:279"]}, {"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac","names":[],"sources":[]}, {"digest":"53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492","names":[],"sources":[]}, - {"digest":"54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0","names":[],"sources":["packages/core/session/src/types.ts:357"]}, + {"digest":"54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0","names":[],"sources":["packages/core/session/src/types.ts:363"]}, {"digest":"557e148d30b79ee48645850832b30c717d7c3b64641deb5d6133b8b4e91ee0b8","names":[],"sources":[]}, {"digest":"5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c","names":[],"sources":[]}, - {"digest":"56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23","names":[],"sources":["packages/core/session/src/types.ts:223"]}, + {"digest":"56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23","names":[],"sources":["packages/core/session/src/types.ts:227"]}, {"digest":"570e85022434613394418ad33e77e9f1f7a05699b33d0b62eae0677a638c3833","names":[],"sources":[]}, {"digest":"576a2de8355eb09850963ff5953420b172e274d9283666eb7d889097db120796","names":[],"sources":[]}, {"digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc","names":[],"sources":[]}, @@ -285,17 +281,18 @@ {"digest":"5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80","names":["packages/llm/llm/src/types.ts#FinishReason"],"sources":["packages/llm/llm/src/types.ts:165"]}, {"digest":"587bb11da730e06aa52b62c087d217d6f8f22459b25f31cb9e1e1baf17944e5e","names":[],"sources":[]}, {"digest":"589d0d91239681f89ee485459dc8c2f73ea47183b5534289d966637024a2c35d","names":[],"sources":[]}, + {"digest":"58b122df513aeaf7ddc5783dbb1fd844e2979bb273964e30ae351a8db6510019","names":[],"sources":[]}, {"digest":"58f94390b982f8a5ac1f4f235ba612557f4a1dc4a995d8b1cec63ce7639decb0","names":[],"sources":[]}, {"digest":"5abab3a27fd2736660ecddd94b28d6bbf114986dd894880153bd1be3ccacb563","names":[],"sources":[]}, {"digest":"5b232a9b9bfcc0fc6794dab7865bcf203a65785c5de5aa647a51228bff628413","names":[],"sources":[]}, {"digest":"5b6fb1f226ff56402db08a6aae82af76100b14056271875f69ec526a1ae05d51","names":[],"sources":["packages/interaction/commands/src/types.ts:112"]}, {"digest":"5bb69b88517f6214f2af4d1a6d2b771e271ad3ad695cc9c2c272069ae698c9bc","names":[],"sources":[]}, - {"digest":"5bc2d7f02503fc6b3e1bb7e0b7fa22c0b9e4b9841fa3175b2ad8e607ae3525a8","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"5bc66269cfaf614c0ecda8ecb469f23101a7609793fc2b693ac23c4aecdb575e","names":[],"sources":[]}, {"digest":"5bd310104cfe448e530beb11422d2f791e2e32e8e95fdc528a15f9ca693dae75","names":[],"sources":[]}, {"digest":"5c45bf4c544a7211dcd8ba6ba7e5f1bc39b49e7a9df9d5cbdc8e87c22771b37b","names":[],"sources":[]}, {"digest":"5cb63db9fe57dc09c3a1c2b84c3edf73ff5b62960155d8436a2377a5c43ce32c","names":[],"sources":[]}, {"digest":"5cdbf342b7748d152bc987b68f58e565e5bba4b227e076e2d2120d87662e45cd","names":[],"sources":[]}, + {"digest":"5d00abf938272f22eb47199e9d40801e8d8ec4c88e2b138b15aa2a5a5bf87520","names":[],"sources":[]}, {"digest":"5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78","names":[],"sources":[]}, {"digest":"5d17f27a1ef9f7026c0232714adf7573961536d75f3303ca97973975f5af170e","names":[],"sources":[]}, {"digest":"5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694","names":[],"sources":[]}, @@ -303,6 +300,7 @@ {"digest":"5dd766ccd1702a1142ad8b4bd44f2242f8e8a6752669ad3de0a5d224329b1438","names":[],"sources":["packages/compaction/compaction/src/types.ts:82"]}, {"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e","names":[],"sources":[]}, {"digest":"5ebce1317eacd2c148456627a09668e927ee15c204f0643f5ea76b46b54a2647","names":[],"sources":[]}, + {"digest":"5ef58f2a1bdc576c1f12fa1f4da1920ece020b13c0f991a578cddcbeb28bc5be","names":[],"sources":[]}, {"digest":"5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca","names":["packages/llm/llm/src/types.ts#LlmFailure"],"sources":["packages/llm/llm/src/types.ts:41"]}, {"digest":"5f26a6c20b37632f8f57729d171c671def4683d994ac6257a8dffcd855101627","names":[],"sources":[]}, {"digest":"5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564","names":["packages/subagent/subagent/src/descriptor.ts#OneShotSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:61"]}, @@ -328,10 +326,10 @@ {"digest":"6ab45ac76839722df912f3ba50f1c6b41f2d614bde11f1aabfae04117ccdc80d","names":[],"sources":[]}, {"digest":"6b23ed9ce1b397e602788dbe033693bf97642a69e23fcab1cc45bf6f13b6d619","names":[],"sources":[]}, {"digest":"6bbe793f6a93dc8d50a4d4d3fbb6f8ed15650bcb7e151d28669628f7b7c3fc25","names":["packages/goal/goal/src/types.ts#GoalSnapshot"],"sources":["packages/goal/goal/src/types.ts:60"]}, - {"digest":"6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5","names":[],"sources":["packages/core/session/src/types.ts:332"]}, - {"digest":"6c03546143cdf6ae88f1745ac98c5139efa1588d1415249bbc5f3104d32ab800","names":[],"sources":["packages/core/session/src/types.ts:192"]}, + {"digest":"6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5","names":[],"sources":["packages/core/session/src/types.ts:338"]}, + {"digest":"6c03546143cdf6ae88f1745ac98c5139efa1588d1415249bbc5f3104d32ab800","names":[],"sources":["packages/core/session/src/types.ts:196"]}, {"digest":"6c1cccb433ddc153facb380c7dda956237ba5c31f8a532a69142dc277890cc9b","names":[],"sources":[]}, - {"digest":"6c5a37d42ebdb12db4410699259335f1e149e3a0260425684c00b153afc8990d","names":[],"sources":["packages/core/session/src/types.ts:193"]}, + {"digest":"6c5a37d42ebdb12db4410699259335f1e149e3a0260425684c00b153afc8990d","names":[],"sources":["packages/core/session/src/types.ts:197"]}, {"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]}, {"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]}, {"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]}, @@ -351,16 +349,19 @@ {"digest":"71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:28"]}, {"digest":"72ec79127a9c0d0241b1106a74cc2b24c81ce467170f1c3c93f7b71a9496d227","names":[],"sources":[]}, {"digest":"7382c4fc95fe8c5db65ca4d9da572b115d0dbb818b7f32c22ab040318ffbb4bc","names":[],"sources":[]}, + {"digest":"7404c6123fdaa591474a37979a5a8ae70d1387af7c9f09bfe568b218935a974d","names":[],"sources":["packages/core/session/src/types.ts:317"]}, {"digest":"750a2b129d67704df675c06823c20c80b65cbae89d5aaa914fd5a72e3df395b5","names":["packages/jobs/jobs/src/types.ts#JobSettleCause"],"sources":["packages/jobs/jobs/src/types.ts:247"]}, {"digest":"7554565be416b0342053ee6a46256c9dab07c487349b4a5f10bab1edc151c619","names":[],"sources":[]}, {"digest":"756b5e23a5cb2c7f486f366a828f7461c88902af21125bd6810881f4ccbff6cc","names":[],"sources":[]}, + {"digest":"7580e7d96dc8475fffe813a42af499665de3d4517fcdb3bc2337d0340e98d616","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"7624ca28c61ca0eaa4f015140f3cea6e0c16cce2f2960fab5f531203d253903e","names":[],"sources":[]}, {"digest":"763c8a20af487263a0080548274f7437ef4a86e0ba8769127d1ced10a72c664d","names":[],"sources":[]}, {"digest":"76632533f67879dcb346fcae640540dbc89b2472b0bce88c59f7e48fc5c0e668","names":[],"sources":[]}, {"digest":"78a9be340090b735a39980e7222ce730c972fbcf1de7c9e0aef2125884b7caee","names":[],"sources":[]}, - {"digest":"79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985","names":[],"sources":["packages/core/session/src/types.ts:214"]}, - {"digest":"79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056","names":[],"sources":[]}, + {"digest":"78b9204976ab9faaf133c3488b73d1dfb6f1a4bb6c0903e3473cd41a5e1e39e1","names":[],"sources":[]}, + {"digest":"79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985","names":[],"sources":["packages/core/session/src/types.ts:218"]}, {"digest":"79ddc907312f97cf9553d4841e8b3fc937097bd8d97900a790efad3c05255e99","names":[],"sources":["packages/todo/tool-todo/src/types.ts:33"]}, + {"digest":"7a5b68bd1a5118c827df9e2466acc5e4d16b83e3f97a2304d992958647592690","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"7a651f3b672c52fb615f22c0ce9cb3cc5b22aad92e6bbeefb0808fd7b9233a50","names":[],"sources":[]}, {"digest":"7af85bf70d4eafce63e739adae38ea85501e68df3a0e22f97f3a0be90fd9e4cb","names":[],"sources":[]}, {"digest":"7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a","names":[],"sources":[]}, @@ -431,7 +432,6 @@ {"digest":"9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114","names":[],"sources":[]}, {"digest":"96bfb916a727cd4445a594df264c523fc039749a31bfed924b8cc7bb308a5bb0","names":[],"sources":[]}, {"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:40"]}, - {"digest":"97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c","names":[],"sources":[]}, {"digest":"9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e","names":[],"sources":["packages/sandbox/sandbox-policy/src/session-mode.ts:33"]}, {"digest":"99135d8a3937977955eaac5f8f04f930dcd1562f8450a54a51cde9b921dd59a6","names":[],"sources":[]}, {"digest":"9947aa5938fe19a895298da412917acbf9ffb5093a73ab8baa257e6ae9e63a07","names":[],"sources":[]}, @@ -440,7 +440,7 @@ {"digest":"99f0e98184709ad10f27bfdeaafb4457d9d2b413f402331764798cdb7d3cbe4d","names":[],"sources":[]}, {"digest":"9a2a9029f8d7ede05336980d8342737557f487b913bfd28853d0ab5214600ab5","names":[],"sources":["packages/web/web-search-deepseek/src/provider.ts:64"]}, {"digest":"9a410ddf56792b2b7cd4a224a2e54dc4dee30a8e8e6b1c2b9a1d19992b217adc","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowAgentStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:20"]}, - {"digest":"9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7","names":["packages/core/session/src/types.ts#EpochHeader"],"sources":["packages/core/session/src/types.ts:242"]}, + {"digest":"9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7","names":["packages/core/session/src/types.ts#EpochHeader"],"sources":["packages/core/session/src/types.ts:246"]}, {"digest":"9ac52f0f84fec2eb9f71c2937e497d759db5ca0da4836d93a679d113684141f2","names":[],"sources":[]}, {"digest":"9acbe713f3f641921dd3333b240aa72e176d795327c24e8d9db9b68b16771b49","names":[],"sources":[]}, {"digest":"9b4fd974c124c83743efe030ba4e1250e087505f3f74588716a521fb8d453267","names":[],"sources":[]}, @@ -464,6 +464,7 @@ {"digest":"a2be0f0cb59f0d4a06557d45847d1938aced238baa77a6da416e590b8b958553","names":[],"sources":[]}, {"digest":"a3a259a1aa5401af786ce08c4a8419681d3000d73467991b5fc563389bc4ebd2","names":[],"sources":[]}, {"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]}, + {"digest":"a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, {"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]}, {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/experimental/agent-team/src/types.ts:232"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, @@ -471,12 +472,11 @@ {"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]}, {"digest":"a720757f704a2512e9d2f20da4a093e582472176236a75ab8b8eb7aaff17d85a","names":[],"sources":[]}, {"digest":"a7cf43ce7c2a4c038feed1885cd7a00d5c6ee2d90a7e0d56b46f78a3e1ca327f","names":[],"sources":[]}, - {"digest":"a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735","names":[],"sources":["packages/core/session/src/types.ts:385","packages/core/tool-executor-ptc/src/types.ts:27"]}, + {"digest":"a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735","names":[],"sources":["packages/core/session/src/types.ts:391","packages/core/tool-executor-ptc/src/types.ts:27"]}, {"digest":"a853902cfab417b8f08aa51ad6855fea46f34ed4588a74e47403035111d0fb86","names":[],"sources":["packages/interaction/commands/src/types.ts:105"]}, {"digest":"a8a2b9011fc067862df7ce40748868467e5778283d7ac193c13f77684a914c57","names":[],"sources":[]}, {"digest":"a9059a81430b320792bbb7c2ee1f8f6c77ff71e40ee44c18aec72e157c522694","names":[],"sources":[]}, {"digest":"a926dc71814edd01a4f041eb5fc766d153db3407ef4704e98f4c1b6527309ebb","names":["packages/schedule/schedule/src/types.ts#LegacyEveryScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:173"]}, - {"digest":"a927d444c928ac0efd9dd8b2e7d7d04d711c264cb71578d81e0e2fd143ff669f","names":[],"sources":[]}, {"digest":"a99ec0993c62c058e50af9f68922202a3b54f5e259bc2abf1915a73381d2316d","names":[],"sources":[]}, {"digest":"a99f76fd149051c363f960bfeeb5b9509587ecaf7591a42797e4bfe810d8987a","names":["packages/todo/tool-todo/src/types.ts#TodoItem"],"sources":["packages/todo/tool-todo/src/types.ts:23"]}, {"digest":"aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8","names":[],"sources":[]}, @@ -485,14 +485,14 @@ {"digest":"aa93010d0e521ad7c42273f58d5c9b8bf1dfc94edf1b805a3971b364a35c1877","names":[],"sources":[]}, {"digest":"aacda84a69739238aa782eea902d3073017d9d74b9808de2bf5a83c9738cbdc6","names":[],"sources":[]}, {"digest":"aae343bc3d663c83f2e8b5e1c7eea5d509dc7a5361bc8bd8b5ac3bb8d3b1ed56","names":[],"sources":[]}, - {"digest":"ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7","names":[],"sources":["packages/core/session/src/types.ts:216","packages/llm/llm/src/types.ts:159"]}, + {"digest":"ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7","names":[],"sources":["packages/core/session/src/types.ts:220","packages/llm/llm/src/types.ts:159"]}, {"digest":"aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba","names":[],"sources":[]}, {"digest":"abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c","names":["packages/web/web-search-deepseek/src/provider.ts#DeepSeekSearchLlmRequest"],"sources":["packages/web/web-search-deepseek/src/provider.ts:55"]}, - {"digest":"ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585","names":[],"sources":["packages/core/session/src/types.ts:392"]}, + {"digest":"ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585","names":[],"sources":["packages/core/session/src/types.ts:398"]}, {"digest":"ad22a5ca5bbaab2e1440779cd3648e85ddfc6f73b5207394eda699555cf31430","names":[],"sources":[]}, {"digest":"ad3b56aa2fc1ad4250e7399295d34a8eae592e1907e85852e2291b2e498a30bc","names":[],"sources":[]}, {"digest":"ad92f473a03ae72599ff26c86e897c25e16269519ad2a87340f0bea946517af8","names":[],"sources":[]}, - {"digest":"ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05","names":["packages/interaction/commands/src/types.ts#CommandSource"],"sources":["packages/core/session/src/types.ts:191","packages/interaction/commands/src/types.ts:75","packages/interaction/commands/src/types.ts:79","packages/llm/llm/src/message.ts:111","packages/session/session-title/src/types.ts:35"]}, + {"digest":"ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05","names":["packages/interaction/commands/src/types.ts#CommandSource"],"sources":["packages/core/session/src/types.ts:195","packages/interaction/commands/src/types.ts:75","packages/interaction/commands/src/types.ts:79","packages/llm/llm/src/message.ts:111","packages/session/session-title/src/types.ts:35"]}, {"digest":"ade057f4fdc387aec6426f48704a1f9359cfc6fee13f27735b07f9b215ec16ca","names":[],"sources":[]}, {"digest":"ae63d9d1080ed8945031594d93f8d96651d7101b848719cc3e1475875cd3d649","names":[],"sources":[]}, {"digest":"aec150797b655c5f15ebb7a501454b0c208fbaccec9ace49d7de2171fc02f47d","names":[],"sources":[]}, @@ -502,15 +502,17 @@ {"digest":"b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d","names":[],"sources":[]}, {"digest":"b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1","names":[],"sources":[]}, {"digest":"b0b0eb1b16490f6bee135b2377e8f8c39227974c799ad7d49ff418af23ecbc35","names":["packages/attachment/attachment/src/types.ts#ImageAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:11"]}, - {"digest":"b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081","names":["packages/core/session/src/types.ts#RequestContext"],"sources":["packages/core/session/src/types.ts:256"]}, + {"digest":"b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081","names":["packages/core/session/src/types.ts#RequestContext"],"sources":["packages/core/session/src/types.ts:260"]}, {"digest":"b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7","names":[],"sources":[]}, {"digest":"b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1","names":[],"sources":["packages/experimental/agent-team/src/types.ts:230"]}, {"digest":"b1f62380f6746fedd629e71065e3583cef83334875b499ac9665c2eb87acfbf5","names":[],"sources":[]}, {"digest":"b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e","names":[],"sources":[]}, {"digest":"b2245a906e13aa94f2d3cc6cf7b04cfa0267f7b7f4d468de1b00b1a314e54626","names":[],"sources":[]}, {"digest":"b2a5c8c008a31e3dc6c3013502f2356aca13894b31c8e70601fa92f2d40d1dba","names":[],"sources":[]}, + {"digest":"b2a948a5340a9341fa3bab99507c7090c5b0456de72ec28b5b157b8df3f1d640","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943","names":["packages/experimental/agent-team/src/types.ts#TeamTaskStatus"],"sources":["packages/experimental/agent-team/src/types.ts:72"]}, {"digest":"b3681c026a0b98e98b103f057dfd217fbe904129c090efd668253ec9aea7fece","names":[],"sources":[]}, + {"digest":"b36fd842cd004b532ce96fd5d6f5be28e017eda043131aa3cf5e69edc436108e","names":[],"sources":[]}, {"digest":"b38f206d59bad891fa58c89f97a0036987b3defe7685acbd318fa55eb85f9751","names":[],"sources":[]}, {"digest":"b4ec0af39fd52ed971990e42f04e5be3204882e555c409eb330f8238eec21569","names":[],"sources":[]}, {"digest":"b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf","names":[],"sources":[]}, @@ -519,12 +521,14 @@ {"digest":"b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4","names":["packages/session/session-title/src/types.ts#SessionTitleEventData"],"sources":["packages/session/session-title/src/types.ts:41"]}, {"digest":"b57a2889408de36e4516807194c73bdecd51a92f633ccd05db81bf049200f420","names":[],"sources":[]}, {"digest":"b57ae1b37e52bee0a21cbdace168dc6a15582bcda8c306dad962c5dcc8a8e28b","names":[],"sources":[]}, + {"digest":"b613f83572fe2fbf7259c72308cafd008f4fceea897749128a48bcd158f290aa","names":[],"sources":[]}, {"digest":"b63b089a00b3b9357675b3e0c3d3e9aa4fdbda1fba9c91a6b0c59ffdbb89f505","names":["packages/schedule/schedule/src/types.ts#LegacyAtScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:167"]}, {"digest":"b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a","names":[],"sources":[]}, - {"digest":"b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0","names":[],"sources":["packages/core/session/src/types.ts:203"]}, + {"digest":"b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0","names":[],"sources":["packages/core/session/src/types.ts:207"]}, {"digest":"b6f94c17dd9bbd7cd57015198b7c7c88407f0e3b89faab176ca3c11c3d864d3e","names":[],"sources":[]}, {"digest":"b70f3e81fe11b7a5f2504fb47a4502fb8d9c8f0731fdfe8ddcaf9b41f5a6e196","names":[],"sources":[]}, {"digest":"b765ee31033cea463863faf3fc6a0a1b85c47c6a2f13fe1ce833abe56105114b","names":[],"sources":[]}, + {"digest":"b782058cef83caf3eb44a16adeef5657e4d5cdff2d50655aba418c4490670a69","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"b79641a9d25e756636dc0c706fdf3c4583ccb7d4920e009e6e86c0759cf4de4c","names":[],"sources":[]}, {"digest":"b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5","names":[],"sources":[]}, {"digest":"b815d7b91cdd9fc6ccc7dcba05cfc5ce1d55fab5fc0c0801803cd3630cedb602","names":[],"sources":[]}, @@ -534,6 +538,7 @@ {"digest":"b9305f0ea75ccd6a6061c27fde19f2ab85534f9f3c1a717b573b077ebae0f611","names":[],"sources":[]}, {"digest":"b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26","names":[],"sources":[]}, {"digest":"b993441f8ce7d27b80e619e113e232ae8e62d3d5a8340f6bfe12d8c4c018e62f","names":[],"sources":[]}, + {"digest":"ba2a1a8f194b1bb21f270713398f6643e206e8ff5f7f26f6d36a88e3f0f0ab10","names":[],"sources":[]}, {"digest":"ba4c40b4381e0dcdfab8fc9fc75b5c9b3e8f09ef56deeb1bec8e703b0ef8cd6d","names":[],"sources":[]}, {"digest":"ba77b58995e36f14bb13886b0493d0a1a63d5db10b1a6cd9f6ac65a72cf832ec","names":[],"sources":[]}, {"digest":"bab68657940075483f086fffb711682a45bcc99c43d659fbe1c36af21ac05dd7","names":[],"sources":[]}, @@ -542,7 +547,7 @@ {"digest":"bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3","names":[],"sources":[]}, {"digest":"bb94f3217aa56c8876bbff0b8460d6a73cbead545659abb29e76cd0cf2cb92d6","names":[],"sources":[]}, {"digest":"bbc5696e3f13c758237b316e3cf08cda0b9fd51ffe672539883457769faa185d","names":[],"sources":[]}, - {"digest":"bcf0caf62d964b2fcf5404bd5c909cfa9a21c3f3c96e6b7e36d9e33565223825","names":[],"sources":["packages/core/session/src/types.ts:466"]}, + {"digest":"bcf0caf62d964b2fcf5404bd5c909cfa9a21c3f3c96e6b7e36d9e33565223825","names":[],"sources":["packages/core/session/src/types.ts:472"]}, {"digest":"bd499eac047efe091a4df7382eadd06d25cf0cb31fee3981e8b24d85762841cd","names":[],"sources":[]}, {"digest":"bf16d2e98a7f13894ded2cffe5ce857bf473954a0e3c0ed9bc7739e2e1c7b78a","names":[],"sources":[]}, {"digest":"c0ea97bc89dfc46fa94eab956f5c3ee6af2f964aa7db6327cb18b451f207bef7","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowAgentEndData"],"sources":["packages/workflow/tool-workflow/src/types.ts:29"]}, @@ -568,6 +573,7 @@ {"digest":"cbca49bde16b656d5b8fbe4a8a507769f7b594c6287782b809c03814941bc5e3","names":[],"sources":[]}, {"digest":"cd186c309e2bef305705e1d324139240f5e681f4f6a957d6a25b0225f65560df","names":[],"sources":[]}, {"digest":"cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc","names":[],"sources":[]}, + {"digest":"cda79b839d8cc0c7a7bd09dd067de04925f6e9037080d878b65aaa5a383f117c","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"cdca80b8dad24d4e7b1d5a288a8475ba8d8d46be385bd2da85f122ee836f1670","names":[],"sources":[]}, {"digest":"cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced","names":[],"sources":["packages/llm/llm-retry/src/types.ts:17"]}, {"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:21"]}, @@ -592,9 +598,11 @@ {"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965","names":[],"sources":[]}, {"digest":"dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd","names":[],"sources":["packages/core/tool-executor-ptc/src/types.ts:68"]}, {"digest":"dbdafceb9a900183cccc80596498d386f3bf7bddead2b8cfde68453aa4739edc","names":[],"sources":[]}, + {"digest":"dd00cdccd0ba487b92362ef67db58cd264f48c1bfd368291424a1da9a899489a","names":[],"sources":[]}, {"digest":"dd2780e7e07684332f7a9ab047237798d260e7eaffb1321cb38eca695d184c3d","names":[],"sources":[]}, {"digest":"dd5ee8762bd8a14b6c6bffedd1753e4e78505bb0f9ea1621018d03a54eef59d5","names":[],"sources":[]}, {"digest":"dd6cc2bb2a8f28248347021ad93565360b196a042360b4cd16b569d3a2a91f2d","names":[],"sources":[]}, + {"digest":"dde0cdf9d426768b649b5a9783d2144e7f900c35ef65229cd89036dbf411d1b0","names":[],"sources":[]}, {"digest":"de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f","names":[],"sources":[]}, {"digest":"de30b1fa12c75f81df6aa4096e1595296d2d04e2be14a7c29849cf8028b79048","names":[],"sources":[]}, {"digest":"deaae5cd30987d4f49398e8f9d994c125907e7f928300745fb1dbf15f145d0e7","names":[],"sources":[]}, @@ -613,8 +621,8 @@ {"digest":"e41bdebe246abe17154de7e7a1d81035437eee8d20e7f1edff0d91e2598ad828","names":[],"sources":[]}, {"digest":"e42b0f40b5d0b628c6e1c44a8fb9eda0780037a81959e1b93d2dfb47b46b0e87","names":[],"sources":[]}, {"digest":"e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498","names":[],"sources":[]}, - {"digest":"e481c46af3813e0010558e93f578b2986acd658404cec2d1679e3ac2a6c48654","names":[],"sources":["packages/core/session/src/types.ts:299"]}, - {"digest":"e4c18e294232c3ba6c9f1999f168263cc55956147cfe4121720899be8e52edd1","names":[],"sources":["packages/core/session/src/types.ts:363"]}, + {"digest":"e481c46af3813e0010558e93f578b2986acd658404cec2d1679e3ac2a6c48654","names":[],"sources":["packages/core/session/src/types.ts:303"]}, + {"digest":"e4c18e294232c3ba6c9f1999f168263cc55956147cfe4121720899be8e52edd1","names":[],"sources":["packages/core/session/src/types.ts:369"]}, {"digest":"e4cac694f8b6e1d52e48c699464f1c4c26111ef87e558f7931040d00fdc8d486","names":[],"sources":[]}, {"digest":"e5d3828df1ec3e2a66879de0659e0f4866d9cfffc00825ee731a66f8a0c03d98","names":[],"sources":[]}, {"digest":"e6063d0451dc02026fd9b54ad3f77c7b1b6392ad3e872a7737ac84e13ca72344","names":["packages/schedule/schedule/src/types.ts#EveryScheduleDispatchChange"],"sources":["packages/schedule/schedule/src/types.ts:244"]}, @@ -624,24 +632,26 @@ {"digest":"e7ce49f12953583fad217bf80fb9f1463acff2c3e9471a823782661675fb124c","names":[],"sources":[]}, {"digest":"e7e154d47d5fbc5366330fb4dade1f108c6dfe4335044d21ded14b7bf1dce084","names":[],"sources":[]}, {"digest":"e7e8f1318170b8284f16ab45a557afd4675c96071c0f2a456636481dab20b427","names":[],"sources":[]}, + {"digest":"e7f4cbb3a57f4cbdb61ea41e95bcba626ad1fff6bb9b1e063b1d50a32da6e864","names":[],"sources":[]}, {"digest":"e8de0f457fed5b9ff94dc4950b59ef75fd8ba8af665cc7899485924a62653367","names":[],"sources":[]}, {"digest":"e95f957154b2b1b58692af53d4131180b6dd19e2264869f876dbaa959991494e","names":[],"sources":[]}, {"digest":"ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403","names":[],"sources":["packages/experimental/agent-team/src/types.ts:234"]}, - {"digest":"eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c","names":[],"sources":["packages/core/session/src/types.ts:197"]}, + {"digest":"eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c","names":[],"sources":["packages/core/session/src/types.ts:201"]}, {"digest":"ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8","names":[],"sources":[]}, {"digest":"ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93","names":[],"sources":[]}, - {"digest":"ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7","names":[],"sources":["packages/core/session/src/types.ts:230"]}, + {"digest":"ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7","names":[],"sources":["packages/core/session/src/types.ts:234"]}, {"digest":"edbbfff1128099fda7b792c56330d2cb04debc9790988e66d1c3f4026e813e0e","names":[],"sources":[]}, {"digest":"edd37b1e4974af648633014435572089b1e8fb71910468a0767a7e28fdd22abf","names":[],"sources":[]}, {"digest":"ee577a0cc18664c9daee50e9455153321155604f8f1817362f8f4a4b6fb9e1ad","names":[],"sources":[]}, {"digest":"eea687d437880bd5f55fb447a561112800ca34334c0724d9cecb98493059d9e4","names":["packages/schedule/schedule/src/types.ts#ScheduleCreateChange"],"sources":["packages/schedule/schedule/src/types.ts:223"]}, {"digest":"eefbea819caeeea91ccb02dfafc16c68dd7f961cf9c1b2be25a2084509cf5c5a","names":[],"sources":[]}, - {"digest":"ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545","names":[],"sources":["packages/core/session/src/types.ts:301","packages/core/session/src/types.ts:303"]}, + {"digest":"ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545","names":[],"sources":["packages/core/session/src/types.ts:305","packages/core/session/src/types.ts:307"]}, {"digest":"ef226a90ddac6cd8e631a3a337a3a3e27f9494be92befb7c718b404681d75e21","names":[],"sources":[]}, {"digest":"ef4f00923b9edbae15c7ef05320280bf61147ca17146cf59690359bfd5e5da45","names":[],"sources":[]}, + {"digest":"efa1a091017ce533e38b0ef3e14d11a5c102ff1793229b18dad719a4268d9adc","names":[],"sources":[]}, {"digest":"f05fb85e715af4ab4de0b6ba111166dd0435bd8bb74a0fa0f32b9d174113fc7c","names":[],"sources":[]}, {"digest":"f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b","names":["packages/core/agent/src/types.ts#ModelSelection"],"sources":["packages/core/agent/src/types.ts:79"]}, - {"digest":"f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402","names":["packages/core/session/src/types.ts#TurnEndReason"],"sources":["packages/core/session/src/types.ts:234"]}, + {"digest":"f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402","names":["packages/core/session/src/types.ts#TurnEndReason"],"sources":["packages/core/session/src/types.ts:238"]}, {"digest":"f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e","names":[],"sources":[]}, {"digest":"f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6","names":["packages/attachment/attachment/src/types.ts#FileAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:39"]}, {"digest":"f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515","names":[],"sources":["packages/llm/llm/src/types.ts:476"]}, @@ -650,7 +660,6 @@ {"digest":"f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef","names":["packages/llm/llm/src/types.ts#FileBlock"],"sources":["packages/llm/llm/src/types.ts:98"]}, {"digest":"f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5","names":["packages/llm/llm/src/types.ts#SystemPromptUpdate"],"sources":["packages/llm/llm/src/types.ts:414"]}, {"digest":"f4a66f654996c093aae700ea4c434ff2ee3569fea9c0e5813ccf6afdccabd163","names":[],"sources":[]}, - {"digest":"f4ed4717b4c259e933b550a8dcd90dc93628cda8a3c7affcaa53b9c13c1d53eb","names":[],"sources":["packages/core/session/src/types.ts:313"]}, {"digest":"f5520c1d261ca86ea03735799c311a0a9ede8babb588cf0a65422576d2ee0ad1","names":[],"sources":[]}, {"digest":"f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f","names":["packages/context/session-reference/src/types.ts#SessionReferenceSource"],"sources":["packages/context/session-reference/src/types.ts:13"]}, {"digest":"f6329fb30c7ea8013e8975ca5e78284fb7685cc6bd620e8aa4e9a99bc81d45eb","names":[],"sources":[]}, @@ -663,10 +672,9 @@ {"digest":"f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87","names":["packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent"],"sources":["packages/subagent/subagent/src/catalog.ts:28"]}, {"digest":"f789af5ab844827eb5a4f357c0afae1eeed108c6c22d8ca1a56e586b349a7c85","names":[],"sources":[]}, {"digest":"f88f1b7b6bb733f685f5fb6a8744a5db9199fa53c90463ea1a33bad51153671d","names":[],"sources":[]}, - {"digest":"f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d","names":[],"sources":["packages/core/session/src/types.ts:290","packages/deliverables/workspace-changes/src/types.ts:106"]}, + {"digest":"f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d","names":[],"sources":["packages/core/session/src/types.ts:294","packages/deliverables/workspace-changes/src/types.ts:106"]}, {"digest":"f97f47b0eb87905ed77cc27c5e4ed75ef805cb372c98905253709de1aab9b63c","names":[],"sources":[]}, {"digest":"f9cfc89e8fe085b4fcc9cd32b74b6f103608c54d7d8cd97b47c01de34f252ba3","names":[],"sources":[]}, - {"digest":"fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be","names":["packages/llm/llm/src/message.ts#AssistantMessage"],"sources":["packages/llm/llm/src/message.ts:167"]}, {"digest":"fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd","names":["packages/sandbox/sandbox/src/index.ts#SandboxMode"],"sources":["packages/sandbox/sandbox/src/index.ts:30"]}, {"digest":"fab35c4520883c0d69e42d079c9f543fc5ac7a2d15b537b5572fcd99059c7018","names":[],"sources":[]}, @@ -684,18 +692,16 @@ {"digest":"fee1caf8ab6a65c1bc981f5a1730164490f22a2a89ffa3d0b76f1f432c44c8b5","names":[],"sources":[]} ], "nodes": { - "00c054d3018c198d": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"a927d444c928ac0e","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "0116b1f89c05221d": {"kind":"literal","value":"web_search_20250305"}, "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, "023a355b2be0d688": {"kind":"object","properties":[{"name":"kind","type":"254dce209e735f9c","optional":false}],"indices":[]}, "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, "0341d4c3dfcd5325": {"kind":"literal","value":"product-interaction"}, - "040cd65423bd6426": {"kind":"union","types":["09f3f8dc417f2782","0f6265188dae7ba6","2cf075b013d363ad","4b259f7f8f0a44ad","fa87537f4a9c1e25"]}, + "05205074b24d8340": {"kind":"literal","value":"action-cards"}, "05a2796621b1ee6c": {"kind":"literal","value":"unknown"}, "05d2ae1e0c7f8770": {"kind":"union","types":["1571525adc95a24e","9e07693fd7cd9bab"]}, "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, - "0755b7d1128319ba": {"kind":"object","properties":[{"name":"data","type":"fa106c5b33b657e8","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "07e1a58c58b593f5": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "07eccc9ea2f9b543": {"kind":"literal","value":"security-privacy-permission"}, "09655b34d7f3cf1e": {"kind":"literal","value":"system-prompt"}, @@ -717,7 +723,6 @@ "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "0f6265188dae7ba6": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"5bc2d7f02503fc6b","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "0f8512903d94f57a": {"kind":"object","properties":[{"name":"data","type":"e481c46af3813e00","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4312134fc6f9cad5","optional":false}],"indices":[]}, "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, "1033093edd0db80f": {"kind":"object","properties":[{"name":"data","type":"1ccb56061942f5cc","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"6ab45ac76839722d","optional":false}],"indices":[]}, @@ -794,11 +799,9 @@ "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, "2abb711460e6238d": {"kind":"object","properties":[{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"6f091fe32ab24407","optional":false}],"indices":[]}, - "2ae8594a998c9862": {"kind":"object","properties":[{"name":"data","type":"2cf075b013d363ad","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "2aed07493b1c65b6": {"kind":"literal","value":"tool/ptc-dispatch-start"}, "2c4c21f19f426a58": {"kind":"literal","value":"unavailable"}, "2c64d4bc7264fd3f": {"kind":"literal","value":"step/start"}, - "2cf075b013d363ad": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"5bc2d7f02503fc6b","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "2d11ca7b0d4493e2": {"kind":"object","properties":[{"name":"max_uses","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"935c8d486292b13c","optional":false},{"name":"type","type":"0116b1f89c05221d","optional":false}],"indices":[]}, "2da725e554cd8930": {"kind":"literal","value":"fork"}, "2e3523541e942ef6": {"kind":"union","types":["18dc21f12bbac8ad","8e8e91646a5cae3f","e6063d0451dc0202","eea687d437880bd5"]}, @@ -810,6 +813,7 @@ "30d5a537b2966857": {"kind":"union","types":["1b5649238876e288","ada310bf0bdb8fed","b81e9de81a481b24"]}, "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "3247e566f4340029": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "326a9bfda27dda58": {"kind":"object","properties":[{"name":"maxTokens","type":"dacf3d5f3e4ff619","optional":true},{"name":"reasoningEffort","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, "3305a4bb6b6c29b4": {"kind":"union","types":["0341d4c3dfcd5325","07eccc9ea2f9b543","5bd310104cfe448e","7180844e1066230c","9d80129e13edfb6f","e4cac694f8b6e1d5","f05fb85e715af4ab"]}, "33341aac09312300": {"kind":"union","types":["3860b4757d512a28","487b65c37bfedb8b","71bb3254ec8f1e49","e72269f931559e90"]}, @@ -852,7 +856,7 @@ "44818000e56acb97": {"kind":"literal","value":"feedback/message-delete"}, "44c8c77aa9aa100b": {"kind":"literal","value":"subagent/catalog"}, "4513e088d43e6c68": {"kind":"object","properties":[{"name":"data","type":"ef11a3b7fa2033ae","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"2c64d4bc7264fd3f","optional":false}],"indices":[]}, - "454a0fa879e38076": {"kind":"array","element":"2cf075b013d363ad"}, + "45f005e6bbb76452": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "45f2a07ea9457312": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":true},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"reason","type":"4cd48f3e5108bd6e","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "477614bca595a9e7": {"kind":"object","properties":[{"name":"targets","type":"9e2c06d8653585b3","optional":false}],"indices":[]}, "47e5d84f1a93450a": {"kind":"literal","value":"session-log-deepseek/delivery-accepted"}, @@ -901,17 +905,18 @@ "5808e36c95355f0e": {"kind":"union","types":["1d76406cbb2ddadb","287211846beea1ec","693f4952fb5565da","8e31bb958e9b015e","ab7ec5f9eb2c6dfa"]}, "587bb11da730e06a": {"kind":"literal","value":"session/title-llm-request"}, "589d0d91239681f8": {"kind":"literal","value":"developer"}, + "58b122df513aeaf7": {"kind":"object","properties":[{"name":"data","type":"b782058cef83caf3","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "58f94390b982f8a5": {"kind":"literal","value":"image/png"}, "5abab3a27fd27366": {"kind":"literal","value":"text-delta"}, "5b232a9b9bfcc0fc": {"kind":"literal","value":"after"}, "5b6fb1f226ff5640": {"kind":"object","properties":[{"name":"commandId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"1ab2ef1296f157ba","optional":false},{"name":"sourceEventSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, - "5bc2d7f02503fc6b": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","deaae5cd30987d4f","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fc2ae6962f9f8801","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, "5bd310104cfe448e": {"kind":"literal","value":"resource-cost"}, "5c45bf4c544a7211": {"kind":"object","properties":[{"name":"data","type":"8a9c990773f798ea","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"ae63d9d1080ed894","optional":false}],"indices":[]}, "5cb63db9fe57dc09": {"kind":"literal","value":"teardown"}, "5cdbf342b7748d15": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "5d00abf938272f22": {"kind":"array","element":"b782058cef83caf3"}, "5d03ba38734809bc": {"kind":"array","element":"0657d3ca5ac4e7bf"}, "5d17f27a1ef9f702": {"kind":"literal","value":"tool-workflow/agent-end"}, "5d542ac7bdf13310": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"version","type":"93d38436347f67c9","optional":false}],"indices":[]}, @@ -919,6 +924,7 @@ "5dd766ccd1702a11": {"kind":"object","properties":[{"name":"shadowedRange","type":"9e41386b3a0c9572","optional":false},{"name":"shadowedSeqs","type":"5d03ba38734809bc","optional":false},{"name":"shadowedTokenCount","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "5e6db6e24948d4a8": {"kind":"object","properties":[{"name":"data","type":"17d1afb770d99419","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1c2686c0682e7bc5","optional":false}],"indices":[]}, "5ebce1317eacd2c1": {"kind":"literal","value":"subagent/descriptor"}, + "5ef58f2a1bdc576c": {"kind":"object","properties":[{"name":"data","type":"7404c6123fdaa591","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "5f2117d1856a5ab4": {"kind":"object","properties":[{"name":"code","type":"4cd48f3e5108bd6e","optional":false},{"name":"message","type":"4cd48f3e5108bd6e","optional":false},{"name":"offloadImages","type":"0657d3ca5ac4e7bf","optional":true},{"name":"providerRetryAfterMs","type":"0657d3ca5ac4e7bf","optional":true},{"name":"requestId","type":"4cd48f3e5108bd6e","optional":true},{"name":"status","type":"0657d3ca5ac4e7bf","optional":true}],"indices":[]}, "5f26a6c20b37632f": {"kind":"object","properties":[{"name":"data","type":"9a410ddf56792b2b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"ad92f473a03ae725","optional":false}],"indices":[]}, "5f758f3de5f00c18": {"kind":"object","properties":[{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, @@ -967,16 +973,19 @@ "71bb3254ec8f1e49": {"kind":"object","properties":[{"name":"dt","type":"5d03ba38734809bc","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"texts","type":"93c33d9687613293","optional":false},{"name":"time0","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"688e853dc149f0d3","optional":false}],"indices":[]}, "72ec79127a9c0d02": {"kind":"array","element":"c592ce75aab73fca"}, "7382c4fc95fe8c5d": {"kind":"literal","value":"remove"}, + "7404c6123fdaa591": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"7580e7d96dc8475f","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "750a2b129d67704d": {"kind":"union","types":["5cb63db9fe57dc09","6cf082d0fb6034de","bab6865794007548"]}, "7554565be416b034": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, "756b5e23a5cb2c7f": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "7580e7d96dc8475f": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"7a5b68bd1a5118c8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "7624ca28c61ca0ea": {"kind":"literal","value":"create"}, "763c8a20af487263": {"kind":"object","properties":[{"name":"data","type":"7d226b9fd95b4ba2","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"237c004c73b6b288","optional":false}],"indices":[]}, "76632533f67879dc": {"kind":"literal","value":"delegation"}, "78a9be340090b735": {"kind":"literal","value":"tmux-context"}, + "78b9204976ab9faa": {"kind":"array","element":"cda79b839d8cc0c7"}, "79650cfa10aa3c52": {"kind":"object","properties":[{"name":"error","type":"5f2117d1856a5ab4","optional":false},{"name":"kind","type":"a2be0f0cb59f0d4a","optional":false}],"indices":[]}, - "79a4c87f8bfb4d6d": {"kind":"object","properties":[{"name":"data","type":"00c054d3018c198d","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "79ddc907312f97cf": {"kind":"object","properties":[{"name":"todos","type":"4b7ddaf3710b88ff","optional":false}],"indices":[]}, + "7a5b68bd1a5118c8": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fc2ae6962f9f8801","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "7a651f3b672c52fb": {"kind":"literal","value":"session/title"}, "7af85bf70d4eafce": {"kind":"object","properties":[{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, @@ -1047,7 +1056,6 @@ "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, - "97e0162f983c2745": {"kind":"object","properties":[{"name":"data","type":"f4ed4717b4c259e9","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "9890113e97dd3541": {"kind":"object","properties":[{"name":"mode","type":"fab2bb09bc5e3b9d","optional":false},{"name":"source","type":"76632533f67879dc","optional":true}],"indices":[]}, "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, "9947aa5938fe19a8": {"kind":"union","types":["576a2de8355eb098","a99ec0993c62c058"]}, @@ -1080,6 +1088,7 @@ "a2be0f0cb59f0d4a": {"kind":"literal","value":"error"}, "a3a259a1aa5401af": {"kind":"literal","value":"hook/result"}, "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a3e299483348f27a": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"78b9204976ab9faa","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a3edd1efd1fef21b": {"kind":"literal","value":"workspace-write"}, "a40d12070f6f4a12": {"kind":"object","properties":[{"name":"task","type":"51edec100659da3a","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, "a6567ccb2e530606": {"kind":"object","properties":[{"name":"data","type":"0d47a8f3d847243c","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0a8a062bc9c3c0d9","optional":false}],"indices":[]}, @@ -1092,7 +1101,6 @@ "a8a2b9011fc06786": {"kind":"literal","value":"team/message/delivered"}, "a9059a81430b3207": {"kind":"literal","value":"hooks-claude-code"}, "a926dc71814edd01": {"kind":"object","properties":[{"name":"everySeconds","type":"0657d3ca5ac4e7bf","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"e42b0f40b5d0b628","optional":false},{"name":"prompt","type":"4cd48f3e5108bd6e","optional":false},{"name":"scheduledAt","type":"4cd48f3e5108bd6e","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, - "a927d444c928ac0e": {"kind":"array","element":"040cd65423bd6426"}, "a99ec0993c62c058": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"llmStreamCall","type":"dacf3d5f3e4ff619","optional":false},{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"rawOutput","type":"b52f215a6cd69105","optional":false},{"name":"shadowedRange","type":"9e41386b3a0c9572","optional":false},{"name":"shadowedSeqs","type":"5d03ba38734809bc","optional":false},{"name":"shadowedTokenCount","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true},{"name":"summary","type":"b52f215a6cd69105","optional":false},{"name":"usage","type":"fec5442b9ee4122e","optional":true}],"indices":[]}, "a99f76fd149051c3": {"kind":"object","properties":[{"name":"content","type":"4cd48f3e5108bd6e","optional":false},{"name":"status","type":"fd0d91a1f9dd2efb","optional":false}],"indices":[]}, "aa0957eca50aeb28": {"kind":"object","properties":[{"name":"data","type":"f90eb4ab2d3897bc","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0af3ecfb8c60d416","optional":false}],"indices":[]}, @@ -1125,8 +1133,10 @@ "b222069eea2d7680": {"kind":"object","properties":[{"name":"data","type":"477614bca595a9e7","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"9acbe713f3f64192","optional":false}],"indices":[]}, "b2245a906e13aa94": {"kind":"literal","value":"forked"}, "b2a5c8c008a31e3d": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "b2a948a5340a9341": {"kind":"object","properties":[{"name":"inserted","type":"5d00abf938272f22","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "b2d2a01e51341a3b": {"kind":"union","types":["1ebc6f9ff3aa9e3a","234142ee4f996f38","94c56aeb1564ad73","e7e8f1318170b828"]}, "b3681c026a0b98e9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "b36fd842cd004b53": {"kind":"object","properties":[{"name":"data","type":"b2a948a5340a9341","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "b38f206d59bad891": {"kind":"literal","value":"disposed"}, "b4ec0af39fd52ed9": {"kind":"object","properties":[{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, "b5086d249e8502e9": {"kind":"object","properties":[{"name":"data","type":"aa7bc2817014ca6c","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"6cda0d6d3b4336d8","optional":false}],"indices":[]}, @@ -1135,12 +1145,14 @@ "b56f6a885da3dc39": {"kind":"object","properties":[{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"source","type":"30d5a537b2966857","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "b57a2889408de36e": {"kind":"literal","value":"failed"}, "b57ae1b37e52bee0": {"kind":"literal","value":"team/task"}, + "b613f83572fe2fbf": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "b63b089a00b3b935": {"kind":"object","properties":[{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"17aae616ef020b8b","optional":false},{"name":"prompt","type":"4cd48f3e5108bd6e","optional":false},{"name":"scheduledAt","type":"4cd48f3e5108bd6e","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "b6896938660a1dfe": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "b6ef9c6210be8350": {"kind":"object","properties":[{"name":"kind","type":"1ebc6f9ff3aa9e3a","optional":false}],"indices":[]}, "b6f94c17dd9bbd7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, "b70f3e81fe11b7a5": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, "b765ee31033cea46": {"kind":"literal","value":"set"}, + "b782058cef83caf3": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"7a5b68bd1a5118c8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "b79641a9d25e7566": {"kind":"object","properties":[{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, "b79ada42962cad01": {"kind":"object","properties":[{"name":"data","type":"86d1d0e9cfebb240","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"5ebce1317eacd2c1","optional":false}],"indices":[]}, "b815d7b91cdd9fc6": {"kind":"literal","value":"goal"}, @@ -1150,6 +1162,7 @@ "b9305f0ea75ccd6a": {"kind":"literal","value":"hook/invoked"}, "b978cff734e62143": {"kind":"object","properties":[{"name":"data","type":"79ddc907312f97cf","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"af0750d32d97d47b","optional":false}],"indices":[]}, "b993441f8ce7d27b": {"kind":"tuple","elements":[{"type":"9a2a9029f8d7ede0","optional":false,"rest":false}]}, + "ba2a1a8f194b1bb2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "ba4c40b4381e0dcd": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "ba77b58995e36f14": {"kind":"array","element":"07e1a58c58b593f5"}, "bab6865794007548": {"kind":"literal","value":"kill"}, @@ -1184,6 +1197,7 @@ "cbca49bde16b656d": {"kind":"literal","value":"block-start"}, "cd186c309e2bef30": {"kind":"literal","value":"approval/asked"}, "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "cda79b839d8cc0c7": {"kind":"union","types":["09f3f8dc417f2782","4b259f7f8f0a44ad","7580e7d96dc8475f","b782058cef83caf3","fa87537f4a9c1e25"]}, "cdca80b8dad24d4e": {"kind":"literal","value":"tool-workflow/run-start"}, "cdee10e87dbed844": {"kind":"object","properties":[{"name":"delayMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"failure","type":"5f2117d1856a5ab4","optional":false},{"name":"maxRetries","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mode","type":"3bc287e765d58dc3","optional":false},{"name":"policyKey","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "cef171adf021a186": {"kind":"object","properties":[{"name":"arguments","type":"bbc5696e3f13c758","optional":false},{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"error","type":"a81059ca06999da1","optional":true},{"name":"isError","type":"aa93010d0e521ad7","optional":false},{"name":"meta","type":"c592ce75aab73fca","optional":true},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parentCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, @@ -1208,9 +1222,11 @@ "db874d463b0fdec7": {"kind":"object","properties":[{"name":"data","type":"3e4e7683b0192c9f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"e2d0367e5716bc18","optional":false}],"indices":[]}, "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, "dbdafceb9a900183": {"kind":"literal","value":"hook"}, + "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "dd2780e7e0768433": {"kind":"literal","value":"fresh"}, "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "de25352d2dc41963": {"kind":"literal","value":"user/message"}, "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, "deaae5cd30987d4f": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, @@ -1240,6 +1256,7 @@ "e7ce49f12953583f": {"kind":"literal","value":"initial"}, "e7e154d47d5fbc53": {"kind":"literal","value":"clear"}, "e7e8f1318170b828": {"kind":"literal","value":"deleted"}, + "e7f4cbb3a57f4cbd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "e8de0f457fed5b9f": {"kind":"literal","value":"notice"}, "e95f957154b2b1b5": {"kind":"literal","value":"assistant/attempt"}, "ea08ec1df0c33108": {"kind":"object","properties":[{"name":"message","type":"2eafefdc6f79a492","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, @@ -1255,6 +1272,7 @@ "ef11a3b7fa2033ae": {"kind":"object","properties":[{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "ef226a90ddac6cd8": {"kind":"literal","value":"killed"}, "ef4f00923b9edbae": {"kind":"literal","value":"compaction/summary"}, + "efa1a091017ce533": {"kind":"object","properties":[{"name":"data","type":"a3e299483348f27a","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "f05fb85e715af4ab": {"kind":"literal","value":"task-result"}, "f09e5e7acf5ff5cc": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "f1687f982fa3d41e": {"kind":"union","types":["023a355b2be0d688","0a30c3be336cdd4f","56b7c047694875a8","79650cfa10aa3c52","ab7ec5f9eb2c6dfa","b6ef9c6210be8350","ec8b5ff7c77ca689"]}, @@ -1266,7 +1284,6 @@ "f2da0f27c1e6add5": {"kind":"object","properties":[{"name":"attachment","type":"f19bee84bf3d2090","optional":false},{"name":"type","type":"277b61f9c6f81704","optional":false}],"indices":[]}, "f311671e07bc7fa6": {"kind":"literal","value":"in-history"}, "f4a66f654996c093": {"kind":"object","properties":[{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, - "f4ed4717b4c259e9": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"0f6265188dae7ba6","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "f5520c1d261ca86e": {"kind":"object","properties":[{"name":"data","type":"e3056e0dda3a379e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"24a8188c4d61b70a","optional":false}],"indices":[]}, "f56c5b59ca016131": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"7f13860ee89a2d35","optional":false},{"name":"references","type":"df843efd87c78cf8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "f6329fb30c7ea801": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, @@ -1282,7 +1299,6 @@ "f90eb4ab2d3897bc": {"kind":"object","properties":[{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "f97f47b0eb87905e": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, "f9cfc89e8fe085b4": {"kind":"literal","value":"text-chunks"}, - "fa106c5b33b657e8": {"kind":"object","properties":[{"name":"inserted","type":"454a0fa879e38076","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, "fab2bb09bc5e3b9d": {"kind":"union","types":["12a0cfa619ac4a5f","18fce9540bb20897","a3edd1efd1fef21b"]}, "fab35c4520883c0d": {"kind":"literal","value":"rejected"}, diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md index 530af09e92..0cef9a67e4 100644 --- a/docs/subsystems/boot.md +++ b/docs/subsystems/boot.md @@ -69,6 +69,53 @@ interface PendingModule { Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.actionCards` — `ActionCards` + +Card tool, kind registry, Session projection, user actions and status notices. + +```ts cordis-catalog +/** + * Register one action kind for the caller's lifetime. + * @param kind - the kind's skill, details schema, verbs and handler. + * @returns disposer removing the kind; actions on its cards are then unavailable. + * @throws when another kind already registered the same action. + */ +register
(kind: ActionCardKind
): () => void + +/** + * Record a card decision in the Session log; the next model request reports it. + * @param agent - agent whose Session owns the card. + * @param decision - the card's new state. + * @throws when no kind ever registered the decision's action with this service, or its phase is one the projection derives. + */ +record(agent: Agent, decision: ActionCardDecision): void + +/** + * Read the cards of one Session. + * @param session - a Session whose projections are prepared. + * @returns proposals and decisions by call id. + */ +view(session: Session): ActionCardView + +/** + * Apply a user action to a logged card. Shared checks run first, then the kind reconciles the card; `dismiss` is + * recorded here and every other verb goes to the card's kind. `dismiss` also works on an unstarted card whose kind + * is no longer registered. Concurrent actions on one card share one operation. + * @param agent - live root resolved from the displayed Session. + * @param callId - the card's `propose_action` call id. + * @param verb - `dismiss` or one of the kind's verbs. + * @param payload - kind-defined fields such as a chosen location. + * @returns acceptance of background work, the decision in effect, or a refusal. + */ +@Remote act(agent: Agent, callId: ToolCallId, verb: string, payload?: Record): Promise +``` + +Types: [Agent](core.md) · [Session](session.md) · [ToolCallId](core.md) + +Source: [`packages/experimental/action-cards/src/index.ts`](../../packages/experimental/action-cards/src/index.ts) + ### `ctx.configEditor` — `ConfigEditor` @@ -250,7 +297,9 @@ Manage profile files and apply their declared reload lifecycle. /** * Install a package using the same pnpm implementation as dsh plugin. GitHub * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; - * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. An + * HTTP(S) Git URL on another host is installed over SSH instead when access probes bounded by gitTransportTimeoutMs + * find that only SSH can read it. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -353,6 +402,26 @@ async entry(url: string, name: string): Promise Source: [`packages/boot/plugin-market/src/index.ts`](../../packages/boot/plugin-market/src/index.ts) + + +### `ctx.pluginRecommendations` — `PluginRecommendations` + +Catalog lookups for card text and the enabling behind the `enable_official_plugin` card. + +```ts cordis-catalog +/** + * Read the current catalog text for a logged card. + * @param agent - Exact live root resolved by the Gateway from the Session id. + * @param callId - The card's `propose_action` call id, never a package supplied by the browser. + * @returns the plugin's title, description and selection, or a refusal when the card names no available official plugin. + */ +@Remote async describe(agent: Agent, callId: ToolCallId): Promise +``` + +Types: [Agent](core.md) · [ToolCallId](core.md) + +Source: [`packages/experimental/plugin-recommendations/src/index.ts`](../../packages/experimental/plugin-recommendations/src/index.ts) + ### `ctx.pluginRegistryProbe` — `PluginRegistryProbe` diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md index 1cab38289f..fe8014ab82 100644 --- a/docs/subsystems/boot.zh.md +++ b/docs/subsystems/boot.zh.md @@ -69,6 +69,53 @@ interface PendingModule { Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.actionCards` — `ActionCards` + +Card tool, kind registry, Session projection, user actions and status notices. + +```ts cordis-catalog +/** + * Register one action kind for the caller's lifetime. + * @param kind - the kind's skill, details schema, verbs and handler. + * @returns disposer removing the kind; actions on its cards are then unavailable. + * @throws when another kind already registered the same action. + */ +register
(kind: ActionCardKind
): () => void + +/** + * Record a card decision in the Session log; the next model request reports it. + * @param agent - agent whose Session owns the card. + * @param decision - the card's new state. + * @throws when no kind ever registered the decision's action with this service, or its phase is one the projection derives. + */ +record(agent: Agent, decision: ActionCardDecision): void + +/** + * Read the cards of one Session. + * @param session - a Session whose projections are prepared. + * @returns proposals and decisions by call id. + */ +view(session: Session): ActionCardView + +/** + * Apply a user action to a logged card. Shared checks run first, then the kind reconciles the card; `dismiss` is + * recorded here and every other verb goes to the card's kind. `dismiss` also works on an unstarted card whose kind + * is no longer registered. Concurrent actions on one card share one operation. + * @param agent - live root resolved from the displayed Session. + * @param callId - the card's `propose_action` call id. + * @param verb - `dismiss` or one of the kind's verbs. + * @param payload - kind-defined fields such as a chosen location. + * @returns acceptance of background work, the decision in effect, or a refusal. + */ +@Remote act(agent: Agent, callId: ToolCallId, verb: string, payload?: Record): Promise +``` + +Types: [Agent](core.zh.md) · [Session](session.zh.md) · [ToolCallId](core.zh.md) + +Source: [`packages/experimental/action-cards/src/index.ts`](../../packages/experimental/action-cards/src/index.ts) + ### `ctx.configEditor` — `ConfigEditor` @@ -250,7 +297,9 @@ Manage profile files and apply their declared reload lifecycle. /** * Install a package using the same pnpm implementation as dsh plugin. GitHub * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; - * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. An + * HTTP(S) Git URL on another host is installed over SSH instead when access probes bounded by gitTransportTimeoutMs + * find that only SSH can read it. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -353,6 +402,26 @@ async entry(url: string, name: string): Promise Source: [`packages/boot/plugin-market/src/index.ts`](../../packages/boot/plugin-market/src/index.ts) + + +### `ctx.pluginRecommendations` — `PluginRecommendations` + +Catalog lookups for card text and the enabling behind the `enable_official_plugin` card. + +```ts cordis-catalog +/** + * Read the current catalog text for a logged card. + * @param agent - Exact live root resolved by the Gateway from the Session id. + * @param callId - The card's `propose_action` call id, never a package supplied by the browser. + * @returns the plugin's title, description and selection, or a refusal when the card names no available official plugin. + */ +@Remote async describe(agent: Agent, callId: ToolCallId): Promise +``` + +Types: [Agent](core.zh.md) · [ToolCallId](core.zh.md) + +Source: [`packages/experimental/plugin-recommendations/src/index.ts`](../../packages/experimental/plugin-recommendations/src/index.ts) + ### `ctx.pluginRegistryProbe` — `PluginRegistryProbe` diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md index 2ebf2c2f64..c7f175b9e6 100644 --- a/docs/subsystems/client-modules.md +++ b/docs/subsystems/client-modules.md @@ -156,7 +156,9 @@ clientPath(id: string): string | undefined * body. Each bundle body is built once on its first `GET`; script * construction never reads maps. A package asset is read on every request * and answers 304 without a body when `If-None-Match` equals its entity tag; - * its body holds the file open until the caller consumes or cancels it. + * its body holds the file open until the caller consumes or cancels it. A + * bundle, map, or chunk body is the held buffer itself, not a copy, so a + * caller must not modify its bytes. * @param request - shell-carrier request for a `/plugins` resource. * @returns the exact response also exposed by the optional Web route. */ diff --git a/docs/subsystems/client-modules.zh.md b/docs/subsystems/client-modules.zh.md index 1049e859f6..b0a665fe9a 100644 --- a/docs/subsystems/client-modules.zh.md +++ b/docs/subsystems/client-modules.zh.md @@ -156,7 +156,9 @@ clientPath(id: string): string | undefined * body. Each bundle body is built once on its first `GET`; script * construction never reads maps. A package asset is read on every request * and answers 304 without a body when `If-None-Match` equals its entity tag; - * its body holds the file open until the caller consumes or cancels it. + * its body holds the file open until the caller consumes or cancels it. A + * bundle, map, or chunk body is the held buffer itself, not a copy, so a + * caller must not modify its bytes. * @param request - shell-carrier request for a `/plugins` resource. * @returns the exact response also exposed by the optional Web route. */ diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md index 8f35e17f05..2c2dedab48 100644 --- a/docs/subsystems/persistence.md +++ b/docs/subsystems/persistence.md @@ -156,7 +156,11 @@ interface SessionHeader { readonly createdAt: number /** Absolute working directory the session was created in (if any). */ readonly cwd?: string - /** The session this one was forked from (seed lineage), if any. */ + /** + * Direct creator of a subagent Session. Ordinary forks omit this field; + * historical ordinary forks may retain their source id here. Presence alone + * does not classify a Session as a subagent; use `origin` for that distinction. + */ readonly parentSession?: SessionId /** * Whether this Session contains a fork-inherited event prefix. The exact prefix diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md index c88b884fdb..aeec8c81c0 100644 --- a/docs/subsystems/persistence.zh.md +++ b/docs/subsystems/persistence.zh.md @@ -156,7 +156,11 @@ interface SessionHeader { readonly createdAt: number /** Absolute working directory the session was created in (if any). */ readonly cwd?: string - /** The session this one was forked from (seed lineage), if any. */ + /** + * Direct creator of a subagent Session. Ordinary forks omit this field; + * historical ordinary forks may retain their source id here. Presence alone + * does not classify a Session as a subagent; use `origin` for that distinction. + */ readonly parentSession?: SessionId /** * Whether this Session contains a fork-inherited event prefix. The exact prefix diff --git a/docs/subsystems/schedule.i18n.yaml b/docs/subsystems/schedule.i18n.yaml index a6e7893cf3..63354f0fd0 100644 --- a/docs/subsystems/schedule.i18n.yaml +++ b/docs/subsystems/schedule.i18n.yaml @@ -27,8 +27,8 @@ en: 93798ad69f74ddd5 zh: 634126ad8accc356 /host-wide-schedule/web-catalog: - en: b5e5ac8125e65f33 - zh: 9ee064d833a4f838 + en: 3377a013bf8cbfb7 + zh: c5666dc3f28b087f /host-wide-schedule/timing-edits: en: ce5d2095a3ad2e82 zh: 167b3715f8eaffc1 diff --git a/docs/subsystems/schedule.md b/docs/subsystems/schedule.md index ef24673f9b..13f1752a31 100644 --- a/docs/subsystems/schedule.md +++ b/docs/subsystems/schedule.md @@ -309,6 +309,14 @@ type ScheduleDeleteResult = | { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' } ``` +```ts type-equiv +/** Successful Host-wide removal of every ended task, whether or not any remained. */ +interface ScheduleDeleteInactiveResult { + /** Ended task rows this call removed; zero when the Host held none. */ + readonly deleted: number +} +``` + ## Web catalog The Remote `schedule.catalog()` method returns active and inactive Host tasks with their original Session bindings, ordered by `scheduledAt` ascending and then lexicographically by `id`. It reads only the Schedule domain, without activating Sessions or reading their history. Browser consumers receive this browser-safe type: @@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { } ``` -The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again. +The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. `schedule.deleteInactive()` removes every ended (inactive) task across Sessions, with the saved delivery records each row carries; on success it answers `{ deleted }` with the count it removed; a normal failed batch rejects with nothing removed; an `unknown-state` failure may have removed rows and quarantines the Host schedule domain, so later schedule operations reject until it is reopened. Any rejection announces `schedule/changed`, and clients then re-read. The Automation tasks page's bulk cleanup uses it. An ended task never arms a timer, so the removal recomputes no target. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again. The shipped Web composition mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. The Rules and Records tabs separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution. @@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor */ @Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise +/** + * Delete every ended Host task, leaving active tasks armed. + * + * Ended tasks never schedule another delivery, so the removal changes no pending + * target and no timer is recomputed. Rows are enumerated and removed in one FIFO + * slot, so a delivery ordered behind this call ends its task after the + * enumeration rather than during it. The batch is one domain write: a normal + * failure rejects with every listed row still stored, while a backend + * `unknown-state` failure may have removed rows and quarantines the Host + * domain, so later schedule operations reject until the domain is reopened. + * Every rejection announces `schedule/changed` because it does not establish + * which rows landed, and clients then re-read the catalog. + * @returns Count of removed rows; zero when the Host held no ended task. + */ +@Remote('deleteInactive') async deleteInactive(): Promise + /** * Update the name, instruction, and timing of an active task within the original Session * binding without activating the Session or changing saved deliveries. diff --git a/docs/subsystems/schedule.zh.md b/docs/subsystems/schedule.zh.md index 2d140c3821..320ae8e743 100644 --- a/docs/subsystems/schedule.zh.md +++ b/docs/subsystems/schedule.zh.md @@ -309,6 +309,14 @@ type ScheduleDeleteResult = | { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' } ``` +```ts type-equiv +/** Successful Host-wide removal of every ended task, whether or not any remained. */ +interface ScheduleDeleteInactiveResult { + /** Ended task rows this call removed; zero when the Host held none. */ + readonly deleted: number +} +``` + ## Web 目录 Remote 方法 `schedule.catalog()` 返回活动和未运行的宿主任务及其原始 Session 绑定,先按 `scheduledAt` 升序排列,再按 `id` 字典序排列。它只读取 Schedule domain,不激活 Session 或读取其历史。浏览器消费方接收以下可安全用于浏览器的类型: @@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { } ``` -Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。 +Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。`schedule.deleteInactive()` 删除跨会话的全部未运行(已结束)任务,连同每行携带的已保存的发送记录;成功时以它移除的计数答复 `{ deleted }`;普通失败时不删除任何行;后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,之后该领域的操作都会拒绝,直到重新打开。任何拒绝都会先发出 `schedule/changed`,客户端重新读取。自动化任务页面的批量清理使用该方法。未运行任务从不会安排定时器,因此该删除不重算任何目标。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。 随发行版交付的 Web 组合把 `ui-schedule` 与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“任务运行记录”页签将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。 @@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor */ @Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise +/** + * Delete every ended Host task, leaving active tasks armed. + * + * Ended tasks never schedule another delivery, so the removal changes no pending + * target and no timer is recomputed. Rows are enumerated and removed in one FIFO + * slot, so a delivery ordered behind this call ends its task after the + * enumeration rather than during it. The batch is one domain write: a normal + * failure rejects with every listed row still stored, while a backend + * `unknown-state` failure may have removed rows and quarantines the Host + * domain, so later schedule operations reject until the domain is reopened. + * Every rejection announces `schedule/changed` because it does not establish + * which rows landed, and clients then re-read the catalog. + * @returns Count of removed rows; zero when the Host held no ended task. + */ +@Remote('deleteInactive') async deleteInactive(): Promise + /** * Update the name, instruction, and timing of an active task within the original Session * binding without activating the Session or changing saved deliveries. diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 1a778923dc..53d89173b4 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -45,8 +45,8 @@ en: 45d1a9ee6d610ac5 zh: 79c061252fde4ceb /sessions/live-session-fork-api: - en: c72c73ce4b4c697c - zh: 8a34785a92df0ad5 + en: 06189884c62a80a1 + zh: 30121aed7a011c72 /sessions/why-a-turn-ended-turnendreasonmap: en: 966cc60273fb3911 zh: de5123ea9dbb0df2 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index e9369b0702..a412bfd6c4 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -65,8 +65,10 @@ interface SessionEventMap { * The rendered system prompt on the model-visible surface. The loop appends * the first one as surface node 0 before the step's first `user/message`. * A prepared in-history route can append nonempty changes in a continuing - * series. An incapable route or new series normalizes text to the first system - * node. Normalization empties nonempty later nodes, then rewrites the head if + * series. An incapable route, or a new series whose effective text differs, + * normalizes text to the first system node; a series start whose latest + * surviving node already holds the rendering leaves it in place. + * Normalization empties nonempty later nodes, then rewrites the head if * needed, through logged per-node replacements. An empty rendering always * clears all active system nodes, leaving no older instructions model-visible. * Empty later nodes are dormant and project to no message; an empty head with @@ -683,7 +685,7 @@ Everything else (`turn/*`, `step/*`, `assistant/attempt`, plugin-owned `llm/retr `ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API: -- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `SessionSeq` boundary (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events, `parentSession`, `isSeeded: true`, the exact `inheritedEventCount`, and inherited `cwd`. +- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `SessionSeq` boundary (default: current last event), closes any open tail, then creates an independent live session with deep-cloned seed events, `isSeeded: true`, the exact `inheritedEventCount`, and inherited `cwd`, without `parentSession`. An explicit `boundary` lets callers fork from any stable between-turn position, including a previous `turn/end` or a later standalone log-only event, even if the source has newer events or an open current turn. The API rejects a prefix that ends inside an open turn instead of clipping silently. `dsh-subagent-fork-in-process` keeps its completed-prefix clipping because tool-time delegation usually starts while the parent turn is open; ordinary session branching should make the requested boundary explicit. @@ -1128,7 +1130,7 @@ get(id: SessionId): Session | undefined list(): Session[] /** - * Create a live child session from an exact prefix of a live source. + * Create an independent live session from an exact prefix of a live source. * `boundary` is an inclusive source event seq; omitted means the source's * current last event. An open tail receives synthetic tool results and * step/turn closers with the forked cause. Closed steps and turns remain diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 7971f5e7fd..9ac7ef6733 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -65,8 +65,10 @@ interface SessionEventMap { * The rendered system prompt on the model-visible surface. The loop appends * the first one as surface node 0 before the step's first `user/message`. * A prepared in-history route can append nonempty changes in a continuing - * series. An incapable route or new series normalizes text to the first system - * node. Normalization empties nonempty later nodes, then rewrites the head if + * series. An incapable route, or a new series whose effective text differs, + * normalizes text to the first system node; a series start whose latest + * surviving node already holds the rendering leaves it in place. + * Normalization empties nonempty later nodes, then rewrites the head if * needed, through logged per-node replacements. An empty rendering always * clears all active system nodes, leaving no older instructions model-visible. * Empty later nodes are dormant and project to no message; an empty head with @@ -685,7 +687,7 @@ declare class Session { `ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork,`SessionStore` 暴露一个策略 API: -- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `SessionSeq` boundary(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的 seed event、`parentSession`、`isSeeded: true`、精确 `inheritedEventCount` 及继承的 `cwd`。 +- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `SessionSeq` boundary(含)为止的源事件(默认为当前最后一个事件),闭合任何开放尾部,然后创建一个独立的活跃会话,包含深克隆的 seed event、`isSeeded: true`、精确 `inheritedEventCount` 及继承的 `cwd`,不含 `parentSession`。 显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。`dsh-subagent-fork-in-process` 保留其已完成前缀截断逻辑,因为工具调用时的委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。 @@ -1132,7 +1134,7 @@ get(id: SessionId): Session | undefined list(): Session[] /** - * Create a live child session from an exact prefix of a live source. + * Create an independent live session from an exact prefix of a live source. * `boundary` is an inclusive source event seq; omitted means the source's * current last event. An open tail receives synthetic tool results and * step/turn closers with the forked cause. Closed steps and turns remain diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index cf71b1d1af..17ae54903e 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/slots.md /web-client-slots: - en: dd504c406c346f45 - zh: 5dadae8b00aafc5b + en: 808a284f5a4280b5 + zh: 1650d6c3a94eae6e /web-client-slots/declaration-and-lifecycle: en: f2d2c0d3b8f14ec7 zh: 441ed13ff3f81d2a diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index 998aebbffc..ee31122d13 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -10,7 +10,7 @@ This page documents slot ownership, component inputs, extension APIs, and the sh The plugin catalog declares `plugins.add.actions`, a root-scoped list for `MenuItemButton` rows after installation in the Add plugin menu. Its owner supplies `onDismiss()` to close the menu before starting an action. The slot follows the owning Plugins page declaration lifetime. -The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Each region persists its own unpinned entry ids; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. +The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Panel and View entries also carry optional `active` state for the current selection, independently of pin state. Each region keeps its own unpinned entry ids in memory; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. [ui-workspace](../../packages/client/ui-workspace/README.md#understand-the-implementation) documents Session-row extension positions, visibility, and keyboard behavior. diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index e2d9217ec6..ca13d60008 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -10,7 +10,7 @@ Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](. 插件目录声明 `plugins.add.actions`,它是 root 作用域的 list,在添加插件菜单的安装项之后提供 `MenuItemButton` 行。owner 提供 `onDismiss()`,用于开始动作前关闭菜单。该 slot 遵循所属插件页面声明的生命周期。 -root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。各区域自行持久化已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 +root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。面板和 View 入口还通过可选的 `active` 状态标识当前选中项,该状态独立于 Pin 状态。各区域在内存中维护已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 [ui-workspace](../../packages/client/ui-workspace/README.zh.md#understand-the-implementation) 记录 Session 行的扩展位置、显示条件和键盘行为。 diff --git a/docs/subsystems/storage.i18n.yaml b/docs/subsystems/storage.i18n.yaml index 632e0788df..5f4126783c 100644 --- a/docs/subsystems/storage.i18n.yaml +++ b/docs/subsystems/storage.i18n.yaml @@ -9,14 +9,14 @@ en: b11e06e7c0e0fd13 zh: 4585f26ff416cb41 /storage/the-backend-contract: - en: 2316787902c6a94e - zh: e3bda06e1c33acac + en: 225e25b9b4b08865 + zh: 53bd3554da610607 /storage/declaring-a-domain: en: 775cce941500f01f zh: 78acbc67f48574cb /storage/the-open-domain: - en: 2468b5df66c49b51 - zh: 1587f460e9769db2 + en: b69b25bc06f1c052 + zh: 8aca58718da520c7 /storage/the-domain-facility-ctx-storagedomain: en: deed74fcd4506ef4 zh: 8e3e6e5d2dc2e989 diff --git a/docs/subsystems/storage.md b/docs/subsystems/storage.md index 674221a263..8dddb5818e 100644 --- a/docs/subsystems/storage.md +++ b/docs/subsystems/storage.md @@ -44,7 +44,7 @@ interface StorageBackend { } ``` -A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data. +A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `deleteRecords`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are opaque strings in the `single` layout and match `[a-zA-Z0-9_-]+` path segments in the `per-record` layout. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium; durability holds for single-record writes and whole-file publishes, while a `per-record` batch deletion relies on the filesystem persisting directory-entry updates. `deleteRecords` applies a whole batch in one call: it resolves once every listed record is absent from the readable state and the backend accepted the batch, and a rejection normally leaves the batch unapplied; a backend that cannot establish or restore the batch's state rejects with `unknown-state`, after which every later operation on that unit rejects with the same error until `close()` releases it (calls after close reject `closed`). A `single`-layout failure after the atomic replacement (a directory-fsync error) can leave the new file on the medium while the caller sees a rejection; the unit keeps its memory consistent and the next publish rewrites the file. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks the shared clauses against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data. ## Declaring a domain @@ -123,7 +123,7 @@ interface Domain { } ``` -Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place. +Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `deleteMany`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium; a backend that reports `unknown-state` instead quarantines the domain, and its reads and later writes reject until `close()` releases it. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. `deleteMany(keys)` removes the present keys as one backend batch in one chain slot, resolves the removed count, and emits one `domain/changed` per removed key; a rejected backend write leaves every record in place and emits nothing; an `unknown-state` rejection quarantines the domain. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place. ## The domain facility: `ctx.storageDomain` diff --git a/docs/subsystems/storage.zh.md b/docs/subsystems/storage.zh.md index 2a4366c84e..7411fa632b 100644 --- a/docs/subsystems/storage.zh.md +++ b/docs/subsystems/storage.zh.md @@ -44,7 +44,7 @@ interface StorageBackend { } ``` -一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查每项条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。 +一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`deleteRecords`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键在 `single` 布局中是不透明字符串,在 `per-record` 布局中则是匹配 `[a-zA-Z0-9_-]+` 的路径片段。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的;单记录写入与整文件发布在 resolve 后即已持久,`per-record` 批量删除则依赖文件系统对目录项更新的持久化。`deleteRecords` 以一次调用应用整批删除:所有列出的记录都从可读状态消失且后端接受该批后才 resolve,拒绝时通常整批未应用;无法建立或恢复批次状态的批量删除会以 `unknown-state` 拒绝,之后该 unit 的所有操作都以同一错误拒绝,直到 `close()` 释放它(关闭后的调用以 `closed` 拒绝)。`single` 布局在原子替换之后失败(目录 fsync 错误)时,新文件可能已落在介质上而调用方看到拒绝;unit 的内存保持一致,下一次发布会重写该文件。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查共享条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。 ## 声明领域 @@ -123,7 +123,7 @@ interface Domain { } ``` -读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。 +读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`deleteMany`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质;若后端报告 `unknown-state`,领域会被隔离,读取与后续写入都会拒绝,直到 `close()` 释放它。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。`deleteMany(keys)` 在一个写链 slot 内把存在的键作为一次后端批量删除移除,resolve 为移除数量,并为每个被移除的键发一次 `domain/changed`;后端写入被拒时所有记录保持原样,也不发事件;`unknown-state` 拒绝会隔离整个领域。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。 ## 领域 facility:`ctx.storageDomain` diff --git a/docs/subsystems/web-server.i18n.yaml b/docs/subsystems/web-server.i18n.yaml index bd4c01c442..d34ca1303f 100644 --- a/docs/subsystems/web-server.i18n.yaml +++ b/docs/subsystems/web-server.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/web-server.md /http-server: - en: 56b268a69964f651 - zh: c8ac8f16bf58a98a + en: a4861c68f3eb0b03 + zh: a3da9f0467fdee02 /http-server/routes: - en: e949c3738a35b58f - zh: 138e3aaeb319ddc4 + en: 57adf92882ff0f77 + zh: d17992cca18cc790 /http-server/config: en: 4b162bd9f280eb06 zh: 3bea98dfee88c582 /http-server/the-service: - en: bbf84a6cd0460dc2 - zh: cc63ce45b0675c6d + en: d1922a5e809e20c3 + zh: 5c1772c81a19784c diff --git a/docs/subsystems/web-server.md b/docs/subsystems/web-server.md index c6c731343a..2420b6713b 100644 --- a/docs/subsystems/web-server.md +++ b/docs/subsystems/web-server.md @@ -2,7 +2,7 @@ English | [中文](web-server.zh.md) -[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: one `node:http` server — `node:https` when `tls` is configured — providing `ctx.webServer`, a named-route registry, optional gzip response compression, index.html transform callbacks, and one fallback handler that a plugin may claim. It is not part of the agent loop and not a capability seam; its only harness concept is the Peer an index render speaks for, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering reference](../../packages/boot/app-boot/README.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server. +[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: one `node:http` server — `node:https` when `tls` is configured — providing `ctx.webServer`, a named-route registry, optional gzip response compression, index.html transform callbacks, and one fallback, a handler or a resource, that a plugin may claim. It is not part of the agent loop and not a capability seam; its only harness concept is the Peer an index render speaks for, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering reference](../../packages/boot/app-boot/README.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server. Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts) @@ -24,8 +24,30 @@ interface WebRoute { } ``` +```ts type-equiv +/** + * Answers one GET or HEAD request with a Fetch response. An HTTP request + * arrives without a Peer and carries the client's headers; an in-process + * {@link WebServer.readResource} read passes the Peer its caller admitted. + */ +type WebResource = (request: Request, peer?: PeerScope) => Promise +``` + +```ts type-equiv +/** One named route answered by a resource instead of a handler. */ +interface WebResourceRoute { + kind: WebRouteKind + /** Absolute pathname, no trailing slash. */ + path: string + /** Answers HTTP GET and HEAD requests and in-process reads. */ + resource: WebResource +} +``` + Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with [`dsh-host-frontend-static`](../../packages/host/frontend-static/src/index.ts), the SPA dist server with locked semantics: Connection authenticates the dist root and configured index before their HTML is read; non-index assets remain public; non-GET/HEAD is 405, traversal outside the dist root is 403, existing files are served directly, absent or non-file targets are empty 404 responses, and unknown extensions ship as octet-stream. +A route or the fallback may give a resource, a function from a Fetch `Request` to a `Response`, instead of a handler. HTTP `GET` and `HEAD` requests reach it without a Peer, and `readResource(request, peer)` reads it in process for the Peer its caller admitted; a read follows the same match order but reaches only resources, so a route that returns owner-private data or performs an action stays a handler even when it answers `GET`. + ## Config @@ -81,7 +103,7 @@ No composition can bind every interface at once: the unspecified address is reje ## The service -`WebServer` (`ctx.webServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. The `tls` pair is read once before the listener binds, and an unreadable, empty, or mismatched pair rejects initialization the same way instead of falling back to HTTP. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. Gzip wraps eligible socket-backed responses inside the server, so route handlers retain direct `ServerResponse` ownership and no response-writing API is added to the service. Existing content encodings, `Cache-Control: no-transform`, ranges, SSE, ZIP, and the packaged `.gz` Worker image remain identity responses. `collectIndexInjections(peer)` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit that passes the Peer the page speaks for, and `renderIndex(html, peer)` renders them into successful root and configured index responses before applying the raw `tapIndex(transform)` escape-hatch transforms in registration order; [dsh-client-modules](../../packages/client/modules) answers the event with the boot manifest rows. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0, and `protocol` reports the scheme the listener serves (`http:` or `https:`). +`WebServer` (`ctx.webServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. The `tls` pair is read once before the listener binds, and an unreadable, empty, or mismatched pair rejects initialization the same way instead of falling back to HTTP. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. Gzip wraps eligible socket-backed responses inside the server, so handlers keep direct `ServerResponse` ownership, and a resource hands the service a `Response` to write back. Existing content encodings, `Cache-Control: no-transform`, ranges, SSE, ZIP, and the packaged `.gz` Worker image remain identity responses. `collectIndexInjections(peer)` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit that passes the Peer the page speaks for, and `renderIndex(html, peer)` renders them into successful root and configured index responses before applying the raw `tapIndex(transform)` escape-hatch transforms in registration order; [dsh-client-modules](../../packages/client/modules) answers the event with the boot manifest rows. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0, and `protocol` reports the scheme the listener serves (`http:` or `https:`). A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. A TLS socket counts as a connection only after its handshake, so the server tracks raw TLS sockets beside upgraded ones and destroys both at disposal. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md). @@ -180,7 +202,7 @@ Source: [`packages/bundle/web-app/src/index.ts`](../../packages/bundle/web-app/s ### `ctx.webServer` — `WebServer` -The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. +The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and a request no named route matches is answered 404 until the fallback owner registers. ```ts cordis-catalog /** @@ -194,10 +216,10 @@ getOrigin(): string /** * Register a named route. Duplicate (kind, path) throws — route patterns are * a composition-level contract, so a collision is a misconfiguration. - * @param route - kind, path, and the owning handler. + * @param route - kind, path, and the owning handler or resource. * @returns the disposer removing the route. */ -register(route: WebRoute): () => void +register(route: WebRoute | WebResourceRoute): () => void /** * Register an exact-path HTTP upgrade route. Duplicate paths throw because @@ -208,14 +230,32 @@ register(route: WebRoute): () => void registerUpgrade(route: WebUpgradeRoute): () => void /** - * Claim the fallback seat: the handler answering every request no named - * route matches (the SPA dist server in the shipped Web composition). One - * owner only — a second registration throws, because two fallbacks cannot - * compose. - * @param handler - owns the full response lifecycle of unmatched requests. + * Claim the fallback seat: the handler or resource answering every request + * no named route matches (the SPA dist server in the shipped Web + * composition). One owner only — a second registration throws, because two + * fallbacks cannot compose. + * @param fallback - a handler owning the full response lifecycle of + * unmatched requests, or `{ resource }` answering them. * @returns the disposer releasing the seat. */ -registerFallback(handler: WebRoute['handler']): () => void +registerFallback(fallback: WebRoute['handler'] | Pick): () => void + +/** + * Read the response an HTTP request for the same URL would receive, as the + * resource computes it for `peer`, without an HTTP exchange. The request + * path picks the responder by the HTTP match order — exact, longest prefix, + * then the fallback — and only a resource answers. This method checks no + * cookie, Host, or Origin: the caller admits the reader before reading. + * @param request - the read; its URL path selects the responder. + * @param peer - the Peer the read speaks for, passed to the resource. + * @returns the resource's response, without a body for HEAD as over HTTP; + * 405 with `Allow: GET, HEAD` for any other method, checked before matching, + * where over HTTP a handler route or an unmatched path answers such a + * method itself; 404 when the path matches a handler route, or reaches the + * fallback seat while it is unclaimed or holds a handler. A resource that + * throws rejects the read, where an HTTP request would receive 400. + */ +async readResource(request: Request, peer: PeerScope): Promise /** * Register a raw-HTML index transform, the escape hatch for markup no diff --git a/docs/subsystems/web-server.zh.md b/docs/subsystems/web-server.zh.md index 21a97be800..a59344ad75 100644 --- a/docs/subsystems/web-server.zh.md +++ b/docs/subsystems/web-server.zh.md @@ -2,7 +2,7 @@ [English](web-server.md) | 中文 -[dsh-host-webserver](../../packages/host/webserver) 是 GUI Host 的浏览器 HTTP 载体:一个 `node:http` 服务器——配置 `tls` 时改用 `node:https`——提供 `ctx.webServer`、具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退处理器。它不属于 agent loop(智能体循环),也不是能力 seam;它唯一涉及的 harness 概念是 index 渲染所代表的 Peer。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../packages/boot/app-boot/README.zh.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。 +[dsh-host-webserver](../../packages/host/webserver) 是 GUI Host 的浏览器 HTTP 载体:一个 `node:http` 服务器——配置 `tls` 时改用 `node:https`——提供 `ctx.webServer`、具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退(handler 或 resource)。它不属于 agent loop(智能体循环),也不是能力 seam;它唯一涉及的 harness 概念是 index 渲染所代表的 Peer。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../packages/boot/app-boot/README.zh.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。 源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts) @@ -24,8 +24,30 @@ interface WebRoute { } ``` +```ts type-equiv +/** + * Answers one GET or HEAD request with a Fetch response. An HTTP request + * arrives without a Peer and carries the client's headers; an in-process + * {@link WebServer.readResource} read passes the Peer its caller admitted. + */ +type WebResource = (request: Request, peer?: PeerScope) => Promise +``` + +```ts type-equiv +/** One named route answered by a resource instead of a handler. */ +interface WebResourceRoute { + kind: WebRouteKind + /** Absolute pathname, no trailing slash. */ + path: string + /** Answers HTTP GET and HEAD requests and in-process reads. */ + resource: WebResource +} +``` + 匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到已注册的回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,任何未被具名路由认领的请求都由回退席位应答;席位只有一个所有者,第二次注册会抛出异常。发布的 Web 组合用 [`dsh-host-frontend-static`](../../packages/host/frontend-static/src/index.ts) 认领席位,即遵循固定语义的 SPA dist 服务器:Connection 在读取 dist 根目录和配置 index 的 HTML 前完成认证;非 index 资产保持公开;非 GET/HEAD 返回 405,越出 dist 根目录的遍历返回 403,现有文件直接提供,缺失或不是文件的目标返回空的 404,未知扩展名按 octet-stream 发送。 +路由或回退可以用 resource——一个把 Fetch `Request` 映射为 `Response` 的函数——代替 handler。HTTP 的 `GET`、`HEAD` 请求不带 Peer 到达它,`readResource(request, peer)` 则在进程内按调用方准入的 Peer 读取它;读取遵循同样的匹配顺序但只到达 resource,因此返回 owner 私有数据或会执行操作的路由即使应答 `GET` 也保持为 handler。 + ## 配置 @@ -81,7 +103,7 @@ interface TlsConfig { ## 服务 -`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`tls` 证书对在监听器绑定前读取一次;文件不可读、为空或二者不匹配时同样会使初始化被拒绝,而不是回退到 HTTP。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。Gzip 在服务器内部包装符合条件且基于 socket 的响应,因此 route handler 继续直接持有 `ServerResponse`,服务也不新增响应写出 API。已有内容编码、`Cache-Control: no-transform`、范围响应、SSE、ZIP 与打包后的 `.gz` Worker 镜像均保持 identity 响应。`collectIndexInjections(peer)` 经一次传入页面所代表 Peer 的 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html, peer)` 把它们渲染进成功的根路径和配置 index 响应,随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口,`protocol` 则报告监听器提供的协议(`http:` 或 `https:`)。 +`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`tls` 证书对在监听器绑定前读取一次;文件不可读、为空或二者不匹配时同样会使初始化被拒绝,而不是回退到 HTTP。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。Gzip 在服务器内部包装符合条件且基于 socket 的响应,因此 handler 继续直接持有 `ServerResponse`,resource 则把 `Response` 交给服务写回。已有内容编码、`Cache-Control: no-transform`、范围响应、SSE、ZIP 与打包后的 `.gz` Worker 镜像均保持 identity 响应。`collectIndexInjections(peer)` 经一次传入页面所代表 Peer 的 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html, peer)` 把它们渲染进成功的根路径和配置 index 响应,随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口,`protocol` 则报告监听器提供的协议(`http:` 或 `https:`)。 处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。TLS socket 只有完成握手后才计入连接,因此服务器会把原始 TLS socket 与升级后的 socket 一并跟踪,并在 dispose 时销毁两者。该包从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.zh.md) 中。 @@ -180,7 +202,7 @@ Source: [`packages/bundle/web-app/src/index.ts`](../../packages/bundle/web-app/s ### `ctx.webServer` — `WebServer` -The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. +The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and a request no named route matches is answered 404 until the fallback owner registers. ```ts cordis-catalog /** @@ -194,10 +216,10 @@ getOrigin(): string /** * Register a named route. Duplicate (kind, path) throws — route patterns are * a composition-level contract, so a collision is a misconfiguration. - * @param route - kind, path, and the owning handler. + * @param route - kind, path, and the owning handler or resource. * @returns the disposer removing the route. */ -register(route: WebRoute): () => void +register(route: WebRoute | WebResourceRoute): () => void /** * Register an exact-path HTTP upgrade route. Duplicate paths throw because @@ -208,14 +230,32 @@ register(route: WebRoute): () => void registerUpgrade(route: WebUpgradeRoute): () => void /** - * Claim the fallback seat: the handler answering every request no named - * route matches (the SPA dist server in the shipped Web composition). One - * owner only — a second registration throws, because two fallbacks cannot - * compose. - * @param handler - owns the full response lifecycle of unmatched requests. + * Claim the fallback seat: the handler or resource answering every request + * no named route matches (the SPA dist server in the shipped Web + * composition). One owner only — a second registration throws, because two + * fallbacks cannot compose. + * @param fallback - a handler owning the full response lifecycle of + * unmatched requests, or `{ resource }` answering them. * @returns the disposer releasing the seat. */ -registerFallback(handler: WebRoute['handler']): () => void +registerFallback(fallback: WebRoute['handler'] | Pick): () => void + +/** + * Read the response an HTTP request for the same URL would receive, as the + * resource computes it for `peer`, without an HTTP exchange. The request + * path picks the responder by the HTTP match order — exact, longest prefix, + * then the fallback — and only a resource answers. This method checks no + * cookie, Host, or Origin: the caller admits the reader before reading. + * @param request - the read; its URL path selects the responder. + * @param peer - the Peer the read speaks for, passed to the resource. + * @returns the resource's response, without a body for HEAD as over HTTP; + * 405 with `Allow: GET, HEAD` for any other method, checked before matching, + * where over HTTP a handler route or an unmatched path answers such a + * method itself; 404 when the path matches a handler route, or reaches the + * fallback seat while it is unclaimed or holds a handler. A resource that + * throws rejects the read, where an HTTP request would receive 400. + */ +async readResource(request: Request, peer: PeerScope): Promise /** * Register a raw-HTML index transform, the escape hatch for markup no diff --git a/docs/subsystems/web.i18n.yaml b/docs/subsystems/web.i18n.yaml index 0de78d5234..f35bc0a539 100644 --- a/docs/subsystems/web.i18n.yaml +++ b/docs/subsystems/web.i18n.yaml @@ -15,8 +15,8 @@ en: b96bcdf42ced9eab zh: aa927e2450238557 /web-access/provider-availability: - en: da78eb96d684ad47 - zh: 4b726cc04c92ef13 + en: 38515cfa348e0950 + zh: 890b624c15d8aa41 /web-access/fetch-network-policy: en: 6851874be5841d90 zh: dc085bf3b82d77df diff --git a/docs/subsystems/web.md b/docs/subsystems/web.md index a3cc37f040..543e80fb47 100644 --- a/docs/subsystems/web.md +++ b/docs/subsystems/web.md @@ -125,6 +125,8 @@ type WebFetchBody = A provider's `available(): boolean` is a cheap LOCAL check (credential presence, parseable config) and **must not make network calls**. It is an input to execution-time selection, not a health system: `search()`/`fetch()` read it to pick a usable provider, and a selection failure surfaces as the structured `WebError` the caller routes on — which carries the branchable detail (the missing id or ambiguous candidate set) in its code and message. +A search or fetch provider may also implement `ready(llmProvider): Promise`, a configuration-only check that must not make network calls: it answers whether an operation made for a model request on that chat route has a credential to send. `searchReady(llmProvider?)` and `fetchReady(llmProvider?)` ask only the provider that `search()` or `fetch()` would select, treat a provider without `ready()` as ready, and answer true when selection fails, because the operation reports that failure as a `WebError`. Readiness is not a selection input; `dsh-tool-web` makes it the activation condition of `web_search` and `web_fetch`, so a model step whose route answers false neither declares nor dispatches the tool. + Selection never depends on registration, config, or HMR order: a capability has an explicit provider id (config `searchProvider`/`fetchProvider`, or the matching env var feeding the same field), or auto-selects when exactly one usable provider is registered; multiple usable providers with no configured id is `WEB_PROVIDER_AMBIGUOUS`, not first-wins. Each operation reads the configured id when it runs, so a saved settings change applies to the next search or fetch without remounting `ctx.web`. ## Fetch network policy @@ -194,6 +196,16 @@ registerFetchProvider(provider: WebFetchProvider): () => void */ async search(request: WebSearchRequest, signal?: AbortSignal): Promise +/** + * Whether the provider `search()` would select can authenticate a search made for a + * model request on the `llmProvider` chat route. Asks only that provider's `ready()`; + * one without `ready()` counts as ready. Makes no network calls. A selection failure + * answers true, because `search()` reports that failure as a structured `WebError`. + * @param llmProvider - the provider route id of the model request, when known. + * @returns false only when the selected provider reports that it cannot authenticate. + */ +async searchReady(llmProvider?: string): Promise + /** * Retrieve one URL through the selected provider. Resolves the provider at * call time with the selection rules above; throws {@link WebError} when the @@ -203,6 +215,16 @@ async search(request: WebSearchRequest, signal?: AbortSignal): Promise + +/** + * Whether the provider `fetch()` would select can authenticate a fetch made for a + * model request on the `llmProvider` chat route. Asks only that provider's `ready()`; + * one without `ready()` counts as ready. Makes no network calls. A selection failure + * answers true, because `fetch()` reports that failure as a structured `WebError`. + * @param llmProvider - the provider route id of the model request, when known. + * @returns false only when the selected provider reports that it cannot authenticate. + */ +async fetchReady(llmProvider?: string): Promise ``` Source: [`packages/web/web/src/index.ts`](../../packages/web/web/src/index.ts) diff --git a/docs/subsystems/web.zh.md b/docs/subsystems/web.zh.md index 95d940c89b..f678fdd442 100644 --- a/docs/subsystems/web.zh.md +++ b/docs/subsystems/web.zh.md @@ -125,6 +125,8 @@ type WebFetchBody = 提供方的 `available(): boolean` 是一个廉价的本地检查(凭证是否存在、配置是否可解析),**禁止发起网络调用**。它是执行时选择提供方的输入,而不是健康检查系统:`search()`/`fetch()` 会读取它来选择可用的提供方。选择失败时,调用方会收到可据以分支处理的结构化 `WebError`;其错误代码和消息会说明缺失的 id 或存在歧义的候选集。 +搜索或抓取提供方还可以实现 `ready(llmProvider): Promise`,这是只检查配置、禁止发起网络调用的判断:它回答为该对话路由上的模型请求发起的操作是否有可发送的凭据。`searchReady(llmProvider?)` 与 `fetchReady(llmProvider?)` 只询问 `search()` 或 `fetch()` 会选中的提供方,把没有 `ready()` 的提供方视为就绪,并在选择失败时回答 true,因为对应操作会把该失败报告为 `WebError`。就绪判断不是选择输入;`dsh-tool-web` 把它作为 `web_search` 与 `web_fetch` 的激活条件,因此路由回答 false 的模型步骤既不声明也不派发该工具。 + 选择从不依赖注册顺序、配置顺序或 HMR(热模块替换)顺序:一项能力要么有显式的提供方 id(配置 `searchProvider`/`fetchProvider`,或填充同一字段的对应环境变量),要么在恰好只有一个可用提供方注册时自动选择;如果存在多个可用提供方却未配置 id,则抛出 `WEB_PROVIDER_AMBIGUOUS`,而不会选用最先注册的提供方。每次操作在执行时读取配置的 id,因此保存的设置变更无需重新挂载 `ctx.web` 即对下一次搜索或抓取生效。 ## 抓取网络策略 @@ -194,6 +196,16 @@ registerFetchProvider(provider: WebFetchProvider): () => void */ async search(request: WebSearchRequest, signal?: AbortSignal): Promise +/** + * Whether the provider `search()` would select can authenticate a search made for a + * model request on the `llmProvider` chat route. Asks only that provider's `ready()`; + * one without `ready()` counts as ready. Makes no network calls. A selection failure + * answers true, because `search()` reports that failure as a structured `WebError`. + * @param llmProvider - the provider route id of the model request, when known. + * @returns false only when the selected provider reports that it cannot authenticate. + */ +async searchReady(llmProvider?: string): Promise + /** * Retrieve one URL through the selected provider. Resolves the provider at * call time with the selection rules above; throws {@link WebError} when the @@ -203,6 +215,16 @@ async search(request: WebSearchRequest, signal?: AbortSignal): Promise + +/** + * Whether the provider `fetch()` would select can authenticate a fetch made for a + * model request on the `llmProvider` chat route. Asks only that provider's `ready()`; + * one without `ready()` counts as ready. Makes no network calls. A selection failure + * answers true, because `fetch()` reports that failure as a structured `WebError`. + * @param llmProvider - the provider route id of the model request, when known. + * @returns false only when the selected provider reports that it cannot authenticate. + */ +async fetchReady(llmProvider?: string): Promise ``` Source: [`packages/web/web/src/index.ts`](../../packages/web/web/src/index.ts) diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 8265143029..bd8974669f 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 6cc2cd9ced70bb09 zh: 1c0e6cb4ba2ed637 /tool-schema-catalog/tool-package-map: - en: 10fe940a3d17cde7 - zh: ef1e8863c28e3c05 + en: 4d42222acd39d083 + zh: 8153a5065bf2ebd6 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -54,8 +54,14 @@ en: 4910ab327694ee97 zh: 17e79e4366cb636d /tool-schema-catalog/deepseek-ai-dsh-experimental-browser-use-stagehand-native/stagehand-tabs: - en: 095b18f9376e073d - zh: abd8785f8acaa555 + en: a0d5b9b891358120 + zh: 25793a62709770b5 +/tool-schema-catalog/deepseek-ai-dsh-action-cards: + en: 60c9df6ffda98a1d + zh: 60c9df6ffda98a1d +/tool-schema-catalog/deepseek-ai-dsh-action-cards/propose-action: + en: 633c32b6df889830 + zh: b88ea00b2033c11b /tool-schema-catalog/deepseek-ai-dsh-tool-ask-user: en: e713a4a11a15d13c zh: e713a4a11a15d13c diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 24101e126f..149cf1a144 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -19,6 +19,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-plugin-market` | `plugin_market` | `ctx.tools`, `ctx.pluginMarket` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - | +| `@deepseek-ai/dsh-action-cards` | `propose_action` | `ctx.tools`, `ctx.agents`, `ctx.sessionProjections`, `ctx.remoteAccess` | `tool/call`, `tool/result`, `plugin:action-cards/decision`, `user/message` | - | Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized recommendations bundle supplies the shipped kinds. | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. | | `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | The PTC executor exposes only run_code to the model and declares other visible capabilities in a generated SDK in the runtime’s language. Program bindings enter the shared guarded tool pipeline in submission order; concurrency-safe bodies overlap up to maxParallelSubCalls, and nested results remain linked to the outer program. | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agents`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | @@ -497,6 +498,49 @@ List, create, select, or close a Stagehand browser tab. Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts) + + +## `@deepseek-ai/dsh-action-cards` + +### `propose_action` + +Show the user one optional action card under this answer. Call only as the skill that names the action instructs. + +```json +{ + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action name from the skill." + }, + "title": { + "type": "string", + "description": "Card title." + }, + "brief": { + "type": "string", + "description": "What the action does, in the user's language." + }, + "details": { + "type": "object", + "description": "Fields the skill lists for this action.", + "additionalProperties": true + } + }, + "required": [ + "action", + "title", + "brief", + "details" + ] +} +``` + +Source: [`packages/experimental/action-cards/src/index.ts`](../packages/experimental/action-cards/src/index.ts) + +Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized recommendations bundle supplies the shipped kinds. + ## `@deepseek-ai/dsh-tool-ask-user` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 27670f9964..6fab7658a0 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -23,6 +23,7 @@ | `@deepseek-ai/dsh-plugin-market` | `plugin_market` | `ctx.tools`, `ctx.pluginMarket` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - | +| `@deepseek-ai/dsh-action-cards` | `propose_action` | `ctx.tools`、`ctx.agents`、`ctx.sessionProjections`、`ctx.remoteAccess` | `tool/call`、`tool/result`、`plugin:action-cards/decision`、`user/message` | - | 只向加载过已注册卡片种类 Skill 的 Agent 声明;各种类的包注册自己的动作,个性化推荐 Bundle 提供随产品发布的种类。 | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 | | `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | PTC 执行器只向模型公开 run_code,并在运行时语言生成的 SDK 中声明其他可见能力。程序绑定按提交顺序进入共享的受守卫工具流水线;并行安全的函数体最多重叠 maxParallelSubCalls 个,嵌套结果保持与外层程序关联。 | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agents`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 | @@ -501,6 +502,49 @@ 来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts) + + +## `@deepseek-ai/dsh-action-cards` + +### `propose_action` + +在本次回答下方向用户展示一张可选操作卡片。只按指定该操作的 Skill 的要求调用。 + +```json +{ + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action name from the skill." + }, + "title": { + "type": "string", + "description": "Card title." + }, + "brief": { + "type": "string", + "description": "What the action does, in the user's language." + }, + "details": { + "type": "object", + "description": "Fields the skill lists for this action.", + "additionalProperties": true + } + }, + "required": [ + "action", + "title", + "brief", + "details" + ] +} +``` + +来源:[`packages/experimental/action-cards/src/index.ts`](../packages/experimental/action-cards/src/index.ts) + +只向加载过已注册卡片种类 Skill 的 Agent 声明;各种类的包注册自己的动作,个性化推荐 Bundle 提供随产品发布的种类。 + ## `@deepseek-ai/dsh-tool-ask-user` diff --git a/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.i18n.yaml new file mode 100644 index 0000000000..b1b9c9f0b6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.md +/: + en: 10a5bce762cfa31b + zh: eddf92b54eb31683 +/approval-requires-the-projection-registry: + en: 89b130eab1f61761 + zh: f1382f60b6f8612f +/approval-requires-the-projection-registry/change: + en: a338aa2a5279676c + zh: 8744d85c90437bfd +/approval-requires-the-projection-registry/migration: + en: fd59212cd75f01a2 + zh: a36f9f6c5101d733 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.md new file mode 100644 index 0000000000..4d0c65f2a3 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.md @@ -0,0 +1,34 @@ +--- +kind: upgrade-guide +description: "Custom ApprovalService compositions must provide sessionProjections and await service activation." +--- + +# Approval requires the Projection registry + +English | [中文](guide.zh.md) + +## Change + +`ApprovalService` requires `sessionProjections`. It waits for projection registration during activation instead of reading the event log when the registry is absent or pending. Custom compositions that mounted approval alone must add the registry. Shipped profiles already provide it. + +Approval outcomes, audit events, policy defaults, and synchronous business methods keep their existing behavior. Session logs and checkpoint formats do not change. The private registration handle is process-local; checkpoints contain only the policy override and turn state. + +## Migration + +1. In custom `cordis.yml` files, mount `@deepseek-ai/dsh-session-projection` before `@deepseek-ai/dsh-user-approval`. Avoid adding a duplicate registry to a shipped profile. +2. In direct host compositions, use `ctx.plugin(ApprovalService)` and await activation before using `ctx.approval`. Do not construct the service with `new ApprovalService(...)`: that does not run `Service.init`. + + ```ts + import { Context } from '@deepseek-ai/cordis' + import SessionStore from '@deepseek-ai/dsh-session' + import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' + import ApprovalService from '@deepseek-ai/dsh-user-approval' + + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(ApprovalService) + ``` + +3. Let the host and registry complete Session recovery before exposing historical state to synchronous readers. Follow the [registry lifecycle](../../../../packages/session/session-projection/README.md); do not add history loading to approval getters or replay runtime hooks. +4. Confirm activation completes and the existing approval tests still produce the same outcomes and audit records. Registration errors must fail activation instead of selecting a full-log fallback. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.zh.md new file mode 100644 index 0000000000..9ab7d31107 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/approval-projection-dependency/guide.zh.md @@ -0,0 +1,34 @@ +--- +kind: upgrade-guide +description: "自定义 ApprovalService 组合必须提供 sessionProjections 并等待服务激活。" +--- + +# 审批服务要求 Projection 注册表 + +[English](guide.md) | 中文 + +## 变更 + +`ApprovalService` 要求 `sessionProjections`。它在激活时等待 Projection 注册,不再在注册表缺失或尚未就绪时读取事件日志。原先只挂载审批服务的自定义组合必须补上注册表。发布的配置已提供该服务。 + +审批结果、审计事件、策略默认值和同步业务方法保持原有行为。会话日志和检查点格式不变。私有注册句柄仅在进程内有效;检查点只保存策略覆盖值和轮次状态。 + +## 迁移 + +1. 在自定义 `cordis.yml` 中,先挂载 `@deepseek-ai/dsh-session-projection`,再挂载 `@deepseek-ai/dsh-user-approval`。不要向发布的配置重复添加注册表。 +2. 在直接挂载服务的宿主中,使用 `ctx.plugin(ApprovalService)` 并等待激活完成,再使用 `ctx.approval`。不要用 `new ApprovalService(...)` 直接构造服务;这不会执行 `Service.init`。 + + ```ts + import { Context } from '@deepseek-ai/cordis' + import SessionStore from '@deepseek-ai/dsh-session' + import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' + import ApprovalService from '@deepseek-ai/dsh-user-approval' + + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(ApprovalService) + ``` + +3. 由宿主和注册表完成会话恢复,再向同步读取方交付历史状态。遵循[注册表生命周期](../../../../packages/session/session-projection/README.zh.md);不要在审批 getter 中加载历史,也不要在回放时触发运行时钩子。 +4. 确认激活完成,原有审批测试仍产生相同的结果和审计记录。注册错误必须使激活失败,不能切换到全日志回退路径。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml index a13ae70849..9e6e37a1bf 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md /: - en: 383326881143f69b - zh: cd7ff0e2b97f9c2f -/japanese-is-a-built-in-language: - en: f97bae63a42a3627 - zh: cec418f8bf6ac168 -/japanese-is-a-built-in-language/change: - en: feb1f2d1cafdcfe0 - zh: 94d527865ec108bb -/japanese-is-a-built-in-language/migration: - en: ced2c2baad9124b4 - zh: 7b0b5b0e345994b8 + en: cfd1ffd2cd268220 + zh: 2ed03b1063f3319d +/built-in-languages-replace-duplicate-language-packs: + en: 5855b69ac12a5f50 + zh: a81de3239837f1c4 +/built-in-languages-replace-duplicate-language-packs/change: + en: 665e68d784661f15 + zh: 2fff6fcfceea7994 +/built-in-languages-replace-duplicate-language-packs/migration: + en: 6f77e0b507e09ca7 + zh: 59e0c71780f06e9e diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md index 256e77ae05..5c6cf0eaea 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md @@ -1,18 +1,18 @@ --- kind: upgrade-guide -description: "Built-in Japanese registrations conflict with third-party Japanese language packs." +description: "Built-in language registrations conflict with third-party language packs using the same ids." --- -# Japanese is a built-in language +# Built-in languages replace duplicate language packs English | [中文](guide.zh.md) ## Change -The locale plugin registers the `ja` language and Japanese dictionaries for `common` and `settings.locale`. Third-party language packs following the previous Japanese README example now fail with an already-registered language or dictionary error. +The locale plugin registers `ja`, `es`, `fr`, `de`, `ru`, and `pt`, with dictionaries for `common`, `settings.locale`, and the built-in UI plugins. Third-party language packs using these ids now fail with an already-registered language or dictionary error, including packs based on the earlier Japanese or French README examples. ## Migration -1. In the language pack's client entry, remove `ctx.locale.addLanguage({ id: 'ja', ... })` and registrations for `common` and `settings.locale` under `ja`. Remove other Japanese dictionary registrations whose namespaces are now supplied by built-in plugins; keep translations for namespaces owned by your own plugins. -2. If the pack represents a distinct language variant, register a separate BCP 47-style id, such as `ja-JP`, for both the definition and its dictionaries instead of `ja`. +1. In the language pack's client entry, remove `ctx.locale.addLanguage(...)` for these ids and dictionary registrations in namespaces supplied by built-in plugins. Keep translations for namespaces owned by your own plugins. +2. If the pack represents a distinct language variant, register a separate BCP 47-style id, such as `fr-CA`, for both the definition and its dictionaries instead of the built-in id. 3. Reload the application with the pack enabled. Confirm it starts without duplicate-registration errors and that selecting the intended language displays its translations. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md index 80382ebe9e..dd98916cc6 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md @@ -1,18 +1,18 @@ --- kind: upgrade-guide -description: "内置日语注册会与第三方日语语言包冲突。" +description: "内置语言注册会与使用相同 id 的第三方语言包冲突。" --- -# 日语成为内置语言 +# 内置语言取代重复的语言包 [English](guide.md) | 中文 ## 变更 -locale 插件注册 `ja` 语言,以及 `common` 和 `settings.locale` 的日文字典。按此前 README 日语示例编写的第三方语言包会因语言或字典已注册而加载失败。 +locale 插件注册 `ja`、`es`、`fr`、`de`、`ru` 和 `pt`,并提供 `common`、`settings.locale` 以及内置 UI 插件的字典。使用这些 id 的第三方语言包会因语言或字典已注册而加载失败,包括按此前 README 日语或法语示例编写的语言包。 ## 迁移 -1. 在语言包的 client 入口中,删除 `ctx.locale.addLanguage({ id: 'ja', ... })`,以及 `common` 和 `settings.locale` 下的 `ja` 注册。删除其他已由内置插件提供的命名空间的日语字典注册;保留自己插件所拥有命名空间的翻译。 -2. 如果语言包代表不同的语言变体,为语言定义及其字典注册独立的 BCP 47 风格 id,例如 `ja-JP`,代替 `ja`。 +1. 在语言包的 client 入口中,删除这些 id 的 `ctx.locale.addLanguage(...)`,以及内置插件已提供的命名空间下的字典注册。保留自己插件所拥有命名空间的翻译。 +2. 如果语言包代表不同的语言变体,为语言定义及其字典注册独立的 BCP 47 风格 id,例如 `fr-CA`,代替内置 id。 3. 启用语言包并重新加载应用。确认启动时没有重复注册错误,且选择目标语言后显示对应译文。 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.i18n.yaml new file mode 100644 index 0000000000..e0d389e024 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.md +/: + en: d6f85fefe3671f5d + zh: 2719b3d5ba1b2120 +/import-bundle-lists-from-deepseek-ai-dsh-official-bundles: + en: 89f87bcf95ee4262 + zh: 00bbb48605743bcc +/import-bundle-lists-from-deepseek-ai-dsh-official-bundles/change: + en: 0d8984d9bead43c6 + zh: 60939a14fcf1ba9a +/import-bundle-lists-from-deepseek-ai-dsh-official-bundles/migration: + en: ef2f3d6258a3b2bb + zh: 65a79e9551f8b237 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.md new file mode 100644 index 0000000000..1316bd299f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "The bundle lists and profile templates move from @deepseek-ai/dsh-app-boot to @deepseek-ai/dsh-official-bundles." +--- + +# Import bundle lists from `@deepseek-ai/dsh-official-bundles` + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-app-boot` no longer exports `PROFILE_TEMPLATES`, `ProfileTemplate`, `DEFAULT_PROFILE_BUNDLES`, `OPTIONAL_BUNDLES`, `ON_DEMAND_BUNDLES`, `OFFICIAL_ON_DEMAND_CATALOG`, or `OfficialBundleCatalogEntry`. The new published package `@deepseek-ai/dsh-official-bundles` exports them with unchanged values and types. Code that imports any of these names from `@deepseek-ai/dsh-app-boot` fails to compile or load. The `dsh` launcher, profiles, and bundle selection behave as before. + +## Migration + +1. Add `@deepseek-ai/dsh-official-bundles` to the `dependencies` of each package that imports one of these names. +2. Change those imports from `@deepseek-ai/dsh-app-boot` to `@deepseek-ai/dsh-official-bundles`, for example `import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'`. Keep importing every other name, such as `initProfile` or `loadProfile`, from `@deepseek-ai/dsh-app-boot`. +3. Run your type check or build; it succeeds when no import of a moved name from `@deepseek-ai/dsh-app-boot` remains. See the [package README](../../../../packages/util/official-bundles/README.md) for each list. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.zh.md new file mode 100644 index 0000000000..50047b0522 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/official-bundle-lists/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "组合包列表和 profile 模板从 @deepseek-ai/dsh-app-boot 移至 @deepseek-ai/dsh-official-bundles。" +--- + +# 从 `@deepseek-ai/dsh-official-bundles` 导入组合包列表 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-app-boot` 不再导出 `PROFILE_TEMPLATES`、`ProfileTemplate`、`DEFAULT_PROFILE_BUNDLES`、`OPTIONAL_BUNDLES`、`ON_DEMAND_BUNDLES`、`OFFICIAL_ON_DEMAND_CATALOG` 和 `OfficialBundleCatalogEntry`。新发布的包 `@deepseek-ai/dsh-official-bundles` 导出它们,值和类型不变。从 `@deepseek-ai/dsh-app-boot` 导入这些名称的代码将无法编译或加载。`dsh` 启动器、profile 和组合包选择的行为不变。 + +## 迁移 + +1. 在每个导入这些名称的包的 `dependencies` 中加入 `@deepseek-ai/dsh-official-bundles`。 +2. 把这些导入从 `@deepseek-ai/dsh-app-boot` 改为 `@deepseek-ai/dsh-official-bundles`,例如 `import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'`。其他名称(如 `initProfile` 或 `loadProfile`)仍从 `@deepseek-ai/dsh-app-boot` 导入。 +3. 运行类型检查或构建;不再有从 `@deepseek-ai/dsh-app-boot` 导入已移动名称的代码时即通过。各列表的说明见[包 README](../../../../packages/util/official-bundles/README.zh.md)。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.i18n.yaml new file mode 100644 index 0000000000..d75920e7c9 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.md +/: + en: 68f919837577038b + zh: 6ae8223b1542c369 +/ordinary-forks-are-independent-sessions: + en: 4556d51bd8086cfe + zh: 8811b03db9225739 +/ordinary-forks-are-independent-sessions/change: + en: 0a2790279aec9e24 + zh: 5cca673af988a228 +/ordinary-forks-are-independent-sessions/migration: + en: 53b586ad6498fdb8 + zh: dfa70928e17478e2 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.md new file mode 100644 index 0000000000..0bff80da2a --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.md @@ -0,0 +1,23 @@ +--- +kind: upgrade-guide +description: "Ordinary Session forks stop recording their source relationship and stop appearing as subagents." +--- + +# Ordinary forks are independent Sessions + +English | [中文](guide.zh.md) + +## Change + +Ordinary forks copy the selected history and continue independently. They omit `SessionHeader.parentSession` and the Client's `parentSessionId`; no replacement source field is written. Subagents, including fork-backend subagents, retain their direct creator. Existing logs retain their recorded metadata. + +SDK `subagent.started` excludes ordinary forks, including historical ones carrying a parent id. ACP list/resume admits ordinary forks; headless adoption also accepts them when cwd and preset checks pass. Client lists no longer indent them or insert newly discovered forks before their sources; saved manual positions remain intact. Source-based trace queries, parent filters, telemetry, and remote logs cannot recover the source of new ordinary forks. Workspace binding, optional title increments, inherited history, and seed boundaries remain available. + +The Session format stays V4. Older V4 readers may reject a parentless fork containing inherited delivery acknowledgements as a corrupt log with `current-generation delivery marker names the wrong Session`. The tested readers refuse before granting a handle or truncating the file. They do not provide an upgrade-specific diagnostic. See the [compatibility decision](../../../../.agents/notes/implemented/architecture/2026-10-23-independent-session-forks.md). + +## Migration + +1. Upgrade every reader and writer sharing the Session directory before resuming newly created forks. Do not edit old headers or delete delivery records to bypass an older reader's rejection. +2. Replace consumer checks that treat any `parentSession` as a subagent with `origin === 'subagent'`. Continuation still requires a valid descriptor from the Session's own event range; inherited descriptors do not establish its identity. +3. Remove any ordinary-fork workflow that requires persistent source tracing. Keep source-dependent actions such as workspace selection within the fork operation. +4. Confirm that an ordinary fork has no parent field, survives restart with inherited delivery records, and produces no `subagent.started`; confirm that real subagents retain their creator and notification. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.zh.md new file mode 100644 index 0000000000..fdd5b53f99 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.zh.md @@ -0,0 +1,23 @@ +--- +kind: upgrade-guide +description: "普通 Session fork 停止记录来源关系,也不再显示为子代理。" +--- + +# 普通 fork 是独立 Session + +[English](guide.md) | 中文 + +## 变更 + +普通 fork 复制选定历史并独立继续运行。它们省略 `SessionHeader.parentSession` 及客户端的 `parentSessionId`,不写入替代来源字段。子代理(包括使用 fork backend 的子代理)保留直接创建者。已有日志保留记录中的元数据。 + +SDK 的 `subagent.started` 排除普通 fork,包括携带父 ID 的历史会话。ACP 列表和恢复允许普通 fork;通过 cwd 和 preset 检查的普通 fork 也可由 headless 沿用。客户端列表不再将它们缩进,也不再把新发现的 fork 插到来源之前;已保存的手动位置保持不变。基于来源的追踪查询、父关系筛选、遥测和远端日志无法还原新普通 fork 的来源。工作区绑定、可选标题递增、继承历史和 seed 边界仍然可用。 + +Session 格式保持 V4。旧 V4 reader 可能将包含继承投递确认的无父关系 fork 作为损坏日志拒绝,报错为 `current-generation delivery marker names the wrong Session`。已测试的 reader 在授予句柄或截断文件之前拒绝,不提供专门的升级诊断。参见[兼容性决策](../../../../.agents/notes/implemented/architecture/2026-10-23-independent-session-forks.zh.md)。 + +## 迁移 + +1. 在恢复新建 fork 之前,升级共享 Session 目录的全部 reader 和 writer。不要编辑旧 header 或删除投递记录来绕过旧 reader 的拒绝。 +2. 将消费方中把任意 `parentSession` 视为子代理的检查替换为 `origin === 'subagent'`。继续运行仍要求来自 Session 自身事件区间的有效 descriptor;继承的 descriptor 不建立其身份。 +3. 移除依赖持久化来源追踪的普通 fork 工作流。工作区选择等依赖来源的动作保留在 fork 操作期间。 +4. 确认普通 fork 不含父字段、携带继承投递记录也能在重启后恢复,且不产生 `subagent.started`;确认真正子代理保留创建者及通知。 diff --git a/docs/user/guide/plugin-market.i18n.yaml b/docs/user/guide/plugin-market.i18n.yaml index 3a940f4be4..28f727d0e0 100644 --- a/docs/user/guide/plugin-market.i18n.yaml +++ b/docs/user/guide/plugin-market.i18n.yaml @@ -2,33 +2,33 @@ # section, a hash of its English and Chinese blocks outside code blocks and generated regions. # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/plugin-market.md -/plugin-markets: - en: 09b2a6328dcae63c - zh: ac7bce767e4f5011 -/plugin-markets/summary: - en: 2716b194b8a12aa7 - zh: b90773d095851969 -/plugin-markets/table-of-contents: +/plugin-indexes: + en: 5ee6a364e2708ab9 + zh: 369781529be73628 +/plugin-indexes/summary: + en: a1d8f17e7c4b8357 + zh: 13b95dcedde344f2 +/plugin-indexes/table-of-contents: en: a8d1e6785c3c3c30 zh: 7ea1eaa8204a5a1e -/plugin-markets/turn-on-discover: - en: a5024e57052f6e36 - zh: 39b84e247adc9a18 -/plugin-markets/browse-and-install: - en: cf616bba26958533 - zh: 1e32df16a6f4bba7 -/plugin-markets/publish-an-index: +/plugin-indexes/turn-on-discover: + en: f98bc0913d4bd046 + zh: 104094714786507e +/plugin-indexes/browse-and-install: + en: 0f5a4082d5debe9b + zh: c0e53f186cb31cd4 +/plugin-indexes/publish-an-index: en: b85b82b22c1e14b8 zh: 920c0d6f20956c07 -/plugin-markets/generate-an-index-with-the-reference-generator: +/plugin-indexes/generate-an-index-with-the-reference-generator: en: 6501406f530ffe33 zh: 700acae3f466c985 -/plugin-markets/index-format-reference: - en: d058d12c27433f64 - zh: f43b85e4934817f3 -/plugin-markets/further-exploration: - en: 3c0a90e8bf73c89b - zh: 95608eb4f6278cb4 -/plugin-markets/dev-note: +/plugin-indexes/index-format-reference: + en: 8fed66180a76f815 + zh: 7069ac4eed4bdfd9 +/plugin-indexes/further-exploration: + en: 2223e92806e7e79a + zh: 093309e1c40caa6c +/plugin-indexes/dev-note: en: b75c8458928b72b9 zh: 003ba4e17b4b5269 diff --git a/docs/user/guide/plugin-market.md b/docs/user/guide/plugin-market.md index a690e46c55..45e2629324 100644 --- a/docs/user/guide/plugin-market.md +++ b/docs/user/guide/plugin-market.md @@ -1,10 +1,10 @@ -# Plugin markets +# Plugin indexes English | [中文](plugin-market.zh.md) ## Summary -A plugin market is one JSON index file that lists installable DSH bundles with their titles, icons, authors, and versions. Turn on the experimental **Custom plugin markets** bundle to browse markets in a **Discover** tab on the Plugins page and install a listed version after a confirmation. To publish a market, write or generate an index file and serve it, together with its icon files, from any static host. +A plugin index is one JSON file that lists installable DSH bundles with their titles, icons, authors, and versions. Turn on the experimental **Custom plugin indexes** bundle to browse indexes in a **Discover** tab on the Plugins page and install a listed version after a confirmation. To publish an index, write or generate an index file and serve it, together with its icon files, from any static host. ## Table of Contents @@ -18,20 +18,20 @@ A plugin market is one JSON index file that lists installable DSH bundles with t ## Turn on Discover -Open **Plugins**, find **Custom plugin markets** under **Experimental plugins**, and switch it on. A **Discover** tab appears next to **Installed**, and Creator mode gains the read-only `plugin_market` tool. Switching the bundle off removes both again. +Open **Plugins**, find **Custom plugin indexes** under **Experimental plugins**, and switch it on. A **Discover** tab appears next to **Installed**, and Creator mode gains the read-only `plugin_market` tool. Switching the bundle off removes both again. -DSH ships no index source. Select **Discover**, choose **Add index source**, and enter the URL of an index file. A URL must use `http:` or `https:` and must not contain a user name, password, or `#` fragment. Anyone on the network path can change an `http:` index, including the install specs it lists, so use `https:` for a market reached over a network you do not control. Each source belongs to the current profile. +DSH ships no index source. Select **Discover**, choose **Add index source**, and enter the URL of an index file. A URL must use `http:` or `https:` and must not contain a user name, password, or `#` fragment. Anyone on the network path can change an `http:` index, including the install specs it lists, so use `https:` for an index reached over a network you do not control. Each source belongs to the current profile. ## Browse and install -The market menu at the left of the toolbar switches between sources and opens **Manage index sources…**, where each source shows its last error. Search matches names, authors, keywords, and titles, descriptions, categories, and labels in every language, and tolerates typos; results come in best-match order until you choose another. The **Labels** menu, category pills, and **Sort** menu use the categories, labels, and sort keys that the market declares, and **Publish date** sorts by each plugin's latest version. +The index menu at the left of the toolbar switches between sources and opens **Manage index sources…**, where each source shows its last error. Search matches names, authors, keywords, and titles, descriptions, categories, and labels in every language, and tolerates typos; results come in best-match order until you choose another. The **Labels** menu, category pills, and **Sort** menu use the categories, labels, and sort keys that the index declares, and **Publish date** sorts by each plugin's latest version. Each card shows the plugin's recommended version: the highest version compatible with the running DSH that is not a prerelease, or the highest compatible prerelease when the plugin lists no version without a prerelease tag. **Install** opens a confirmation with that version's source: the npm package and registry, or the Git repository, commit, and directory. Confirming starts the normal plugin installation, including its compatibility and registry checks; nothing is installed without that confirmation. Select a card to open its page. The **Versions** list shows every listed version with its compatibility and offers **Install** for any version DSH accepts, including older releases and prereleases. A card that shows **Installed**, **Update available**, or **Provided by DSH** opens the installed bundle instead. -Markets are third-party content. A market chooses each plugin's title, description, icon, author, and source; DSH checks the file format but not those claims. Add only sources you trust, and check the source in the confirmation before installing. +Indexes are third-party content. An index chooses each plugin's title, description, icon, author, and source; DSH checks the file format but not those claims. Add only sources you trust, and check the source in the confirmation before installing. ## Publish an index @@ -109,10 +109,10 @@ Text fields marked `LocalizedText` are a string or an object of strings keyed by | Index field | Required | Meaning | |---|---|---| | `format` | yes | `1`. | -| `title`, `description` | `title` only | Market name and description, `LocalizedText`. | +| `title`, `description` | `title` only | Index name and description, `LocalizedText`. | | `metadata.categories`, `metadata.labels` | no | Objects mapping an id to `{ "title": LocalizedText }`. | | `metadata.sortKeys` | no | Object mapping an id to `{ "title": LocalizedText, "order": "asc" \| "desc" }`; `order` is the initial direction. | -| `plugins` | yes | Plugins in the market's default order. | +| `plugins` | yes | Plugins in the index's default order. | | Plugin field | Required | Meaning | |---|---|---| @@ -140,7 +140,7 @@ DSH installs an npm version as `name@version`, asking `registry` first when pres ## Further Exploration - [Discover tab reference](../../../packages/client/ui-plugin-market/README.md) describes the tab's controls and states. -- [Plugin market service](../../../packages/boot/plugin-market/README.md) covers fetch limits, caching, and the `plugin_market` tool. +- [Plugin index service](../../../packages/boot/plugin-market/README.md) covers fetch limits, caching, and the `plugin_market` tool. - [Index validator](../../../packages/util/plugin-market-index/README.md) is the library generators can use to check an index before publishing it. ## Dev Note diff --git a/docs/user/guide/plugin-market.zh.md b/docs/user/guide/plugin-market.zh.md index 2385d0a75b..423b463a57 100644 --- a/docs/user/guide/plugin-market.zh.md +++ b/docs/user/guide/plugin-market.zh.md @@ -1,10 +1,10 @@ -# 插件市场 +# 插件索引 [English](plugin-market.md) | 中文 ## 概述 -插件市场是一个 JSON 索引文件,列出可安装的 DSH 组合包(bundle)及其标题、图标、作者和版本。打开实验性的**自定义插件市场**组合包后,可以在插件页的**发现**标签页中浏览市场,并在确认后安装列出的版本。要发布市场,编写或生成一个索引文件,并把它和图标文件一起放到任意静态主机上。 +插件索引是一个 JSON 文件,列出可安装的 DSH 组合包(bundle)及其标题、图标、作者和版本。打开实验性的**自定义插件索引**组合包后,可以在插件页的**发现**标签页中浏览索引,并在确认后安装列出的版本。要发布索引,编写或生成一个索引文件,并把它和图标文件一起放到任意静态主机上。 ## 目录 @@ -18,20 +18,20 @@ ## 打开「发现」 -打开**插件**,在**实验性插件**下找到**自定义插件市场**并打开开关。**已安装**旁边会出现**发现**标签页,Creator 模式也会获得只读的 `plugin_market` 工具。关闭该组合包会再次移除这两者。 +打开**插件**,在**实验性插件**下找到**自定义插件索引**并打开开关。**已安装**旁边会出现**发现**标签页,Creator 模式也会获得只读的 `plugin_market` 工具。关闭该组合包会再次移除这两者。 -DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,输入一个索引文件的 URL。URL 必须使用 `http:` 或 `https:`,并且不能包含用户名、密码或 `#` 片段。网络路径上的任何人都能修改 `http:` 索引,包括其中列出的安装 spec,因此经由不受你控制的网络访问的市场应使用 `https:`。每个索引源属于当前 profile。 +DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,输入一个索引文件的 URL。URL 必须使用 `http:` 或 `https:`,并且不能包含用户名、密码或 `#` 片段。网络路径上的任何人都能修改 `http:` 索引,包括其中列出的安装 spec,因此经由不受你控制的网络访问的索引应使用 `https:`。每个索引源属于当前 profile。 ## 浏览和安装 -工具栏左侧的市场菜单用于切换索引源,并可打开**管理索引源…**,其中每个索引源显示它最近一次的错误。搜索匹配名称、作者、关键词,以及所有语言的标题、描述、分类和标签,并容忍拼写错误;在你选择其他排序之前,结果按最佳匹配排序。**标签**菜单、分类按钮和**排序**菜单使用市场声明的分类、标签和排序键,**发布时间**按每个插件最新版本的时间排序。 +工具栏左侧的索引菜单用于切换索引源,并可打开**管理索引源…**,其中每个索引源显示它最近一次的错误。搜索匹配名称、作者、关键词,以及所有语言的标题、描述、分类和标签,并容忍拼写错误;在你选择其他排序之前,结果按最佳匹配排序。**标签**菜单、分类按钮和**排序**菜单使用索引声明的分类、标签和排序键,**发布时间**按每个插件最新版本的时间排序。 每张卡片显示插件的推荐版本:与当前运行的 DSH 兼容、且不是预发布版本的最高版本;插件没有列出任何非预发布版本时,则是兼容的最高预发布版本。**安装**会打开确认框,显示该版本的来源:npm 包和注册表,或 Git 仓库、提交和目录。确认后开始常规的插件安装,包括其中的兼容性检查和注册表检查;未经确认不会安装任何内容。 选择卡片会打开插件页面。**版本**列表显示列出的每个版本及其兼容性,并为 DSH 接受的任何版本提供**安装**,包括旧版本和预发布版本。显示**已安装**、**有新版本**或**随 DSH 提供**的卡片会改为打开已安装的组合包。 -市场是第三方内容。每个插件的标题、描述、图标、作者和来源都由市场决定;DSH 检查文件格式,但不验证这些声明。只添加你信任的索引源,并在安装前核对确认框中的来源。 +索引是第三方内容。每个插件的标题、描述、图标、作者和来源都由索引决定;DSH 检查文件格式,但不验证这些声明。只添加你信任的索引源,并在安装前核对确认框中的来源。 ## 发布索引 @@ -109,10 +109,10 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start --input plugins.txt | 索引字段 | 必填 | 含义 | |---|---|---| | `format` | 是 | `1`。 | -| `title`、`description` | 仅 `title` | 市场名称和描述,类型为 `LocalizedText`。 | +| `title`、`description` | 仅 `title` | 索引名称和描述,类型为 `LocalizedText`。 | | `metadata.categories`、`metadata.labels` | 否 | 将 id 映射为 `{ "title": LocalizedText }` 的对象。 | | `metadata.sortKeys` | 否 | 将 id 映射为 `{ "title": LocalizedText, "order": "asc" \| "desc" }` 的对象;`order` 是初始方向。 | -| `plugins` | 是 | 按市场默认顺序排列的插件。 | +| `plugins` | 是 | 按索引默认顺序排列的插件。 | | 插件字段 | 必填 | 含义 | |---|---|---| @@ -140,7 +140,7 @@ DSH 将 npm 版本安装为 `name@version`,存在 `registry` 时先询问它 ## 延伸阅读 - [「发现」标签页参考](../../../packages/client/ui-plugin-market/README.zh.md)说明该标签页的控件和状态。 -- [插件市场服务](../../../packages/boot/plugin-market/README.zh.md)说明获取限制、缓存和 `plugin_market` 工具。 +- [插件索引服务](../../../packages/boot/plugin-market/README.zh.md)说明获取限制、缓存和 `plugin_market` 工具。 - [索引校验器](../../../packages/util/plugin-market-index/README.zh.md)是生成器可以用来在发布前检查索引的库。 ## 开发备注 diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml index 93bc2cc11f..b04aada43b 100644 --- a/docs/user/guide/providers.i18n.yaml +++ b/docs/user/guide/providers.i18n.yaml @@ -6,8 +6,8 @@ en: b1631dcf25e805a1 zh: 8ded02e2ae87f00b /configure-models/configure-deepseek: - en: 2c6b665adc256b4e - zh: 2240e4c1573ebe9c + en: 44852da64a6a39ec + zh: 4c280e428f0fec08 /configure-models/add-a-third-party-provider: en: f6fb44a85dc5e4dd zh: 2b86ea4492c32ed7 diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md index 144b33d835..fe831bcaf9 100644 --- a/docs/user/guide/providers.md +++ b/docs/user/guide/providers.md @@ -12,6 +12,8 @@ Open **Settings → Models**. The DeepSeek card exposes one API-key field; enter Keys are write-only. The page receives a redacted descriptor after saving, never the literal secret. The key is stored in `$DSH_HOME/.credentials.yaml`, while settings retain only its credential reference. +With a key saved and the default endpoint, DeepSeek's models follow the list the DeepSeek API publishes. The list loads when the models are first needed, refreshes in the background about every ten minutes while they are in use, and falls back to the built-in models when it cannot load. Editing **Models** under **Customized settings** keeps your own list instead and stops following the published one; **Restore defaults** follows it again and reloads it. While the published list is in use, the card shows the built-in models, so a newly published model appears only in the model picker. The [API-key provider reference](../../../packages/llm/llm-deepseek-api-key/README.md) owns the listing, fallback, and gating rules. + ## Add a third-party provider Choose **Add model provider**. The card opens on **Third-party model provider**: pick a provider dsh ships with — the list shows provider ids such as `anthropic`, `openai`, `moonshotai` for Kimi, or `zai` for GLM — enter its API key, and save. The installed catalog supplies the endpoint, protocol, and model list. diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md index e20d1eb4aa..bb518c42b9 100644 --- a/docs/user/guide/providers.zh.md +++ b/docs/user/guide/providers.zh.md @@ -12,6 +12,8 @@ 密钥是只写的。保存后,页面只会收到脱敏描述符,永远不会收到明文密钥。密钥存储在 `$DSH_HOME/.credentials.yaml` 中,settings 只保留它的凭据引用。 +保存了密钥且使用默认端点时,DeepSeek 的模型跟随 DeepSeek API 发布的列表。第一次需要模型时加载列表,使用期间约每十分钟在后台刷新一次,无法加载时退回内置模型。在**自定义设置**里编辑**模型目录**会改用你自己的列表,不再跟随发布的列表;点**恢复默认模型**会重新跟随并重新加载。使用发布的列表时,卡片上显示的是内置模型,因此新发布的模型只出现在模型选择器中。列表、兜底和门控规则见 [API key 提供方参考](../../../packages/llm/llm-deepseek-api-key/README.zh.md)。 + ## 添加第三方提供商 选择**添加模型提供商**。卡片默认打开在**第三方模型提供商**:选取 dsh 自带的提供商——列表显示的是提供商 id,例如 `anthropic`、`openai`、Kimi 对应的 `moonshotai`、GLM 对应的 `zai`——输入其 API 密钥并保存。已安装目录会提供端点、协议和模型列表。 diff --git a/package.json b/package.json index 5bc1973359..01e6ce1bad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "license": "MIT", "private": true, "type": "module", @@ -24,7 +24,7 @@ "build:bench": "npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts", "build:official": "tsx scripts/build.ts --profile official", "build:lib": "pnpm run build:lib:host && pnpm run build:lib:client", - "build:lib:host": "tsx scripts/compile-referenced-projects.ts host-libraries && node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -p tsconfig.host.json && pnpm --filter @deepseek-ai/dsh-desktop run bundle", + "build:lib:host": "tsx scripts/compile-referenced-projects.ts host-libraries && node --max-old-space-size=8192 ./node_modules/typescript/bin/tsc -p tsconfig.host.json && pnpm --filter @deepseek-ai/dsh-desktop run bundle", "build:lib:client": "tsc -b tsconfig.client.json && tsdown --config-loader native --env.DSH_BUILD_FACE client", "build:web": "pnpm --filter @deepseek-ai/dsh-web-frontend run build", "build:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run build", @@ -134,7 +134,7 @@ "docs:build": "tsx website/build.ts && pnpm run verify-doc-site-fragments", "docs:build:mpa": "tsx website/build.ts --mpa && pnpm run verify-doc-site-fragments", "docs:preview": "pnpm --filter @deepseek-ai/website run preview", - "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts website/tests/mermaid-viewer.spec.ts website/tests/image-viewer.spec.ts website/tests/code-groups.spec.ts website/tests/page-markdown-actions.spec.ts website/tests/raw-markdown.spec.ts && pnpm run docs:build", + "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts website/tests/mermaid-viewer.spec.ts website/tests/image-viewer.spec.ts website/tests/code-groups.spec.ts website/tests/page-markdown-actions.spec.ts website/tests/raw-markdown.spec.ts website/tests/table-filter.spec.ts && pnpm run docs:build", "website:dev": "pnpm run docs:dev", "website:build": "pnpm run docs:build", "verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts", @@ -300,6 +300,7 @@ "@deepseek-ai/dsh-cua-bundle": "workspace:*", "@deepseek-ai/dsh-experimental-subagent-oar": "workspace:*", "@deepseek-ai/dsh-experimental-tool-subagent-oar": "workspace:*", - "@deepseek-ai/dsh-experimental-cua-macos": "workspace:*" + "@deepseek-ai/dsh-experimental-cua-macos": "workspace:*", + "@deepseek-ai/dsh-action-cards": "workspace:*" } } diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 4412f95938..92c89f5ff2 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -15,8 +15,8 @@ en: ffc55792d6e922e1 zh: 83bd759638857e2d /packages/package-groups: - en: ff0b0b8388457f47 - zh: b9ace22bb3cbad48 + en: c397fc0c1885058a + zh: c720d0e91dd1d15e /packages/release-expectations: en: 236081c1659b25ab zh: 809110adf003802e diff --git a/packages/README.md b/packages/README.md index 9485cc1001..ed7c55ec5a 100644 --- a/packages/README.md +++ b/packages/README.md @@ -79,7 +79,7 @@ Every package lives in exactly one group; new packages join existing groups, and | [`host/`](host/README.md) | Web GUI host services, directory picking, application launch, plugin inventory, and product telemetry | | [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | | [`test-support/`](test-support/README.md) | Test infrastructure (testkits, replay, Loader smokes) | -| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (`Branded`, home/path helpers, timeout, retention) | +| [`util/`](util/README.md) | Low-level utilities and data-only package lists shared across groups (`Branded`, home/path helpers, timeout, retention, official bundle lists) | ----- diff --git a/packages/README.zh.md b/packages/README.zh.md index ec2a9b81f8..ae3a18bbc1 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -79,7 +79,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`host/`](host/README.zh.md) | Web GUI 宿主服务、目录选择、应用启动、插件清单和产品遥测 | | [`client/`](client/README.zh.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 | | [`test-support/`](test-support/README.zh.md) | 测试基础设施(testkit、回放、Loader 冒烟测试) | -| [`util/`](util/README.zh.md) | 组间共享的低层零依赖工具(`Branded`、home/路径辅助函数、超时、留存) | +| [`util/`](util/README.zh.md) | 组间共享的低层工具与纯数据包列表(`Branded`、home/路径辅助函数、超时、留存、官方组合包列表) | ----- diff --git a/packages/acp/acp/README.i18n.yaml b/packages/acp/acp/README.i18n.yaml index aca817ae8a..bbd4e8b862 100644 --- a/packages/acp/acp/README.i18n.yaml +++ b/packages/acp/acp/README.i18n.yaml @@ -27,8 +27,8 @@ en: 4ed8e37834267edb zh: a32d5dedb4e8e919 /deepseek-ai-dsh-acp/use-this-package/protocol-contract: - en: 8f358b006f93f6ab - zh: a02dc9640cac4edd + en: 195315c12edf895f + zh: de307288858a8ece /deepseek-ai-dsh-acp/understand-the-implementation: en: ea8afe89dbdd1934 zh: 6567979f92f34afd diff --git a/packages/acp/acp/README.md b/packages/acp/acp/README.md index 44a63a1da2..06ba85def5 100644 --- a/packages/acp/acp/README.md +++ b/packages/acp/acp/README.md @@ -73,6 +73,8 @@ One connection can run several sessions at once, each independent. The calls a c | `session/update` | Committed assistant messages and thoughts, generic tool lifecycle, configuration changes, and context usage, serialized per session. | | `session/request_permission` | A permission prompt with one-shot allow/reject choices; your client can answer automatically. | +Session listing and resume admit ordinary forks, including historical logs with `parentSession`; `origin: subagent` remains excluded. + A prompt remains in flight while managed descendants run, while their results trigger later root turns, and while committed output drains. Root work accepted during that output drain joins the same activity interval before completion. Its completion includes the latest root stop reason and any assistant output delivery failure; only the original prompt turn pins the model selection captured at admission. Cancellation stops the root activity present at that call and waits for that activity and its queued output. Later child messages may start autonomous work and do not delay the cancelled response; the session remains available for later prompts. Idle descendants with parked inbox messages remain resident but do not delay prompt completion. Closing the session also drains its descendants. A failure to append `turn/start` or `turn/end` rejects the prompt even when a preceding turn completed. diff --git a/packages/acp/acp/README.zh.md b/packages/acp/acp/README.zh.md index 500c90f5d2..a22b08c924 100644 --- a/packages/acp/acp/README.zh.md +++ b/packages/acp/acp/README.zh.md @@ -73,6 +73,8 @@ kind: "package-reference" | `session/update` | 已提交 assistant 消息与 thought、通用工具生命周期、配置变化与上下文用量,按会话串行交付。 | | `session/request_permission` | 带一次性允许/拒绝选项的权限提示;你的客户端可以自动回答。 | +会话列表和恢复允许普通 fork,包括含有 `parentSession` 的历史日志;`origin: subagent` 仍被排除。 + 受管理后代运行期间、它们的结果触发根 Agent 后续轮次期间,以及已提交输出仍在交付期间,提示词保持进行中。输出交付期间接受的根 Agent 工作会加入同一活动区间,并在该区间完成后结算。最终结果包含最新根轮次的停止原因与所有助手输出交付错误;只有原始提示词轮次固定使用接受时捕获的模型选择。取消会停止调用时已有的根 Agent 活动,并等待该活动及其已排队输出完成。之后到达的子 Agent 消息可以启动自主工作,但不会延迟取消响应;会话仍可接受后续提示词。收件箱消息处于停放状态的空闲后代继续驻留,但不延迟提示词完成。关闭会话还会释放其后代。 即使前一轮已经完成,追加 `turn/start` 或 `turn/end` 失败仍会使提示词请求失败。 diff --git a/packages/acp/acp/locale/de.json b/packages/acp/acp/locale/de.json new file mode 100644 index 0000000000..1d616ae6f6 --- /dev/null +++ b/packages/acp/acp/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP-Server", + "description": "Führen Sie Agenten zur Automatisierung über JSON-RPC über Standardeingabe und -ausgabe aus." + } +} diff --git a/packages/acp/acp/locale/es.json b/packages/acp/acp/locale/es.json new file mode 100644 index 0000000000..4970a451fb --- /dev/null +++ b/packages/acp/acp/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "servidor ACP", + "description": "Ejecute agentes a través de JSON-RPC a través de entrada y salida estándar para automatización." + } +} diff --git a/packages/acp/acp/locale/fr.json b/packages/acp/acp/locale/fr.json new file mode 100644 index 0000000000..a8faaceeaa --- /dev/null +++ b/packages/acp/acp/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Serveur ACP", + "description": "Exécutez les agents via JSON-RPC via une entrée et une sortie standard pour l'automatisation." + } +} diff --git a/packages/acp/acp/locale/pt.json b/packages/acp/acp/locale/pt.json new file mode 100644 index 0000000000..cad2b3ae48 --- /dev/null +++ b/packages/acp/acp/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP servidor", + "description": "Execute agentes por meio de JSON-RPC em entradas e saídas padrão para automação." + } +} diff --git a/packages/acp/acp/locale/ru.json b/packages/acp/acp/locale/ru.json new file mode 100644 index 0000000000..fefd7d956d --- /dev/null +++ b/packages/acp/acp/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP сервер", + "description": "Запускайте агенты через JSON-RPC через стандартный ввод и вывод для автоматизации." + } +} diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index ca9ad8609d..41962dd596 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/acp/acp/src/index.ts b/packages/acp/acp/src/index.ts index c05ccbf3e5..792ec6a424 100644 --- a/packages/acp/acp/src/index.ts +++ b/packages/acp/acp/src/index.ts @@ -248,7 +248,7 @@ export function apply(ctx: Context, config: AcpConfig): void { activating.add(sessionId) return (async (): Promise => { const persisted = (await persistence.stat(sessionId, { signal }))?.header - if (persisted === undefined || persisted.origin === 'subagent' || persisted.parentSession !== undefined) { + if (persisted === undefined || persisted.origin === 'subagent') { throw invalidParams(`session is not resumable: ${sessionId}`) } if (!await sameDirectory(persisted.cwd, params.cwd)) { @@ -308,7 +308,6 @@ export function apply(ctx: Context, config: AcpConfig): void { || activating.has(header.id) || ctx.sessions.get(header.id) !== undefined || header.origin === 'subagent' - || header.parentSession !== undefined || header.cwd === undefined || !isAbsolute(header.cwd) ) return undefined diff --git a/packages/acp/acp/tests/bridge.spec.ts b/packages/acp/acp/tests/bridge.spec.ts index dcfcfddef6..c13044799e 100644 --- a/packages/acp/acp/tests/bridge.spec.ts +++ b/packages/acp/acp/tests/bridge.spec.ts @@ -6,7 +6,7 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { AttachmentError } from '@deepseek-ai/dsh-attachment' import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' -import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session' +import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionHeader } from '@deepseek-ai/dsh-session' import { SessionPersistenceRevision, type SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence' import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit' @@ -388,6 +388,22 @@ describe('automation-only ACP bridge', () => { await expect(harness.client.listSessions({ cursor: 'not-a-cursor' })).rejects.toThrow(/cursor is invalid/) }) + it.each([false, true])('resumes an ordinary fork with legacy parent metadata=%s', async (legacy) => { + harness = await makeBridgeHarness() + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const id = SessionId('ordinary-fork') + const writer = await harness.ctx.sessionPersistence.create({ + version: SESSION_FORMAT_VERSION, id, createdAt: 1, isSeeded: true, + cwd: process.cwd(), ...(legacy ? { parentSession: SessionId('source') } : {}), + }, { inheritedEventCount: SessionLogOffset(0) }) + try { + await writer.append([{ type: 'session/end-seed', seq: SessionSeq(0), time: 1, data: { inherited: true } }]) + } finally { await writer.close() } + await expect(harness.client.listSessions({})).resolves.toMatchObject({ sessions: [{ sessionId: id }] }) + await harness.client.resumeSession({ sessionId: id, cwd: process.cwd() }) + expect(harness.ctx.agents.get(id)?.session.header.parentSession).toBe(legacy ? 'source' : undefined) + }) + it('filters non-resumable headers and canonical missing workspaces', async () => { harness = await makeBridgeHarness() await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) @@ -411,6 +427,7 @@ describe('automation-only ACP bridge', () => { await expect(harness.client.listSessions({ cwd: 'relative' })).rejects.toThrow(/absolute path/) await expect(harness.client.listSessions({ cwd: '/missing/filter' })).resolves.toEqual({ sessions: [ + { sessionId: 'fork', cwd: '/missing/filter' }, { sessionId: 'valid-a', cwd: '/missing/filter' }, { sessionId: 'valid-b', cwd: '/missing/filter' }, ], diff --git a/packages/api/account-controller/locale/de.json b/packages/api/account-controller/locale/de.json new file mode 100644 index 0000000000..f1e7b8ca6c --- /dev/null +++ b/packages/api/account-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Benutzeroberfläche für den Kontobetrieb", + "description": "Bieten Sie authentifizierten Clients Kontoverwaltungsfunktionen." + } +} diff --git a/packages/api/account-controller/locale/es.json b/packages/api/account-controller/locale/es.json new file mode 100644 index 0000000000..7bfaac6431 --- /dev/null +++ b/packages/api/account-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de operación de cuenta", + "description": "Proporcionar capacidades de operación de cuentas a clientes autenticados." + } +} diff --git a/packages/api/account-controller/locale/fr.json b/packages/api/account-controller/locale/fr.json new file mode 100644 index 0000000000..a77bd7e7a1 --- /dev/null +++ b/packages/api/account-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'opération de compte", + "description": "Fournir des capacités d’exploitation de compte aux clients authentifiés." + } +} diff --git a/packages/api/account-controller/locale/pt.json b/packages/api/account-controller/locale/pt.json new file mode 100644 index 0000000000..22670a3e3b --- /dev/null +++ b/packages/api/account-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de operação da conta", + "description": "Fornece recursos de operação de conta para clientes autenticados." + } +} diff --git a/packages/api/account-controller/locale/ru.json b/packages/api/account-controller/locale/ru.json new file mode 100644 index 0000000000..dd3e515e30 --- /dev/null +++ b/packages/api/account-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс работы со счетом", + "description": "Предоставьте возможности управления учетной записью аутентифицированным клиентам." + } +} diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index 088357e98f..30aa41c6da 100644 --- a/packages/api/account-controller/package.json +++ b/packages/api/account-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-account-controller", "description": "Expose safe account operations over authenticated Remote", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/locale/de.json b/packages/api/gateway/locale/de.json new file mode 100644 index 0000000000..f2fd7153f5 --- /dev/null +++ b/packages/api/gateway/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Remote-Call-Gateway", + "description": "Empfangen Sie den Schnittstellenaufruf des Clients und verteilen Sie ihn an den entsprechenden Dienst." + } +} diff --git a/packages/api/gateway/locale/es.json b/packages/api/gateway/locale/es.json new file mode 100644 index 0000000000..7c02c79a8e --- /dev/null +++ b/packages/api/gateway/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "puerta de enlace de llamada remota", + "description": "Recibir la llamada de interfaz del cliente y distribuirla al servicio correspondiente." + } +} diff --git a/packages/api/gateway/locale/fr.json b/packages/api/gateway/locale/fr.json new file mode 100644 index 0000000000..2eec7623b4 --- /dev/null +++ b/packages/api/gateway/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "passerelle d'appel à distance", + "description": "Recevez l'appel d'interface du client et distribuez-le au service correspondant." + } +} diff --git a/packages/api/gateway/locale/pt.json b/packages/api/gateway/locale/pt.json new file mode 100644 index 0000000000..856eba1312 --- /dev/null +++ b/packages/api/gateway/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "gateway de chamada remota", + "description": "Receba a chamada de interface do cliente e distribua-a ao serviço correspondente." + } +} diff --git a/packages/api/gateway/locale/ru.json b/packages/api/gateway/locale/ru.json new file mode 100644 index 0000000000..7e7cf69bd3 --- /dev/null +++ b/packages/api/gateway/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "шлюз удаленных вызовов", + "description": "Получите вызов интерфейса клиента и передайте его соответствующему сервису." + } +} diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index d525c2dedf..2b4183a776 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-gateway", "description": "Typert Remote Host dispatcher and Client API endpoint", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/tests/gateway.host.spec.ts b/packages/api/gateway/tests/gateway.host.spec.ts index 6ec0e64bbb..4272ee90bb 100644 --- a/packages/api/gateway/tests/gateway.host.spec.ts +++ b/packages/api/gateway/tests/gateway.host.spec.ts @@ -153,7 +153,7 @@ class FakeConnectionService extends Service { function fakeHttpServer(routes: WebRoute[]): Pick { return { - register(route) { + register(route: WebRoute) { if (routes.some(candidate => candidate.kind === route.kind && candidate.path === route.path)) { throw new Error(`duplicate route ${route.path}`) } diff --git a/packages/api/job-controller/locale/de.json b/packages/api/job-controller/locale/de.json new file mode 100644 index 0000000000..ffddc2d9c6 --- /dev/null +++ b/packages/api/job-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Beobachtung von Hintergrundaufgaben", + "description": "Stellen Sie dem Kunden kontinuierlich den Status und die Ausgabe von Hintergrundaufgaben zur Verfügung." + } +} diff --git a/packages/api/job-controller/locale/es.json b/packages/api/job-controller/locale/es.json new file mode 100644 index 0000000000..d7fbfdc02e --- /dev/null +++ b/packages/api/job-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de observación de tareas en segundo plano", + "description": "Proporcionar continuamente el estado y la salida de las tareas en segundo plano al cliente." + } +} diff --git a/packages/api/job-controller/locale/fr.json b/packages/api/job-controller/locale/fr.json new file mode 100644 index 0000000000..aad494710a --- /dev/null +++ b/packages/api/job-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'observation des tâches en arrière-plan", + "description": "Fournir en permanence l'état et la sortie des tâches en arrière-plan au client." + } +} diff --git a/packages/api/job-controller/locale/pt.json b/packages/api/job-controller/locale/pt.json new file mode 100644 index 0000000000..c98a63561b --- /dev/null +++ b/packages/api/job-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de observação de tarefas em segundo plano", + "description": "Fornece continuamente o status e a saída de tarefas em segundo plano ao cliente." + } +} diff --git a/packages/api/job-controller/locale/ru.json b/packages/api/job-controller/locale/ru.json new file mode 100644 index 0000000000..60ecb46b94 --- /dev/null +++ b/packages/api/job-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс наблюдения за фоновыми задачами", + "description": "Постоянно предоставляйте клиенту статус и вывод фоновых задач." + } +} diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index da53adf0fe..40649ebff3 100644 --- a/packages/api/job-controller/package.json +++ b/packages/api/job-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-job-controller", "description": "Job Remote observation stream and the reference-counted client job-output service", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/remote-access/locale/de.json b/packages/api/remote-access/locale/de.json new file mode 100644 index 0000000000..14b83f143c --- /dev/null +++ b/packages/api/remote-access/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Fernzugriffskontrolle", + "description": "Steuern Sie, mit welchen Methoden Nicht-Eigentümer-Verbindungen auf Berechtigungs-Snapshots für jede Verbindung zugreifen und diese beibehalten können." + } +} diff --git a/packages/api/remote-access/locale/es.json b/packages/api/remote-access/locale/es.json new file mode 100644 index 0000000000..dda54e1529 --- /dev/null +++ b/packages/api/remote-access/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "control de acceso remoto", + "description": "Controle a qué métodos pueden acceder las conexiones que no son propietarios y retener instantáneas de permisos para cada conexión." + } +} diff --git a/packages/api/remote-access/locale/fr.json b/packages/api/remote-access/locale/fr.json new file mode 100644 index 0000000000..96082a9342 --- /dev/null +++ b/packages/api/remote-access/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Contrôle d'accès à distance", + "description": "Contrôlez les méthodes auxquelles les connexions non propriétaires peuvent accéder et conserver les instantanés d'autorisation pour chaque connexion." + } +} diff --git a/packages/api/remote-access/locale/pt.json b/packages/api/remote-access/locale/pt.json new file mode 100644 index 0000000000..840eaf556f --- /dev/null +++ b/packages/api/remote-access/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Controle de acesso remoto", + "description": "Controle quais métodos as conexões não proprietárias podem acessar e reter instantâneos de permissão para cada conexão." + } +} diff --git a/packages/api/remote-access/locale/ru.json b/packages/api/remote-access/locale/ru.json new file mode 100644 index 0000000000..1faa15dd38 --- /dev/null +++ b/packages/api/remote-access/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Управление удаленным доступом", + "description": "Контролируйте, какие методы соединения, не являющиеся владельцами, могут получать доступ и сохранять снимки разрешений для каждого соединения." + } +} diff --git a/packages/api/remote-access/package.json b/packages/api/remote-access/package.json index bcc911aee9..f4e3e13975 100644 --- a/packages/api/remote-access/package.json +++ b/packages/api/remote-access/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remote-access", "description": "Method-level Remote access control for non-owner Peers and the per-Peer access snapshot", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/locale/de.json b/packages/api/remotes/locale/de.json new file mode 100644 index 0000000000..dcc7ff1da3 --- /dev/null +++ b/packages/api/remotes/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Zusammenfassung des Kunden API", + "description": "Fasst die Serverfunktionen zusammen, die Clients durch Anwendungen zur Verfügung stehen." + } +} diff --git a/packages/api/remotes/locale/es.json b/packages/api/remotes/locale/es.json new file mode 100644 index 0000000000..d731b0c788 --- /dev/null +++ b/packages/api/remotes/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Resumen del cliente API", + "description": "Resume las capacidades del servidor abiertas a los clientes por las aplicaciones." + } +} diff --git a/packages/api/remotes/locale/fr.json b/packages/api/remotes/locale/fr.json new file mode 100644 index 0000000000..a85835665d --- /dev/null +++ b/packages/api/remotes/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Récapitulatif du client API", + "description": "Résume les fonctionnalités du serveur ouvertes aux clients par les applications." + } +} diff --git a/packages/api/remotes/locale/pt.json b/packages/api/remotes/locale/pt.json new file mode 100644 index 0000000000..9bc13b83ff --- /dev/null +++ b/packages/api/remotes/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Resumo do cliente API", + "description": "Resume os recursos do servidor abertos aos clientes pelos aplicativos." + } +} diff --git a/packages/api/remotes/locale/ru.json b/packages/api/remotes/locale/ru.json new file mode 100644 index 0000000000..804cf94148 --- /dev/null +++ b/packages/api/remotes/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Сводная информация о клиенте API", + "description": "Обобщает возможности сервера, открытые для клиентов приложениями." + } +} diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index c0ae5bdf82..288aabea9b 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remotes", "description": "Remote BFF assembly for application-selected Host capabilities", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 6df1300fd1..46ac8d032c 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -15,14 +15,14 @@ en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce /session-controller/use-this-package: - en: aabd5be911e477f5 - zh: 2f7fdb44bc8777bc + en: 966e2852184146bf + zh: 31e3587651d991be /session-controller/client-references: en: 41bdda1ac32df558 zh: aa5d0e325924066b /session-controller/session-media-references: - en: 25e40cb508265c01 - zh: c8ce39a94997af00 + en: 954923654eebe2dc + zh: 6bd9a7d2ea965aed /session-controller/configuration: en: c92590fbaf75e45e zh: fcac67017bb0e3ab diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index abbb52cd63..de8ee6e9aa 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -62,7 +62,7 @@ During uninterrupted following, transcript and steering echoes stay in Chat thro The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent. -Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. +Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. A resume blocked by an existing write handle returns `session/writer-held` with the Session id; other resume failures retain `gateway/internal`. @@ -86,7 +86,7 @@ References keep local Session data, scoped Contexts, and history streams alive, ## Session media references -`SessionMediaReferences` mounts `GET|HEAD /api/file?path=` on the authenticated `connection.fetch` channel when `connection`, `fs`, and `attachments` are composed. It reads ordinary files through `ctx.fs`, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; `mime-types` supplies the response type, with `application/octet-stream` for unknown extensions. GET reuses `readBytes` for preflight and ongoing byte limits; HEAD reads metadata only. All files use `ctx.attachments.imageLimits.maxImageBytes` (normally 50 MiB); exceeding this limit returns 413. Responses contain the complete file, ignore Range, and carry `private, no-store`, `nosniff`, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in `ui-chat` (`AssistantMarkdown`); audio/video responses are available, while Markdown audio/video player nodes remain separate work. +`SessionMediaReferences` mounts `GET|HEAD /api/file?path=` on the authenticated `connection.fetch` channel when `connection`, `fs`, and `attachments` are composed. It reads ordinary files through `ctx.fs`, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; `mime-types` supplies the response type, with `application/octet-stream` for unknown extensions. GET reuses `readBytes` for preflight and ongoing byte limits; HEAD reads metadata only. All files use `ctx.attachments.imageLimits.maxImageBytes` (normally 50 MiB); exceeding this limit returns 413. A request whose `v` parameter differs from the file's current `ctx.fs` version, including a request without one, receives a `private, no-store` 302 to the same route with only `path` and the current `v`; the versioned response is `private, max-age=31536000, immutable`, so browsers reuse cached bytes until the file changes, and private file contents may enter the browser disk cache. Responses contain the complete file, ignore Range, and carry `nosniff`, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in `ui-chat` (`AssistantMarkdown`); audio/video responses are available, while Markdown audio/video player nodes remain separate work. GUI model selection requires the exact provider/model pair in the available catalog; unavailable choices reject with `session/model-unavailable`. Prompt admission retains the saved route without a catalog availability gate, so request execution reports missing credentials or unavailable models. `initializeDefaultModel()` saves the first available account model after login when no other provider has a configured API key; credential checks use configured references independently of model availability. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 58bdbbf48b..191821f434 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -62,7 +62,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用 面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。 -Fork 复制 `atSeq` 所选的精确事件前缀,包含切点事件,允许在开放轮次内截取。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 +Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 恢复会话时若已有写句柄占用,返回 `session/writer-held`,并携带会话 id;其他恢复失败仍返回 `gateway/internal`。 @@ -86,7 +86,7 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以 ## 会话媒体引用 -当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 50 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。 +当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 50 MiB);超过此上限返回 413。`v` 参数与文件当前 `ctx.fs` 版本不同(包括没有该参数)的请求,会收到 `private, no-store` 的 302,跳转到只含 `path` 与当前 `v` 的同一路由;版本化响应为 `private, max-age=31536000, immutable`,因此浏览器在文件变化前复用缓存内容,私有文件内容可能进入浏览器磁盘缓存。响应包含完整文件,忽略 Range,并携带 `nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。 GUI 模型选择要求确切提供方/模型对出现在可用目录中;不可用的选择以 `session/model-unavailable` 拒绝。提示词准入保留已保存的路由,不按目录可用性阻断发送,由请求执行报告凭据缺失或模型不可用。`initializeDefaultModel()` 在账号登录后、其他提供方均未配置 API key 时,将第一个可用账号模型保存为默认模型;凭据检查使用已配置的引用,不依赖模型是否可用。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。 diff --git a/packages/api/session-controller/locale/de.json b/packages/api/session-controller/locale/de.json new file mode 100644 index 0000000000..217f0baf00 --- /dev/null +++ b/packages/api/session-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sitzungssteuerungsschnittstelle", + "description": "Bietet Clients Funktionen zum Lesen von Sitzungen, zur Laufsteuerung und zur Statusaktualisierung." + } +} diff --git a/packages/api/session-controller/locale/es.json b/packages/api/session-controller/locale/es.json new file mode 100644 index 0000000000..764c28e00b --- /dev/null +++ b/packages/api/session-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interfaz de control de sesión", + "description": "Proporciona capacidades de lectura de sesiones, control de ejecución y actualización de estado a los clientes." + } +} diff --git a/packages/api/session-controller/locale/fr.json b/packages/api/session-controller/locale/fr.json new file mode 100644 index 0000000000..689e058b56 --- /dev/null +++ b/packages/api/session-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de contrôle de session", + "description": "Fournit des fonctionnalités de lecture de session, de contrôle d’exécution et de mise à jour de statut aux clients." + } +} diff --git a/packages/api/session-controller/locale/pt.json b/packages/api/session-controller/locale/pt.json new file mode 100644 index 0000000000..537a8f934d --- /dev/null +++ b/packages/api/session-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de controle de sessão", + "description": "Fornece leitura de sessão, controle de execução e recursos de atualização de status aos clientes." + } +} diff --git a/packages/api/session-controller/locale/ru.json b/packages/api/session-controller/locale/ru.json new file mode 100644 index 0000000000..885910a705 --- /dev/null +++ b/packages/api/session-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "интерфейс управления сеансом", + "description": "Предоставляет клиентам возможности чтения сеанса, управления запуском и обновления статуса." + } +} diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index faf27a4fa3..0f7b7bb63e 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-session-controller", "description": "Session Remote commands, cold reads, and live control transport", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/src/client/sessions/lineage.ts b/packages/api/session-controller/src/client/sessions/lineage.ts index 298ff2183b..b6d8bbdfc5 100644 --- a/packages/api/session-controller/src/client/sessions/lineage.ts +++ b/packages/api/session-controller/src/client/sessions/lineage.ts @@ -60,7 +60,8 @@ export function flattenLineage( const children = new Map() const roots: TitledSessionSummary[] = [] for (const s of summaries) { - if (s.parentSessionId !== undefined && byId.has(s.parentSessionId)) { + // Historical ordinary forks may retain a parent id without being subagents. + if (s.origin === 'subagent' && s.parentSessionId !== undefined && byId.has(s.parentSessionId)) { const list = children.get(s.parentSessionId) ?? [] list.push(s) children.set(s.parentSessionId, list) diff --git a/packages/api/session-controller/src/client/sessions/manager.ts b/packages/api/session-controller/src/client/sessions/manager.ts index f800a8c2be..105930d1ef 100644 --- a/packages/api/session-controller/src/client/sessions/manager.ts +++ b/packages/api/session-controller/src/client/sessions/manager.ts @@ -553,7 +553,7 @@ export class SessionManager { * immediately (same synchronous-addressability guarantee as create). * Blankness starts provisionally true so the authoritative Host summary can * preserve it or lower it after an exact cut before the first `turn/start`; - * lineage rides parentSessionId. A child published before Workspace + * the fork has no durable source relationship. A child published before Workspace * attachment fails is also reconciled into the list. * @param opts - source session, optional exact inclusive boundary seq, and permission to start migration. * @returns the fork result (the child session id). @@ -573,7 +573,6 @@ export class SessionManager { if (childId !== undefined) { this.recordMutation({ kind: 'placeholder', summary: { agentAvailable: true, sessionId: childId, updatedAt: Date.now(), running: false, blank: true, - parentSessionId: opts.sessionId, ...(source?.cwd !== undefined ? { cwd: source.cwd } : {}), } }) } diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index c91f6e5b02..fc7240b83f 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -273,7 +273,6 @@ export class SessionCommandController { inheritedEventCount: SessionLogOffset(boundary + 1), meta: { ...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }), - parentSession: source.header.id, isSeeded: true, ...(composition.agentPreset === undefined ? {} diff --git a/packages/api/session-controller/src/media-references.ts b/packages/api/session-controller/src/media-references.ts index 9da80029f5..7effd7a18e 100644 --- a/packages/api/session-controller/src/media-references.ts +++ b/packages/api/session-controller/src/media-references.ts @@ -1,7 +1,9 @@ /** * Authenticated GET/HEAD /api/file reads bounded file responses through * the composed filesystem provider. Paths and MIME types do not restrict access; - * the connection service authenticates requests before this handler. + * the connection service authenticates requests before this handler. A request + * without the current `v` version is redirected, uncached, to the versioned URL, + * whose response is cacheable indefinitely. * @module @deepseek-ai/dsh-api-session-controller/media-references */ @@ -13,33 +15,38 @@ import { FsError, type FileSystem } from '@deepseek-ai/dsh-fs' import mime from 'mime-types' const BASE_HEADERS = { - 'Cache-Control': 'private, no-store', 'X-Content-Type-Options': 'nosniff', // HTML and SVG files may be opened directly on the authenticated API origin. 'Content-Security-Policy': "sandbox; default-src 'none'", } +const NO_STORE = { ...BASE_HEADERS, 'Cache-Control': 'private, no-store' } +// A versioned URL names one file version, so its response never changes. +const VERSIONED = { ...BASE_HEADERS, 'Cache-Control': 'private, max-age=31536000, immutable' } async function serveFile(request: Request, fs: FileSystem, maxBytes: number): Promise { const fail = (status: number, text: string): Response => - new Response(request.method === 'HEAD' ? null : text, { status, headers: BASE_HEADERS }) - const path = new URL(request.url).searchParams.get('path') + new Response(request.method === 'HEAD' ? null : text, { status, headers: NO_STORE }) + const params = new URL(request.url).searchParams + const path = params.get('path') if (path === null || path.length === 0) return fail(400, 'missing path') if (path.includes('\0') || !isAbsolute(path)) return fail(400, 'absolute path required') try { const target = await fs.resolve(path, { signal: request.signal }) - const mediaType = mime.lookup(target.displayPath) || 'application/octet-stream' + const info = await fs.stat(target, request.signal) + if (info === undefined) return fail(404, 'not found') + if (info.type !== 'file') return fail(403, 'not a regular file') + if (info.size !== undefined && info.size > maxBytes) return fail(413, 'file exceeds byte limit') + if (params.get('v') !== info.version) { + // Relative to the request URL, so deployment prefixes and the Desktop scheme are kept. + const location = `?${new URLSearchParams({ path, v: info.version }).toString()}` + return new Response(null, { status: 302, headers: { ...NO_STORE, Location: location } }) + } const headers: Record = { - ...BASE_HEADERS, - 'Content-Type': mediaType, + ...VERSIONED, + 'Content-Type': mime.lookup(target.displayPath) || 'application/octet-stream', } if (request.method === 'HEAD') { - const info = await fs.stat(target, request.signal) - if (info === undefined) return fail(404, 'not found') - if (info.type !== 'file') return fail(403, 'not a regular file') - if (info.size !== undefined) { - if (info.size > maxBytes) return fail(413, 'file exceeds byte limit') - headers['Content-Length'] = String(info.size) - } + if (info.size !== undefined) headers['Content-Length'] = String(info.size) return new Response(null, { headers }) } const bytes = await fs.readBytes(target, request.signal, maxBytes) diff --git a/packages/api/session-controller/tests/lineage.client.spec.ts b/packages/api/session-controller/tests/lineage.client.spec.ts index 16f10c14f4..ae9c4389fe 100644 --- a/packages/api/session-controller/tests/lineage.client.spec.ts +++ b/packages/api/session-controller/tests/lineage.client.spec.ts @@ -9,7 +9,7 @@ import { flattenLineage } from '../src/client/sessions/lineage.ts' const s = (id: string, updatedAt: number, parent?: string): SessionSummary => ({ agentAvailable: true, sessionId: id as SessionId, updatedAt, running: false, blank: false, - ...(parent !== undefined ? { parentSessionId: parent as SessionId } : {}), + ...(parent !== undefined ? { parentSessionId: parent as SessionId, origin: 'subagent' as const } : {}), }) describe('Session lineage flattening', () => { @@ -26,6 +26,16 @@ describe('Session lineage flattening', () => { ]) }) + it('keeps historical ordinary forks at root level in input order', () => { + const out = flattenLineage([ + s('source', 10), s('unrelated', 20), + { ...s('legacy-fork', 30), parentSessionId: 'source' as SessionId }, + ]) + expect(out.map(row => [row.sessionId, row.depth])).toEqual([ + ['source', 0], ['unrelated', 0], ['legacy-fork', 0], + ]) + }) + it('degrades an orphan (absent parent) to root level without dropping it', () => { const out = flattenLineage([s('orphan', 20, 'ghost-parent'), s('root', 10)]) expect(out.map(e => [e.sessionId, e.depth])).toEqual([['orphan', 0], ['root', 0]]) diff --git a/packages/api/session-controller/tests/manager.client.spec.ts b/packages/api/session-controller/tests/manager.client.spec.ts index a4f9367735..57eb9ecbfa 100644 --- a/packages/api/session-controller/tests/manager.client.spec.ts +++ b/packages/api/session-controller/tests/manager.client.spec.ts @@ -1261,7 +1261,8 @@ describe('remaining branches', () => { expect(remote.session.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: S1, atSeq: 7, ...permission }) expect(remote.session.follow).not.toHaveBeenCalled() - expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S2, parentSessionId: S1 }) + expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S2 }) + expect(manager.getListSnapshot().items[0]).not.toHaveProperty('parentSessionId') }) it('reconciles a fork child published before workspace attachment fails', async ({ mock, remote }) => { @@ -1273,13 +1274,12 @@ describe('remaining branches', () => { expect(result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } }) expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ sessionId: S2, - parentSessionId: S1, blank: true, })]) - manager.handleSessionAdded(summary(S2, { blank: true, parentSessionId: S1 })) + manager.handleSessionAdded(summary(S2, { blank: true })) expect(manager.getListSnapshot().items[0]?.blank).toBe(true) - manager.handleSessionAdded(summary(S2, { blank: false, parentSessionId: S1 })) + manager.handleSessionAdded(summary(S2, { blank: false })) expect(manager.getListSnapshot().items[0]?.blank).toBe(false) }) diff --git a/packages/api/session-controller/tests/media-references.host.spec.ts b/packages/api/session-controller/tests/media-references.host.spec.ts index 61572ab0be..d25885ed7f 100644 --- a/packages/api/session-controller/tests/media-references.host.spec.ts +++ b/packages/api/session-controller/tests/media-references.host.spec.ts @@ -47,8 +47,15 @@ describe('SessionMediaReferences /api/file', () => { if (handler === undefined) throw new Error('route not registered') return handler(new Request(url, init)) } + const url = (path: string) => `http://127.0.0.1/api/file?path=${encodeURIComponent(path)}` return { - call: (path: string, init?: RequestInit) => raw(`http://127.0.0.1/api/file?path=${encodeURIComponent(path)}`, init), + /** Request a path and follow its version redirect, as an image element does. */ + call: async (path: string, init?: RequestInit) => { + const first = await raw(url(path), init) + const location = first.headers.get('location') + return location === null ? first : raw(new URL(location, url(path)).href, init) + }, + url, raw, fs: ctx.fs as LocalFileSystem, unregister, @@ -105,7 +112,7 @@ describe('SessionMediaReferences /api/file', () => { expect(response.status).toBe(200) expect(response.headers.get('content-type')).toBe(mediaType) expect(response.headers.get('content-length')).toBe(String(PNG_BYTES.length)) - expect(response.headers.get('cache-control')).toBe('private, no-store') + expect(response.headers.get('cache-control')).toBe('private, max-age=31536000, immutable') expect(response.headers.get('x-content-type-options')).toBe('nosniff') expect(response.headers.get('content-security-policy')).toBe("sandbox; default-src 'none'") expect(await responseBytes(response)).toEqual(PNG_BYTES) @@ -121,6 +128,29 @@ describe('SessionMediaReferences /api/file', () => { expect((await route.call(path, { method: 'HEAD' })).status).toBe(413) }) + it('redirects uncached to the current version and serves versioned URLs as immutable', async () => { + const route = await mount() + const path = join(root, 'chart.png') + await writeFile(path, PNG_BYTES) + const first = await route.raw(`${route.url(path)}&load=7`) + expect(first.status).toBe(302) + expect(first.headers.get('cache-control')).toBe('private, no-store') + expect(first.body).toBeNull() + const location = new URL(first.headers.get('location')!, route.url(path)) + expect(location.pathname).toBe('/api/file') + expect([...location.searchParams.keys()]).toEqual(['path', 'v']) + expect(location.searchParams.get('path')).toBe(path) + const versioned = await route.raw(location.href) + expect(versioned.status).toBe(200) + expect(await responseBytes(versioned)).toEqual(PNG_BYTES) + await writeFile(path, new Uint8Array([...PNG_BYTES, 9])) + const stale = await route.raw(location.href) + expect(stale.status).toBe(302) + const next = new URL(stale.headers.get('location')!, route.url(path)) + expect(next.searchParams.get('v')).not.toBe(location.searchParams.get('v')) + expect((await responseBytes(await route.raw(next.href))).at(-1)).toBe(9) + }) + it('ignores Range headers and returns complete bodies without advertising ranges', async () => { const route = await mount() const path = join(root, 'clip.mp4') @@ -192,6 +222,7 @@ describe('SessionMediaReferences /api/file', () => { const route = await mount() const target = { targetKey: FsTargetKey('opaque-remote-id'), displayPath: '/remote/photo.png' } vi.spyOn(route.fs, 'resolve').mockResolvedValue(target) + vi.spyOn(route.fs, 'stat').mockResolvedValue({ type: 'file', version: FsVersion('r1') }) const read = vi.spyOn(route.fs, 'readBytes').mockResolvedValue(PNG_BYTES) expect(await responseBytes(await route.call('/remote/photo.png'))).toEqual(PNG_BYTES) expect(read).toHaveBeenCalledWith(target, expect.any(AbortSignal), DEFAULT_LIMIT) diff --git a/packages/api/session-controller/tests/session-fork.host.spec.ts b/packages/api/session-controller/tests/session-fork.host.spec.ts index 5b00f060f1..72b21aa31e 100644 --- a/packages/api/session-controller/tests/session-fork.host.spec.ts +++ b/packages/api/session-controller/tests/session-fork.host.spec.ts @@ -328,7 +328,7 @@ describe('sessions.fork', () => { }, ) - it('cuts exactly at a completed turn and records lineage and cwd', async () => { + it('cuts exactly at a completed turn and retains cwd without source lineage', async () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-source', 2) const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: 2 })) @@ -338,7 +338,7 @@ describe('sessions.fork', () => { expect(child?.snapshotEvents().map(event => event.type)).toEqual([ 'turn/start', 'user/message', 'turn/end', 'session/end-seed', ]) - expect(child?.header.parentSession).toBe(source.id) + expect(child?.header.parentSession).toBeUndefined() expect(child?.header.cwd).toBe('/proj') await ctx.fiber.dispose() }) @@ -379,10 +379,10 @@ describe('sessions.fork', () => { if (!response.ok) return expect(attachSession).toHaveBeenCalledWith(response.value.sessionId) expect(ctx.sessions.get(response.value.sessionId)?.header).toMatchObject({ - parentSession: grandchild.id, cwd: '/proj', }) expect(ctx.sessions.get(response.value.sessionId)?.header.origin).toBeUndefined() + expect(ctx.sessions.get(response.value.sessionId)?.header.parentSession).toBeUndefined() await ctx.fiber.dispose() }) @@ -435,10 +435,10 @@ describe('sessions.fork', () => { expect(resume).not.toHaveBeenCalled() expect(ctx.agents.get(sourceId)).toBeUndefined() expect(ctx.sessions.get(response.value.sessionId)?.header).toMatchObject({ - parentSession: sourceId, cwd: '/proj', }) expect(ctx.sessions.get(response.value.sessionId)?.header.origin).toBeUndefined() + expect(ctx.sessions.get(response.value.sessionId)?.header.parentSession).toBeUndefined() await ctx.fiber.dispose() }) diff --git a/packages/api/session-controller/tests/sessions-service.client.spec.ts b/packages/api/session-controller/tests/sessions-service.client.spec.ts index d92a83198e..f43e32e64e 100644 --- a/packages/api/session-controller/tests/sessions-service.client.spec.ts +++ b/packages/api/session-controller/tests/sessions-service.client.spec.ts @@ -1002,10 +1002,10 @@ describe('fork', () => { expect(b.svc.binding(sid('child'))).toBeUndefined() expect(b.svc.retainInfo(sid('child')).getSnapshot().referenceCount).toBe(0) await Promise.resolve() + expect(b.svc.list.getSnapshot().byId[sid('child')]).not.toHaveProperty('parentId') expect(b.svc.list.getSnapshot().byId[sid('child')]).toMatchObject({ title: childTitle, displayTitle: childTitle, - parentId: 'source', }) }) diff --git a/packages/api/settings-controller/locale/de.json b/packages/api/settings-controller/locale/de.json new file mode 100644 index 0000000000..f27d4899d3 --- /dev/null +++ b/packages/api/settings-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lese- und Schreibschnittstelle einrichten", + "description": "Bieten Sie dem Kunden die Möglichkeit, verschiedene Einstellungen zu lesen und zu ändern." + } +} diff --git a/packages/api/settings-controller/locale/es.json b/packages/api/settings-controller/locale/es.json new file mode 100644 index 0000000000..39d21f44e7 --- /dev/null +++ b/packages/api/settings-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurar la interfaz de lectura y escritura", + "description": "Proporcionar al cliente la capacidad de leer y modificar varias configuraciones." + } +} diff --git a/packages/api/settings-controller/locale/fr.json b/packages/api/settings-controller/locale/fr.json new file mode 100644 index 0000000000..7fe6fcd1d9 --- /dev/null +++ b/packages/api/settings-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurer l'interface de lecture et d'écriture", + "description": "Offrir au client la possibilité de lire et de modifier divers paramètres." + } +} diff --git a/packages/api/settings-controller/locale/pt.json b/packages/api/settings-controller/locale/pt.json new file mode 100644 index 0000000000..46507f0ae7 --- /dev/null +++ b/packages/api/settings-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurar interface de leitura e gravação", + "description": "Forneça ao cliente a capacidade de ler e modificar várias configurações." + } +} diff --git a/packages/api/settings-controller/locale/ru.json b/packages/api/settings-controller/locale/ru.json new file mode 100644 index 0000000000..a24ae67e2d --- /dev/null +++ b/packages/api/settings-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройка интерфейса чтения и записи", + "description": "Предоставьте клиенту возможность читать и изменять различные настройки." + } +} diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 260193276a..1a01d513c4 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-settings-controller", "description": "Remote owner for the configuration surfaces over the settings-domain seams", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/terminal-controller/locale/de.json b/packages/api/terminal-controller/locale/de.json new file mode 100644 index 0000000000..40a84f00ec --- /dev/null +++ b/packages/api/terminal-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Terminal-Steuerungsschnittstelle", + "description": "Bietet Clients die Möglichkeit, Sitzungsterminals zu erstellen, einzugeben, zu lesen und wiederherzustellen." + } +} diff --git a/packages/api/terminal-controller/locale/es.json b/packages/api/terminal-controller/locale/es.json new file mode 100644 index 0000000000..89251b9b70 --- /dev/null +++ b/packages/api/terminal-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interfaz de control terminal", + "description": "Proporciona a los clientes la capacidad de crear, ingresar, leer y restaurar terminales de sesión." + } +} diff --git a/packages/api/terminal-controller/locale/fr.json b/packages/api/terminal-controller/locale/fr.json new file mode 100644 index 0000000000..e016120a32 --- /dev/null +++ b/packages/api/terminal-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de contrôle des terminaux", + "description": "Fournit aux clients la possibilité de créer, saisir, lire et restaurer des terminaux de session." + } +} diff --git a/packages/api/terminal-controller/locale/pt.json b/packages/api/terminal-controller/locale/pt.json new file mode 100644 index 0000000000..3e2aa9afb6 --- /dev/null +++ b/packages/api/terminal-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de controle de terminal", + "description": "Fornece aos clientes a capacidade de criar, inserir, ler e restaurar terminais de sessão." + } +} diff --git a/packages/api/terminal-controller/locale/ru.json b/packages/api/terminal-controller/locale/ru.json new file mode 100644 index 0000000000..c2c8651003 --- /dev/null +++ b/packages/api/terminal-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "интерфейс управления терминалом", + "description": "Предоставляет клиентам возможность создавать, вводить, читать и восстанавливать сеансовые терминалы." + } +} diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index 1ac43492f5..a7e7ca3a0e 100644 --- a/packages/api/terminal-controller/package.json +++ b/packages/api/terminal-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-terminal-controller", "description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-controller/locale/de.json b/packages/api/workspace-controller/locale/de.json new file mode 100644 index 0000000000..f36dfe99d3 --- /dev/null +++ b/packages/api/workspace-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Arbeitsbereichssteuerung", + "description": "Bietet Clients Arbeitsbereichsvorgänge und einen Status, der bei erneuter Verbindung wiederhergestellt werden kann." + } +} diff --git a/packages/api/workspace-controller/locale/es.json b/packages/api/workspace-controller/locale/es.json new file mode 100644 index 0000000000..3a451aba01 --- /dev/null +++ b/packages/api/workspace-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de control del espacio de trabajo", + "description": "Proporciona a los clientes operaciones en el espacio de trabajo y un estado que se puede restaurar tras la reconexión." + } +} diff --git a/packages/api/workspace-controller/locale/fr.json b/packages/api/workspace-controller/locale/fr.json new file mode 100644 index 0000000000..d6b5f2a527 --- /dev/null +++ b/packages/api/workspace-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de contrôle de l'espace de travail", + "description": "Fournit aux clients des opérations d’espace de travail et un état qui peut être restauré lors de la reconnexion." + } +} diff --git a/packages/api/workspace-controller/locale/pt.json b/packages/api/workspace-controller/locale/pt.json new file mode 100644 index 0000000000..0564e0743f --- /dev/null +++ b/packages/api/workspace-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de controle do espaço de trabalho", + "description": "Fornece aos clientes operações de espaço de trabalho e um estado que pode ser restaurado após a reconexão." + } +} diff --git a/packages/api/workspace-controller/locale/ru.json b/packages/api/workspace-controller/locale/ru.json new file mode 100644 index 0000000000..19da086fb1 --- /dev/null +++ b/packages/api/workspace-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс управления рабочей областью", + "description": "Предоставляет клиентам операции в рабочей области и состояние, которое можно восстановить при повторном подключении." + } +} diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index 142b9dfbf3..802e26fb0e 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-controller", "description": "Workspace Remote commands and reconnect-safe state transport", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/locale/de.json b/packages/api/workspace-files/locale/de.json new file mode 100644 index 0000000000..60f31b3a27 --- /dev/null +++ b/packages/api/workspace-files/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Workspace-Dateischnittstelle", + "description": "Bietet das Lesen von Arbeitsbereichsdateien, das Auflisten von Verzeichnissen und die Aktualisierung von Dateiinformationen." + } +} diff --git a/packages/api/workspace-files/locale/es.json b/packages/api/workspace-files/locale/es.json new file mode 100644 index 0000000000..91db1da1f2 --- /dev/null +++ b/packages/api/workspace-files/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de archivos del espacio de trabajo", + "description": "Proporciona lectura de archivos del espacio de trabajo, listado de directorios y actualizaciones de información de archivos." + } +} diff --git a/packages/api/workspace-files/locale/fr.json b/packages/api/workspace-files/locale/fr.json new file mode 100644 index 0000000000..490d88dcbf --- /dev/null +++ b/packages/api/workspace-files/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de fichier de l'espace de travail", + "description": "Fournit la lecture des fichiers de l'espace de travail, la liste des répertoires et les mises à jour des informations sur les fichiers." + } +} diff --git a/packages/api/workspace-files/locale/pt.json b/packages/api/workspace-files/locale/pt.json new file mode 100644 index 0000000000..e0d49a666d --- /dev/null +++ b/packages/api/workspace-files/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de arquivo do espaço de trabalho", + "description": "Fornece leitura de arquivos do espaço de trabalho, listagem de diretórios e atualizações de informações de arquivos." + } +} diff --git a/packages/api/workspace-files/locale/ru.json b/packages/api/workspace-files/locale/ru.json new file mode 100644 index 0000000000..3d514ad55b --- /dev/null +++ b/packages/api/workspace-files/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Файловый интерфейс рабочей области", + "description": "Обеспечивает чтение файлов рабочей области, список каталогов и обновление информации о файлах." + } +} diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index ca8ac9b61e..4d1e0ca86a 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-files", "description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/locale/de.json b/packages/attachment/attachment-local/locale/de.json new file mode 100644 index 0000000000..1095b25bb2 --- /dev/null +++ b/packages/attachment/attachment-local/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lokaler Anhangsspeicher", + "description": "Speichern Sie Anhänge wie Bilder in der Sitzung und lesen Sie sie entsprechend der Inhaltskennung." + } +} diff --git a/packages/attachment/attachment-local/locale/es.json b/packages/attachment/attachment-local/locale/es.json new file mode 100644 index 0000000000..59ca4b5d35 --- /dev/null +++ b/packages/attachment/attachment-local/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Almacenamiento de archivos adjuntos locales", + "description": "Guarde archivos adjuntos como imágenes de la sesión y léalos según el identificador de contenido." + } +} diff --git a/packages/attachment/attachment-local/locale/fr.json b/packages/attachment/attachment-local/locale/fr.json new file mode 100644 index 0000000000..cb5019bfc5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Stockage local des pièces jointes", + "description": "Enregistrez les pièces jointes telles que les images dans la session et lisez-les en fonction de l'identifiant du contenu." + } +} diff --git a/packages/attachment/attachment-local/locale/pt.json b/packages/attachment/attachment-local/locale/pt.json new file mode 100644 index 0000000000..030a96aed5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Armazenamento local de anexos", + "description": "Salve anexos como fotos na sessão e leia-os de acordo com o identificador de conteúdo." + } +} diff --git a/packages/attachment/attachment-local/locale/ru.json b/packages/attachment/attachment-local/locale/ru.json new file mode 100644 index 0000000000..d452d5a8f5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Локальное хранилище вложений", + "description": "Сохраняйте вложения, например изображения, в сеансе и читайте их в соответствии с идентификатором контента." + } +} diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index b8163bbe34..6ad5b54083 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment-local", "description": "Private content-addressed DSH_HOME attachment storage", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index 65958a9643..7ef5c29d66 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment", "description": "Durable immutable attachment storage seam for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/boot/README.i18n.yaml b/packages/boot/README.i18n.yaml index cc38560dae..0a33b9fd4f 100644 --- a/packages/boot/README.i18n.yaml +++ b/packages/boot/README.i18n.yaml @@ -15,8 +15,8 @@ en: 09848f4d056e4fc1 zh: 8df6139cff0288f6 /boot-shared-app-bin-boot-glue/packages: - en: 626cbd4bc04cf9bb - zh: 3c2f6154e58908c2 + en: 358a944a024ffeb1 + zh: f430e25832085068 /boot-shared-app-bin-boot-glue/related-documentation: en: 77af4b40ee7b98a3 zh: e9b4e817823b973d diff --git a/packages/boot/README.md b/packages/boot/README.md index 7d657279b5..04e4325fe3 100644 --- a/packages/boot/README.md +++ b/packages/boot/README.md @@ -27,7 +27,7 @@ The boot group launches profile applications and manages their installed composi | [`hmr`](hmr/README.md) | Coordinates module and configuration reloads with package mutations | `hmr` | | [`config-editor`](config-editor/README.md) | Persists active profile configuration through Loader reconciliation | `configEditor` | | [`plugin-manager`](plugin-manager/README.md) | Manages current-profile plugins and bundle packages through shared CLI operations | `pluginManager` | -| [`plugin-market`](plugin-market/README.md) | Reads the plugin market indexes the user configures for Discover and the `plugin_market` tool | `pluginMarket` | +| [`plugin-market`](plugin-market/README.md) | Reads the plugin indexes the user configures for Discover and the `plugin_market` tool | `pluginMarket` | ## Related documentation diff --git a/packages/boot/README.zh.md b/packages/boot/README.zh.md index a4b30225cf..afdee696e2 100644 --- a/packages/boot/README.zh.md +++ b/packages/boot/README.zh.md @@ -27,7 +27,7 @@ boot 组负责启动 profile 应用并管理其已安装组合。`app-boot` 解 | [`hmr`](hmr/README.zh.md) | 协调模块与配置重载,并与包修改互斥执行 | `hmr` | | [`config-editor`](config-editor/README.zh.md) | 通过 Loader 应用并持久化当前 profile 配置 | `configEditor` | | [`plugin-manager`](plugin-manager/README.zh.md) | 通过共享 CLI 操作管理当前 profile 插件与组合包 | `pluginManager` | -| [`plugin-market`](plugin-market/README.zh.md) | 读取用户配置的插件市场索引,供发现页和 `plugin_market` 工具使用 | `pluginMarket` | +| [`plugin-market`](plugin-market/README.zh.md) | 读取用户配置的插件索引,供发现页和 `plugin_market` 工具使用 | `pluginMarket` | ## 相关文档 diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 82f7210aef..0c16677a8b 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -33,8 +33,8 @@ en: c4a301652b851ce9 zh: 784871e19cd9db0a /deepseek-ai-dsh-app-boot/use-this-package/reading-plugin-display-metadata: - en: a9afb263b03d112c - zh: 31f5e797cd953c15 + en: 91d80c444ac0cfe5 + zh: e7db496043528ffb /deepseek-ai-dsh-app-boot/use-this-package/startup-and-reload-failures: en: dc13dfa1f79292e4 zh: d51682ced7a18ac2 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 39b2684db0..20204ef7c8 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -86,7 +86,7 @@ The projector preserves native omission behavior by checking literal defaults ag Use `readPluginMeta(specifier, parentURL)` or `ctx.pluginPackages.metaOf(specifier, parentURL)` to read installed package display text without importing or activating the plugin. Lookup uses the complete package specifier and the caller's resolution base, respecting Node exports. File paths and file URLs return no metadata without resolving resources. For a package-root specifier, missing locale fields fall back to the accessible `package.json`; a subpath specifier never reads a `package.json`. Malformed metadata returns an `error` diagnostic. Results retain translations for Client-side language selection. The reader also loads an image data URL, even when locale text is complete: a package root uses its manifest `icon`, or `/icon` when that field is omitted; a subpath uses `/icon`. An icon error preserves valid text alongside the diagnostic. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format. -`ON_DEMAND_BUNDLES` names the public Official packages installed only on request. `OFFICIAL_ON_DEMAND_CATALOG` embeds their package-owned localized metadata and icons for offline discovery; `pnpm gen-official-bundle-catalog` explicitly updates generated content, and `verify-official-bundle-catalog` checks resource completeness and freshness; builds consume the committed catalog without importing provider code. The catalog contains no install target: [Plugin Manager](../plugin-manager/README.md#use-this-package) derives it from the running version and installation and uses the ordinary bundle installer. The [product-use check](../../../scripts/verify-product-use.ts) composes shipped and on-demand selections against Web without executing providers. +The profile templates, installation-owned profile tuples, and retired bundle list that app-boot applies live in [`@deepseek-ai/dsh-official-bundles`](../../util/official-bundles/README.md), together with the installation's default, optional, and on-demand bundle lists. ### Startup and reload failures diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 04d9237ea0..eb59327df8 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -86,7 +86,7 @@ profile 导入插件前,DSH 会将其声明的 DSH 版本要求与 `getDshRunt 使用 `readPluginMeta(specifier, parentURL)` 或 `ctx.pluginPackages.metaOf(specifier, parentURL)` 读取已安装包的展示文本,无需导入或激活插件。查询使用完整包标识与调用方的解析基准,并遵循 Node exports。文件路径与文件 URL 不解析资源,直接返回无元信息。包根标识缺失的 locale 字段回退到可访问的 `package.json`;子路径标识从不读取 `package.json`。格式错误的元信息返回 `error` 诊断。结果保留翻译,由 Client 选择语言。即使 locale 文本完整,读取器也会加载图片 data URL:包根使用清单 `icon`,省略该字段时使用 `<包名>/icon`;子路径使用 `<标识>/icon`。图标出错时,保留有效文本并附上诊断。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。 -`ON_DEMAND_BUNDLES` 列出仅按需安装的官方公开包。`OFFICIAL_ON_DEMAND_CATALOG` 内嵌这些包拥有的本地化元信息和图标,支持离线发现;`pnpm gen-official-bundle-catalog` 显式更新生成内容,`verify-official-bundle-catalog` 检查资源完整性与新鲜度;构建只读取已提交的目录,不导入提供者代码。目录不保存安装目标;[插件管理器](../plugin-manager/README.zh.md#use-this-package)根据当前版本和安装位置确定目标,并使用普通组合包安装器。[产品用途检查](../../../scripts/verify-product-use.ts)将随附和按需选择与 Web 组合,但不执行提供者。 +app-boot 应用的 profile 模板、安装方拥有的 profile 组合包元组和已退役组合包列表位于 [`@deepseek-ai/dsh-official-bundles`](../../util/official-bundles/README.zh.md),安装方的默认、可选和按需组合包列表也在该包中。 ### 启动与重载失败 diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index b1bceec433..07e9757746 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-app-boot", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, @@ -33,6 +33,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/dsh-atomic-write": "workspace:*", + "@deepseek-ai/dsh-official-bundles": "workspace:*", "@deepseek-ai/dsh-package-manifest": "workspace:*", "@eslint-community/regexpp": "^4.12.2", "ajv": "^8.20.0", diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index 67c8618b9d..fb68e4ba08 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -29,7 +29,6 @@ import { ProfileRuntimeResolution, type RuntimeResolution } from './profile.ts' import { ResolutionRestartRequired } from './profile-resolution/resolver.ts' export { prepareProfileEntries, prepareProfilePatches } from './compatibility-preflight.ts' export { readPluginMeta, resolvePluginResource } from './package-meta.ts' -export { ON_DEMAND_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG, type OfficialBundleCatalogEntry } from './official-bundles.ts' export { generateConfigSchema, type ConfigSchemaDump, type NativeConfigSchema } from './config-schema/index.ts' export { createConfigProjector, LOADER_EXPRESSION_SCHEMA, type ConfigProjection } from './config-schema/projector.ts' export { isNativeConfigSchema } from './config-schema/native.ts' @@ -59,8 +58,6 @@ export { composeEntries, createRuntimeResolution, ProfileRuntimeResolution, - DEFAULT_PROFILE_BUNDLES, - OPTIONAL_BUNDLES, bundlePatchFiles, bundlePatchPaths, initProfile, @@ -68,7 +65,6 @@ export { loadProfile, loadProfileDirectory, PROFILE_PATCH_FILENAME, - PROFILE_TEMPLATES, PROFILES_DIR, readProfileManifest, reportSkippedBundles, @@ -83,7 +79,6 @@ export { type RuntimeResolutionOptions, type RuntimeResolutionEntry, type RuntimeResolution, - type ProfileTemplate, } from './profile.ts' export { PluginPackages, diff --git a/packages/boot/app-boot/src/official-bundle-packages.ts b/packages/boot/app-boot/src/official-bundle-packages.ts deleted file mode 100644 index b77f9fcdd9..0000000000 --- a/packages/boot/app-boot/src/official-bundle-packages.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** Package admission shared by offline catalog generation and release ordering. */ - -/** Public DSH release members available through the ordinary on-demand bundle installer. */ -export const ON_DEMAND_BUNDLES: readonly string[] = [ - '@deepseek-ai/dsh-subagent-claude-code', - '@deepseek-ai/dsh-subagent-codex', -] diff --git a/packages/boot/app-boot/src/official-bundles.generated.ts b/packages/boot/app-boot/src/official-bundles.generated.ts deleted file mode 100644 index cb993e2d24..0000000000 --- a/packages/boot/app-boot/src/official-bundles.generated.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */ - -import type { OfficialBundleCatalogEntry } from './official-bundles.ts' - -/** Official on-demand entries, independent of installed packages and registry availability. */ -export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [ - { - packageName: '@deepseek-ai/dsh-subagent-claude-code', - meta: { - title: { 'en': 'Claude Code subagent', 'ja': 'Claude Code \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' }, - description: { 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'ja': 'Claude Code \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, - icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', - }, - }, - { - packageName: '@deepseek-ai/dsh-subagent-codex', - meta: { - title: { 'en': 'Codex subagent', 'ja': 'Codex \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' }, - description: { 'en': 'Delegate one task to Codex using its native authentication and settings.', 'ja': 'Codex \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, - icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', - }, - }, -] diff --git a/packages/boot/app-boot/src/official-bundles.ts b/packages/boot/app-boot/src/official-bundles.ts deleted file mode 100644 index fdcb8fda52..0000000000 --- a/packages/boot/app-boot/src/official-bundles.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** Official bundles offered without installing their provider runtimes in the DSH distribution. */ - -import type { PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest' - -export { ON_DEMAND_BUNDLES } from './official-bundle-packages.ts' - -/** Offline discovery metadata; installation state and the requested version belong to the running host. */ -export interface OfficialBundleCatalogEntry { - /** Exact npm package name accepted by the existing bundle installer. */ - readonly packageName: string - /** Package-owned localized text and a self-contained icon, embedded without provider code. */ - readonly meta: Required> -} - -export { OFFICIAL_ON_DEMAND_CATALOG } from './official-bundles.generated.ts' diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 98ff84a21a..1472e06c64 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -28,6 +28,7 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' import type { DshBundleManifest, DshPackageManifest } from '@deepseek-ai/dsh-package-manifest' +import { INSTALLATION_OWNED_PROFILE_TUPLES, PROFILE_TEMPLATES, RETIRED_BUNDLES } from '@deepseek-ai/dsh-official-bundles' import { evaluatePluginCompatibility, pluginCompatibilityWarning } from './plugin-compatibility.ts' import { readProfileVersionExemptions } from './profile-compatibility.ts' import { loadOverlayPatches } from './index.ts' @@ -39,12 +40,6 @@ export const PROFILES_DIR = 'profiles' /** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */ export const PROFILE_PATCH_FILENAME = 'cordis.patch.yml' -/** Installation-owned defaults used when a shipped profile is first opened. */ -export interface ProfileTemplate { - /** Ordered bundle layer list. */ - bundles: readonly string[] -} - /** Package metadata accepted by the profile reader; local profiles need no published identity. */ export type ProfileManifest = Partial @@ -175,68 +170,6 @@ export function resolveProfileDir(name: string, home: string = resolveDshHome()) return join(home, PROFILES_DIR, name) } -/** The shipped profile templates auto-initialized on first use, by name. */ -export const PROFILE_TEMPLATES: Record = { - acp: { - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-acp-app'], - }, - web: { - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'], - }, - headless: { - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'], - }, - sdk: { - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], - }, - 'sdk-minimal': { - bundles: ['@deepseek-ai/dsh-sdk-minimal'], - }, -} - -/** Installation-owned bundle tuples normalized to the shipped template. */ -const INSTALLATION_OWNED_PROFILE_TUPLES: Record = { - headless: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'], -} - -/** - * Bundles an earlier release shipped and the installation no longer carries; - * {@link loadProfileDirectory} removes them from the profile's bundle list. - */ -const RETIRED_BUNDLES: ReadonlySet = new Set([ - // The Web composition mounts Schedule itself - // ([upgrade guide](../../../../docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.md)). - '@deepseek-ai/dsh-experimental-schedule-bundle', -]) - -/** The bundle list a `dsh plugin` init uses for a name with no shipped template. */ -export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-base'] - -/** - * The bundles the dsh installation ships for a person to switch on: each a - * runtime dependency of the installation that declares `dsh.bundle.patch`, - * an `icon`, and `./locale/*.json` display metadata, selected by no shipped - * template, and offered switched off by the plugin manager except persistent - * terminals, which have no page switch - * ([rationale](../../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md), - * [admission](../../../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)). - */ -export const OPTIONAL_BUNDLES: readonly string[] = [ - '@deepseek-ai/dsh-cua-bundle', - '@deepseek-ai/dsh-experimental-session-search', - '@deepseek-ai/dsh-experimental-ralph-bundle', - '@deepseek-ai/dsh-experimental-terminal-bundle', - '@deepseek-ai/dsh-experimental-badge-skill-bundle', - '@deepseek-ai/dsh-experimental-session-titles-bundle', - '@deepseek-ai/dsh-experimental-agent-team-profile', - '@deepseek-ai/dsh-experimental-voice-input-bundle', - '@deepseek-ai/dsh-experimental-cot-translation-bundle', - '@deepseek-ai/dsh-experimental-auto-review', - '@deepseek-ai/dsh-experimental-inspector-profile', - '@deepseek-ai/dsh-experimental-plugin-market-bundle', - '@deepseek-ai/dsh-experimental-tool-worktree', -] - const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer: # a top-level YAML array of loader patch entries (id-targeted config # overrides, disables, and insert lists; \`!!js\` expressions allowed). diff --git a/packages/boot/app-boot/tests/profile-sanitize.spec.ts b/packages/boot/app-boot/tests/profile-sanitize.spec.ts index 6104656be7..f02b1fc001 100644 --- a/packages/boot/app-boot/tests/profile-sanitize.spec.ts +++ b/packages/boot/app-boot/tests/profile-sanitize.spec.ts @@ -4,7 +4,8 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSy import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it, vi } from 'vitest' -import { initProfile, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, readProfileManifest, sanitizeProfile } from '../src/index.ts' +import { initProfile, PROFILE_PATCH_FILENAME, readProfileManifest, sanitizeProfile } from '../src/index.ts' +import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles' vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal() diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index fbd47c20bf..390e888050 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -21,7 +21,6 @@ import { loadProfileDirectory, PROFILE_COMPATIBILITY_FILENAME, PROFILE_PATCH_FILENAME, - PROFILE_TEMPLATES, readProfileManifest, readProfilePatches, removeLinkProjections, @@ -32,6 +31,7 @@ import { type Profile, type RuntimeResolution, } from '../src/index.ts' +import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles' import { installRuntimeInterception } from '../src/profile-resolution/resolver.ts' const tempRoots: string[] = [] @@ -341,16 +341,6 @@ describe('loadProfile', () => { const home = tmp() expect(() => loadProfile('t', 'custom', anchor, home)) .toThrow('profile "custom" does not exist') - expect(PROFILE_TEMPLATES.web?.bundles).toContain('@deepseek-ai/dsh-base') - expect(PROFILE_TEMPLATES.acp).toEqual({ - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-acp-app'], - }) - expect(PROFILE_TEMPLATES.sdk).toEqual({ - bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], - }) - expect(PROFILE_TEMPLATES['sdk-minimal']).toEqual({ - bundles: ['@deepseek-ai/dsh-sdk-minimal'], - }) loadProfile('t', 'web', anchor, home) expect(readProfileManifest('t', resolveProfileDir('web', home)).dsh?.profile?.bundles) .toEqual([...PROFILE_TEMPLATES.web?.bundles ?? []]) diff --git a/packages/boot/app-boot/tsconfig.json b/packages/boot/app-boot/tsconfig.json index 9c3e57c809..b5b42ed67a 100644 --- a/packages/boot/app-boot/tsconfig.json +++ b/packages/boot/app-boot/tsconfig.json @@ -26,6 +26,9 @@ { "path": "../../util/home-paths" }, + { + "path": "../../util/official-bundles" + }, { "path": "../../util/package-manifest" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index 95ca31e410..804bb2bc11 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cmdline", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/boot/config-editor/locale/de.json b/packages/boot/config-editor/locale/de.json new file mode 100644 index 0000000000..d72e2c7250 --- /dev/null +++ b/packages/boot/config-editor/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Konfigurationseditor", + "description": "Speichern Sie die Plug-in-Konfiguration und übernehmen Sie Änderungen auf die aktuell ausgeführte Konfiguration." + } +} diff --git a/packages/boot/config-editor/locale/es.json b/packages/boot/config-editor/locale/es.json new file mode 100644 index 0000000000..a2448c1f5d --- /dev/null +++ b/packages/boot/config-editor/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Editor de configuración de complementos", + "description": "Guarde la configuración del complemento y aplique los cambios a la configuración en ejecución actual." + } +} diff --git a/packages/boot/config-editor/locale/fr.json b/packages/boot/config-editor/locale/fr.json new file mode 100644 index 0000000000..f2b3eb621d --- /dev/null +++ b/packages/boot/config-editor/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Éditeur de configuration de plug-in", + "description": "Enregistrez la configuration du plug-in et appliquez les modifications à la configuration en cours d'exécution." + } +} diff --git a/packages/boot/config-editor/locale/pt.json b/packages/boot/config-editor/locale/pt.json new file mode 100644 index 0000000000..8f2a80a8aa --- /dev/null +++ b/packages/boot/config-editor/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Editor de configuração de plug-ins", + "description": "Salve a configuração do plug-in e aplique as alterações à configuração em execução atual." + } +} diff --git a/packages/boot/config-editor/locale/ru.json b/packages/boot/config-editor/locale/ru.json new file mode 100644 index 0000000000..b14ff9c92c --- /dev/null +++ b/packages/boot/config-editor/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Редактор конфигурации плагина", + "description": "Сохраните конфигурацию плагина и примените изменения к текущей работающей конфигурации." + } +} diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json index 4b41336ae3..d8bfc04659 100644 --- a/packages/boot/config-editor/package.json +++ b/packages/boot/config-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-config-editor", "description": "Persist plugin configuration through profile patches and Loader reconciliation", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/boot/hmr/locale/de.json b/packages/boot/hmr/locale/de.json new file mode 100644 index 0000000000..45ffdf967a --- /dev/null +++ b/packages/boot/hmr/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hmr", + "description": "Laden Sie den Plugin-Code und die Konfiguration neu, während die App ausgeführt wird." + } +} diff --git a/packages/boot/hmr/locale/es.json b/packages/boot/hmr/locale/es.json new file mode 100644 index 0000000000..06af2d1b51 --- /dev/null +++ b/packages/boot/hmr/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hmr", + "description": "Vuelva a cargar el código y la configuración del complemento mientras la aplicación se está ejecutando." + } +} diff --git a/packages/boot/hmr/locale/fr.json b/packages/boot/hmr/locale/fr.json new file mode 100644 index 0000000000..096306a34e --- /dev/null +++ b/packages/boot/hmr/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Rechargez le code et la configuration du plugin pendant que l'application est en cours d'exécution." + } +} diff --git a/packages/boot/hmr/locale/pt.json b/packages/boot/hmr/locale/pt.json new file mode 100644 index 0000000000..b9ee71fba2 --- /dev/null +++ b/packages/boot/hmr/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Recarregue o código e a configuração do plugin enquanto o aplicativo estiver em execução." + } +} diff --git a/packages/boot/hmr/locale/ru.json b/packages/boot/hmr/locale/ru.json new file mode 100644 index 0000000000..0ed5dc0fdc --- /dev/null +++ b/packages/boot/hmr/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "хмр", + "description": "Перезагрузите код и конфигурацию плагина во время работы приложения." + } +} diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index a4efcfe4c6..93fa7fb127 100644 --- a/packages/boot/hmr/package.json +++ b/packages/boot/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hmr", "description": "Coordinated module and profile configuration hot reload", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-manager/README.i18n.yaml b/packages/boot/plugin-manager/README.i18n.yaml index 21e1435f8a..b1d0317fd6 100644 --- a/packages/boot/plugin-manager/README.i18n.yaml +++ b/packages/boot/plugin-manager/README.i18n.yaml @@ -15,14 +15,14 @@ en: 87679c51aa3693f5 zh: 11684a48a5806afb /deepseek-ai-dsh-plugin-manager/use-this-package: - en: 425473b242360a78 - zh: bd91b346ab3d7a78 + en: 7ec407151aa14244 + zh: 7f09ed0329449c4d /deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions: en: 483285658e267dd5 zh: 846bec2daac59a96 /deepseek-ai-dsh-plugin-manager/use-this-package/configuration: - en: 64089ca8084f5e0f - zh: 8328a53cd6420a82 + en: ccec56c785b3ca28 + zh: b945f933888fc7cb /deepseek-ai-dsh-plugin-manager/use-this-package/bundle-detail-documents: en: 99b084ff497011d2 zh: 6c047796c1991784 @@ -48,8 +48,8 @@ en: c29f60be7f3edc1a zh: d9b9388b96ef95cc /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work: - en: eeeaa73140fa45d7 - zh: 12b5c10f0390310e + en: d26075545fe1e990 + zh: 068c07d642843c94 /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/failure-behavior: en: c670eb2330b441e8 zh: 2c8223e03e1e3ef0 diff --git a/packages/boot/plugin-manager/README.md b/packages/boot/plugin-manager/README.md index f288fc4d43..7d74a95d7c 100644 --- a/packages/boot/plugin-manager/README.md +++ b/packages/boot/plugin-manager/README.md @@ -57,6 +57,8 @@ Official on-demand installs call the same `installBundle` operation as other bun `installBundle` checks GitHub repositories with `git ls-remote` before starting pnpm, using the profile directory and the installer's Git and proxy configuration. `githubConnectionTimeoutMs` defaults to 5000 ms and limits only this check, not package download or builds. The check disables credential helpers and prompts; only network failures and timeouts stop installation, reporting `failedAt: 'spec-host'` with the existing failure kind and diagnostic log. Authentication, repository lookup and other failures are left to pnpm, including its HTTPS-to-SSH fallback. Cancellation and manager disposal stop the check and its descendants. Registry packages, paths, tarballs and other Git hosts skip this check. A reachable repository can still fail during download or bundle validation. +An HTTP(S) Git URL on a host for which pnpm has no HTTPS-to-SSH fallback, that is, any host `hosted-git-info` does not know, such as a self-hosted GitLab, is probed with `git ls-remote` before pnpm starts. The probe uses the installer's Git configuration and credential helpers with Git's terminal and askpass prompts disabled; on POSIX it also has no controlling terminal, so SSH cannot prompt. When HTTP(S) cannot read the repository and `ssh://git@/` can, pnpm installs that SSH URL with the original fragment, and the profile records it. Otherwise the spec is installed unchanged. Each probe is bounded by `gitTransportTimeoutMs`, 5000 ms by default; cancellation and manager disposal stop it. URLs with credentials or a query skip the probe. + Registries are asked in turn. The plan starts at `options.registry`, else the configured `registry` (`null` is the one pnpm's own configuration names), and continues through `fallbackRegistries` while a registry is unreachable, times out, or answers that it has no such package or version, which a mirror not yet synced does. A registry outside the configured set is asked alone, so a private registry never falls through to a public one; pnpm's own registry counts as part of the set only while what it names, read through `pnpm config get registry` before each plan, is npm's own registry or one of the fallbacks, and is otherwise asked alone as a private one. A registry pnpm's own configuration already names is asked once. The lookup runs `pnpm view` with `--registry` and without pnpm's own retries, so a dead registry is reported within `inspectTimeoutMs` and the next one is asked; pnpm prints its refusal as JSON on stdout, which is read like stderr. The install keeps pnpm's retry settings. `registries()` answers the configured set and what pnpm names, for a picker. The [registry Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.md) owns the rationale. The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFICIAL_NPM_REGISTRY` and `NPMMIRROR_REGISTRY` for consumers that identify those public registries. @@ -65,7 +67,7 @@ The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFIC `waitForInstall(requestId)` lets a client recover a lost response by waiting for the active installation, including its non-cancellable application phase. It returns the same outcome as the original call, or `null` if the request is not active. Completed results are not retained; `null` establishes neither success nor cancellation. -When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts and names the run reported as ignored. pnpm writes those entries into `pnpm-workspace.yaml` only when it can prompt, so the manager records the reported names itself; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice. +When pnpm 11 blocks dependency scripts, the failed installation reports every pending `allowBuilds` key in the profile under `pendingBuilds`, including keys left by earlier attempts, names the run reported as ignored, and the key pnpm suggests for a git-hosted package it refused to prepare. A key whose git URL carries a credential (a password, or a user name over HTTP(S)) is not recorded, so that installation offers only manual approval. pnpm writes those entries into `pnpm-workspace.yaml` only when it can prompt, so the manager records the reported names itself; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by the recorded key in this profile, which pins a git-hosted package to its commit, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice. ### Version compatibility and exemptions @@ -85,6 +87,7 @@ The CLI exposes `dsh plugin --profile version-exemptions`, `allow-vers | `pnpmCommand` | `pnpm` | The pnpm executable name or path, resolved through `PATH` like the `dsh plugin` command. | | `inspectTimeoutMs` | `20000` | Bound on one registry lookup an inspection runs, in milliseconds. | | `githubConnectionTimeoutMs` | `5000` | Deadline for the GitHub repository check before installation, in milliseconds. | +| `gitTransportTimeoutMs` | `5000` | Deadline for each HTTP(S) and SSH access probe of a self-hosted Git repository before installation, in milliseconds. | | `registry` | pnpm's own | The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. | | `fallbackRegistries` | `['https://registry.npmmirror.com/']` | Registries asked in turn, as http(s) URLs, while the one before is unreachable or holds no copy of the package; pnpm's own registry joins the order only while it names npm's own registry or one of these. | | `outputBytes` | `16384` | Maximum pnpm diagnostic bytes returned per operation; the full output remains in the returned log path. | @@ -158,9 +161,11 @@ Tool results append to the transcript. Enabling or disabling other tools can cha - Web approves the entire displayed pending group; it has no per-package selection. +- An approved git key stays in `pnpm-workspace.yaml` after the plugin moves to a newer commit; nothing removes stale approvals. - pnpm verifies every lockfile tarball URL against its default registry when a command changes the lockfile. After a package was installed from another registry, installing a different package with the default registry, or removing that package after a restart, can fail with `ERR_PNPM_TARBALL_URL_MISMATCH`. - An update saves the target as an exact version, and a reinstallation after a failed update saves the previous version exactly, even when the profile had saved a range. With HMR the reinstalled bundle joins the end of the selection, which can change which bundle's configuration takes precedence. - An update runs its removal and its installation as two profile operations; another operation can run between them. +- The Git transport probe keeps the parent console on Windows, so an SSH host-key or passphrase prompt can appear in the launching terminal until `gitTransportTimeoutMs`. On POSIX a host missing from `known_hosts` fails the SSH probe, so installation stays on HTTPS until the host key is accepted once, for example with `ssh -T git@`. A credential-helper dialog or ssh-agent confirmation, such as a Touch ID or `ssh-add -c` dialog, that takes longer than the deadline counts as a failed probe, and an HTTPS probe that times out lets a reachable SSH form be saved to the profile. A private repository waits up to twice `gitTransportTimeoutMs` before pnpm starts. The SSH form always uses the `git` user and SSH's default port; other SSH endpoints need a `url..insteadOf` rule. When both probes fail, the diagnostic is pnpm's HTTPS failure; the SSH probe's reason is not reported. - A newer version whose dependencies need new build-script approval fails to update and is reinstalled at the previous version; approve its scripts through an ordinary installation after removing the package. - Package replacements require restarting the process without HMR. A package relinked outside Plugin Manager while it is still installed waits for a restart. - Startup-only profiles cannot remove packages used to start the current process; stop it and use `dsh plugin`. diff --git a/packages/boot/plugin-manager/README.zh.md b/packages/boot/plugin-manager/README.zh.md index 1146316cbe..85e62dadc1 100644 --- a/packages/boot/plugin-manager/README.zh.md +++ b/packages/boot/plugin-manager/README.zh.md @@ -57,6 +57,8 @@ kind: "package-reference" `installBundle` 在启动 pnpm 前通过 `git ls-remote` 检查 GitHub 仓库,使用 profile 目录及安装器的 Git 与代理配置。`githubConnectionTimeoutMs` 默认为 5000 毫秒,只限制这次检查,不限制包下载或构建。检查禁用凭据助手和认证提示;只有网络失败与超时会停止安装,通过现有失败类型与诊断日志返回,并标记 `failedAt: 'spec-host'`。认证、仓库查找及其他失败继续交给 pnpm,包括其 HTTPS 到 SSH 的回退。取消安装或销毁管理器会停止检查及其子进程。注册表包、本地路径、压缩包和其他 Git 主机跳过此检查。仓库可达后,下载或组合包验证仍可能失败。 +HTTP(S) Git URL 指向 pnpm 没有 HTTPS 到 SSH 回退的主机(即 `hosted-git-info` 不认识的任何主机,例如自托管 GitLab)时,在启动 pnpm 前通过 `git ls-remote` 探测。探测使用安装器的 Git 配置与凭据助手,并禁用 Git 的终端与 askpass 提示;在 POSIX 上探测也没有控制终端,因此 SSH 无法提示。HTTP(S) 无法读取仓库而 `ssh://git@/` 可以读取时,pnpm 以该 SSH URL 加上原片段安装,profile 也记录该 URL。否则按原 spec 安装。每次探测受 `gitTransportTimeoutMs` 限制,默认为 5000 毫秒;取消安装或销毁管理器会停止探测。带凭据或查询串的 URL 跳过探测。 + 注册表按顺序询问。计划从 `options.registry` 开始,否则从配置的 `registry`(`null` 即 pnpm 自身配置指定的那个)开始,并在一个注册表不可达、超时或答复没有这个包或版本(尚未同步的镜像会如此)时继续问 `fallbackRegistries`。配置集合之外的注册表只问它自己,因而私有源永远不会落到公共源;pnpm 自身的注册表只在它指向的地址(每次做计划前经 `pnpm config get registry` 读出)是 npm 官方源或某个备选源时才算集合成员,否则视为私有源只问它自己。pnpm 自身配置已经指向的注册表只问一次。查询以 `--registry` 运行 `pnpm view` 且不带 pnpm 自身的重试,所以死掉的注册表会在 `inspectTimeoutMs` 内报告并转问下一个;pnpm 把拒绝以 JSON 打印在 stdout,读法与 stderr 相同。安装保留 pnpm 的重试设置。`registries()` 把配置集合和 pnpm 指向的地址答复给选择器。[注册表 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.zh.md) 拥有理由。 可在浏览器使用的 `@deepseek-ai/dsh-plugin-manager/registry` 入口导出 `OFFICIAL_NPM_REGISTRY` 和 `NPMMIRROR_REGISTRY`,供消费方识别这两个公共注册表。 @@ -65,7 +67,7 @@ kind: "package-reference" `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。 -pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的,以及本次运行报告为已忽略的。pnpm 只在能够交互提示时才把这些条目写入 `pnpm-workspace.yaml`,因此由管理器自行记录本次运行报告的包名;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。 +pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的 `allowBuilds` 键,包括先前尝试留下的键、本次运行报告为已忽略的包名,以及 pnpm 为其拒绝准备的 git 托管包建议的键。git URL 携带凭据(密码,或 HTTP(S) 上的用户名)的键不会被记录,因此该安装只提供手动批准。pnpm 只在能够交互提示时才把这些条目写入 `pnpm-workspace.yaml`,因此由管理器自行记录本次运行报告的包名;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按记录的键保存在当前 profile,该键会把 git 托管包固定到其提交,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。 ### 版本兼容性与豁免 @@ -85,6 +87,7 @@ CLI 提供 `dsh plugin --profile version-exemptions`、`allow-version | `pnpmCommand` | `pnpm` | pnpm 可执行文件名或路径,与 `dsh plugin` 命令一样通过 `PATH` 解析。 | | `inspectTimeoutMs` | `20000` | 单次检查所做注册表查询的上限,单位毫秒。 | | `githubConnectionTimeoutMs` | `5000` | 安装前 GitHub 仓库连接检查的时限,单位毫秒。 | +| `gitTransportTimeoutMs` | `5000` | 安装前对自托管 Git 仓库每次 HTTP(S) 与 SSH 访问探测的时限,单位毫秒。 | | `registry` | pnpm 自身配置 | 查询与安装首先询问的注册表,http(s) URL;缺省为 pnpm 自身配置指定的那个。 | | `fallbackRegistries` | `['https://registry.npmmirror.com/']` | 前一个注册表不可达或没有该包副本时依次询问的注册表,http(s) URL;pnpm 自身的注册表只在它指向 npm 官方源或这里的某一个时才进入顺序。 | | `outputBytes` | `16384` | 每次操作返回的 pnpm 诊断字节上限;完整输出保留在返回的日志路径中。 | @@ -158,9 +161,11 @@ service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止, - Web 一次批准显示出来的整组待决定包,没有逐包选择。 +- 插件移到更新的提交后,已批准的 git 键仍留在 `pnpm-workspace.yaml` 中;没有任何机制移除过期的批准。 - 命令改动锁文件时,pnpm 按默认注册表校验锁文件中的每个 tarball URL。某个包从其他注册表安装后,使用默认注册表安装另一个包,或在重启后删除该包,可能以 `ERR_PNPM_TARBALL_URL_MISMATCH` 失败。 - 更新把目标保存为确切版本,失败后的重新安装也把原版本保存为确切版本,即使 profile 原先保存的是范围。有 HMR 时,重新安装的组合包排到选中列表末尾,这可能改变哪个组合包的配置优先。 - 更新把移除与安装作为两次 profile 操作运行,其他操作可能在两者之间运行。 +- 在 Windows 上,Git 传输探测沿用父进程的控制台,SSH 的主机密钥或密码提示可能出现在启动终端中,直到 `gitTransportTimeoutMs` 到期。在 POSIX 上,`known_hosts` 中没有的主机会让 SSH 探测失败,因此在接受一次主机密钥(例如执行 `ssh -T git@`)之前一直走 HTTPS。凭据助手对话框或 ssh-agent 确认(例如 Touch ID 或 `ssh-add -c` 对话框)超过时限时算作探测失败;HTTPS 探测超时时,可达的 SSH 形式会写入 profile。私有仓库在 pnpm 启动前最多等待两倍 `gitTransportTimeoutMs`。SSH 形式固定使用 `git` 用户和 SSH 默认端口,其他 SSH 端点需要 `url..insteadOf` 规则。两种探测都失败时,诊断是 pnpm 的 HTTPS 失败,不报告 SSH 探测失败的原因。 - 依赖需要新的构建脚本批准的新版本会更新失败,并重新安装为原版本;移除该包后通过普通安装批准其脚本。 - 没有 HMR 时,替换包需要重启进程。在 Plugin Manager 之外重新链接仍处于安装状态的包,需要等待重启。 - 仅启动时加载的 profile 不能删除当前进程启动时使用的包;停止进程后使用 `dsh plugin`。 diff --git a/packages/boot/plugin-manager/locale/de.json b/packages/boot/plugin-manager/locale/de.json new file mode 100644 index 0000000000..d9a82e0464 --- /dev/null +++ b/packages/boot/plugin-manager/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Verwaltungsdienst", + "description": "Verwalten Sie Plug-Ins und Plug-In-Pakete in der aktuell ausgeführten Konfiguration und unterstützen Sie Installation, Entfernung, Starten und Stoppen." + } +} diff --git a/packages/boot/plugin-manager/locale/es.json b/packages/boot/plugin-manager/locale/es.json new file mode 100644 index 0000000000..b4a6c2409a --- /dev/null +++ b/packages/boot/plugin-manager/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Servicio de gestión de complementos", + "description": "Administre complementos y paquetes de complementos en la configuración en ejecución actual, admitiendo la instalación, eliminación, inicio y detención." + } +} diff --git a/packages/boot/plugin-manager/locale/fr.json b/packages/boot/plugin-manager/locale/fr.json new file mode 100644 index 0000000000..51fb19f402 --- /dev/null +++ b/packages/boot/plugin-manager/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Service de gestion des plug-ins", + "description": "Gérez les plug-ins et les packages de plug-ins dans la configuration en cours d'exécution, prenant en charge l'installation, la suppression, le démarrage et l'arrêt." + } +} diff --git a/packages/boot/plugin-manager/locale/pt.json b/packages/boot/plugin-manager/locale/pt.json new file mode 100644 index 0000000000..63e8e2ffdb --- /dev/null +++ b/packages/boot/plugin-manager/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Serviço de gerenciamento de plug-ins", + "description": "Gerencie plug-ins e pacotes de plug-ins na configuração atual em execução, oferecendo suporte à instalação, remoção, início e parada." + } +} diff --git a/packages/boot/plugin-manager/locale/ru.json b/packages/boot/plugin-manager/locale/ru.json new file mode 100644 index 0000000000..1cd9a4db0d --- /dev/null +++ b/packages/boot/plugin-manager/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Служба управления плагинами", + "description": "Управляйте плагинами и пакетами плагинов в текущей работающей конфигурации, поддерживая установку, удаление, запуск и остановку." + } +} diff --git a/packages/boot/plugin-manager/locale/tools/de.json b/packages/boot/plugin-manager/locale/tools/de.json new file mode 100644 index 0000000000..0c51343ea1 --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/de.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Werkzeuge zur Plugin-Verwaltung" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/es.json b/packages/boot/plugin-manager/locale/tools/es.json new file mode 100644 index 0000000000..43198339a7 --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/es.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Herramientas de gestión de plugins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/fr.json b/packages/boot/plugin-manager/locale/tools/fr.json new file mode 100644 index 0000000000..4119d2e33a --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/fr.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Outils de gestion des plugins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/pt.json b/packages/boot/plugin-manager/locale/tools/pt.json new file mode 100644 index 0000000000..d1b8e8d2ac --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/pt.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Ferramentas de gerenciamento de plug-ins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/ru.json b/packages/boot/plugin-manager/locale/tools/ru.json new file mode 100644 index 0000000000..9bf83c6a3d --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/ru.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Инструменты управления плагинами" + } +} diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index 55ede9391f..3822c32f62 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-manager", "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, @@ -62,6 +62,7 @@ "dependencies": { "@deepseek-ai/dsh-app-boot": "workspace:*", "@deepseek-ai/dsh-atomic-write": "workspace:*", + "@deepseek-ai/dsh-official-bundles": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "execa": "^10.0.1", "semver": "^7.8.5", @@ -69,7 +70,8 @@ "zod": "4.4.3", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-plugin-detail": "workspace:*", - "@deepseek-ai/dsh-timeout": "workspace:*" + "@deepseek-ai/dsh-timeout": "workspace:*", + "hosted-git-info": "^9.0.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", @@ -114,6 +116,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-sandbox": "workspace:*", "@deepseek-ai/dsh-user-approval": "workspace:*", - "@deepseek-ai/dsh-client-connection": "workspace:*" + "@deepseek-ai/dsh-client-connection": "workspace:*", + "@types/hosted-git-info": "^3.0.5" } } diff --git a/packages/boot/plugin-manager/src/build-approval.ts b/packages/boot/plugin-manager/src/build-approval.ts index 75600f8c22..bdf0583119 100644 --- a/packages/boot/plugin-manager/src/build-approval.ts +++ b/packages/boot/plugin-manager/src/build-approval.ts @@ -11,6 +11,20 @@ const UNDECIDED_BUILD = 'set this to true or false' /** Where a non-interactive run names the build scripts it skipped. */ const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/ +/** The `allowBuilds` key pnpm suggests when it refuses to prepare a git-hosted package; a subdirectory key can contain spaces. */ +const GIT_DEP_PREPARE = /ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED[\s\S]*?^allowBuilds:\r?\n[ \t]+(.+?): true$/m + +/** + * Whether an `allowBuilds` key names a git URL that carries a credential: a password, or any user name over HTTP(S). + * Such a key is not recorded, so the credential stays out of the profile settings and the pending list. + */ +function carriesCredential(key: string): boolean { + const at = key.indexOf('@git+') + if (at === -1) return false + const url = URL.parse(key.slice(at + '@git+'.length).replace(/#.*$/s, '')) + return url !== null && (url.password !== '' || (url.username !== '' && /^https?:$/.test(url.protocol))) +} + async function readPolicy(dir: string) { let text: string try { text = await readFile(join(dir, 'pnpm-workspace.yaml'), 'utf8') } @@ -37,19 +51,19 @@ async function readPolicy(dir: string) { } /** - * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision. + * Record the build scripts a failed run reported as blocked, then list every `allowBuilds` key awaiting a decision. * * pnpm writes those policy entries itself only when it can prompt, and the * manager always runs it non-interactively, so the run's own report is the * record that survives. * @param dir Current profile directory. * @param output Captured output of the failed run. - * @returns Exact package names awaiting a build decision; wildcard rules are excluded. + * @returns Exact `allowBuilds` keys awaiting a build decision; wildcard rules are excluded. */ export async function recordPendingBuilds(dir: string, output: string): Promise { const { document, pending, keys } = await readPolicy(dir) const known = new Set(keys) - const added = ignoredBuildNames(output).filter(name => !known.has(name)) + const added = blockedBuildNames(output).filter(name => !known.has(name)) if (added.length === 0) return pending for (const name of added) document.setIn(['allowBuilds', name], UNDECIDED_BUILD) await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 }) @@ -57,14 +71,15 @@ export async function recordPendingBuilds(dir: string, output: string): Promise< } /** - * Read the package names a pnpm run reported as ignored build scripts. + * Read the `allowBuilds` keys a pnpm run reported as ignored build scripts or as a git-hosted package it refused to prepare. * @param output Captured output of the run. - * @returns Exact names in report order; wildcard patterns are excluded. + * @returns Exact keys in report order; wildcard patterns and git keys carrying a credential are excluded. */ -export function ignoredBuildNames(output: string): string[] { - const reported = IGNORED_BUILDS.exec(output)?.[1] - if (reported === undefined) return [] - return reported.split(',').map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name)) +export function blockedBuildNames(output: string): string[] { + const ignored = IGNORED_BUILDS.exec(output)?.[1]?.split(',') ?? [] + const git = GIT_DEP_PREPARE.exec(output)?.[1] + return [...ignored, ...git === undefined ? [] : [git]] + .map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name) && !carriesCredential(name)) } /** Persist approval without running scripts; the caller holds the profile manifest lock. diff --git a/packages/boot/plugin-manager/src/git-transport.ts b/packages/boot/plugin-manager/src/git-transport.ts new file mode 100644 index 0000000000..199b8f6394 --- /dev/null +++ b/packages/boot/plugin-manager/src/git-transport.ts @@ -0,0 +1,72 @@ +/** The Git transport an installation of a self-hosted repository uses, chosen by probing access before pnpm starts. */ +import { execa } from 'execa' +import hostedGitInfo from 'hosted-git-info' +import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess' +import type { ParsedInstallSpec } from './install-spec.ts' + +/** Deadline and cancellation for each read-only access probe. */ +export interface GitTransportOptions { + timeoutMs: number + signal: AbortSignal + /** The application-owned package manager's environment, also used by its Git children. */ + env?: Readonly> +} + +/** An HTTP(S) Git URL with an optional `git+` prefix, which pnpm clones over that transport as written. */ +const HTTP_GIT_URL = /^(?:git\+)?https?:\/\//i + +/** + * The SSH form of an HTTP(S) Git spec on a host pnpm has no transport fallback for. pnpm tries HTTPS and SSH itself + * for the hosts `hosted-git-info` knows, such as GitHub and GitLab.com, and clones any other URL over the transport + * it names. + * @param spec The parsed installation address. + * @returns `git+ssh://git@/` on SSH's default port with the spec's fragment, or `undefined` for a spec that + * is not an HTTP(S) Git URL on such a host, or whose URL carries credentials or a query. + */ +export function sshGitSpec(spec: ParsedInstallSpec): string | undefined { + if (spec.kind !== 'git' || !HTTP_GIT_URL.test(spec.spec) || hostedGitInfo.fromUrl(spec.spec) !== undefined) return undefined + const url = new URL(spec.spec.replace(/^git\+/i, '')) + if (url.username !== '' || url.password !== '' || url.search !== '') return undefined + // The fragment is copied as written: WHATWG serialization would percent-encode some of its characters. + const hash = /#.*$/s.exec(spec.spec)?.[0] ?? '' + return `git+ssh://git@${url.hostname}${url.pathname}${hash}` +} + +/** + * Probe whether Git can read a repository with the installer's Git configuration and credential helpers. Git's + * terminal and askpass prompts are disabled, `core.askPass` included because Windows drops the empty `GIT_ASKPASS`, + * and on POSIX the probe has no controlling terminal, so SSH cannot ask about host keys or passphrases either. + * @param repository The repository URL Git reads. + * @param dir The profile directory where installation runs. + * @param options The probe deadline, cancellation and environment. + * @returns Whether `git ls-remote` listed the repository's HEAD. + */ +async function readable(repository: string, dir: string, options: GitTransportOptions): Promise { + const result = await execa('git', ['-c', 'core.askPass=', 'ls-remote', '--exit-code', '--', repository, 'HEAD'], { + cwd: dir, + env: { + ...scrubbedParentEnv(), ...options.env, LC_ALL: 'C', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never', + GIT_ASKPASS: '', SSH_ASKPASS: '', SSH_ASKPASS_REQUIRE: 'never', + }, + extendEnv: false, stdin: 'ignore', stdout: 'ignore', stderr: 'ignore', reject: false, + timeout: options.timeoutMs, cancelSignal: options.signal, killDescendants: true, killSignal: 'SIGKILL', + }) + return !result.failed +} + +/** + * Choose how pnpm clones an HTTP(S) Git repository that {@link sshGitSpec} has an SSH form for: probe the spec's URL, + * and when HTTP(S) cannot read it, probe the SSH form. + * @param spec The parsed installation address. + * @param dir The profile directory where installation runs. + * @param options The deadline of each probe, cancellation and the installer's environment. + * @returns The SSH form when only SSH can read the repository; otherwise the spec unchanged. + */ +export async function chooseGitTransport(spec: ParsedInstallSpec, dir: string, options: GitTransportOptions): Promise { + const ssh = sshGitSpec(spec) + if (ssh === undefined) return spec.spec + const strip = (value: string): string => value.replace(/^git\+/i, '').replace(/#.*$/s, '') + if (await readable(strip(spec.spec), dir, options)) return spec.spec + if (options.signal.aborted || !await readable(strip(ssh), dir, options)) return spec.spec + return ssh +} diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index 1b66065515..44d652a408 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -19,11 +19,12 @@ import { readBundleDocument, readBundleImage, type BundleDetailOptions } from '. import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory' import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, - readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, + readProfilePatches, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, - OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, ResolutionRestartRequired, + getDshRuntimeVersion, ResolutionRestartRequired, } from '@deepseek-ai/dsh-app-boot' +import { OPTIONAL_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG, PROTECTED_PLUGINS } from '@deepseek-ai/dsh-official-bundles' import type {} from '@deepseek-ai/dsh-hmr' import type { ProfileContext, ProfileManifest } from '@deepseek-ai/dsh-app-boot' import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from './operations.ts' @@ -35,6 +36,7 @@ import { flatten, writePluginEnabled } from './patch.ts' import { incompatiblePlugin, ManagementFailure } from './failure.ts' import { approveBuilds, recordPendingBuilds } from './build-approval.ts' import { checkGithubConnection } from './github-connection.ts' +import { chooseGitTransport, sshGitSpec } from './git-transport.ts' import { newerVersions, VERSION_FIELDS, VERSION_LIST_FIELDS, versionFacts } from './versions.ts' import type { BundleDetail, BundleInfo, BundleRowInfo, ChangeResult, InspectOptions, InstallBundleOptions, ListVersionsOptions, ManagementError, @@ -58,6 +60,8 @@ export interface Config { inspectTimeoutMs?: number /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ githubConnectionTimeoutMs?: number + /** Maximum duration of each HTTP(S) and SSH access probe of a self-hosted Git repository before installation, in milliseconds. */ + gitTransportTimeoutMs?: number /** Maximum time one captured package run may print nothing before the manager terminates it, in milliseconds. */ idleTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ @@ -88,18 +92,6 @@ const DETAIL_FILE_PATH = '/api/plugin-manager/detail-file' /** An http(s) URL, as pnpm's `--registry` takes it. */ const REGISTRY_URL = /^https?:\/\/\S+$/ -const protectedModules = new Set([ - '@deepseek-ai/dsh-plugin-manager', '@deepseek-ai/cordis-plugin-loader', - '@deepseek-ai/cordis-plugin-include', '@deepseek-ai/dsh-api-gateway', - '@deepseek-ai/dsh-host-webserver', '@deepseek-ai/dsh-client-modules', - '@deepseek-ai/dsh-client-ui-settings-plugin-inventory', '@deepseek-ai/dsh-client-ui-plugin-manager', - '@deepseek-ai/dsh-host-plugin-inventory', '@deepseek-ai/dsh-typert-registry', - '@deepseek-ai/dsh-api-remotes', '@deepseek-ai/dsh-api-remote-access', - '@deepseek-ai/cordis-plugin-timer', '@deepseek-ai/dsh-client-connection', - '@deepseek-ai/dsh-host-frontend-static', '@deepseek-ai/dsh-tools', - '@deepseek-ai/dsh-hmr', -]) - /** The profile files an installation writes and a failed or cancelled one restores. */ const RESTORED_FILES = ['package.json', 'pnpm-lock.yaml'] as const @@ -248,7 +240,7 @@ function bundleSelection(manifest: ProfileManifest): readonly string[] { /** * The package an installation spec names before pnpm runs: a registry name, a local directory's manifest name, or the - * dependency saved with this git or tarball spec, compared as the plugin list shows saved specs. + * dependency saved with this git or tarball spec or the SSH form of its Git URL, compared as the plugin list shows saved specs. * @param spec The installation spec. * @param dependencies The profile's saved dependencies. * @param profileDir The profile directory saved local paths are relative to. @@ -258,7 +250,8 @@ function specPackageName(spec: string, dependencies: Readonly specs.includes(dependencySpec(dependency, saved, profileDir)))?.[0] } @@ -299,6 +292,7 @@ export class PluginManager extends TypertRemoteService { lockWaitMs: z.number().step(1).min(0).default(120000), inspectTimeoutMs: z.number().step(1).min(1000).default(20000), githubConnectionTimeoutMs: z.number().step(1).min(1000).default(5000), + gitTransportTimeoutMs: z.number().step(1).min(1000).default(5000), idleTimeoutMs: z.number().step(1).min(1000).default(600000), registry: z.string().pattern(REGISTRY_URL), fallbackRegistries: z.array(z.string().pattern(REGISTRY_URL)).default([NPMMIRROR_REGISTRY]), @@ -323,6 +317,7 @@ export class PluginManager extends TypertRemoteService { private readonly lockWaitMs: number private readonly inspectTimeoutMs: number private readonly githubConnectionTimeoutMs: number + private readonly gitTransportTimeoutMs: number private readonly idleTimeoutMs: number private readonly pnpmCommand: string private readonly versionListLimit: number @@ -344,6 +339,7 @@ export class PluginManager extends TypertRemoteService { this.lockWaitMs = (config as Required).lockWaitMs this.inspectTimeoutMs = (config as Required).inspectTimeoutMs this.githubConnectionTimeoutMs = (config as Required).githubConnectionTimeoutMs + this.gitTransportTimeoutMs = (config as Required).gitTransportTimeoutMs this.idleTimeoutMs = (config as Required).idleTimeoutMs this.pnpmCommand = (config as Required).pnpmCommand this.versionListLimit = (config as Required).versionListLimit @@ -448,7 +444,7 @@ export class PluginManager extends TypertRemoteService { const actual = [...this.ctx.loader.entries()].find(row => row.id === entry.entryId) const candidates = rows.filter(row => row.id === actual?.options.id) const candidate = candidates[0] - if (protectedModules.has(entry.moduleName) || entry.entryId === this.ownerEntryId) { + if (PROTECTED_PLUGINS.has(entry.moduleName) || entry.entryId === this.ownerEntryId) { return { ...entry, readOnlyReason: 'management-required' as const } } if (candidate === undefined || candidates.length > 1 || candidate.name !== entry.moduleName @@ -826,7 +822,9 @@ export class PluginManager extends TypertRemoteService { /** * Install a package using the same pnpm implementation as dsh plugin. GitHub * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; - * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. An + * HTTP(S) Git URL on another host is installed over SSH instead when access probes bounded by gitTransportTimeoutMs + * find that only SSH can read it. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -861,10 +859,11 @@ export class PluginManager extends TypertRemoteService { let version: string | undefined try { result.registries = [] - const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, { - timeoutMs: this.githubConnectionTimeoutMs, outputBytes: this.outputBytes, - signal: AbortSignal.any([this.abort.signal, control.abort.signal]), - ...this.profile.packageManager?.env === undefined ? {} : { env: this.profile.packageManager.env }, + const parsed = parsedForRegistry(spec) + const signal = AbortSignal.any([this.abort.signal, control.abort.signal]) + const env = this.profile.packageManager?.env === undefined ? {} : { env: this.profile.packageManager.env } + const connection = checkGithubConnection(parsed, this.profile.dir, { + timeoutMs: this.githubConnectionTimeoutMs, outputBytes: this.outputBytes, signal, ...env, }) this.packageOperations.add(connection) let connectionFailure: PackageResult | undefined @@ -876,6 +875,12 @@ export class PluginManager extends TypertRemoteService { result.failedAt = 'spec-host' throw new Error(connectionFailure.output) } + const transport = chooseGitTransport(parsed, this.profile.dir, { timeoutMs: this.gitTransportTimeoutMs, signal, ...env }) + this.packageOperations.add(transport) + let installSpec: string + try { installSpec = await transport } + finally { this.packageOperations.delete(transport) } + if (stopped()) throw new InstallCancelledError() // The last run is the result's; the registries asked stay listed whatever the outcome. const plan = registryPlan(options?.registry, await this.registries()) let run: PackageResult | undefined @@ -887,7 +892,7 @@ export class PluginManager extends TypertRemoteService { result.registries.push(registry) used = registry announce('installing', { registry, index: index + 1, total: plan.length }) - run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId) + run = await this.runPnpm(['add', installSpec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId) result.packageResult = run if (stopped()) throw new InstallCancelledError() // A compatibility refusal is the package's own answer, so no other registry is asked. @@ -900,7 +905,7 @@ export class PluginManager extends TypertRemoteService { delete result.failedAt // A run this manager terminated got no answer from the registry at all, so no registry explains it. if (run.timedOut === true) break - const failedAt = attributeFailure(run.kind, run.output, parsedForRegistry(spec)) + const failedAt = attributeFailure(run.kind, run.output, parsedForRegistry(installSpec)) if (failedAt !== 'other') result.failedAt = failedAt if (failedAt !== 'registry' || index === plan.length - 1) break } @@ -910,7 +915,7 @@ export class PluginManager extends TypertRemoteService { const succeeded = run.exitCode === 0 && run.timedOut !== true if (succeeded) delete result.failedAt if (!succeeded) { - // pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval. + // pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as blocked, are offered for approval. try { result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output) } catch (error) { this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error) @@ -1150,7 +1155,7 @@ export class PluginManager extends TypertRemoteService { // Unreadable bundles contribute no new rows; listBundles reports their diagnostics. return false } - const protectedBundle = rows.some(row => protectedModules.has(row.name) || `include:${row.id}` === this.ownerEntryId) + const protectedBundle = rows.some(row => PROTECTED_PLUGINS.has(row.name) || `include:${row.id}` === this.ownerEntryId) if (protectedBundle) this.managementBundles.add(name) return protectedBundle } diff --git a/packages/boot/plugin-manager/src/install-failure.ts b/packages/boot/plugin-manager/src/install-failure.ts index 009eeb1ad9..5d2b23ca7c 100644 --- a/packages/boot/plugin-manager/src/install-failure.ts +++ b/packages/boot/plugin-manager/src/install-failure.ts @@ -19,7 +19,7 @@ export interface InstallFailureFacts { /** Patterns in the order they decide: a specific code before the generic network family. */ const LOG_KINDS: readonly [PluginInstallFailureKind, RegExp][] = [ - ['build-blocked', /ERR_PNPM_IGNORED_BUILDS|Ignored build scripts/], + ['build-blocked', /ERR_PNPM_IGNORED_BUILDS|Ignored build scripts|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/], ['not-found', /ERR_PNPM_FETCH_404|\bE404\b|404 Not Found|Not Found - GET/], ['no-matching-version', /ERR_PNPM_NO_MATCHING_VERSION|\bETARGET\b|No matching version/], ['disk-full', /\bENOSPC\b|no space left on device/i], diff --git a/packages/boot/plugin-manager/src/operations.ts b/packages/boot/plugin-manager/src/operations.ts index d3452139fe..8950ad83a3 100644 --- a/packages/boot/plugin-manager/src/operations.ts +++ b/packages/boot/plugin-manager/src/operations.ts @@ -7,10 +7,11 @@ import { execa } from 'execa' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' import { - DEFAULT_PROFILE_BUNDLES, bundlePatchPaths, initProfile, PROFILE_TEMPLATES, readProfileManifest, + bundlePatchPaths, initProfile, readProfileManifest, resolveBundleDir, resolveProfileDir, loadOverlayPatches, composeEntries, readProfileVersionExemptions, evaluatePluginCompatibility, pluginCompatibilityWarning, type ProfileManifest, } from '@deepseek-ai/dsh-app-boot' +import { PROFILE_TEMPLATES, DEFAULT_PROFILE_BUNDLES } from '@deepseek-ai/dsh-official-bundles' import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess' import { parseInstallSpec } from './install-spec.ts' import { awaitTreeGone, leadsOwnGroup, treeAlive, type RunTree } from './run-tree.ts' diff --git a/packages/boot/plugin-manager/src/types.ts b/packages/boot/plugin-manager/src/types.ts index 1c395517f6..1c21d336e4 100644 --- a/packages/boot/plugin-manager/src/types.ts +++ b/packages/boot/plugin-manager/src/types.ts @@ -170,7 +170,7 @@ export interface ChangeResult { bundle?: string /** The installed bundle's manifest version, when declared; pnpm's `minimumReleaseAge` can make it older than the newest release. */ version?: string - /** Exact package names awaiting explicit script approval in the profile's pnpm settings, read after a failed run. */ + /** Exact `allowBuilds` keys awaiting explicit script approval in the profile's pnpm settings, read after a failed run. */ pendingBuilds?: string[] /** Package script permissions saved before this installation attempt. */ approvedBuilds?: string[] diff --git a/packages/boot/plugin-manager/tests/build-approval.spec.ts b/packages/boot/plugin-manager/tests/build-approval.spec.ts index f7620aee82..1d5d3ad41f 100644 --- a/packages/boot/plugin-manager/tests/build-approval.spec.ts +++ b/packages/boot/plugin-manager/tests/build-approval.spec.ts @@ -1,5 +1,5 @@ /** Pending script permissions survive cleanup and preserve unrelated workspace settings. */ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { expect, it, onTestFinished } from 'vitest' @@ -89,6 +89,40 @@ it('records the ignored build scripts a non-interactive run reported', async () expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({ allowBuilds: { 'native@1.0.0': true } }) }) +function gitPrepareRefusal(key: string, eol = '\n') { + return [ + '[ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED] Failed to prepare git-hosted package fetched from "https://example.com/sidechat.git": The git-hosted package "@scope/sidechat@0.2.0" needs to execute build scripts but is not in the "allowBuilds" allowlist.', + '', + 'Add the package to "allowBuilds" in your project\'s pnpm-workspace.yaml to allow it to run scripts. For example:', + 'allowBuilds:', + ` ${key}: true`, + '', + ].join(eol) +} + +it.each([ + ['LF', '@scope/sidechat@git+https://example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', '\n'], + ['CRLF', '@scope/sidechat@git+https://example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', '\r\n'], + ['a subdirectory with a space', '@scope/sidechat@git+file:///repo#244b67419a80844f5fb1fd7971109b9c19a20b76&path:plugin folder', '\n'], + ['an SSH user name', '@scope/sidechat@git+ssh://git@example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', '\n'], +])('records the allowBuilds key pnpm suggests for a git-hosted package it refused to prepare (%s)', async (_label, key, eol) => { + const { dir, filename } = fixture() + expect(await recordPendingBuilds(dir, gitPrepareRefusal(key, eol))).toEqual([key]) + await approveBuilds(dir, [key]) + expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({ allowBuilds: { [key]: true } }) +}) + +it.each([ + '@scope/sidechat@git+https://user:token@example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', + '@scope/sidechat@git+http://token@example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', + '@scope/sidechat@git+ssh://git:secret@example.com/sidechat.git#244b67419a80844f5fb1fd7971109b9c19a20b76', +])('does not record a git key that carries a credential: %s', async (key) => { + const { dir, filename } = fixture() + expect(await recordPendingBuilds(dir, gitPrepareRefusal(key))).toEqual([]) + expect(await recordPendingBuilds(dir, `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: ${key}\n`)).toEqual([]) + expect(existsSync(filename)).toBe(false) +}) + it.each([ ['no report', 'Progress: resolved 1, reused 1, downloaded 0, added 1, done\n'], ['an empty report', '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: \n'], diff --git a/packages/boot/plugin-manager/tests/bundle-detail.spec.ts b/packages/boot/plugin-manager/tests/bundle-detail.spec.ts index 8f1de9641c..dc3ad0c268 100644 --- a/packages/boot/plugin-manager/tests/bundle-detail.spec.ts +++ b/packages/boot/plugin-manager/tests/bundle-detail.spec.ts @@ -4,7 +4,8 @@ import { realpath } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { Service, type Context } from '@deepseek-ai/cordis' -import { boot, getDshRuntimeVersion, initProfile, loadProfileDirectory, OFFICIAL_ON_DEMAND_CATALOG, readProfileManifest, readProfilePatches, type ProfileContext } from '@deepseek-ai/dsh-app-boot' +import { boot, getDshRuntimeVersion, initProfile, loadProfileDirectory, readProfileManifest, readProfilePatches, type ProfileContext } from '@deepseek-ai/dsh-app-boot' +import { OFFICIAL_ON_DEMAND_CATALOG } from '@deepseek-ai/dsh-official-bundles' import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol' import { afterEach, expect, it, onTestFinished, vi } from 'vitest' import PluginManager, { type Config } from '../src/index.ts' diff --git a/packages/boot/plugin-manager/tests/git-transport.spec.ts b/packages/boot/plugin-manager/tests/git-transport.spec.ts new file mode 100644 index 0000000000..36d043b6b8 --- /dev/null +++ b/packages/boot/plugin-manager/tests/git-transport.spec.ts @@ -0,0 +1,153 @@ +/** Real Git access probes that choose the transport of a self-hosted repository installation. */ +import { once } from 'node:events' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import * as subprocess from 'execa' +import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest' +import { chooseGitTransport } from '../src/git-transport.ts' +import { parseInstallSpec } from '../src/install-spec.ts' +import { isolateGitCommandLineConfig } from './git-environment.ts' + +vi.mock('execa', async (original) => { + const module = await original() + return { ...module, execa: vi.fn(module.execa) } +}) + +// These cases spawn real Git, so the host's command-line configuration group must not reach its children. +let restoreGitCommandLineConfig: () => void +beforeAll(() => { restoreGitCommandLineConfig = isolateGitCommandLineConfig() }) +afterAll(() => { restoreGitCommandLineConfig() }) + +const SPEC = `git+https://git.example/team/sub/plugin.git#${'a'.repeat(40)}&path:/packages/a` +const SSH_SPEC = `git+ssh://git@git.example/team/sub/plugin.git#${'a'.repeat(40)}&path:/packages/a` + +async function fixture() { + const dir = await mkdtemp(join(tmpdir(), 'dsh-git-transport-')) + onTestFinished(() => rm(dir, { recursive: true, force: true })) + const config = join(dir, 'git.config') + await writeFile(config, '') + const env = { GIT_CONFIG_GLOBAL: config, GIT_CONFIG_NOSYSTEM: '1' } + const git = (...args: string[]) => subprocess.execa('git', args, { env }) + const repository = join(dir, 'plugin.git') + await git('init', '--bare', repository) + // ls-remote --exit-code needs a HEAD that names a commit. + const work = join(dir, 'work') + await git('init', work) + await git('-C', work, '-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'init') + await git('-C', work, 'push', repository, 'HEAD:refs/heads/main') + await git('-C', repository, 'symbolic-ref', 'HEAD', 'refs/heads/main') + const controller = new AbortController() + const tasks: Promise[] = [] + // Registered after the directory removal, so it runs first: no probe outlives the directory it runs in. + onTestFinished(async () => { controller.abort(); await Promise.allSettled(tasks) }) + const choose = (spec: string, timeoutMs = 5000) => { + const task = chooseGitTransport(parseInstallSpec(spec), dir, { timeoutMs, signal: controller.signal, env }) + tasks.push(task) + return task + } + return { dir, config, repository: pathToFileURL(repository).href, controller, choose } +} + +/** An HTTP Git endpoint that answers every request with `handler`. */ +async function server(handler: (request: IncomingMessage, response: ServerResponse) => void): Promise { + const http = createServer(handler) + onTestFinished(async () => { + http.closeAllConnections() + await new Promise((resolve, reject) => { http.close((error) => { if (error) reject(error); else resolve() }) }) + }) + http.listen(0, '127.0.0.1') + await once(http, 'listening') + const address = http.address() + if (address === null || typeof address === 'string') throw new Error('server did not bind a TCP port') + return `http://127.0.0.1:${String(address.port)}/` +} + +const unauthorized = (_request: IncomingMessage, response: ServerResponse): void => { + response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="fixture"' }) + response.end() +} + +it('keeps the HTTPS spec when HTTPS can read the repository', async () => { + const { config, repository, choose } = await fixture() + await writeFile(config, `[url "${repository}"]\n insteadOf = https://git.example/team/sub/plugin.git\n insteadOf = ssh://git@git.example/team/sub/plugin.git\n`) + expect(await choose(SPEC)).toBe(SPEC) +}) + +it('installs over SSH when HTTPS asks for credentials that SSH does not need', async () => { + const { config, repository, choose } = await fixture() + const http = await server(unauthorized) + await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n insteadOf = http://git.example/\n[url "${repository}"]\n insteadOf = ssh://git@git.example/team/sub/plugin.git\n[http]\n proxy =\n`) + expect(await choose(SPEC)).toBe(SSH_SPEC) + // The fragment is copied as written, not as URL serialization would encode it. + expect(await choose('git+https://git.example/team/sub/plugin.git#semver:<2 >1')).toBe('git+ssh://git@git.example/team/sub/plugin.git#semver:<2 >1') + expect(await choose('git+http://git.example/team/sub/plugin.git')).toBe('git+ssh://git@git.example/team/sub/plugin.git') +}) + +it('keeps the HTTPS spec when neither transport can read the repository', async () => { + const { config, choose } = await fixture() + const http = await server(unauthorized) + await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[url "file:///nonexistent/plugin.git"]\n insteadOf = ssh://git@git.example/\n[http]\n proxy =\n`) + expect(await choose(SPEC)).toBe(SPEC) +}) + +it('lets configured credential helpers answer the HTTPS probe', async () => { + const { dir, config, repository, choose } = await fixture() + const expected = `Basic ${Buffer.from('person:token').toString('base64')}` + const http = await server((request, response) => { + if (request.headers.authorization === expected) { + // Any reply Git can parse as a ref advertisement is enough; a dumb-HTTP info/refs listing is the smallest. + response.writeHead(200, { 'Content-Type': 'text/plain' }) + response.end(`${'b'.repeat(40)}\tHEAD\n`) + } else unauthorized(request, response) + }) + const helper = join(dir, 'credential-helper.cjs') + await writeFile(helper, 'process.stdout.write("username=person\\npassword=token\\n")') + await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[url "${repository}"]\n insteadOf = ssh://git@git.example/team/sub/plugin.git\n[http]\n proxy =\n`) + await subprocess.execa('git', ['config', '--file', config, 'credential.helper', `!"${process.execPath.replaceAll('\\', '/')}" "${helper.replaceAll('\\', '/')}"`]) + expect(await choose(SPEC)).toBe(SPEC) +}) + +it.each([ + '@acme/plugin', + 'github:acme/plugin', + 'https://gitlab.com/acme/plugin.git', + 'git+https://github.com/acme/plugin.git#main', + 'git+ssh://git@git.example/team/plugin.git', + 'git@git.example:team/plugin.git', + 'git+https://person:token@git.example/team/plugin.git', + 'git+https://git.example/team/plugin.git?ref=x', +])('leaves %s to pnpm without probing', async (spec) => { + const { choose } = await fixture() + const run = vi.mocked(subprocess.execa) + run.mockClear() + expect(await choose(spec)).toBe(spec) + expect(run).not.toHaveBeenCalled() +}) + +it('ends an in-flight HTTPS probe on cancellation and keeps the spec', async () => { + const { config, controller, choose } = await fixture() + const requested = Promise.withResolvers() + // Never answers, so only cancellation ends the probe. + const http = await server(() => { requested.resolve(undefined) }) + await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[http]\n proxy =\n`) + const run = vi.mocked(subprocess.execa) + run.mockClear() + const choosing = choose(SPEC, 60000) + await requested.promise + controller.abort() + expect(await choosing).toBe(SPEC) + expect(run).toHaveBeenCalledTimes(1) + expect(run.mock.calls[0]?.[1]).toEqual(['-c', 'core.askPass=', 'ls-remote', '--exit-code', '--', 'https://git.example/team/sub/plugin.git', 'HEAD']) +}) + +it('does not probe SSH after cancellation', async () => { + const { controller, choose } = await fixture() + const run = vi.mocked(subprocess.execa) + run.mockClear() + controller.abort() + expect(await choose(SPEC)).toBe(SPEC) + expect(run).toHaveBeenCalledTimes(1) +}) diff --git a/packages/boot/plugin-manager/tests/install-spec.spec.ts b/packages/boot/plugin-manager/tests/install-spec.spec.ts index 10a4a76398..26f591cc6c 100644 --- a/packages/boot/plugin-manager/tests/install-spec.spec.ts +++ b/packages/boot/plugin-manager/tests/install-spec.spec.ts @@ -64,6 +64,7 @@ describe('classifyInstallFailure', () => { expect(classifyInstallFailure({ log: 'ENOSPC', timedOut: true })).toBe('timeout') expect(classifyInstallFailure({ log: '', cause: Object.assign(new Error('spawn pnpm ENOENT'), { code: 'ENOENT' }) })).toBe('pnpm-missing') expect(classifyInstallFailure({ log: 'ERR_PNPM_IGNORED_BUILDS Ignored build scripts: node-pty' })).toBe('build-blocked') + expect(classifyInstallFailure({ log: '[ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED] Failed to prepare git-hosted package' })).toBe('build-blocked') expect(classifyInstallFailure({ log: 'npm error code E404\nnpm error 404 Not Found - GET https://registry/x' })).toBe('not-found') expect(classifyInstallFailure({ log: 'ERR_PNPM_NO_MATCHING_VERSION No matching version found for x@9' })).toBe('no-matching-version') expect(classifyInstallFailure({ log: 'npm error code ETARGET' })).toBe('no-matching-version') diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 9020fd3ade..c6fe5f9312 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -12,9 +12,10 @@ import type { Context } from '@deepseek-ai/cordis' import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest' import { boot, composeEntries, initProfile, loadProfileDirectory, readProfilePatches, readProfileManifest, - reconcileProfilePatches, OPTIONAL_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG, PluginPackages, readPluginMeta, getDshRuntimeVersion, + reconcileProfilePatches, PluginPackages, readPluginMeta, getDshRuntimeVersion, type ProfileContext, type RuntimeResolution, } from '@deepseek-ai/dsh-app-boot' +import { OPTIONAL_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG } from '@deepseek-ai/dsh-official-bundles' import PluginManager, { type Config, type PluginChange, type PluginInstallLogChunk, type PluginInstallProgress, type PluginInstallRequestId } from '../src/index.ts' import Hmr from '@deepseek-ai/dsh-hmr' import Timer from '@deepseek-ai/cordis-plugin-timer' @@ -22,6 +23,7 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { Group } from '@deepseek-ai/cordis-plugin-loader' import * as operations from '../src/operations.ts' import * as githubConnection from '../src/github-connection.ts' +import * as gitTransport from '../src/git-transport.ts' import { parse, parseDocument } from 'yaml' import { isolateGitCommandLineConfig } from './git-environment.ts' @@ -137,6 +139,47 @@ it('cancels an active GitHub check before starting pnpm', async () => { expect(pnpm).not.toHaveBeenCalled() }) +it('installs the spec the Git transport probe chooses with the profile Git environment and deadline', async () => { + const env = { GIT_CONFIG_GLOBAL: '/application/git.config' } + const { manager, dir } = await fixture('live', false, undefined, { gitTransportTimeoutMs: 7000 }, { command: 'pnpm', args: [], env }) + const spec = 'git+https://git.example/team/sub/plugin.git#main' + const transport = vi.spyOn(gitTransport, 'chooseGitTransport').mockResolvedValue('git+ssh://git@git.example/team/sub/plugin.git#main') + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ exitCode: 1, output: 'stop', truncated: false, logPath: 'pnpm.log', kind: 'unknown' }) + onTestFinished(() => { transport.mockRestore(); pnpm.mockRestore() }) + expect((await manager.installBundle(spec)).application).toBe('failed') + expect(transport).toHaveBeenCalledWith(expect.objectContaining({ kind: 'git', spec }), dir, expect.objectContaining({ timeoutMs: 7000, env })) + expect(pnpm.mock.calls[0]?.[1]).toEqual(['add', 'git+ssh://git@git.example/team/sub/plugin.git#main']) +}) + +it('attributes a failure to the host of the SSH spec the probe chose, which carries no HTTPS port', async () => { + const { manager } = await fixture() + const transport = vi.spyOn(gitTransport, 'chooseGitTransport').mockResolvedValue('git+ssh://git@git.example/team/plugin.git#main') + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ + exitCode: 1, output: 'ssh: Could not resolve hostname git.example: nodename nor servname provided', truncated: false, logPath: 'pnpm.log', + }) + onTestFinished(() => { transport.mockRestore(); pnpm.mockRestore() }) + expect(await manager.installBundle('git+https://git.example:8443/team/plugin.git#main')).toMatchObject({ application: 'failed', failedAt: 'spec-host' }) + expect(pnpm).toHaveBeenCalledOnce() +}) + +it('cancels an active Git transport probe before starting pnpm', async () => { + const { manager } = await fixture() + const entered = Promise.withResolvers() + const transport = vi.spyOn(gitTransport, 'chooseGitTransport').mockImplementation(async (spec, _dir, options) => { + entered.resolve(undefined) + await new Promise((resolve) => { options.signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + return spec.spec + }) + const pnpm = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { transport.mockRestore(); pnpm.mockRestore() }) + const requestId = '5f0c1f8e-3f5e-4a59-9a43-1d0c5c3f7a21' as PluginInstallRequestId + const installing = manager.installBundle('git+https://git.example/team/plugin.git', { requestId }) + await entered.promise + expect(await manager.cancelInstall(requestId)).toEqual({ status: 'cancelled' }) + expect(await installing).toMatchObject({ application: 'cancelled', changed: false }) + expect(pnpm).not.toHaveBeenCalled() +}) + it('waits for the GitHub check before installing and activating the bundle', async () => { const { manager, dir, bundle, connection } = await fixture() const entered = Promise.withResolvers() @@ -623,6 +666,38 @@ it('runs a real pnpm dependency script only after approval and retry', async () expect(readFileSync(built, 'utf8')).toBe('built') }) +it('offers approval for a real git-hosted package pnpm refused to prepare', async () => { + // Both the fixture's Git and the manager's pnpm child read only this empty configuration. + const env = { GIT_CONFIG_GLOBAL: '', GIT_CONFIG_NOSYSTEM: '1' } + const pnpm = fileURLToPath(new URL('../../../../apps/desktop/node_modules/pnpm/bin/pnpm.mjs', import.meta.url)) + const { manager, dir, profile } = await fixture('startup', false, undefined, {}, { command: process.execPath, args: ['--expose-internals', pnpm], env }) + const repository = join(profile.cwd, 'repository') + mkdirSync(repository) + env.GIT_CONFIG_GLOBAL = join(profile.cwd, 'git.config') + writeFileSync(env.GIT_CONFIG_GLOBAL, '') + const git = (args: string[]) => execa('git', args, { cwd: repository, env }) + await git(['init', '--initial-branch=main']) + writeFileSync(join(repository, 'package.json'), JSON.stringify({ name: '@test/git-prepared', version: '1.0.0', + scripts: { prepare: 'node build.cjs' }, dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(repository, 'build.cjs'), 'require("node:fs").writeFileSync("built.txt", "built")\n') + writeFileSync(join(repository, 'cordis.patch.yml'), '[]\n') + await git(['add', '.']) + await git(['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-m', 'fixture bundle']) + writeFileSync(join(dir, 'package.json'), '{"name":"git-approval-fixture","private":true}\n') + const policy = parseDocument(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')) + policy.set('storeDir', join(profile.cwd, 'store')) + writeFileSync(join(dir, 'pnpm-workspace.yaml'), String(policy)) + const spec = `git+${pathToFileURL(repository).href}` + const blocked = await manager.installBundle(spec, { enabled: false }) + expect(blocked, JSON.stringify(blocked)).toMatchObject({ application: 'failed', packageResult: { kind: 'build-blocked' } }) + expect(blocked.pendingBuilds).toEqual([expect.stringMatching(/^@test\/git-prepared@git\+file:.*#[0-9a-f]{40}$/)]) + const built = join(dir, 'node_modules', '@test', 'git-prepared', 'built.txt') + expect(existsSync(built)).toBe(false) + const allowed = await manager.installBundle(spec, { enabled: false, approvedBuilds: blocked.pendingBuilds! }) + expect(allowed, JSON.stringify(allowed)).toMatchObject({ application: 'restart-required', packageResult: { exitCode: 0 } }) + expect(readFileSync(built, 'utf8')).toBe('built') +}) + it.each(['[', 'allowBuilds: false\n'])('preserves pnpm diagnostics when pending approvals cannot be read: %s', async (policy) => { const { manager, dir } = await fixture() writeFileSync(join(dir, 'pnpm-workspace.yaml'), policy) @@ -832,6 +907,10 @@ it('refuses to install over an installed package, by name, version or local dire expect(await manager.installBundle(join(dir, '..', 'extra-1.0.0.tgz'), { approvedBuilds: ['native-provider'] })) .toMatchObject({ application: 'failed', changed: false, error: { code: 'already-installed' } }) expect(existsSync(workspace) ? readFileSync(workspace, 'utf8') : undefined).toBe(settings) + // A self-hosted HTTPS Git spec matches the SSH form an earlier installation saved. + writeFileSync(join(dir, 'package.json'), JSON.stringify({ ...manifest, dependencies: { extra: 'git+ssh://git@git.example/team/sub/extra.git#main' } })) + expect(await manager.installBundle('git+https://git.example/team/sub/extra.git#main')) + .toMatchObject({ application: 'failed', changed: false, error: { code: 'already-installed' } }) writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) expect(install).not.toHaveBeenCalled() expect(readProfileManifest('test', dir).dependencies).toEqual({ extra: '1.0.0' }) diff --git a/packages/boot/plugin-manager/tsconfig.json b/packages/boot/plugin-manager/tsconfig.json index dac19ccf9e..97732ec2d5 100644 --- a/packages/boot/plugin-manager/tsconfig.json +++ b/packages/boot/plugin-manager/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../app-boot" }, + { + "path": "../../util/official-bundles" + }, { "path": "../../util/atomic-write" }, diff --git a/packages/boot/plugin-market/README.i18n.yaml b/packages/boot/plugin-market/README.i18n.yaml index a9b46ad8cd..fb47f93c88 100644 --- a/packages/boot/plugin-market/README.i18n.yaml +++ b/packages/boot/plugin-market/README.i18n.yaml @@ -3,20 +3,20 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/boot/plugin-market/README.md /: - en: 847c61ab6dac5fd2 - zh: 4107ba58fbda723e + en: 043de7aa04105a53 + zh: b3d966c6029037e1 /deepseek-ai-dsh-plugin-market: en: ee5478f5717295df zh: 75a03a6db791fc6b /deepseek-ai-dsh-plugin-market/summary: - en: 5fc3f5439344cb9a - zh: 73e549c0b21b3843 + en: 4def97be3ad32ece + zh: f855102a82dbb6ec /deepseek-ai-dsh-plugin-market/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-plugin-market/use-this-package: - en: 0553ab42ee4d83ff - zh: 4cc8163884357247 + en: fef9850d643e327f + zh: c6e8cab773cf93b9 /deepseek-ai-dsh-plugin-market/use-this-package/plugin-detail-documents: en: 0e15de273c9acaae zh: 4fd93c92b71d4398 @@ -39,8 +39,8 @@ en: 02cf6033092eb236 zh: cb928bfa6d39a7f7 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work: - en: 427d9c58ee09bea4 - zh: ec1c2bade3b56014 + en: 54122c96969ebbc2 + zh: 3fbe84bc6069cb46 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/boot/plugin-market/README.md b/packages/boot/plugin-market/README.md index e805dcb2e4..b93adc2bf9 100644 --- a/packages/boot/plugin-market/README.md +++ b/packages/boot/plugin-market/README.md @@ -1,5 +1,5 @@ --- -description: "Host reader of the plugin market indexes a user configures, with installation status, compatibility, and the read-only plugin_market tool." +description: "Host reader of the plugin indexes a user configures, with installation status, compatibility, and the read-only plugin_market tool." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`pluginMarket` reads the plugin market indexes the user configures, validates them with [`dsh-plugin-market-index`](../../util/plugin-market-index/README.md), and annotates every entry with the profile's installation status, DSH compatibility, and the exact arguments that install it. It also reads each market entry's long description, a Markdown document with images, for Discover entry pages. The Web [Discover tab](../../client/ui-plugin-market/README.md) and the `plugin_market` agent tool read through it; installation itself goes through [`pluginManager.installBundle`](../plugin-manager/README.md). The format and its rationale live in the [index format Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md); the Host and Web design lives in the [Discover Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md). +`pluginMarket` reads the plugin indexes the user configures, validates them with [`dsh-plugin-market-index`](../../util/plugin-market-index/README.md), and annotates every entry with the profile's installation status, DSH compatibility, and the exact arguments that install it. It also reads each index entry's long description, a Markdown document with images, for Discover entry pages. The Web [Discover tab](../../client/ui-plugin-market/README.md) and the `plugin_market` agent tool read through it; installation itself goes through [`pluginManager.installBundle`](../plugin-manager/README.md). The format and its rationale live in the [index format Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md); the Host and Web design lives in the [Discover Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md). ## Table of Contents @@ -24,7 +24,7 @@ English | [中文](README.zh.md) ## Use this package -The optional [custom plugin markets bundle](../../experimental/plugin-market-bundle/README.md) mounts the service as the `plugin-market` row; shipped profiles leave the bundle off. Like `plugin-manager`, the row is disabled without a profile, and it waits for `pluginManager`. No index source is configured by default, so the Host sends no market request until a user adds one. +The optional [custom plugin indexes bundle](../../experimental/plugin-market-bundle/README.md) mounts the service as the `plugin-market` row; shipped profiles leave the bundle off. Like `plugin-manager`, the row is disabled without a profile, and it waits for `pluginManager`. No index source is configured by default, so the Host sends no index request until a user adds one. | Config | Default | Meaning | |---|---|---| @@ -123,10 +123,10 @@ Append-only: results enter the conversation as ordinary tool results and do not - **No upgrade** — an installed bundle with a newer index version is reported as `update-available`; upgrading remains uninstall followed by install. -- **Unsigned indexes** — index content, including a Git entry's name and version, is the market's claim; the installed manifest decides what is installed. +- **Unsigned indexes** — index content, including a Git entry's name and version, is the index's claim; the installed manifest decides what is installed. - **Last write wins** — `setIndexes` replaces the whole list, so two windows editing sources at the same time can overwrite each other's change. - **Redirects refused** — an index served only behind a redirect must be configured at its final URL. -- **Whole-document reads** — the Host downloads and keeps each index up to `maxBytes` and builds its text index in memory; a synthetic index of 20,000 plugins took about 0.8 s of build time and 130 MiB of heap. Parsing runs in a Worker, but receiving the validated index still blocks the Host event loop while the clone is deserialized: about 35 ms for a synthetic 7.6 MiB index of 20,000 plugins and about 190 ms for 38 MiB and 100,000 plugins. Larger markets need a server-side query protocol. +- **Whole-document reads** — the Host downloads and keeps each index up to `maxBytes` and builds its text index in memory; a synthetic index of 20,000 plugins took about 0.8 s of build time and 130 MiB of heap. Parsing runs in a Worker, but receiving the validated index still blocks the Host event loop while the clone is deserialized: about 35 ms for a synthetic 7.6 MiB index of 20,000 plugins and about 190 ms for 38 MiB and 100,000 plugins. Larger indexes need a server-side query protocol. - **Default-branch READMEs** — a repository README is read at the default branch, which can differ from the listed version; npm records no commit the CDNs expose. - **Private npm registries** — the public CDNs do not serve packages of other registries, so only the entry's repository can provide such a version's document. - **Fetching an unadvertised commit** — a pinned Git entry is fetched by commit id, which the server must allow (`uploadpack.allowReachableSHA1InWant` or `allowAnySHA1InWant`); GitHub and GitLab allow it, and a server that refuses reports `denied`. diff --git a/packages/boot/plugin-market/README.zh.md b/packages/boot/plugin-market/README.zh.md index 31dae179db..5d82b46c77 100644 --- a/packages/boot/plugin-market/README.zh.md +++ b/packages/boot/plugin-market/README.zh.md @@ -1,5 +1,5 @@ --- -description: "读取用户配置的插件市场索引的 Host 服务,提供安装状态、兼容性以及只读的 plugin_market 工具。" +description: "读取用户配置的插件索引的 Host 服务,提供安装状态、兼容性以及只读的 plugin_market 工具。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`pluginMarket` 读取用户配置的插件市场索引,用 [`dsh-plugin-market-index`](../../util/plugin-market-index/README.zh.md) 校验,并为每个条目标注 profile 中的安装状态、DSH 兼容性以及安装它所需的精确参数。它还为发现区的条目页面读取每个市场条目的详细介绍,即一份带图片的 Markdown 文档。Web 的[发现标签页](../../client/ui-plugin-market/README.zh.md)和 `plugin_market` agent 工具通过它读取;安装本身经由 [`pluginManager.installBundle`](../plugin-manager/README.zh.md) 进行。格式及其理由见[索引格式 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md);Host 与 Web 的设计见[发现 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md)。 +`pluginMarket` 读取用户配置的插件索引,用 [`dsh-plugin-market-index`](../../util/plugin-market-index/README.zh.md) 校验,并为每个条目标注 profile 中的安装状态、DSH 兼容性以及安装它所需的精确参数。它还为发现区的条目页面读取每个索引条目的详细介绍,即一份带图片的 Markdown 文档。Web 的[发现标签页](../../client/ui-plugin-market/README.zh.md)和 `plugin_market` agent 工具通过它读取;安装本身经由 [`pluginManager.installBundle`](../plugin-manager/README.zh.md) 进行。格式及其理由见[索引格式 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md);Host 与 Web 的设计见[发现 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md)。 ## 目录 @@ -24,7 +24,7 @@ kind: "package-reference" ## 使用本包 -可选的[自定义插件市场组合包](../../experimental/plugin-market-bundle/README.zh.md)把该服务挂载为 `plugin-market` 行;随附的 profile 默认不启用该组合包。与 `plugin-manager` 一样,该行在没有 profile 时被禁用,并等待 `pluginManager`。默认不配置任何索引源,因此在用户添加索引源之前,Host 不发送任何市场请求。 +可选的[自定义插件索引组合包](../../experimental/plugin-market-bundle/README.zh.md)把该服务挂载为 `plugin-market` 行;随附的 profile 默认不启用该组合包。与 `plugin-manager` 一样,该行在没有 profile 时被禁用,并等待 `pluginManager`。默认不配置任何索引源,因此在用户添加索引源之前,Host 不发送任何索引请求。 | 配置 | 默认值 | 含义 | |---|---|---| @@ -124,10 +124,10 @@ Host 使用进程的 `fetch` 获取,因此配置的出站代理生效,并拒 - **不支持升级**——索引版本更新的已安装组合包报告为 `update-available`;升级仍需先卸载再安装。 -- **索引没有签名**——索引内容(包括 Git 条目的名称和版本)是市场的声明;实际安装什么由已安装的 manifest(元数据清单)决定。 +- **索引没有签名**——索引内容(包括 Git 条目的名称和版本)是索引的声明;实际安装什么由已安装的 manifest(元数据清单)决定。 - **后写入者覆盖**——`setIndexes` 会替换整个列表,因此两个窗口同时编辑索引源时,可能互相覆盖对方的修改。 - **拒绝重定向**——只能经重定向访问的索引必须以其最终 URL 配置。 -- **读取整份文档**——Host 下载并保留每个不超过 `maxBytes` 的索引,并在内存中构建其文本索引;一个包含 20,000 个插件的合成索引构建用时约 0.8 秒,占用约 130 MiB 堆内存。解析在 Worker 中运行,但接收校验后的索引时,反序列化 clone 仍会阻塞 Host 事件循环:包含 20,000 个插件的 7.6 MiB 合成索引约 35 毫秒,包含 100,000 个插件的 38 MiB 索引约 190 毫秒。更大的市场需要服务端查询协议。 +- **读取整份文档**——Host 下载并保留每个不超过 `maxBytes` 的索引,并在内存中构建其文本索引;一个包含 20,000 个插件的合成索引构建用时约 0.8 秒,占用约 130 MiB 堆内存。解析在 Worker 中运行,但接收校验后的索引时,反序列化 clone 仍会阻塞 Host 事件循环:包含 20,000 个插件的 7.6 MiB 合成索引约 35 毫秒,包含 100,000 个插件的 38 MiB 索引约 190 毫秒。更大的索引需要服务端查询协议。 - **默认分支的 README**——仓库 README 读取的是默认分支上的文件,可能与列出的版本不一致;CDN 公开的 npm 数据不记录提交。 - **私有 npm 注册表**——公共 CDN 不提供其他注册表的包,因此此类版本的文档只能来自条目的仓库。 - **获取未公布的提交**——固定版本的 Git 条目按提交 id 获取,这需要服务器允许(`uploadpack.allowReachableSHA1InWant` 或 `allowAnySHA1InWant`);GitHub 和 GitLab 允许这样做,拒绝的服务器报告 `denied`。 diff --git a/packages/boot/plugin-market/locale/de.json b/packages/boot/plugin-market/locale/de.json new file mode 100644 index 0000000000..68800a3ea4 --- /dev/null +++ b/packages/boot/plugin-market/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plugin-Indizes", + "description": "Konfigurierte Plugin-Indizes für die Plugin-Suche und das Tool plugin_market verwalten." + } +} diff --git a/packages/boot/plugin-market/locale/en.json b/packages/boot/plugin-market/locale/en.json index 5baedc3f0f..8e479c69e9 100644 --- a/packages/boot/plugin-market/locale/en.json +++ b/packages/boot/plugin-market/locale/en.json @@ -1,6 +1,6 @@ { "meta": { - "title": "Plugin market indexes", - "description": "Manage configured market indexes for plugin discovery and the plugin_market tool." + "title": "Plugin indexes", + "description": "Manage configured plugin indexes for plugin discovery and the plugin_market tool." } } diff --git a/packages/boot/plugin-market/locale/es.json b/packages/boot/plugin-market/locale/es.json new file mode 100644 index 0000000000..bb6b779704 --- /dev/null +++ b/packages/boot/plugin-market/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Índices de complementos", + "description": "Administre los índices de complementos configurados para descubrir complementos y para la herramienta plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/fr.json b/packages/boot/plugin-market/locale/fr.json new file mode 100644 index 0000000000..9753ef1d48 --- /dev/null +++ b/packages/boot/plugin-market/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Index de plugins", + "description": "Gérez les index de plugins configurés pour la découverte de plugins et l’outil plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/ja.json b/packages/boot/plugin-market/locale/ja.json index 150534a797..0da10adfa0 100644 --- a/packages/boot/plugin-market/locale/ja.json +++ b/packages/boot/plugin-market/locale/ja.json @@ -1,6 +1,6 @@ { "meta": { - "title": "プラグインマーケットのインデックス", - "description": "プラグイン検索ページと plugin_market ツールで使う、設定済みのマーケットのインデックスを管理します。" + "title": "プラグインインデックス", + "description": "プラグイン検索ページと plugin_market ツールで使う、設定済みのプラグインインデックスを管理します。" } } diff --git a/packages/boot/plugin-market/locale/pt.json b/packages/boot/plugin-market/locale/pt.json new file mode 100644 index 0000000000..848d35af30 --- /dev/null +++ b/packages/boot/plugin-market/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Índices de plug-ins", + "description": "Gerencie os índices de plug-ins configurados para a descoberta de plug-ins e a ferramenta plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/ru.json b/packages/boot/plugin-market/locale/ru.json new file mode 100644 index 0000000000..8d80e808eb --- /dev/null +++ b/packages/boot/plugin-market/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Индексы плагинов", + "description": "Управляйте настроенными индексами плагинов для поиска плагинов и инструмента plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/zh.json b/packages/boot/plugin-market/locale/zh.json index 5113c07601..67a8526b9d 100644 --- a/packages/boot/plugin-market/locale/zh.json +++ b/packages/boot/plugin-market/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { - "title": "插件市场索引", - "description": "为插件发现页面和 plugin_market 工具管理已配置的市场索引。" + "title": "插件索引", + "description": "为插件发现页面和 plugin_market 工具管理已配置的插件索引。" } } diff --git a/packages/boot/plugin-market/package.json b/packages/boot/plugin-market/package.json index 8dd7d7e0ec..863b4300b3 100644 --- a/packages/boot/plugin-market/package.json +++ b/packages/boot/plugin-market/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-market", "description": "Configured plugin market indexes for the Web Discover page and the plugin_market agent tool", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-market/src/tools.ts b/packages/boot/plugin-market/src/tools.ts index f1783a2969..a881f45b9d 100644 --- a/packages/boot/plugin-market/src/tools.ts +++ b/packages/boot/plugin-market/src/tools.ts @@ -60,7 +60,7 @@ function paged(items: readonly T[], page: Page): { readonly slice: T[]; reado /** * Register the read-only `plugin_market` tool while the `pluginMarket` service is mounted. The service comes from the - * optional custom plugin markets bundle, so a preset can keep this row without the tool appearing or the row waiting. + * optional custom plugin indexes bundle, so a preset can keep this row without the tool appearing or the row waiting. * @param ctx - Agent-scoped tool registration context. */ export function apply(ctx: Context): void { @@ -136,6 +136,6 @@ function registerTool(ctx: Context): void { default: return assertNever(args.action) } }, - presentCall: args => ({ card: 'generic', title: 'Browse plugin market', kind: 'read', rawInput: args }), + presentCall: args => ({ card: 'generic', title: 'Browse plugin indexes', kind: 'read', rawInput: args }), })) } diff --git a/packages/browser-use/browser-use/locale/de.json b/packages/browser-use/browser-use/locale/de.json new file mode 100644 index 0000000000..21cf74b920 --- /dev/null +++ b/packages/browser-use/browser-use/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registrierung des Browser-Anbieters", + "description": "Registrieren Sie Browser-Automatisierungsanbieter namentlich, mit einem Anbieter pro Name." + } +} diff --git a/packages/browser-use/browser-use/locale/es.json b/packages/browser-use/browser-use/locale/es.json new file mode 100644 index 0000000000..971b6dc650 --- /dev/null +++ b/packages/browser-use/browser-use/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registro de proveedores de navegador", + "description": "Registre proveedores de automatización del navegador por nombre, con un proveedor por nombre." + } +} diff --git a/packages/browser-use/browser-use/locale/fr.json b/packages/browser-use/browser-use/locale/fr.json new file mode 100644 index 0000000000..a8d05dae20 --- /dev/null +++ b/packages/browser-use/browser-use/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registre des fournisseurs de navigateur", + "description": "Enregistrez les fournisseurs d'automatisation de navigateur par nom, avec un fournisseur par nom." + } +} diff --git a/packages/browser-use/browser-use/locale/pt.json b/packages/browser-use/browser-use/locale/pt.json new file mode 100644 index 0000000000..a737a4da16 --- /dev/null +++ b/packages/browser-use/browser-use/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registro do provedor de navegador", + "description": "Registre provedores de automação de navegador por nome, com um provedor por nome." + } +} diff --git a/packages/browser-use/browser-use/locale/ru.json b/packages/browser-use/browser-use/locale/ru.json new file mode 100644 index 0000000000..070de4c045 --- /dev/null +++ b/packages/browser-use/browser-use/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Реестр поставщиков браузеров", + "description": "Зарегистрируйте поставщиков автоматизации браузера по имени, по одному поставщику на имя." + } +} diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index ec5aa9392c..a32c90047e 100644 --- a/packages/browser-use/browser-use/package.json +++ b/packages/browser-use/browser-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-browser-use", "description": "Exclusive named browser-use provider registration", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/locale/de.json b/packages/bundle/acp-app/locale/de.json new file mode 100644 index 0000000000..69374bc3d2 --- /dev/null +++ b/packages/bundle/acp-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP-Profil", + "description": "Bietet rein automatisierte ACP-Kommunikation und Prozesslebenszyklusmanagement über die dsh-Basis." + } +} diff --git a/packages/bundle/acp-app/locale/es.json b/packages/bundle/acp-app/locale/es.json new file mode 100644 index 0000000000..3ea80933ba --- /dev/null +++ b/packages/bundle/acp-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil ACP", + "description": "Proporcione comunicación ACP exclusivamente automatizada y gestión del ciclo de vida del proceso a través de la base dsh." + } +} diff --git a/packages/bundle/acp-app/locale/fr.json b/packages/bundle/acp-app/locale/fr.json new file mode 100644 index 0000000000..320c4228e2 --- /dev/null +++ b/packages/bundle/acp-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil ACP", + "description": "Fournissez une communication ACP uniquement automatisée et une gestion du cycle de vie des processus sur la base dsh." + } +} diff --git a/packages/bundle/acp-app/locale/pt.json b/packages/bundle/acp-app/locale/pt.json new file mode 100644 index 0000000000..b402f4d287 --- /dev/null +++ b/packages/bundle/acp-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP perfil", + "description": "Forneça comunicação ACP somente para automação e gerenciamento do ciclo de vida do processo em dshbase." + } +} diff --git a/packages/bundle/acp-app/locale/ru.json b/packages/bundle/acp-app/locale/ru.json new file mode 100644 index 0000000000..ef78a7910e --- /dev/null +++ b/packages/bundle/acp-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP профиль", + "description": "Обеспечьте связь только с ACP для автоматизации и управление жизненным циклом процессов через базу dsh." + } +} diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index 86dc1bc2c5..0d3500d904 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 0a439190ee..e6daed00a9 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/README.i18n.yaml b/packages/bundle/headless/README.i18n.yaml index 64186c0f36..d636b5d4b3 100644 --- a/packages/bundle/headless/README.i18n.yaml +++ b/packages/bundle/headless/README.i18n.yaml @@ -24,8 +24,8 @@ en: ca8217837ee9c698 zh: fa20daf27d4e09ef /deepseek-ai-dsh-headless/use-this-package/choosing-the-session-identity: - en: 0ef0c533ef9aebea - zh: fcb267b61bd23437 + en: 85120c2d6db2cac4 + zh: bac532df6c84e755 /deepseek-ai-dsh-headless/use-this-package/supervising-implementation-tasks: en: b10dee3026e8d999 zh: 63eda004e228a48a @@ -66,8 +66,8 @@ en: 0b0f74b412c7c6d8 zh: 6e8f918d0c4bf342 /deepseek-ai-dsh-headless/known-limitations-and-deferred-work: - en: ff000db625d2d0db - zh: 7d08a158aade75f9 + en: 94d8c7731c1435c3 + zh: d0ddc9dd4e030b3f /deepseek-ai-dsh-headless/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/bundle/headless/README.md b/packages/bundle/headless/README.md index 528a2e7e36..f9c53e7d2e 100644 --- a/packages/bundle/headless/README.md +++ b/packages/bundle/headless/README.md @@ -72,7 +72,7 @@ The restored store uses [saved credentials before environment fallbacks](../../c ### Choosing the session identity -Every invocation defaults to a fresh `session-` identity, which `--json` reports in its opening `session` event. Pass `--session-id ` to continue that conversation: the runner adopts the persisted Session with that id, and an id with no stored Session fails before the task runs rather than quietly opening an empty history. Adoption requires the composed `sessionPersistence` and `sessionQuery` services, so a profile that omits either fails loudly instead of returning an id whose history dies with the process. An Agent already live under the requested id in this process is refused: another owner may still drive it, so the runner cannot claim an exclusive run interval over it. The identity is opaque, so the exact string is used, whitespace included. The working directory is resolved through the mounted filesystem provider (`fs.resolve('.')` and `fs.processPath()`), or the process cwd when no filesystem service is mounted; new Sessions record that directory. Adoption compares the recorded cwd with the same provider-resolved directory and refuses a Session that is a subagent or forked session, that recorded no working directory, that runs under an agent preset this profile does not compose, or whose preset record is malformed — the check reads the preset the Session log currently records, so a Session that switched preset while blank is rejected too. A supervisor therefore cannot silently drive someone else's conversation under a different composition; any mismatch fails before the task runs. +Every invocation defaults to a fresh `session-` identity, which `--json` reports in its opening `session` event. Pass `--session-id ` to continue that conversation: the runner adopts the persisted Session with that id, and an id with no stored Session fails before the task runs rather than quietly opening an empty history. Adoption requires the composed `sessionPersistence` and `sessionQuery` services, so a profile that omits either fails loudly instead of returning an id whose history dies with the process. An Agent already live under the requested id in this process is refused: another owner may still drive it, so the runner cannot claim an exclusive run interval over it. The identity is opaque, so the exact string is used, whitespace included. The working directory is resolved through the mounted filesystem provider (`fs.resolve('.')` and `fs.processPath()`), or the process cwd when no filesystem service is mounted; new Sessions record that directory. Adoption compares the recorded cwd with the same provider-resolved directory and refuses a Session that is a subagent session, that recorded no working directory, that runs under an agent preset this profile does not compose, or whose preset record is malformed — the check reads the preset the Session log currently records, so a Session that switched preset while blank is rejected too. A supervisor therefore cannot silently drive someone else's conversation under a different composition; any mismatch fails before the task runs. On adoption, the runner records the current deployment route before submitting the task. An omitted effort preserves an explicit saved effort only for the same route. A step already prepared in this process keeps its captured route; subsequent steps read the shared Session selection. @@ -186,7 +186,7 @@ These limits tell you when headless does not fit and what it needs from the `dsh - **No pre-token heartbeat** — in default mode stderr stays silent until the provider emits a non-empty reasoning delta; a delayed first token exposes no earlier progress signal. - **Reasoning enters stderr logs** — in default mode, redirection and supervisors may retain substantially more and potentially sensitive model output; route stderr to a controlled sink when needed. - **Default stdout carries only the final answer** — a run without an assistant message prints an empty stdout line and exits 1; intermediate tool output is not printed unless you opt into `--json`. -- **Adoption is cwd-, ownership-, and preset-scoped** — `--session-id` refuses a Session recorded in another working directory, one that recorded no working directory, one that is a subagent or forked session, or one that runs under an agent preset this profile does not compose or whose preset record is malformed, and requires the composed Session query and persistence services; an identity already live in the process is refused too, because the runner cannot own an exclusive run interval over it. +- **Adoption is cwd-, ownership-, and preset-scoped** — `--session-id` refuses a Session recorded in another working directory, one that recorded no working directory, one that is a subagent session, or one that runs under an agent preset this profile does not compose or whose preset record is malformed, and requires the composed Session query and persistence services; an identity already live in the process is refused too, because the runner cannot own an exclusive run interval over it. - **The event stream is a projection, not the log** — `--json` caps every string except the terminal `final` at 8 KiB and omits events the projection does not model, so it is not a lossless copy of the Session log. diff --git a/packages/bundle/headless/README.zh.md b/packages/bundle/headless/README.zh.md index 97845ae19c..8600a6f2f3 100644 --- a/packages/bundle/headless/README.zh.md +++ b/packages/bundle/headless/README.zh.md @@ -72,7 +72,7 @@ agent 会完成该任务,把提供方的每个非空推理(reasoning)增 ### 选择 Session 标识 -每次调用默认使用全新的 `session-` 标识,`--json` 会在开头的 `session` 事件里报告它。传入 `--session-id ` 延续这段对话:runner 沿用该 id 对应的持久化 Session,而该 id 没有持久化 Session 时会在任务运行前失败,而不是悄悄开出一段空历史。沿用要求已组合 `sessionPersistence` 与 `sessionQuery` 服务,因此缺少任一服务的 profile 会显式失败,而不会返回一个历史随进程消失的 id。本进程中已存在持有该 id 的存活 Agent 时会被拒绝:它的原 owner 可能仍在驱动它,runner 无法取得独占的运行区间。标识是不透明的,因此会原样使用调用方给出的字符串,包括空白字符。工作目录通过已挂载的文件系统提供方解析(`fs.resolve('.')` 与 `fs.processPath()`);未挂载文件系统服务时使用进程 cwd,新 Session 会记录该目录。沿用会将已记录 cwd 与同一提供方解析出的目录比较,并拒绝子 agent 或 fork 会话、未记录工作目录的会话、运行在本 profile 不组合的 agent preset 下的会话,以及 preset 记录畸形的会话——该检查读取 Session 日志当前记录的 preset,因此在空白期切换过 preset 的会话同样会被拒绝。因此监督进程无法在另一套组合下悄悄驱动他人的会话;任一不匹配都会在任务运行前失败。 +每次调用默认使用全新的 `session-` 标识,`--json` 会在开头的 `session` 事件里报告它。传入 `--session-id ` 延续这段对话:runner 沿用该 id 对应的持久化 Session,而该 id 没有持久化 Session 时会在任务运行前失败,而不是悄悄开出一段空历史。沿用要求已组合 `sessionPersistence` 与 `sessionQuery` 服务,因此缺少任一服务的 profile 会显式失败,而不会返回一个历史随进程消失的 id。本进程中已存在持有该 id 的存活 Agent 时会被拒绝:它的原 owner 可能仍在驱动它,runner 无法取得独占的运行区间。标识是不透明的,因此会原样使用调用方给出的字符串,包括空白字符。工作目录通过已挂载的文件系统提供方解析(`fs.resolve('.')` 与 `fs.processPath()`);未挂载文件系统服务时使用进程 cwd,新 Session 会记录该目录。沿用会将已记录 cwd 与同一提供方解析出的目录比较,并拒绝子 agent 会话、未记录工作目录的会话、运行在本 profile 不组合的 agent preset 下的会话,以及 preset 记录畸形的会话——该检查读取 Session 日志当前记录的 preset,因此在空白期切换过 preset 的会话同样会被拒绝。因此监督进程无法在另一套组合下悄悄驱动他人的会话;任一不匹配都会在任务运行前失败。 接管会话时,运行器会在提交任务前记录当前部署路由。未声明推理强度时,仅在路由相同的情况下保留已保存的显式强度。本进程中已准备的步骤保留捕获的路由,后续步骤读取共享的 Session 选择。 @@ -186,7 +186,7 @@ runner 不向请求前缀添加任何内容;它只是驱动组合出的配置 - **首个 token 前没有心跳**——默认模式下,提供方发出第一个非空推理增量前,stderr 保持静默;延迟首个 token 的提供方不会更早给出进度信号。 - **推理进入 stderr 日志**——默认模式下,重定向与监督进程可能保留显著更多且可能敏感的模型输出;需要时应把 stderr 路由到受控位置。 - **默认 stdout 只承载最终答案**——没有 assistant 消息的运行向 stdout 打印空行并以 1 退出;中间工具输出不会打印,除非显式启用 `--json`。 -- **沿用受 cwd、归属与 preset 限制**——`--session-id` 会拒绝记录在其他工作目录、未记录工作目录、属于子 agent 或 fork 会话,或运行在本 profile 不组合的 agent preset 下的 Session、preset 记录畸形的 Session,并要求已组合的 Session 查询与持久化服务;本进程中已存活的身份同样会被拒绝,因为 runner 无法对它取得独占的运行区间。 +- **沿用受 cwd、归属与 preset 限制**——`--session-id` 会拒绝记录在其他工作目录、未记录工作目录、属于子 agent 会话,或运行在本 profile 不组合的 agent preset 下的 Session、preset 记录畸形的 Session,并要求已组合的 Session 查询与持久化服务;本进程中已存活的身份同样会被拒绝,因为 runner 无法对它取得独占的运行区间。 - **事件流是投影而非日志**——`--json` 除终止 `final` 外把每个字符串与对象键限制在 8 KiB,并省略投影未建模的事件,因此它不是 Session 日志的无损副本。 diff --git a/packages/bundle/headless/locale/de.json b/packages/bundle/headless/locale/de.json new file mode 100644 index 0000000000..df096072aa --- /dev/null +++ b/packages/bundle/headless/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Kopfloses Profil", + "description": "Führen Sie eine einzelne Aufgabe direkt mit dem Kernagenten/Sitzungsläufer über die dsh-Basis aus, ohne Host, HTTP oder Browserebene." + } +} diff --git a/packages/bundle/headless/locale/es.json b/packages/bundle/headless/locale/es.json new file mode 100644 index 0000000000..3916ac6658 --- /dev/null +++ b/packages/bundle/headless/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "perfil sin cabeza", + "description": "Ejecute una sola tarea directamente con el agente principal/ejecutor de sesión sobre la base dsh, sin un host, HTTP o una capa de navegador." + } +} diff --git a/packages/bundle/headless/locale/fr.json b/packages/bundle/headless/locale/fr.json new file mode 100644 index 0000000000..101db58c1b --- /dev/null +++ b/packages/bundle/headless/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil sans tête", + "description": "Exécutez une seule tâche directement avec le principal agent/exécuteur de session sur la base dsh, sans hôte, HTTP ou couche de navigateur." + } +} diff --git a/packages/bundle/headless/locale/pt.json b/packages/bundle/headless/locale/pt.json new file mode 100644 index 0000000000..ff379c6db0 --- /dev/null +++ b/packages/bundle/headless/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil sem cabeça", + "description": "Execute uma única tarefa diretamente com o executor principal de agente/sessão em dsh-base, sem um host, HTTP ou camada de navegador." + } +} diff --git a/packages/bundle/headless/locale/ru.json b/packages/bundle/headless/locale/ru.json new file mode 100644 index 0000000000..e84fbba427 --- /dev/null +++ b/packages/bundle/headless/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Безголовый профиль", + "description": "Запустите одну задачу напрямую с помощью основного средства запуска агента/сеанса через базу dsh, без уровня хоста, HTTP или браузера." + } +} diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index cf07060097..d54cb3bb06 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts index 6ad84ed4f6..d02729c097 100644 --- a/packages/bundle/headless/src/index.ts +++ b/packages/bundle/headless/src/index.ts @@ -180,7 +180,6 @@ function streamReasoning( interface AdoptableHeader { cwd?: string | undefined origin?: 'subagent' | undefined - parentSession?: SessionId | undefined agentPreset?: string | undefined } @@ -231,8 +230,8 @@ function assertAdoptable(header: AdoptableHeader, events: Iterable `session "${sessionId}" runs under agent preset "${preset}", which the one-shot runner does not compose`, ) } - if (header.origin === 'subagent' || header.parentSession !== undefined) { - throw new Error(`session "${sessionId}" is a subagent or forked session and cannot be driven directly`) + if (header.origin === 'subagent') { + throw new Error(`session "${sessionId}" is a subagent session and cannot be driven directly`) } if (header.cwd === undefined) { throw new Error(`session "${sessionId}" recorded no working directory, so it cannot be adopted`) diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index 54067fe50c..0d00029409 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -14,7 +14,7 @@ import type { } from '@deepseek-ai/dsh-agent' import AgentDefaultModelConfig from '@deepseek-ai/dsh-agent-default-model' import { LlmAttemptId, ReasoningEffortId, ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage, type MessageId, type StreamChunk } from '@deepseek-ai/dsh-llm' -import SessionStore from '@deepseek-ai/dsh-session' +import SessionStore, { SessionLogOffset } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import ApprovalService, { setApprovalPolicy, type ApprovalPolicy } from '@deepseek-ai/dsh-user-approval' @@ -987,7 +987,7 @@ describe('headless runner', () => { }) const result = await test.run() expect(result.code).toBe(1) - expect(result.err).toContain('is a subagent or forked session') + expect(result.err).toContain('is a subagent session') await test.ctx.fiber.dispose() }) @@ -1055,7 +1055,7 @@ describe('headless runner', () => { }) const result = await test.run() expect(result.code).toBe(1) - expect(result.err).toContain('is a subagent or forked session') + expect(result.err).toContain('is a subagent session') await test.ctx.fiber.dispose() }) @@ -1161,19 +1161,23 @@ describe('headless runner', () => { await test.ctx.fiber.dispose() }) - it('rejects a persisted Session linked to a parent', async () => { - const test = await bench({ afterPrompt: () => {} }, { + it.each([false, true])('resumes an ordinary fork with historical parent metadata: %s', async (legacy) => { + const header = { cwd: process.cwd(), ...(legacy ? { parentSession: brandString('parent-1') } : {}) } + const test = await bench({ + afterPrompt(session, message) { appendTurn(session, 1, message, 'fork resumed', true) }, + }, { sessionId: 'session-exact', - observe: () => Promise.resolve({ - header: { cwd: process.cwd(), origin: 'user', parentSession: 'parent-1' }, - events: [], - [Symbol.dispose]() {}, - }), + observe: () => Promise.resolve({ header, events: [], [Symbol.dispose]() {} }), }) - const result = await test.run() - expect(result.code).toBe(1) - expect(result.err).toContain('is a subagent or forked session') - await test.ctx.fiber.dispose() + try { + const session = test.ctx.sessions.create(brandString('session-exact'), { + seed: [], inheritedEventCount: SessionLogOffset(0), meta: { ...header, isSeeded: true }, + }) + expect(await test.run()).toMatchObject({ code: 0, out: 'fork resumed\n', err: '' }) + expect(session.header.parentSession).toBe(header.parentSession) + } finally { + await test.ctx.fiber.dispose() + } }) it('propagates a Session query failure that is not a missing log', async () => { diff --git a/packages/bundle/sdk-app/locale/de.json b/packages/bundle/sdk-app/locale/de.json new file mode 100644 index 0000000000..efe87657eb --- /dev/null +++ b/packages/bundle/sdk-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK-Profil", + "description": "Stellen Sie JSON-RPC über die Standardeingabe und -ausgabe bereit und verwalten Sie den Prozesslebenszyklus über die dsh-Basis." + } +} diff --git a/packages/bundle/sdk-app/locale/es.json b/packages/bundle/sdk-app/locale/es.json new file mode 100644 index 0000000000..b1b409e2b9 --- /dev/null +++ b/packages/bundle/sdk-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil SDK", + "description": "Proporcione JSON-RPC a través de entrada y salida estándar y administre el ciclo de vida del proceso a través de la base dsh." + } +} diff --git a/packages/bundle/sdk-app/locale/fr.json b/packages/bundle/sdk-app/locale/fr.json new file mode 100644 index 0000000000..9de8913e48 --- /dev/null +++ b/packages/bundle/sdk-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil SDK", + "description": "Fournissez JSON-RPC sur une entrée et une sortie standard et gérez le cycle de vie du processus sur la base dsh." + } +} diff --git a/packages/bundle/sdk-app/locale/pt.json b/packages/bundle/sdk-app/locale/pt.json new file mode 100644 index 0000000000..187ca1d1e0 --- /dev/null +++ b/packages/bundle/sdk-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK perfil", + "description": "Forneça JSON-RPC sobre entrada e saída padrão e gerencie o ciclo de vida do processo em dsh-base." + } +} diff --git a/packages/bundle/sdk-app/locale/ru.json b/packages/bundle/sdk-app/locale/ru.json new file mode 100644 index 0000000000..0502de6690 --- /dev/null +++ b/packages/bundle/sdk-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK профиль", + "description": "Обеспечьте JSON-RPC поверх стандартного ввода и вывода и управляйте жизненным циклом процесса через dsh-base." + } +} diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index ee90ed6fb4..9734be57f2 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index 512f282bc7..c7b1195003 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/locale/de.json b/packages/bundle/web-app/locale/de.json new file mode 100644 index 0000000000..e7f067a8fd --- /dev/null +++ b/packages/bundle/web-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Unterstützung bei der Ausführung von Webanwendungen", + "description": "Stellen Sie Front-End-Seitenressourcen bereit und verbinden Sie die für die Webanwendung erforderliche laufende Konfiguration." + } +} diff --git a/packages/bundle/web-app/locale/es.json b/packages/bundle/web-app/locale/es.json new file mode 100644 index 0000000000..70e2cd3b9b --- /dev/null +++ b/packages/bundle/web-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Soporte de ejecución de aplicaciones web", + "description": "Proporcione recursos de la página de inicio y conecte la configuración en ejecución requerida por la aplicación web." + } +} diff --git a/packages/bundle/web-app/locale/fr.json b/packages/bundle/web-app/locale/fr.json new file mode 100644 index 0000000000..d33d200823 --- /dev/null +++ b/packages/bundle/web-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Prise en charge de l'exécution d'applications Web", + "description": "Fournissez les ressources de la page frontale et connectez la configuration en cours requise par l'application Web." + } +} diff --git a/packages/bundle/web-app/locale/pt.json b/packages/bundle/web-app/locale/pt.json new file mode 100644 index 0000000000..acf7b54d8e --- /dev/null +++ b/packages/bundle/web-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Suporte à execução de aplicações web", + "description": "Fornece recursos de página front-end e conecta a configuração de execução exigida pelo aplicativo da web." + } +} diff --git a/packages/bundle/web-app/locale/ru.json b/packages/bundle/web-app/locale/ru.json new file mode 100644 index 0000000000..bfddc6aa87 --- /dev/null +++ b/packages/bundle/web-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Поддержка выполнения веб-приложений", + "description": "Предоставьте ресурсы интерфейсной страницы и подключите работающую конфигурацию, необходимую веб-приложению." + } +} diff --git a/packages/bundle/web-app/locale/startup/de.json b/packages/bundle/web-app/locale/startup/de.json new file mode 100644 index 0000000000..3efc2eb5ef --- /dev/null +++ b/packages/bundle/web-app/locale/startup/de.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Web-Startkonfiguration" + } +} diff --git a/packages/bundle/web-app/locale/startup/es.json b/packages/bundle/web-app/locale/startup/es.json new file mode 100644 index 0000000000..b9ad99c565 --- /dev/null +++ b/packages/bundle/web-app/locale/startup/es.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuración de inicio web" + } +} diff --git a/packages/bundle/web-app/locale/startup/fr.json b/packages/bundle/web-app/locale/startup/fr.json new file mode 100644 index 0000000000..da69faa205 --- /dev/null +++ b/packages/bundle/web-app/locale/startup/fr.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuration du démarrage Web" + } +} diff --git a/packages/bundle/web-app/locale/startup/pt.json b/packages/bundle/web-app/locale/startup/pt.json new file mode 100644 index 0000000000..fe1f8baa0c --- /dev/null +++ b/packages/bundle/web-app/locale/startup/pt.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuração de inicialização Web" + } +} diff --git a/packages/bundle/web-app/locale/startup/ru.json b/packages/bundle/web-app/locale/startup/ru.json new file mode 100644 index 0000000000..c2c9f6862c --- /dev/null +++ b/packages/bundle/web-app/locale/startup/ru.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Настройки запуска веб-приложения" + } +} diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 8281a74594..aee3da55b1 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, bash runtime variables, URL line)", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/connection/locale/de.json b/packages/client/connection/locale/de.json new file mode 100644 index 0000000000..85da6331b4 --- /dev/null +++ b/packages/client/connection/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Verbindung", + "description": "Richten Sie authentifizierte Client-Kommunikation ein und verwalten Sie den Verbindungslebenszyklus." + } +} diff --git a/packages/client/connection/locale/es.json b/packages/client/connection/locale/es.json new file mode 100644 index 0000000000..fa2080ac53 --- /dev/null +++ b/packages/client/connection/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "conexión", + "description": "Establezca comunicaciones autenticadas con el cliente y administre el ciclo de vida de la conexión." + } +} diff --git a/packages/client/connection/locale/fr.json b/packages/client/connection/locale/fr.json new file mode 100644 index 0000000000..d1665120c9 --- /dev/null +++ b/packages/client/connection/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "connexion", + "description": "Établissez des communications client authentifiées et gérez le cycle de vie des connexions." + } +} diff --git a/packages/client/connection/locale/pt.json b/packages/client/connection/locale/pt.json new file mode 100644 index 0000000000..760379ae67 --- /dev/null +++ b/packages/client/connection/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "conexão", + "description": "Estabeleça comunicações autenticadas de clientes e gerencie o ciclo de vida da conexão." + } +} diff --git a/packages/client/connection/locale/ru.json b/packages/client/connection/locale/ru.json new file mode 100644 index 0000000000..adfddadc3d --- /dev/null +++ b/packages/client/connection/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "соединение", + "description": "Установите аутентифицированную клиентскую связь и управляйте жизненным циклом соединения." + } +} diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 1a78b312f4..3392544e2d 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport and generation lifecycle", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/connection/tests/node-half.host.spec.ts b/packages/client/connection/tests/node-half.host.spec.ts index c3497a95c2..8be225209f 100644 --- a/packages/client/connection/tests/node-half.host.spec.ts +++ b/packages/client/connection/tests/node-half.host.spec.ts @@ -35,7 +35,7 @@ function fakeHttpServer( return { host, protocol, - register(route) { + register(route: WebRoute) { if (routes.some(candidate => candidate.kind === route.kind && candidate.path === route.path)) { throw new Error(`duplicate route ${route.path}`) } diff --git a/packages/client/file-upload/locale/de.json b/packages/client/file-upload/locale/de.json new file mode 100644 index 0000000000..80c9e5ab6f --- /dev/null +++ b/packages/client/file-upload/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Datei-Upload", + "description": "Laden Sie Dateien vom Browser in die aktuelle Sitzung hoch, um sie von Nachrichten und Tools zu verwenden." + } +} diff --git a/packages/client/file-upload/locale/es.json b/packages/client/file-upload/locale/es.json new file mode 100644 index 0000000000..45732e15c5 --- /dev/null +++ b/packages/client/file-upload/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Carga de archivos", + "description": "Cargue archivos desde el navegador a la sesión actual para utilizarlos en mensajes y herramientas." + } +} diff --git a/packages/client/file-upload/locale/fr.json b/packages/client/file-upload/locale/fr.json new file mode 100644 index 0000000000..a518389efe --- /dev/null +++ b/packages/client/file-upload/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Téléchargement de fichiers", + "description": "Téléchargez des fichiers du navigateur vers la session en cours pour les utiliser par les messages et les outils." + } +} diff --git a/packages/client/file-upload/locale/pt.json b/packages/client/file-upload/locale/pt.json new file mode 100644 index 0000000000..e50ef62390 --- /dev/null +++ b/packages/client/file-upload/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Carregamento de arquivo", + "description": "Carregue arquivos do navegador para a sessão atual para uso por mensagens e ferramentas." + } +} diff --git a/packages/client/file-upload/locale/ru.json b/packages/client/file-upload/locale/ru.json new file mode 100644 index 0000000000..b8a733cc10 --- /dev/null +++ b/packages/client/file-upload/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Загрузка файла", + "description": "Загружайте файлы из браузера в текущий сеанс для использования сообщениями и инструментами." + } +} diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 1dcf9a9cef..ff5eb04d4e 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/locale/de.json b/packages/client/hmr/locale/de.json new file mode 100644 index 0000000000..6a8ba35a12 --- /dev/null +++ b/packages/client/hmr/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hmr", + "description": "Synchronisieren Sie die Plug-Ins im Browser und aktualisieren Sie die entsprechende Schnittstelle, nachdem der Code neu erstellt wurde." + } +} diff --git a/packages/client/hmr/locale/es.json b/packages/client/hmr/locale/es.json new file mode 100644 index 0000000000..f2ca5cf4fa --- /dev/null +++ b/packages/client/hmr/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hmr", + "description": "Sincronice los complementos en el navegador y actualice la interfaz relevante después de reconstruir el código." + } +} diff --git a/packages/client/hmr/locale/fr.json b/packages/client/hmr/locale/fr.json new file mode 100644 index 0000000000..295a751727 --- /dev/null +++ b/packages/client/hmr/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Synchronisez les plug-ins dans le navigateur et mettez à jour l'interface correspondante une fois le code reconstruit." + } +} diff --git a/packages/client/hmr/locale/pt.json b/packages/client/hmr/locale/pt.json new file mode 100644 index 0000000000..33c1516cf2 --- /dev/null +++ b/packages/client/hmr/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Sincronize os plug-ins no navegador e atualize a interface relevante após a reconstrução do código." + } +} diff --git a/packages/client/hmr/locale/ru.json b/packages/client/hmr/locale/ru.json new file mode 100644 index 0000000000..cb8f218fe1 --- /dev/null +++ b/packages/client/hmr/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "хмр", + "description": "Синхронизируйте плагины в браузере и обновите соответствующий интерфейс после перестройки кода." + } +} diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 15d1b7e7c6..2e64b3e705 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Web client graph synchronization and rebuilt-bundle reload transport", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/tests/node-half.client.spec.ts b/packages/client/hmr/tests/node-half.client.spec.ts index 7965ca79b3..fb212b71c8 100644 --- a/packages/client/hmr/tests/node-half.client.spec.ts +++ b/packages/client/hmr/tests/node-half.client.spec.ts @@ -79,7 +79,7 @@ function fakeClientModuleHost(rows: Map, options: FakeHostOption // carries private state a literal cannot (and need not) reproduce. function fakeHttpServer(routes: WebRoute[]): WebServer { const fake: Pick = { - register(route) { + register(route: WebRoute) { routes.push(route) return () => { routes.splice(routes.indexOf(route), 1) } }, diff --git a/packages/client/locale/README.i18n.yaml b/packages/client/locale/README.i18n.yaml index f2259ee13c..a90c3133e5 100644 --- a/packages/client/locale/README.i18n.yaml +++ b/packages/client/locale/README.i18n.yaml @@ -9,8 +9,8 @@ en: 9365090d78467dea zh: 74b6fbb9769d795f /deepseek-ai-dsh-client-locale/summary: - en: a2c282377f0a043e - zh: 2274f1ea41774549 + en: cbbe8f61f5e19f39 + zh: 2cec7ec41f1a7e84 /deepseek-ai-dsh-client-locale/table-of-contents: en: d152484eb41ac6b4 zh: bda879a892f240f0 @@ -21,8 +21,8 @@ en: 2b82458e185ba321 zh: e55a7e709a3f0158 /deepseek-ai-dsh-client-locale/use-this-package/registering-a-dictionary: - en: 69b07cde5b2b4deb - zh: b8204c127b10a711 + en: f19407c3ce8a94cc + zh: 3e5300ce7fa02987 /deepseek-ai-dsh-client-locale/use-this-package/resolving-package-text: en: 3b63e84fe0fdf8f4 zh: e97b5d1b92dff7ec @@ -45,7 +45,7 @@ en: 68373e745761494c zh: 43a5b8c19d03268a /deepseek-ai-dsh-client-locale/understand-the-implementation/source-map: - en: fc93a8f6804aa5bd + en: 6f329e4872398634 zh: 9d8364dc4c03d055 /deepseek-ai-dsh-client-locale/further-exploration: en: f73d411c9e90dead diff --git a/packages/client/locale/README.md b/packages/client/locale/README.md index e5afbfbde0..0ed329b47a 100644 --- a/packages/client/locale/README.md +++ b/packages/client/locale/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh-client-locale` to switch the web GUI between the shipped English, Chinese, and Japanese locales or languages added by client plugins. User selections take effect immediately; every authenticated browser persists them as `locale.preference` in `$DSH_HOME/cordis.patch.yml` through the Host settings service. New browsers use the first supported language requested by the browser until the Host read delivers a stored preference. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. +Use `dsh-client-locale` to switch the web GUI between the shipped English, Chinese, Japanese, Spanish, French, German, Russian, and Brazilian Portuguese (`pt`, including `pt-PT` browser requests) locales or languages added by client plugins. User selections take effect immediately; every authenticated browser persists them as `locale.preference` in `$DSH_HOME/cordis.patch.yml` through the Host settings service. New browsers use the first supported language requested by the browser until the Host read delivers a stored preference. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. ## Table of Contents @@ -37,7 +37,7 @@ Native shells may provide `__DSH_LOCALE__` with an asynchronous `read()` and an ### Registering a dictionary -Call `ctx.locale.register(ns, { zh, en, ja })` with a namespace merged into `LocaleNamespaceMap`; the compiler checks every key against the namespace's typed key union and requires both baseline locales (`zh` and `en`). Additional locale ids are optional, and their dictionaries are checked against the same namespace keys. Consumers translate through `ctx.locale.bind(ns)` or the framework-injected `t` seat. A dictionary registered after the UI is already mounted is picked up without a remount. +Call `ctx.locale.register(ns, { zh, en, ja })` with a namespace merged into `LocaleNamespaceMap`; the compiler checks every key against the namespace's typed key union and requires both baseline locales (`zh` and `en`). Additional locale ids are optional, and their dictionaries are checked against the same namespace keys. Consumers translate through `ctx.locale.bind(ns)` or the framework-injected `t` seat. A dictionary registered after the UI is already mounted is picked up without a remount. Preserve named placeholders, slash commands, and API field names verbatim in translations; `pnpm run check:i18n` checks first-party dictionaries and rejects zero-width spaces (U+200B) and byte-order marks (U+FEFF) in locale resources. ### Resolving package text @@ -52,13 +52,13 @@ export const inject = ['locale'] export function apply(ctx) { ctx.effect( - () => ctx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => ctx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'my-locale: language', ) ctx.effect( - () => ctx.locale.register('common', 'fr', { - cancel: 'Annuler', - close: 'Fermer', + () => ctx.locale.register('common', 'it', { + cancel: 'Annulla', + close: 'Chiudi', }), 'my-locale: common dictionary', ) @@ -102,7 +102,7 @@ The typed object form requires complete dictionaries for both baseline locales. | [`src/client/index.ts`](src/client/index.ts) | `LocaleRuntime`, dictionary registry, Language row registration, `locale/change` event | | [`src/index.ts`](src/index.ts) | Node half: registers the `locale` settings namespace | | [`src/locale-settings.ts`](src/locale-settings.ts) | The durable schema for `locale.preference` | -| [`src/locales/`](src/locales/) | The shipped `zh`/`en`/`ja` dictionaries | +| [`src/locales/`](src/locales/) | The shipped language dictionaries |
diff --git a/packages/client/locale/README.zh.md b/packages/client/locale/README.zh.md index 84965430c9..4c2d8037ed 100644 --- a/packages/client/locale/README.zh.md +++ b/packages/client/locale/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文、中文和日文 locale,或 client 插件添加的语言。用户选择会立即生效;每个已认证浏览器都通过 Host settings 服务把选择以 `locale.preference` 持久化到 `$DSH_HOME/cordis.patch.yml`。全新浏览器会使用浏览器请求的第一个受支持语言,直到 Host 读取送达已存储偏好。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 +使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文、中文、日语、西班牙语、法语、德语、俄语和巴西葡萄牙语(`pt`,也匹配 `pt-PT` 浏览器请求)locale,或 client 插件添加的语言。用户选择会立即生效;每个已认证浏览器都通过 Host settings 服务把选择以 `locale.preference` 持久化到 `$DSH_HOME/cordis.patch.yml`。全新浏览器会使用浏览器请求的第一个受支持语言,直到 Host 读取送达已存储偏好。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 ## 目录 @@ -37,7 +37,7 @@ kind: "package-reference" ### 注册字典 -用已合并进 `LocaleNamespaceMap` 的命名空间调用 `ctx.locale.register(ns, { zh, en, ja })`;编译器会对照该命名空间的类型化键并集检查每个键,并要求两个基础 locale(`zh` 和 `en`)齐全。其他 locale id 为可选项,其字典同样按该命名空间的键检查。消费方通过 `ctx.locale.bind(ns)` 或框架注入的 `t` 席位翻译。UI 已挂载后再注册的字典无需重新挂载即可生效。 +用已合并进 `LocaleNamespaceMap` 的命名空间调用 `ctx.locale.register(ns, { zh, en, ja })`;编译器会对照该命名空间的类型化键并集检查每个键,并要求两个基础 locale(`zh` 和 `en`)齐全。其他 locale id 为可选项,其字典同样按该命名空间的键检查。消费方通过 `ctx.locale.bind(ns)` 或框架注入的 `t` 席位翻译。UI 已挂载后再注册的字典无需重新挂载即可生效。翻译必须原样保留命名占位符、斜杠命令和 API 字段名;`pnpm run check:i18n` 检查仓库内的字典,并拒绝语言资源中的零宽空格(U+200B)和字节顺序标记(U+FEFF)。 ### 解析包文本 @@ -52,13 +52,13 @@ export const inject = ['locale'] export function apply(ctx) { ctx.effect( - () => ctx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => ctx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'my-locale: language', ) ctx.effect( - () => ctx.locale.register('common', 'fr', { - cancel: 'Annuler', - close: 'Fermer', + () => ctx.locale.register('common', 'it', { + cancel: 'Annulla', + close: 'Chiudi', }), 'my-locale: common dictionary', ) diff --git a/packages/client/locale/locale/de.json b/packages/client/locale/locale/de.json new file mode 100644 index 0000000000..f7eb734545 --- /dev/null +++ b/packages/client/locale/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Gebietsschema", + "description": "Verwalten Sie die Spracheinstellungen der Benutzeroberfläche und die Übersetzungskopie jedes Plug-Ins." + } +} diff --git a/packages/client/locale/locale/es.json b/packages/client/locale/locale/es.json new file mode 100644 index 0000000000..ea4d2f0869 --- /dev/null +++ b/packages/client/locale/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "local", + "description": "Administre las preferencias de idioma de la interfaz y la copia de traducción de cada complemento." + } +} diff --git a/packages/client/locale/locale/fr.json b/packages/client/locale/locale/fr.json new file mode 100644 index 0000000000..c3ff597891 --- /dev/null +++ b/packages/client/locale/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "paramètres régionaux", + "description": "Gérez les préférences linguistiques de l’interface et la copie de traduction de chaque plug-in." + } +} diff --git a/packages/client/locale/locale/pt.json b/packages/client/locale/locale/pt.json new file mode 100644 index 0000000000..7b4b751941 --- /dev/null +++ b/packages/client/locale/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "localidade", + "description": "Gerencie as preferências de idioma da interface e a cópia de tradução de cada plug-in." + } +} diff --git a/packages/client/locale/locale/ru.json b/packages/client/locale/locale/ru.json new file mode 100644 index 0000000000..4b11903438 --- /dev/null +++ b/packages/client/locale/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "локаль", + "description": "Управляйте языковыми настройками интерфейса и копией перевода каждого плагина." + } +} diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index dd011167ef..023f018f5e 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index c6484b8908..3ba1bae29b 100644 --- a/packages/client/locale/src/client/index.ts +++ b/packages/client/locale/src/client/index.ts @@ -20,9 +20,10 @@ import { LOCALE_ID_PATTERN, LOCALE_IDS, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, type BuiltInLocaleId, type LocaleId, type LocaleSettings, } from '../locale-settings.ts' -import { en, ja, zh, type CommonKey } from '../locales/index.ts' +import { en, ja, es, fr, de, ru, pt, zh, type CommonKey } from '../locales/index.ts' import { - en as settingsEn, ja as settingsJa, zh as settingsZh, type SettingsLocaleKey, + en as settingsEn, ja as settingsJa, es as settingsEs, fr as settingsFr, + de as settingsDe, ru as settingsRu, pt as settingsPt, zh as settingsZh, type SettingsLocaleKey, } from '../locales/settings.ts' import type { LanguageRowInjected } from './LanguageRow.tsx' import { LanguageRow } from './LanguageRow.tsx' @@ -579,9 +580,11 @@ export async function apply(ctx: ClientContext): Promise { } const host = ctx.configForms.get(LOCALE_SETTINGS_NAMESPACE) const locale = new LocaleRuntime(ctx, host, bootstrap) - ctx.effect(() => locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }), 'locale: Japanese language') - ctx.effect(() => locale.register(COMMON_NS, { zh, en, ja }), 'locale: common dictionaries') - ctx.effect(() => locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn, ja: settingsJa }), 'locale: settings dictionaries') + for (const [id, label] of [['ja', '日本語'], ['es', 'Español'], ['fr', 'Français'], ['de', 'Deutsch'], ['ru', 'Русский'], ['pt', 'Português']] as const) { + ctx.effect(() => locale.addLanguage({ id, label, fallback: 'en' }), `locale: ${id} language`) + } + ctx.effect(() => locale.register(COMMON_NS, { zh, en, ja, es, fr, de, ru, pt }), 'locale: common dictionaries') + ctx.effect(() => locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn, ja: settingsJa, es: settingsEs, fr: settingsFr, de: settingsDe, ru: settingsRu, pt: settingsPt }), 'locale: settings dictionaries') ctx.provide('locale', locale) if (bridge !== undefined) { ctx.on('locale/change', (snapshot) => { bridge.onChange(snapshot.active) }) diff --git a/packages/client/locale/src/locales/de.ts b/packages/client/locale/src/locales/de.ts new file mode 100644 index 0000000000..dd88cf6fc4 --- /dev/null +++ b/packages/client/locale/src/locales/de.ts @@ -0,0 +1,47 @@ +/** German common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete German copy for the common namespace. */ +export const de = { + 'ok': 'OK', + 'cancel': 'Abbrechen', + 'close': 'Schließen', + 'copy': 'Kopieren', + 'copied': 'Kopiert', + 'codeBlock.title': 'Codeblock', + 'codeBlock.wrap': 'Zeilen umbrechen', + 'codeBlock.unwrap': 'Zeilen nicht umbrechen', + 'copy.failed': 'Das Kopieren ist fehlgeschlagen', + 'copy.value': 'Wert kopieren', + 'copy.json': 'JSON kopieren', + 'copy.path': 'Eigenschaftspfad kopieren', + 'copy.prettyJson': 'Formatiertes JSON kopieren', + 'copy.compactJson': 'Kompaktes JSON kopieren', + 'copy.optionsHint': '{action}; Klicken Sie mit der rechten Maustaste, um die Kopieroptionen anzuzeigen', + 'retry': 'Erneut versuchen', + 'loading': 'Laden…', + 'load.failed': 'Laden fehlgeschlagen', + 'submit': 'Senden', + 'submitting': 'Wird gesendet…', + 'next': 'Weiter', + 'previous': 'Zurück', + 'skip': 'Überspringen', + 'delete': 'Löschen', + 'edit': 'Bearbeiten', + 'save': 'Speichern', + 'search': 'Suchen', + 'more': 'Mehr', + 'collapse': 'Einklappen', + 'expand': 'Ausklappen', + 'back': 'Zurück', + 'brand.localBuild': 'Lokaler DSH-Build', + 'workspace.defaultName': 'Standardarbeitsbereich', + 'unknown': 'Unbekannt', + 'none': 'Keine', + 'truncated': 'Abgeschnitten', + 'json.label': 'JSON', + 'markdown.footnotes': 'Fußnoten', + 'markdown.truncatedCharacters': '… nach {total} Zeichen abgeschnitten', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/es.ts b/packages/client/locale/src/locales/es.ts new file mode 100644 index 0000000000..7eb95374e1 --- /dev/null +++ b/packages/client/locale/src/locales/es.ts @@ -0,0 +1,47 @@ +/** Spanish common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Spanish copy for the common namespace. */ +export const es = { + 'ok': 'OK', + 'cancel': 'Cancelar', + 'close': 'Cerrar', + 'copy': 'Copiar', + 'copied': 'Copiado', + 'codeBlock.title': 'Bloque de código', + 'codeBlock.wrap': 'Ajustar líneas', + 'codeBlock.unwrap': 'No ajustar líneas', + 'copy.failed': 'Copia fallida', + 'copy.value': 'Copiar valor', + 'copy.json': 'Copiar JSON', + 'copy.path': 'Copiar ruta de propiedad', + 'copy.prettyJson': 'Copiar JSON con formato', + 'copy.compactJson': 'Copiar JSON compacto', + 'copy.optionsHint': '{action}; haga clic derecho para opciones de copia', + 'retry': 'Reintentar', + 'loading': 'Cargando…', + 'load.failed': 'No se pudo cargar', + 'submit': 'Enviar', + 'submitting': 'Enviando…', + 'next': 'Siguiente', + 'previous': 'Anterior', + 'skip': 'Saltar', + 'delete': 'Eliminar', + 'edit': 'Editar', + 'save': 'Guardar', + 'search': 'Buscar', + 'more': 'Más', + 'collapse': 'Contraer', + 'expand': 'Expandir', + 'back': 'Atrás', + 'brand.localBuild': 'Compilación local de DSH', + 'workspace.defaultName': 'Espacio de trabajo predeterminado', + 'unknown': 'Desconocido', + 'none': 'Ninguno', + 'truncated': 'Truncado', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notas a pie de página', + 'markdown.truncatedCharacters': '… truncado a {total} caracteres', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/fr.ts b/packages/client/locale/src/locales/fr.ts new file mode 100644 index 0000000000..c57f1a3b11 --- /dev/null +++ b/packages/client/locale/src/locales/fr.ts @@ -0,0 +1,47 @@ +/** French common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete French copy for the common namespace. */ +export const fr = { + 'ok': 'OK', + 'cancel': 'Annuler', + 'close': 'Fermer', + 'copy': 'Copier', + 'copied': 'Copié', + 'codeBlock.title': 'Bloc de code', + 'codeBlock.wrap': 'Activer le retour à la ligne', + 'codeBlock.unwrap': 'Désactiver le retour à la ligne', + 'copy.failed': 'Échec de la copie', + 'copy.value': 'Copier la valeur', + 'copy.json': 'Copier le JSON', + 'copy.path': 'Copier le chemin de la propriété', + 'copy.prettyJson': 'Copier le JSON formaté', + 'copy.compactJson': 'Copier le JSON compact', + 'copy.optionsHint': '{action}; clic droit pour les options de copie', + 'retry': 'Réessayer', + 'loading': 'Chargement…', + 'load.failed': 'Échec du chargement', + 'submit': 'Soumettre', + 'submitting': 'Envoi…', + 'next': 'Suivant', + 'previous': 'Précédent', + 'skip': 'Sauter', + 'delete': 'Supprimer', + 'edit': 'Modifier', + 'save': 'Enregistrer', + 'search': 'Rechercher', + 'more': 'Plus', + 'collapse': 'Réduire', + 'expand': 'Développer', + 'back': 'Retour', + 'brand.localBuild': 'Version locale de DSH', + 'workspace.defaultName': 'Espace de travail par défaut', + 'unknown': 'Inconnu', + 'none': 'Aucun', + 'truncated': 'Tronqué', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notes de bas de page', + 'markdown.truncatedCharacters': '… tronqué à {total} caractères', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/index.ts b/packages/client/locale/src/locales/index.ts index a01489ccfd..6690d74aeb 100644 --- a/packages/client/locale/src/locales/index.ts +++ b/packages/client/locale/src/locales/index.ts @@ -1,8 +1,13 @@ /** - * Common-namespace dictionaries. Chinese defines the keys; English and - * Japanese are checked against the same keys by TypeScript. + * Common-namespace dictionaries. Chinese defines the keys; + * every shipped language is checked against them by TypeScript. */ export { zh } from './zh.ts' export { en } from './en.ts' export { ja } from './ja.ts' export type { CommonKey } from './zh.ts' +export { es } from './es.ts' +export { fr } from './fr.ts' +export { de } from './de.ts' +export { ru } from './ru.ts' +export { pt } from './pt.ts' diff --git a/packages/client/locale/src/locales/pt.ts b/packages/client/locale/src/locales/pt.ts new file mode 100644 index 0000000000..75cdb19c29 --- /dev/null +++ b/packages/client/locale/src/locales/pt.ts @@ -0,0 +1,47 @@ +/** Portuguese common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Portuguese copy for the common namespace. */ +export const pt = { + 'ok': 'OK', + 'cancel': 'Cancelar', + 'close': 'Fechar', + 'copy': 'Copiar', + 'copied': 'Copiado', + 'codeBlock.title': 'Bloco de código', + 'codeBlock.wrap': 'Quebrar linhas', + 'codeBlock.unwrap': 'Não quebrar linhas', + 'copy.failed': 'Falha na cópia', + 'copy.value': 'Copiar valor', + 'copy.json': 'Copiar JSON', + 'copy.path': 'Copiar caminho da propriedade', + 'copy.prettyJson': 'Copiar JSON formatado', + 'copy.compactJson': 'Copiar JSON compacto', + 'copy.optionsHint': '{action}; clique com o botão direito para opções de cópia', + 'retry': 'Tentar novamente', + 'loading': 'Carregando…', + 'load.failed': 'Falha ao carregar', + 'submit': 'Enviar', + 'submitting': 'Enviando…', + 'next': 'Próximo', + 'previous': 'Anterior', + 'skip': 'Pular', + 'delete': 'Excluir', + 'edit': 'Editar', + 'save': 'Salvar', + 'search': 'Pesquisar', + 'more': 'Mais', + 'collapse': 'Recolher', + 'expand': 'Expandir', + 'back': 'Voltar', + 'brand.localBuild': 'Versão local do DSH', + 'workspace.defaultName': 'Espaço de trabalho padrão', + 'unknown': 'Desconhecido', + 'none': 'Nenhum', + 'truncated': 'Truncado', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notas de rodapé', + 'markdown.truncatedCharacters': '… truncado em {total} caracteres', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/ru.ts b/packages/client/locale/src/locales/ru.ts new file mode 100644 index 0000000000..a8a6d829da --- /dev/null +++ b/packages/client/locale/src/locales/ru.ts @@ -0,0 +1,47 @@ +/** Russian common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Russian copy for the common namespace. */ +export const ru = { + 'ok': 'OK', + 'cancel': 'Отмена', + 'close': 'Закрыть', + 'copy': 'Копировать', + 'copied': 'Скопировано', + 'codeBlock.title': 'Блок кода', + 'codeBlock.wrap': 'Переносить строки', + 'codeBlock.unwrap': 'Не переносить строки', + 'copy.failed': 'Не удалось скопировать', + 'copy.value': 'Копировать значение', + 'copy.json': 'Копировать JSON', + 'copy.path': 'Копировать путь к свойству', + 'copy.prettyJson': 'Копировать форматированный JSON', + 'copy.compactJson': 'Копировать компактный JSON', + 'copy.optionsHint': '{action}; щелкните правой кнопкой мыши, чтобы просмотреть параметры копирования', + 'retry': 'Повторить', + 'loading': 'Загрузка…', + 'load.failed': 'Не удалось загрузить', + 'submit': 'Отправить', + 'submitting': 'Отправка…', + 'next': 'Далее', + 'previous': 'Назад', + 'skip': 'Пропустить', + 'delete': 'Удалить', + 'edit': 'Редактировать', + 'save': 'Сохранить', + 'search': 'Поиск', + 'more': 'Ещё', + 'collapse': 'Свернуть', + 'expand': 'Развернуть', + 'back': 'Назад', + 'brand.localBuild': 'Локальная сборка DSH', + 'workspace.defaultName': 'Рабочая область по умолчанию', + 'unknown': 'Неизвестно', + 'none': 'Нет', + 'truncated': 'Усечено', + 'json.label': 'JSON', + 'markdown.footnotes': 'Сноски', + 'markdown.truncatedCharacters': '… обрезано до {total} символов', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/settings.ts b/packages/client/locale/src/locales/settings.ts index 76c5fd1ffb..8aafb48c44 100644 --- a/packages/client/locale/src/locales/settings.ts +++ b/packages/client/locale/src/locales/settings.ts @@ -17,3 +17,28 @@ export const en = { export const ja = { 'language.title': '言語', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'language.title': 'Idioma', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'language.title': 'Langue', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'language.title': 'Sprache', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'language.title': 'Язык', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'language.title': 'Idioma', +} satisfies Record diff --git a/packages/client/locale/tests/apply.client.spec.ts b/packages/client/locale/tests/apply.client.spec.ts index 97209c4ab3..43920d60c0 100644 --- a/packages/client/locale/tests/apply.client.spec.ts +++ b/packages/client/locale/tests/apply.client.spec.ts @@ -1,3 +1,4 @@ +import { SHIPPED_LOCALE_IDS } from '../../../../scripts/shipped-locales.ts' /** locale apply wiring: service + dictionaries provision, declaration-aware * Language row registration, snapshot projection into the row store, and * recovery after an HMR collapse of the declaring entry. */ @@ -155,7 +156,7 @@ describe('locale apply', () => { const { entry, instance, face } = faceOf(b.slots) // The inject-time re-sync sealed the init window: the mirror is current. expect(instance.getSnapshot().active).toBe('en') - expect(instance.getSnapshot().options.map(o => o.id)).toEqual(['zh', 'en', 'ja']) + expect(instance.getSnapshot().options.map(o => o.id).sort()).toEqual([...SHIPPED_LOCALE_IDS].sort()) // Copy rides the standard locale seat: the entry declares the namespace. expect(entry.locale).toBe(SETTINGS_NS) expect(locale.bind(SETTINGS_NS)('language.title')).toBe('Language') @@ -176,7 +177,7 @@ describe('locale apply', () => { const languagePack = b.ctx.plugin({ inject: ['locale'], apply: packCtx => packCtx.effect( - () => packCtx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => packCtx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'test language pack registration', ), }) @@ -185,33 +186,45 @@ describe('locale apply', () => { { id: 'zh', label: '中文' }, { id: 'en', label: 'English' }, { id: 'ja', label: '日本語' }, + { id: 'es', label: 'Español' }, { id: 'fr', label: 'Français' }, + { id: 'de', label: 'Deutsch' }, + { id: 'ru', label: 'Русский' }, + { id: 'pt', label: 'Português' }, + { id: 'it', label: 'Italiano' }, ]) await languagePack.dispose() - expect(instance.getSnapshot().options.map(option => option.id)).toEqual(['zh', 'en', 'ja']) + expect(instance.getSnapshot().options.map(option => option.id)).toEqual(['zh', 'en', 'ja', 'es', 'fr', 'de', 'ru', 'pt']) }) - it('restores Japanese from the Host and releases its language and dictionaries on unload', async () => { - const b = await bench('ja') + it.each([ + ['ja', '日本語', '言語', 'キャンセル'], + ['es', 'Español', 'Idioma', 'Cancelar'], + ['fr', 'Français', 'Langue', 'Annuler'], + ['de', 'Deutsch', 'Sprache', 'Abbrechen'], + ['ru', 'Русский', 'Язык', 'Отмена'], + ['pt', 'Português', 'Idioma', 'Cancelar'], + ])('restores %s from the Host and releases its language and dictionaries on unload', async (id, label, languageTitle, cancel) => { + const b = await bench(id) onTestFinished(() => b.ctx.fiber.dispose()) declareItems(b.slots) const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() const locale = b.ctx.get('locale') as LocaleRuntime const { instance, face } = faceOf(b.slots) - expect(instance.getSnapshot().active).toBe('ja') - expect(locale.bind(SETTINGS_NS)('language.title')).toBe('言語') - expect(locale.bind('common')('cancel')).toBe('キャンセル') + expect(instance.getSnapshot().active).toBe(id) + expect(locale.bind(SETTINGS_NS)('language.title')).toBe(languageTitle) + expect(locale.bind('common')('cancel')).toBe(cancel) face.setLocale('en') - face.setLocale('ja') + face.setLocale(id) await vi.waitFor(() => { expect(b.mutate).toHaveBeenCalledTimes(2) }) - expect(b.mutate.mock.calls.at(-1)?.[1]).toEqual([{ op: 'set', path: ['preference'], value: 'ja' }]) + expect(b.mutate.mock.calls.at(-1)?.[1]).toEqual([{ op: 'set', path: ['preference'], value: id }]) await fiber.dispose() expect(locale.getSnapshot().locales.map(option => option.id)).toEqual(['zh', 'en']) - expect(() => { locale.setLocale('ja') }).toThrow('not registered') - const remove = locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }) - locale.setLocale('ja') + expect(() => { locale.setLocale(id) }).toThrow('not registered') + const remove = locale.addLanguage({ id, label, fallback: 'en' }) + locale.setLocale(id) expect(locale.bind('common')('cancel')).toBe('cancel') remove() }) diff --git a/packages/client/locale/tests/document-language.client.spec.ts b/packages/client/locale/tests/document-language.client.spec.ts index 9e72b5149c..a513d988b5 100644 --- a/packages/client/locale/tests/document-language.client.spec.ts +++ b/packages/client/locale/tests/document-language.client.spec.ts @@ -103,7 +103,7 @@ describe('document language', () => { const observer = new MutationObserver(() => {}) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] }) try { - const remove = locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }) + const remove = locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }) expect(observer.takeRecords()).toHaveLength(0) remove() expect(observer.takeRecords()).toHaveLength(0) diff --git a/packages/client/modules/locale/de.json b/packages/client/modules/locale/de.json new file mode 100644 index 0000000000..4e5c22fba2 --- /dev/null +++ b/packages/client/modules/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Module", + "description": "Verwalten Sie Browser-Plug-in-Module und ihre Ladeabhängigkeiten." + } +} diff --git a/packages/client/modules/locale/es.json b/packages/client/modules/locale/es.json new file mode 100644 index 0000000000..f005ac4417 --- /dev/null +++ b/packages/client/modules/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "módulos", + "description": "Administre los módulos de complementos del navegador y sus dependencias de carga." + } +} diff --git a/packages/client/modules/locale/fr.json b/packages/client/modules/locale/fr.json new file mode 100644 index 0000000000..29801bfe86 --- /dev/null +++ b/packages/client/modules/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "modules", + "description": "Gérez les modules de plug-in du navigateur et leurs dépendances de chargement." + } +} diff --git a/packages/client/modules/locale/pt.json b/packages/client/modules/locale/pt.json new file mode 100644 index 0000000000..167cc3f507 --- /dev/null +++ b/packages/client/modules/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "módulos", + "description": "Gerencie módulos de plug-in do navegador e suas dependências de carregamento." + } +} diff --git a/packages/client/modules/locale/ru.json b/packages/client/modules/locale/ru.json new file mode 100644 index 0000000000..67041fde84 --- /dev/null +++ b/packages/client/modules/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "модули", + "description": "Управляйте подключаемыми модулями браузера и их зависимостями при загрузке." + } +} diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 7d216fb5d8..0ad5561a70 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/src/index.ts b/packages/client/modules/src/index.ts index c8b48a0fea..c87f28d4b8 100644 --- a/packages/client/modules/src/index.ts +++ b/packages/client/modules/src/index.ts @@ -28,11 +28,9 @@ import { createHash } from 'node:crypto' import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs' import { readFile } from 'node:fs/promises' -import type { IncomingMessage, ServerResponse } from 'node:http' import { createRequire } from 'node:module' import { dirname, isAbsolute, join } from 'node:path' import { Readable } from 'node:stream' -import { pipeline } from 'node:stream/promises' import { fileURLToPath, pathToFileURL } from 'node:url' import { isDeepStrictEqual } from 'node:util' import { Service } from '@deepseek-ai/cordis' @@ -255,6 +253,19 @@ function transforStreamBody(stream: Readable): ReadableStream { }) } +/** + * Expose a held bundle buffer as a Response body without copying it: the + * single chunk is the buffer itself, which readers must not modify. + */ +function heldBody(buffer: Buffer): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.enqueue(buffer) + controller.close() + }, + }) +} + /** Combo query addressing one ordered plugin-file list. */ function comboSearch(ids: readonly string[], rev: string, sourceMap = false): string { const resources = ids.map(id => `${id}/client.js${sourceMap ? '.map' : ''}`).join(',') @@ -689,7 +700,7 @@ export class ClientModuleRegistry extends Service { const registerWebCarrier = (webCtx: Context): void => { webCtx.effect( - () => webCtx.webServer.register({ kind: 'prefix', path: PLUGIN_ROUTE, handler: this.serveBundle }), + () => webCtx.webServer.register({ kind: 'prefix', path: PLUGIN_ROUTE, resource: request => this.fetchBundle(request) }), 'client-modules: bundle route', ) } @@ -723,14 +734,16 @@ export class ClientModuleRegistry extends Service { * body. Each bundle body is built once on its first `GET`; script * construction never reads maps. A package asset is read on every request * and answers 304 without a body when `If-None-Match` equals its entity tag; - * its body holds the file open until the caller consumes or cancels it. + * its body holds the file open until the caller consumes or cancels it. A + * bundle, map, or chunk body is the held buffer itself, not a copy, so a + * caller must not modify its bytes. * @param request - shell-carrier request for a `/plugins` resource. * @returns the exact response also exposed by the optional Web route. */ async fetchBundle(request: Request): Promise { const resource = await this.bundleResource(request.method, request.url, request.headers.get('if-none-match') ?? undefined) const body = resource.body === undefined ? null - : resource.body instanceof Readable ? transforStreamBody(resource.body) : Uint8Array.from(resource.body) + : resource.body instanceof Readable ? transforStreamBody(resource.body) : heldBody(resource.body) return new Response(body, { status: resource.status, ...(resource.headers === undefined ? {} : { headers: resource.headers }), @@ -1256,21 +1269,6 @@ export class ClientModuleRegistry extends Service { return { status: 404 } } - private readonly serveBundle = async (req: IncomingMessage, res: ServerResponse): Promise => { - /* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */ - const response = await this.bundleResource(req.method, req.url ?? '/', req.headers['if-none-match']) - res.writeHead(response.status, response.headers) - if (!(response.body instanceof Readable)) { - res.end(response.body) - return - } - try { - await pipeline(response.body, res) - } catch (error) { - // The browser cancelled the request, as leaving an onboarding step does; pipeline already closed the file. - if ((error as NodeJS.ErrnoException).code !== 'ERR_STREAM_PREMATURE_CLOSE') throw error - } - } } export default ClientModuleRegistry diff --git a/packages/client/modules/tests/node-half.client.spec.ts b/packages/client/modules/tests/node-half.client.spec.ts index e85c6371e6..283383149c 100644 --- a/packages/client/modules/tests/node-half.client.spec.ts +++ b/packages/client/modules/tests/node-half.client.spec.ts @@ -2,7 +2,6 @@ import { Hash } from 'node:crypto' import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs' -import type { IncomingMessage, ServerResponse } from 'node:http' import { SourceMap } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' @@ -10,7 +9,7 @@ import { pathToFileURL } from 'node:url' import { runInNewContext } from 'node:vm' import { Context, FiberState, type Fiber } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' -import { renderIndexInjections, type WebServer, type WebRoute } from '@deepseek-ai/dsh-host-webserver' +import { renderIndexInjections, type WebServer, type WebResourceRoute, type WebRoute } from '@deepseek-ai/dsh-host-webserver' import * as modulesClient from '../src/client/index.ts' import { ClientModuleRegistry, bootInjections, orderByModuleGraph } from '../src/index.ts' import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '../src/client/index.ts' @@ -68,7 +67,7 @@ const BOOTSTRAP_URL = comboReference([MODULES_ID], 'boot') const APPLICATION_URL = comboReference([UI_RENDERER_ID], 'app') let root: string | undefined -const contexts: { ctx: Context; ready?: Promise }[] = [] +const contexts: { ctx: Context; ready?: Promise }[] = [] afterEach(async () => { await Promise.all(contexts.splice(0).map(async ({ ctx, ready }) => { @@ -166,7 +165,7 @@ function constructWithRoute( entryBaseUrl?: string internal?: NonNullable } = {}, -): { context: Context; service: ClientModuleRegistry; route: Promise } { +): { context: Context; service: ClientModuleRegistry; route: Promise } { const ctx = new Context() const owned: typeof contexts[number] = { ctx } contexts.push(owned) @@ -184,11 +183,11 @@ function constructWithRoute( } }, }) - const route = Promise.withResolvers() + const route = Promise.withResolvers() const webServer: Pick = { port: 0, register: (candidate) => { - if (candidate.path === '/plugins') route.resolve(candidate) + if (candidate.path === '/plugins' && 'resource' in candidate) route.resolve(candidate) return () => {} }, tapIndex: () => () => {}, @@ -204,28 +203,15 @@ function construct(packageNames: string[]): ClientModuleRegistry { return constructWithRoute(packageNames).service } -/** Invoke the registered plugin route and capture status, headers, and bytes. */ -async function routeRequest(route: Promise, url: string, method = 'GET'): Promise<{ +/** Invoke the registered plugin resource and capture status, headers, and bytes. */ +async function routeRequest(route: Promise, url: string, method = 'GET'): Promise<{ status: number - headers: Record | undefined + headers: Record body: Buffer }> { - let status = 0 - let headers: Record | undefined - let body = Buffer.alloc(0) - const response = { - writeHead(nextStatus: number, nextHeaders?: Record) { - status = nextStatus - headers = nextHeaders - return response - }, - end(chunk?: Uint8Array) { - body = chunk === undefined ? Buffer.alloc(0) : Buffer.from(chunk) - return response - }, - } as unknown as ServerResponse - await (await route).handler({ method, url, headers: {} } as IncomingMessage, response) - return { status, headers, body } + const { resource } = await route + const response = await resource(new Request(new URL(url, 'http://localhost/'), { method })) + return { status: response.status, headers: Object.fromEntries(response.headers), body: Buffer.from(await response.arrayBuffer()) } } /** Execute the exact first inline script emitted by the Host boot rows. */ @@ -903,7 +889,7 @@ describe('client bundle activation', () => { expect(batch).toMatchObject({ phase: 'application', entries: [packageName] }) const batchScript = await routeRequest(route, batch.url) expect(batchScript.status).toBe(200) - expect(batchScript.headers?.['cache-control']).toBe('public, max-age=31536000, immutable') + expect(batchScript.headers['cache-control']).toBe('public, max-age=31536000, immutable') expect(batchScript.body.toString('utf8')).toContain(`//# sourceMappingURL=${mapReference(batch.url)}`) const shellResponse = await service.fetchBundle(new Request(`dsh-app://app/${batch.url}`)) expect(shellResponse.status).toBe(200) diff --git a/packages/client/modules/tests/package-assets.client.spec.ts b/packages/client/modules/tests/package-assets.client.spec.ts index acbb8889b7..1e63c951ac 100644 --- a/packages/client/modules/tests/package-assets.client.spec.ts +++ b/packages/client/modules/tests/package-assets.client.spec.ts @@ -121,7 +121,10 @@ interface Host { port: number /** Warnings any plugin of the composed Host logged. */ warnings: unknown[][] - /** Send one raw request target through the Web route and the shell carrier and require identical answers. */ + /** + * Send one raw request target over HTTP, through `webServer.readResource`, + * and through `fetchBundle`, and require identical answers. + */ both: (path: string, init?: AssetRequestInit) => Promise } @@ -159,14 +162,19 @@ async function composeHost(): Promise { outgoing.on('error', reject) outgoing.end() }) + const reader = { principal: { kind: 'owner' } } as Parameters[1] const both = async (path: string, init?: AssetRequestInit): Promise => { const answer = await http(path, init) - const response = await service.fetchBundle(new Request(`http://localhost${path}`, init)) - expect({ - status: response.status, - headers: assetHeaders(name => response.headers.get(name)), - body: Buffer.from(await response.arrayBuffer()), - }).toEqual(answer) + for (const response of [ + await context.webServer.readResource(new Request(`http://localhost${path}`, init), reader), + await service.fetchBundle(new Request(`http://localhost${path}`, init)), + ]) { + expect({ + status: response.status, + headers: assetHeaders(name => response.headers.get(name)), + body: Buffer.from(await response.arrayBuffer()), + }).toEqual(answer) + } return answer } // The Web route registers once the injected webServer is ready. @@ -268,9 +276,9 @@ describe('package asset route', () => { }) const stream = opened.findLast(entry => entry.path === file)?.stream if (stream === undefined) throw new Error('the asset route opened no file stream') - // events.once would reject on the premature-close error the cancelled pipeline reports; only the close matters here. + // The cancelled response body closes the file stream; only the close matters here. if (!stream.closed) await new Promise((resolve) => { stream.once('close', () => { resolve() }) }) - // pipeline settles its rejection in the turn after the file stream closes; the route logs nothing then. + // The webserver settles the cancelled exchange in the turn after the file stream closes; nothing is logged then. await new Promise(resolve => setImmediate(resolve)) expect(warnings).toEqual([]) }) diff --git a/packages/client/product-analytics/locale/de.json b/packages/client/product-analytics/locale/de.json new file mode 100644 index 0000000000..fb577f04ad --- /dev/null +++ b/packages/client/product-analytics/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Statistiken zur Desktop-Nutzung", + "description": "Sammeln Sie bestimmte Interaktionsereignisse auf dem Desktop und übermitteln Sie sie an den Produktstatistikdienst." + } +} diff --git a/packages/client/product-analytics/locale/es.json b/packages/client/product-analytics/locale/es.json new file mode 100644 index 0000000000..838528b1c4 --- /dev/null +++ b/packages/client/product-analytics/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Estadísticas de uso de escritorio", + "description": "Recopile eventos de interacción específicos en el escritorio y envíelos al servicio de estadísticas del producto." + } +} diff --git a/packages/client/product-analytics/locale/fr.json b/packages/client/product-analytics/locale/fr.json new file mode 100644 index 0000000000..2edef0a2b3 --- /dev/null +++ b/packages/client/product-analytics/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Statistiques d'utilisation du bureau", + "description": "Collectez les événements d'interaction spécifiés sur le bureau et soumettez-les au service de statistiques du produit." + } +} diff --git a/packages/client/product-analytics/locale/pt.json b/packages/client/product-analytics/locale/pt.json new file mode 100644 index 0000000000..de66d5a74b --- /dev/null +++ b/packages/client/product-analytics/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Estatísticas de uso da área de trabalho", + "description": "Colete eventos de interação especificados na área de trabalho e envie-os ao serviço de estatísticas do produto." + } +} diff --git a/packages/client/product-analytics/locale/ru.json b/packages/client/product-analytics/locale/ru.json new file mode 100644 index 0000000000..f089774778 --- /dev/null +++ b/packages/client/product-analytics/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Статистика использования рабочего стола", + "description": "Собирайте указанные события взаимодействия на рабочем столе и отправляйте их в сервис статистики продукта." + } +} diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index 9e1b06c156..e73a62160b 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-product-analytics", "description": "Desktop product event collection and authenticated Host reporting", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/locale/de.json b/packages/client/resources/locale/de.json new file mode 100644 index 0000000000..c5d2e7508e --- /dev/null +++ b/packages/client/resources/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ressourcen", + "description": "Lösen Sie Ressourcenadressen wie Dateien in Inhalte auf, die von der Schnittstelle gelesen werden können." + } +} diff --git a/packages/client/resources/locale/es.json b/packages/client/resources/locale/es.json new file mode 100644 index 0000000000..a09f476f6d --- /dev/null +++ b/packages/client/resources/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "recursos", + "description": "Resuelva direcciones de recursos, como archivos, en contenido que la interfaz pueda leer." + } +} diff --git a/packages/client/resources/locale/fr.json b/packages/client/resources/locale/fr.json new file mode 100644 index 0000000000..4c02e4e9fe --- /dev/null +++ b/packages/client/resources/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ressources", + "description": "Résolvez les adresses de ressources telles que les fichiers en contenu pouvant être lu par l'interface." + } +} diff --git a/packages/client/resources/locale/pt.json b/packages/client/resources/locale/pt.json new file mode 100644 index 0000000000..9b5f1c60fa --- /dev/null +++ b/packages/client/resources/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "recursos", + "description": "Resolva endereços de recursos, como arquivos, em conteúdo que possa ser lido pela interface." + } +} diff --git a/packages/client/resources/locale/ru.json b/packages/client/resources/locale/ru.json new file mode 100644 index 0000000000..e5571b2d32 --- /dev/null +++ b/packages/client/resources/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ресурсы", + "description": "Преобразуйте адреса ресурсов, такие как файлы, в контент, который может быть прочитан интерфейсом." + } +} diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index 419a20b714..944f8f8092 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/shortcuts/locale/de.json b/packages/client/shortcuts/locale/de.json new file mode 100644 index 0000000000..0de9fbe7f1 --- /dev/null +++ b/packages/client/shortcuts/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Abkürzungen", + "description": "Registrieren Sie Tastenkombinationen für Anwendungen und verteilen Sie Tastenoperationen auf entsprechende Funktionen." + } +} diff --git a/packages/client/shortcuts/locale/es.json b/packages/client/shortcuts/locale/es.json new file mode 100644 index 0000000000..e6d9faac53 --- /dev/null +++ b/packages/client/shortcuts/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "atajos", + "description": "Registre las teclas de acceso directo de la aplicación y distribuya las operaciones clave a las funciones correspondientes." + } +} diff --git a/packages/client/shortcuts/locale/fr.json b/packages/client/shortcuts/locale/fr.json new file mode 100644 index 0000000000..85dcaf0f78 --- /dev/null +++ b/packages/client/shortcuts/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "raccourcis", + "description": "Enregistrez les touches de raccourci de l'application et distribuez les opérations clés aux fonctions correspondantes." + } +} diff --git a/packages/client/shortcuts/locale/pt.json b/packages/client/shortcuts/locale/pt.json new file mode 100644 index 0000000000..10f9714ea8 --- /dev/null +++ b/packages/client/shortcuts/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "atalhos", + "description": "Registre teclas de atalho do aplicativo e distribua as operações das teclas para as funções correspondentes." + } +} diff --git a/packages/client/shortcuts/locale/ru.json b/packages/client/shortcuts/locale/ru.json new file mode 100644 index 0000000000..eedd6069c5 --- /dev/null +++ b/packages/client/shortcuts/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ярлыки", + "description": "Зарегистрируйте сочетания клавиш приложения и распределите ключевые операции по соответствующим функциям." + } +} diff --git a/packages/client/shortcuts/package.json b/packages/client/shortcuts/package.json index e6d70af8d7..77316c991f 100644 --- a/packages/client/shortcuts/package.json +++ b/packages/client/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-shortcuts", "description": "Application keyboard command registry and physical-key routing", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index 9da09e9bc4..70f255823e 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index c80b06b289..ca7bbc4154 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -82,7 +82,7 @@ function styleInjectionModule( * Everything else under @deepseek-ai/* is either a module-table entry * (external) or a leak the purity gate rejects. */ -export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:file-reference|session|llm|tools|brand|deque|output-retention|typert-protocol|util-crypto|util-values|util-workspace-path)(?:\/|$)|@deepseek-ai\/dsh-util-file-drag$|@deepseek-ai\/dsh-token-meter\/client$|@deepseek-ai\/dsh-native-command\/types$|@deepseek-ai\/dsh-host-open-in-app\/shared$|@deepseek-ai\/dsh-plugin-manager\/registry$|@deepseek-ai\/dsh-agent-preset-registry\/display$|@deepseek-ai\/dsh-api-workspace-controller\/default-workspace$|@deepseek-ai\/dsh-spill-policy\/notice$)/ +export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:file-reference|session|llm|tools|brand|official-bundles|deque|output-retention|typert-protocol|util-crypto|util-values|util-workspace-path)(?:\/|$)|@deepseek-ai\/dsh-util-file-drag$|@deepseek-ai\/dsh-token-meter\/client$|@deepseek-ai\/dsh-native-command\/types$|@deepseek-ai\/dsh-host-open-in-app\/shared$|@deepseek-ai\/dsh-plugin-manager\/registry$|@deepseek-ai\/dsh-agent-preset-registry\/display$|@deepseek-ai\/dsh-api-workspace-controller\/default-workspace$|@deepseek-ai\/dsh-spill-policy\/notice$)/ /** * Vendored framework libraries: rescoped into @deepseek-ai, so the gate below diff --git a/packages/client/ui-agent-preset/locale/de.json b/packages/client/ui-agent-preset/locale/de.json new file mode 100644 index 0000000000..bb0817b9bb --- /dev/null +++ b/packages/client/ui-agent-preset/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Standardschnittstelle des Agenten", + "description": "Wählen Sie Agent-Standardeinstellungen für aktuelle und neue Sitzungen aus und bearbeiten Sie die von den Standardeinstellungen verwendeten Plugins." + } +} diff --git a/packages/client/ui-agent-preset/locale/es.json b/packages/client/ui-agent-preset/locale/es.json new file mode 100644 index 0000000000..71f9b56076 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz predeterminada del agente", + "description": "Seleccione los valores predeterminados del agente para las sesiones actuales y nuevas, y edite los complementos utilizados por los valores predeterminados." + } +} diff --git a/packages/client/ui-agent-preset/locale/fr.json b/packages/client/ui-agent-preset/locale/fr.json new file mode 100644 index 0000000000..55d48a7007 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface par défaut de l'agent", + "description": "Sélectionnez les valeurs par défaut de l'agent pour les sessions actuelles et les nouvelles, et modifiez les plugins utilisés par les valeurs par défaut." + } +} diff --git a/packages/client/ui-agent-preset/locale/pt.json b/packages/client/ui-agent-preset/locale/pt.json new file mode 100644 index 0000000000..794e0ca90c --- /dev/null +++ b/packages/client/ui-agent-preset/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface padrão do agente", + "description": "Selecione Padrões do agente para sessões atuais e novas e edite os plug-ins usados pelos padrões." + } +} diff --git a/packages/client/ui-agent-preset/locale/ru.json b/packages/client/ui-agent-preset/locale/ru.json new file mode 100644 index 0000000000..5ee3dcb323 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс агента по умолчанию", + "description": "Выберите параметры агента по умолчанию для текущего и нового сеансов и отредактируйте плагины, используемые по умолчанию." + } +} diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index f4a8bfb805..d370dff454 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/src/client/guide-locales.ts b/packages/client/ui-agent-preset/src/client/guide-locales.ts index ff5a5b5529..d92283aad2 100644 --- a/packages/client/ui-agent-preset/src/client/guide-locales.ts +++ b/packages/client/ui-agent-preset/src/client/guide-locales.ts @@ -173,3 +173,378 @@ export const guideJa = { 'guideCordisExplanation': '### 作成できるもの\n\nクリエイターモードには標準的なタスクツールに加え、ランタイムの検査、Cordis プラグインやエージェントプリセットの作成ガイドが含まれます。機能や UI を追加するプラグインを作成することも、特定の作業向けにツールとプロンプトを組み合わせたプリセットを作成することもできます。\n\n### プラグインとモード\n\nプラグインはツール、サービス接続、UI エントリなど、DSH に機能を追加します。モードはエージェントプリセットであり、ツールを選択し、タスク内でのエージェントの動作を定義します。プラグインはカスタムプリセットに含めることができます。\n\n### 結果を反映させる方法\n\nソースコードを生成するだけでなく、エージェントにインストールと確認を依頼してください。プラグインは変更内容によっては即座に読み込まれるか、再起動が必要になる場合があります。新しく作成したプリセットは、新しいタスクを始めるときに選択されます。', 'guideCordisUsage': '### UI を追加する\n\n> サイドバーにプロジェクトメモのエントリを追加する DSH プラグインを作成してください。このワークスペース内の Markdown ファイルを閲覧でき、選択したメモをプレビューできるようにしてください。インストールしてページが開くことを確認してください。\n\n期待される出力: 動作するエントリとプレビューページを備えた、インストール済みのプラグイン。残っている有効化手順があればそれも含みます。\n\n### ツールを追加する\n\n> このプロジェクトのテストレポートを読み、失敗したテストを要約するツールを備えたプラグインを作成してください。登録し、サンプルのレポートで確認してください。\n\n期待される出力: 呼び出し可能なツールを備えたプラグインと、確認済みのサンプル呼び出し。\n\n### 独自のモードを作成する\n\n> 標準モードをベースに「コードレビュー」モードを作成してください。潜在的なバグとテストの不足を優先し、ファイルパスと行番号を示し、ファイルを変更する前に確認を求めるようにしてください。選択可能なプリセットとして保存してください。\n\n期待される出力: 新しいタスク用のカスタムプリセット。これらのレビュー指示はエージェントを導くものであり、実行できる操作は権限設定によって決まります。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const guideEs = { + modeExplanation: 'Detalles del modo', + howToUse: 'como usar', + guideSections: 'Secciones de guía', + guideExampleTask: 'Tarea de ejemplo', + guideCopy: 'Copiar', + guideCopied: 'Copiado', + guideFootnotes: 'Notas a pie de página', + guideStandardIntro: 'Elija el modo Estándar al iniciar una nueva tarea. Describe lo que quieres lograr, señala los archivos relevantes y explica cómo verificar el resultado.', + guideStandardExplanation: [ + '### Cómo funciona', + 'El agente llama a herramientas directamente para leer y editar archivos, buscar y ejecutar comandos de terminal. Incluye habilidades, planificación, objetivos, subagentes, flujos de trabajo, gestión de complementos y compactación de contexto.', + '### Cuándo elegirlo', + 'Comience aquí para la codificación, el trabajo con archivos y la investigación diaria. El modo estándar también puede escribir scripts y procesar archivos en lotes. PTC cambia la forma en que se organizan las llamadas a herramientas; no es necesario para tareas por lotes.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Corregir un error', + '> Descubra por qué enviar el formulario de búsqueda dos veces hace que los resultados desaparezcan. Arréglalo y ejecuta las pruebas pertinentes. Explique la causa y qué cambió.', + 'Resultado esperado: un cambio de código, los resultados de las pruebas relevantes y una explicación de la causa.', + '### Organizar notas del proyecto', + '> Lea las Markdown notas de este proyecto. Resuma las decisiones acordadas y las preguntas abiertas, con enlaces a los archivos fuente.', + 'Resultado esperado: un resumen con referencias que puede comparar con las notas originales.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Elija el modo PTC al iniciar una nueva tarea. Especifique los archivos de entrada, las reglas de procesamiento y el formato de salida. El agente escribe el código.', + guidePtcExplanation: [ + '### Cómo se llaman las herramientas', + 'PTC significa llamada a herramientas programáticas. En este ajuste preestablecido integrado, el agente usa run_code para escribir un programa TypeScript que llama a herramientas a través de un SDK generado. El programa puede utilizar bucles, condiciones, manejo de errores y llamadas simultáneas cuando corresponda.', + '### Lo que llega al modelo', + 'Los resultados de la herramienta llegan primero al programa, que puede filtrarlos y combinarlos. El modelo recibe lo que el programa imprime o devuelve; Los resultados de la imagen se adjuntan por separado. Las llamadas a herramientas anidadas aún se registran y permanecen sujetas a los permisos de la herramienta.', + '### Comparado con el modo Estándar', + 'Ambos modos pueden manejar tareas de codificación y por lotes. El modo estándar expone herramientas individuales directamente; PTC organiza llamadas a herramientas en código. El ajuste preestablecido actual PTC deja las herramientas de administración de complementos y flujo de trabajo deshabilitadas. La velocidad y el uso de tokens dependen de la tarea y de cómo el programa maneja sus resultados.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Verifique un conjunto de archivos de configuración', + '> Verifique todos los JSON archivos en configs/. Enumere los campos obligatorios faltantes y los valores no válidos en el archivo Schema.json. Guarde un CSV con una fila por número. Incluya archivos ilegibles en el informe y siga revisando el resto. Deje los archivos originales sin cambios.', + 'Resultado esperado: un resumen del problema y un informe CSV. El programa puede repetir las mismas comprobaciones, gestionar fallos individuales y recopilar los resultados.', + '### Resumir registros de errores', + '> Analizar los archivos de registro en logs/. Agrupe errores por servicio y tipo de error. Muestra los diez grupos más frecuentes y un ejemplo de cada uno. Guarde los recuentos completos en un CSV.', + 'Resultado esperado: los principales grupos de errores y una tabla de recuento completa. Los datos intermedios se pueden agregar en el programa antes de que el resumen llegue al modelo.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Elija el modo mínimo para una nueva tarea. Para realizar una comparación, mantenga constantes el modelo, los permisos, la entrada y el estado inicial del espacio de trabajo en todas las ejecuciones.', + guideMinimalExplanation: [ + '### ¿Qué está incluido?', + 'Una herramienta de shell persistente, read_image, y un indicador de sistema fijo. La lectura de imágenes requiere un modelo compatible con imágenes. La historia más antigua se compacta automáticamente bajo la presión del contexto; use /compact mientras el agente esté inactivo para compactarlo manualmente. El ajuste preestablecido integrado no carga las habilidades, la planificación ni el contexto de tiempo de ejecución estándar.', + '### Cuándo elegirlo', + 'Úselo como base para experimentos y comparaciones. Todavía puede leer archivos y ejecutar scripts mediante comandos de shell, pero ofrece menos formas integradas de gestionar una tarea larga. Menos herramientas no necesariamente lo hace más fácil para un principiante.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Comparar el rendimiento en una pequeña corrección de errores', + '> Ejecute las pruebas para este proyecto, encuentre la causa del error y realice la solución más pequeña. Ejecute las pruebas pertinentes nuevamente e informe el resultado.', + 'Ejecute la misma tarea por separado en los modos Estándar y Mínimo desde el mismo estado inicial. Compare la finalización de tareas, las llamadas a herramientas y los cambios resultantes. El modo mínimo realiza el trabajo a través de comandos de terminal.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Elija el modo Creador para una nueva tarea. Describe la capacidad que deseas, dónde debería aparecer y cómo la verificarás.', + guideCordisExplanation: [ + '### Lo que puedes crear', + 'El modo Creador incluye las herramientas de tareas estándar además de inspección del tiempo de ejecución y orientación para crear Cordis complementos y ajustes preestablecidos de agentes. Puede crear un complemento que agregue una capacidad o interfaz de usuario, o un ajuste preestablecido que combine herramientas e indicaciones para un trabajo en particular.', + '### Complementos y modos', + 'Un complemento agrega capacidades a DSH, como una herramienta, una conexión de servicio o una entrada de UI. Un modo es un ajuste preestablecido del agente que selecciona herramientas y define cómo trabaja el agente en una tarea. Se puede incluir un complemento en un ajuste preestablecido personalizado.', + '### Cómo surte efecto el resultado', + 'Pídale al agente que instale y verifique el resultado, no solo que genere el código fuente. Un complemento puede cargarse inmediatamente o requerir un reinicio, dependiendo de lo que cambie. Se selecciona un ajuste preestablecido recién creado al iniciar una nueva tarea.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Agregar una interfaz de usuario', + '> Cree un complemento DSH que agregue una entrada de notas del proyecto a la barra lateral. Permítanme explorar los Markdown archivos en este espacio de trabajo y obtener una vista previa de una nota seleccionada. Instálalo y verifica que se abre la página.', + 'Resultado esperado: un complemento instalado con una entrada funcional y una página de vista previa, además de los pasos de activación restantes.', + '### Agregar una herramienta', + '> Cree un complemento con una herramienta que lea el informe de prueba de este proyecto y resuma las pruebas fallidas. Regístrelo y verifíquelo con un informe de muestra.', + 'Resultado esperado: un complemento con una herramienta invocable y una llamada de muestra verificada.', + '### Crear mi propio modo', + '> Cree un modo de “Revisión de código” basado en el modo Estándar. Haga que priorice posibles errores y pruebe las lagunas, cite rutas y líneas de archivos y pregunte antes de modificar archivos. Guárdelo como un ajuste preestablecido seleccionable.', + 'Resultado esperado: un ajuste preestablecido personalizado para nuevas tareas. Estas instrucciones de revisión guían al agente; La configuración de permisos determina qué acciones puede ejecutar.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** French dictionary, checked against the English keys. */ +export const guideFr = { + modeExplanation: 'Détails du mode', + howToUse: 'Comment utiliser', + guideSections: 'Sections du guide', + guideExampleTask: 'Exemple de tâche', + guideCopy: 'Copier', + guideCopied: 'Copié', + guideFootnotes: 'Notes de bas de page', + guideStandardIntro: 'Choisissez le mode Standard lorsque vous démarrez une nouvelle tâche. Décrivez ce que vous souhaitez accomplir, pointez vers les fichiers pertinents et expliquez comment vérifier le résultat.', + guideStandardExplanation: [ + '### Comment ça marche', + "L'agent appelle directement des outils pour lire et modifier des fichiers, rechercher et exécuter des commandes de terminal. Il comprend les compétences, la planification, les objectifs, les sous-agents, les flux de travail, la gestion des plugins et le compactage du contexte.", + '### Quand le choisir', + "Commencez ici pour le codage quotidien, le travail sur les fichiers et la recherche. Le mode standard peut également écrire des scripts et traiter des fichiers par lots. PTC modifie la façon dont les appels d'outils sont organisés ; il n'est pas requis pour les tâches par lots.", + ].join('\n\n\n\n'), + guideStandardUsage: [ + "### Correction d'un bug", + '> Découvrez pourquoi le fait de soumettre deux fois le formulaire de recherche fait disparaître les résultats. Corrigez-le et exécutez les tests appropriés. Expliquez la cause et ce qui a changé.', + 'Résultat attendu : un changement de code, les résultats des tests pertinents et une explication de la cause.', + '### Organiser les notes de projet', + '> Lisez les Markdown notes de ce projet. Résumez les décisions convenues et les questions ouvertes, avec des liens vers les fichiers sources.', + 'Résultat attendu : un résumé avec des références que vous pouvez vérifier par rapport aux notes originales.', + ].join('\n\n\n\n'), + guidePtcIntro: "Choisissez le mode PTC lorsque vous démarrez une nouvelle tâche. Spécifiez les fichiers d'entrée, les règles de traitement et le format de sortie. L'agent écrit le code.", + guidePtcExplanation: [ + '### Comment sont appelés les outils', + "PTC signifie appel d'outil de programmation. Dans ce préréglage intégré, l'agent utilise run_code pour écrire un programme TypeScript qui appelle des outils via un SDK généré. Le programme peut utiliser des boucles, des conditions, une gestion des erreurs et des appels simultanés, le cas échéant.", + '### Ce qui arrive au modèle', + "Les résultats des outils parviennent d’abord au programme, qui peut les filtrer et les combiner. Le modèle reçoit ce que le programme imprime ou renvoie ; les résultats des images sont joints séparément. Les appels d'outils imbriqués sont toujours enregistrés et restent soumis aux autorisations de l'outil.", + '### Par rapport au mode Standard', + "Les deux modes peuvent gérer les tâches de codage et par lots. Le mode standard expose directement les outils individuels ; PTC organise les appels d'outils dans le code. Le préréglage PTC actuel laisse les outils de gestion de flux de travail et de plugins désactivés. La vitesse et l'utilisation des jetons dépendent de la tâche et de la manière dont le programme gère ses résultats.", + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Vérifier un ensemble de fichiers de configuration', + '> Vérifiez tous les JSON fichiers dans configs/. Répertoriez les champs obligatoires manquants et les valeurs non valides par rapport à schema.json. Enregistrez un CSV avec une ligne par numéro. Incluez les fichiers illisibles dans le rapport et continuez à vérifier le reste. Laissez les fichiers originaux inchangés.', + 'Résultat attendu : un résumé du problème et un rapport CSV. Le programme peut répéter les mêmes vérifications, gérer les échecs individuels et collecter les résultats.', + "### Résumer les journaux d'erreurs", + '> Analysez les fichiers journaux dans logs/. Regroupez les erreurs par service et type d’erreur. Montrez les dix groupes les plus fréquents et un exemple de chacun. Enregistrez les décomptes complets dans un CSV.', + "Résultat attendu : les principaux groupes d’erreurs et un tableau de décompte complet. Les données intermédiaires peuvent être agrégées dans le programme avant que le résumé n'atteigne le modèle.", + ].join('\n\n\n\n'), + guideMinimalIntro: 'Choisissez le mode Minimal pour une nouvelle tâche. À des fins de comparaison, maintenez le modèle, les autorisations, l’entrée et l’état de départ de l’espace de travail constants d’une exécution à l’autre.', + guideMinimalExplanation: [ + '### Ce qui est inclus', + "Un outil shell persistant, read_image et une invite système fixe. La lecture d'images nécessite un modèle compatible avec les images. L’historique plus ancien est automatiquement compacté sous la pression du contexte ; utilisez /compact pendant que l'agent est inactif pour le compacter manuellement. Le préréglage intégré ne charge pas les compétences, la planification ou le contexte d'exécution standard.", + '### Quand le choisir', + 'Utilisez-le comme base de référence pour des expériences et des comparaisons. Il peut toujours lire des fichiers et exécuter des scripts via des commandes shell, mais offre moins de moyens intégrés pour gérer une tâche longue. Moins d’outils ne facilite pas nécessairement la tâche d’un débutant.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Comparez les performances sur une petite correction de bug', + "> Exécutez les tests pour ce projet, trouvez la cause de l'échec et effectuez le moindre correctif. Exécutez à nouveau les tests pertinents et rapportez le résultat.", + "Exécutez la même tâche séparément en modes Standard et Minimal à partir du même état de départ. Comparez l'achèvement des tâches, les appels d'outils et les modifications qui en résultent. Le mode minimal effectue le travail via les commandes du terminal.", + ].join('\n\n\n\n'), + guideCordisIntro: 'Choisissez le mode Créateur pour une nouvelle tâche. Décrivez la fonctionnalité souhaitée, où elle doit apparaître et comment vous la vérifierez.', + guideCordisExplanation: [ + '### Ce que vous pouvez créer', + "Le mode Créateur inclut les outils de tâches standard, ainsi qu'une inspection d'exécution et des conseils pour la création de Cordis plug-ins et préréglages d'agent. Il peut créer un plugin qui ajoute une fonctionnalité ou une interface utilisateur, ou un préréglage combinant des outils et des invites pour une tâche particulière.", + '### Plugins et modes', + "Un plug-in ajoute des fonctionnalités à DSH, telles qu'un outil, une connexion à un service ou une entrée d'interface utilisateur. Un mode est un préréglage d'agent qui sélectionne les outils et définit la manière dont l'agent fonctionne dans une tâche. Un plugin peut être inclus dans un préréglage personnalisé.", + '### Comment le résultat prend effet', + "Demandez à l'agent d'installer et de vérifier le résultat, pas seulement de générer le code source. Un plugin peut se charger immédiatement ou nécessiter un redémarrage, en fonction de ce qu'il modifie. Un préréglage nouvellement créé est sélectionné lors du démarrage d'une nouvelle tâche.", + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Ajouter une interface utilisateur', + "> Créez un plugin DSH qui ajoute une entrée de notes de projet à la barre latérale. Permettez-moi de parcourir les Markdown fichiers de cet espace de travail et de prévisualiser une note sélectionnée. Installez-le et vérifiez que la page s'ouvre.", + "Résultat attendu : un plugin installé avec une page d'entrée et d'aperçu fonctionnelle, ainsi que toutes les étapes d'activation restantes.", + '### Ajouter un outil', + '> Créez un plugin avec un outil qui lit le rapport de test de ce projet et résume les tests échoués. Enregistrez-le et vérifiez-le avec un exemple de rapport.', + "Résultat attendu : un plugin avec un outil appelable et un exemple d'appel vérifié.", + '### Créer mon propre mode', + '> Créer un mode « Revue de code » basé sur le mode Standard. Demandez-lui de prioriser les bogues potentiels et de tester les lacunes, de citer les chemins et les lignes des fichiers et de demander avant de modifier les fichiers. Enregistrez-le en tant que préréglage sélectionnable.', + "Résultat attendu : un préréglage personnalisé pour les nouvelles tâches. Ces instructions de révision guident l'agent ; les paramètres d'autorisation déterminent les actions qu'il peut exécuter.", + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** German dictionary, checked against the English keys. */ +export const guideDe = { + modeExplanation: 'Modusdetails', + howToUse: 'Wie zu verwenden', + guideSections: 'Leitfadenabschnitte', + guideExampleTask: 'Beispielaufgabe', + guideCopy: 'Kopieren', + guideCopied: 'Kopiert', + guideFootnotes: 'Fußnoten', + guideStandardIntro: 'Wählen Sie den Standardmodus, wenn Sie eine neue Aufgabe starten. Beschreiben Sie, was Sie erreichen möchten, zeigen Sie auf die relevanten Dateien und erklären Sie, wie Sie das Ergebnis überprüfen können.', + guideStandardExplanation: [ + '### Wie es funktioniert', + 'Der Agent ruft Tools direkt auf, um Dateien zu lesen und zu bearbeiten, zu suchen und Terminalbefehle auszuführen. Es umfasst Fähigkeiten, Planung, Ziele, Subagenten, Arbeitsabläufe, Plugin-Verwaltung und Kontextkomprimierung.', + '### Wann man es wählt', + 'Beginnen Sie hier mit alltäglicher Codierung, Dateiarbeit und Recherche. Im Standardmodus können auch Skripts geschrieben und Dateien stapelweise verarbeitet werden. PTC ändert die Organisation von Toolaufrufen; Für Batch-Aufgaben ist es nicht erforderlich.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Einen Fehler beheben', + '> Finden Sie heraus, warum das zweimalige Absenden des Suchformulars dazu führt, dass die Ergebnisse verschwinden. Beheben Sie das Problem und führen Sie die entsprechenden Tests durch. Erklären Sie die Ursache und was sich geändert hat.', + 'Erwartete Ausgabe: eine Codeänderung, die relevanten Testergebnisse und eine Erklärung der Ursache.', + '### Projektnotizen organisieren', + '> Lesen Sie die Markdown-Hinweise in diesem Projekt. Fassen Sie die vereinbarten Entscheidungen und offenen Fragen zusammen, mit Links zu den Quelldateien.', + 'Erwartete Ausgabe: eine Zusammenfassung mit Referenzen, die Sie mit den Originalnotizen vergleichen können.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Wählen Sie den Modus PTC, wenn Sie eine neue Aufgabe starten. Geben Sie die Eingabedateien, Verarbeitungsregeln und das Ausgabeformat an. Der Agent schreibt den Code.', + guidePtcExplanation: [ + '### Wie Werkzeuge heißen', + 'PTC bedeutet Programmatic Tool Calling. In dieser integrierten Voreinstellung verwendet der Agent run_code, um ein TypeScript-Programm zu schreiben, das Tools über ein generiertes SDK aufruft. Das Programm kann gegebenenfalls Schleifen, Bedingungen, Fehlerbehandlung und gleichzeitige Aufrufe verwenden.', + '### Was das Modell erreicht', + 'Werkzeugergebnisse gelangen zunächst in das Programm, das diese filtern und kombinieren kann. Das Modell empfängt, was das Programm ausgibt oder zurückgibt; Bildergebnisse sind separat beigefügt. Verschachtelte Werkzeugaufrufe werden weiterhin aufgezeichnet und unterliegen weiterhin den Werkzeugberechtigungen.', + '### Im Vergleich zum Standardmodus', + 'Beide Modi können Codierungs- und Batch-Aufgaben bewältigen. Im Standardmodus werden einzelne Tools direkt verfügbar gemacht. PTC organisiert Toolaufrufe im Code. Die aktuelle Voreinstellung PTC lässt die Workflow- und Plugin-Verwaltungstools deaktiviert. Geschwindigkeit und Token-Nutzung hängen von der Aufgabe ab und davon, wie das Programm mit seinen Ergebnissen umgeht.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Überprüfen Sie eine Reihe von Konfigurationsdateien', + '> Überprüfen Sie alle JSON Dateien in configs/. Fehlende Pflichtfelder und ungültige Werte für schema.json auflisten. Speichern Sie eine CSV-Datei mit einer Zeile pro Ausgabe. Fügen Sie unleserliche Dateien in den Bericht ein und überprüfen Sie den Rest weiter. Lassen Sie die Originaldateien unverändert.', + 'Erwartete Ausgabe: eine Problemzusammenfassung und ein CSV-Bericht. Das Programm kann dieselben Prüfungen wiederholen, einzelne Fehler behandeln und die Ergebnisse sammeln.', + '### Fehlerprotokolle zusammenfassen', + '> Analysieren Sie die Protokolldateien in logs/. Gruppieren Sie Fehler nach Dienst und Fehlertyp. Zeigen Sie die zehn häufigsten Gruppen und jeweils ein Beispiel. Speichern Sie die vollständigen Zählungen in einer CSV-Datei.', + 'Erwartete Ausgabe: die Top-Fehlergruppen und eine vollständige Zähltabelle. Zwischendaten können im Programm aggregiert werden, bevor die Zusammenfassung das Modell erreicht.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Wählen Sie den Minimalmodus für eine neue Aufgabe. Halten Sie für einen Vergleich das Modell, die Berechtigungen, die Eingabe und den Status des Startarbeitsbereichs über alle Ausführungen hinweg konstant.', + guideMinimalExplanation: [ + '### Was ist enthalten', + 'Ein persistentes Shell-Tool, read_image, und eine feste Systemeingabeaufforderung. Das Lesen von Bildern erfordert ein bildfähiges Modell. Der ältere Verlauf wird unter Kontextdruck automatisch komprimiert; Verwenden Sie /compact, während der Agent inaktiv ist, um ihn manuell zu komprimieren. Die integrierte Voreinstellung lädt weder Skills, Planung noch den Standardlaufzeitkontext.', + '### Wann man es wählt', + 'Verwenden Sie es als Grundlage für Experimente und Vergleiche. Es kann weiterhin Dateien lesen und Skripte über Shell-Befehle ausführen, bietet jedoch weniger integrierte Möglichkeiten zur Verwaltung einer langen Aufgabe. Weniger Werkzeuge machen es einem Anfänger nicht unbedingt einfacher.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Vergleichen Sie die Leistung mit einer kleinen Fehlerbehebung', + '> Führen Sie die Tests für dieses Projekt durch, finden Sie die Fehlerursache und nehmen Sie die kleinste Lösung vor. Führen Sie die entsprechenden Tests erneut durch und melden Sie das Ergebnis.', + 'Führen Sie dieselbe Aufgabe separat im Standard- und Minimalmodus aus, und zwar im selben Startzustand. Vergleichen Sie die Aufgabenerledigung, Werkzeugaufrufe und die daraus resultierenden Änderungen. Der Minimalmodus führt die Arbeit über Terminalbefehle aus.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Wählen Sie den Erstellermodus für eine neue Aufgabe. Beschreiben Sie die gewünschte Funktion, wo sie angezeigt werden soll und wie Sie sie überprüfen.', + guideCordisExplanation: [ + '### Was Sie erstellen können', + 'Der Erstellermodus umfasst die Standardaufgabentools sowie eine Laufzeitprüfung und Anleitungen zum Erstellen von Cordis-Plugins und Agent-Voreinstellungen. Es kann ein Plugin erstellt werden, das eine Funktion oder Benutzeroberfläche hinzufügt, oder eine Voreinstellung, die Tools und Eingabeaufforderungen für einen bestimmten Job kombiniert.', + '### Plugins und Modi', + 'Ein Plugin fügt DSH Funktionen hinzu, beispielsweise ein Tool, eine Dienstverbindung oder einen UI-Eintrag. Ein Modus ist eine Agentenvoreinstellung, die Tools auswählt und definiert, wie der Agent in einer Aufgabe arbeitet. Ein Plugin kann in eine benutzerdefinierte Voreinstellung eingebunden werden.', + '### Wie das Ergebnis wirksam wird', + 'Bitten Sie den Agenten, das Ergebnis zu installieren und zu überprüfen und nicht nur Quellcode zu generieren. Abhängig von den Änderungen kann ein Plugin sofort geladen werden oder einen Neustart erfordern. Beim Starten einer neuen Aufgabe wird eine neu erstellte Voreinstellung ausgewählt.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Fügen Sie eine Benutzeroberfläche hinzu', + '> Erstellen Sie ein DSH-Plugin, das der Seitenleiste einen Eintrag für Projektnotizen hinzufügt. Lassen Sie mich die Markdown Dateien in diesem Arbeitsbereich durchsuchen und eine Vorschau einer ausgewählten Notiz anzeigen. Installieren Sie es und stellen Sie sicher, dass die Seite geöffnet wird.', + 'Erwartete Ausgabe: ein installiertes Plugin mit einer funktionierenden Einstiegs- und Vorschauseite sowie allen verbleibenden Aktivierungsschritten.', + '### Ein Werkzeug hinzufügen', + '> Erstellen Sie ein Plugin mit einem Tool, das den Testbericht dieses Projekts liest und die fehlgeschlagenen Tests zusammenfasst. Registrieren Sie es und überprüfen Sie es anhand eines Beispielberichts.', + 'Erwartete Ausgabe: ein Plugin mit einem aufrufbaren Tool und einem verifizierten Beispielaufruf.', + '### Erstelle meinen eigenen Modus', + '> Erstellen Sie einen „Code Review“-Modus basierend auf dem Standardmodus. Lassen Sie potenzielle Fehler priorisieren und Lücken testen, geben Sie Dateipfade und -zeilen an und fragen Sie nach, bevor Sie Dateien ändern. Speichern Sie es als auswählbare Voreinstellung.', + 'Erwartete Ausgabe: eine benutzerdefinierte Voreinstellung für neue Aufgaben. Diese Überprüfungsanweisungen leiten den Agenten; Berechtigungseinstellungen bestimmen, welche Aktionen ausgeführt werden können.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** Russian dictionary, checked against the English keys. */ +export const guideRu = { + modeExplanation: 'Подробности режима', + howToUse: 'Как использовать', + guideSections: 'Разделы руководства', + guideExampleTask: 'Пример задачи', + guideCopy: 'Копировать', + guideCopied: 'Скопировано', + guideFootnotes: 'Сноски', + guideStandardIntro: 'Выбирайте стандартный режим при запуске новой задачи. Опишите, чего вы хотите добиться, укажите соответствующие файлы и объясните, как проверить результат.', + guideStandardExplanation: [ + '### Как это работает', + 'Агент напрямую вызывает инструменты для чтения и редактирования файлов, поиска и выполнения команд терминала. Он включает в себя навыки, планирование, цели, субагенты, рабочие процессы, управление плагинами и сжатие контекста.', + '### Когда это выбирать', + 'Начните здесь ежедневное программирование, работу с файлами и исследования. Стандартный режим также позволяет писать сценарии и обрабатывать файлы в пакетном режиме. PTC изменяет организацию вызовов инструментов; для пакетных задач это не требуется.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Исправить ошибку', + '> Узнайте, почему при двойной отправке формы поиска результаты исчезают. Исправьте это и запустите соответствующие тесты. Объясните причину и что изменилось.', + 'Ожидаемый результат: изменение кода, соответствующие результаты испытаний и объяснение причины.', + '### Организация заметок по проекту', + '> Прочтите примечания Markdown в этом проекте. Кратко изложите согласованные решения и открытые вопросы со ссылками на исходные файлы.', + 'Ожидаемый результат: резюме со ссылками, которые вы можете сверить с исходными заметками.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Выберите режим PTC при запуске новой задачи. Укажите входные файлы, правила обработки и выходной формат. Агент пишет код.', + guidePtcExplanation: [ + '### Как называются инструменты', + 'PTC означает программный вызов инструмента. В этой встроенной настройке агент использует run_code для написания программы TypeScript, которая вызывает инструменты через сгенерированный SDK. Программа может использовать циклы, условия, обработку ошибок и параллельные вызовы, где это необходимо.', + '### Что достигает модели', + 'Результаты инструмента сначала поступают в программу, которая может их фильтровать и комбинировать. Модель получает то, что программа печатает или возвращает; Результаты изображения прилагаются отдельно. Вызовы вложенных инструментов по-прежнему записываются, и для них действуют разрешения инструмента.', + '### По сравнению со стандартным режимом', + 'Оба режима могут выполнять кодирование и пакетные задачи. Стандартный режим напрямую предоставляет доступ к отдельным инструментам; PTC организует вызовы инструментов в коде. Текущая предустановка PTC оставляет инструменты управления рабочим процессом и плагинами отключенными. Скорость и использование токенов зависят от задачи и того, как программа обрабатывает ее результаты.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Проверка набора файлов конфигурации', + '> Проверьте все файлы JSON в configs/. Перечислите отсутствующие обязательные поля и недопустимые значения в файле Schema.json. Сохраните CSV-файл с одной строкой на каждую проблему. Включите в отчет нечитаемые файлы и продолжайте проверять остальные. Оставьте исходные файлы без изменений.', + 'Ожидаемый результат: краткое описание проблемы и отчет в формате CSV. Программа может повторять одни и те же проверки, обрабатывать отдельные сбои и собирать результаты.', + '### Обобщение журналов ошибок', + '> Проанализируйте файлы журналов в logs/. Группируйте ошибки по сервису и типу ошибки. Покажите десять наиболее часто встречающихся групп и по одному примеру из каждой. Сохраните полные данные в формате CSV.', + 'Ожидаемый результат: основные группы ошибок и полная таблица подсчета. Промежуточные данные могут быть агрегированы в программе до того, как сводка достигнет модели.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Выберите минимальный режим для новой задачи. Для сравнения сохраняйте постоянство модели, разрешений, входных данных и состояния начальной рабочей области во всех запусках.', + guideMinimalExplanation: [ + '### Что включено', + 'Один постоянный инструмент оболочки read_image и фиксированное системное приглашение. Для чтения изображений требуется модель с поддержкой изображений. Старая история автоматически сжимается под давлением контекста; используйте /compact, пока агент простаивает, чтобы сжать его вручную. Встроенная предустановка не загружает навыки, планирование или стандартный контекст времени выполнения.', + '### Когда это выбирать', + 'Используйте его как основу для экспериментов и сравнений. Он по-прежнему может читать файлы и выполнять сценарии с помощью команд оболочки, но предлагает меньше встроенных способов управления длительными задачами. Меньшее количество инструментов не обязательно облегчает работу новичку.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Сравните производительность с исправлением небольшой ошибки', + '> Запустите тесты для этого проекта, найдите причину сбоя и внесите минимальные исправления. Запустите соответствующие тесты еще раз и сообщите о результате.', + 'Запустите одну и ту же задачу отдельно в стандартном и минимальном режимах из одного и того же начального состояния. Сравните выполнение задач, вызовы инструментов и результирующие изменения. Минимальный режим выполняет работу посредством команд терминала.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Выберите режим «Создатель» для новой задачи. Опишите желаемую возможность, где она должна появиться и как вы ее проверите.', + guideCordisExplanation: [ + '### Что вы можете создать', + 'Режим Creator включает в себя стандартные инструменты задач, а также проверку во время выполнения и рекомендации по созданию подключаемых модулей Cordis и предустановок агента. Он может создать плагин, добавляющий возможности или пользовательский интерфейс, или предустановку, объединяющую инструменты и подсказки для конкретной работы.', + '### Плагины и режимы', + 'Плагин добавляет к DSH возможности, такие как инструмент, подключение к службе или запись пользовательского интерфейса. Режим — это предустановка агента, которая выбирает инструменты и определяет, как агент работает над задачей. Плагин может быть включен в пользовательский пресет.', + '### Как результат вступает в силу', + 'Попросите агента установить и проверить результат, а не просто генерировать исходный код. Плагин может загрузиться немедленно или потребовать перезагрузки, в зависимости от того, что он меняет. Вновь созданный пресет выбирается при запуске новой задачи.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Добавьте пользовательский интерфейс', + '> Создайте плагин DSH, который добавляет запись примечаний к проекту на боковую панель. Позвольте мне просмотреть файлы Markdown в этом рабочем пространстве и просмотреть выбранную заметку. Установите его и убедитесь, что страница открывается.', + 'Ожидаемый результат: установленный плагин с работающей страницей входа и предварительного просмотра, а также все оставшиеся шаги активации.', + '### Добавить инструмент', + '> Создайте плагин с инструментом, который читает отчет об испытаниях этого проекта и суммирует неудачные тесты. Зарегистрируйте его и проверьте с помощью образца отчета.', + 'Ожидаемый результат: плагин с вызываемым инструментом и проверенным примером вызова.', + '### Создать свой режим', + '> Создайте режим «Проверка кода» на основе стандартного режима. Попросите его определить приоритетность потенциальных ошибок и проверить пробелы, указать пути и строки к файлам и спросить, прежде чем изменять файлы. Сохраните его как выбираемую предустановку.', + 'Ожидаемый результат: индивидуальная настройка для новых задач. Эти инструкции по проверке помогут агенту; настройки разрешений определяют, какие действия он может выполнять.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** Portuguese dictionary, checked against the English keys. */ +export const guidePt = { + modeExplanation: 'Detalhes do modo', + howToUse: 'Como usar', + guideSections: 'Seções do guia', + guideExampleTask: 'Tarefa de exemplo', + guideCopy: 'Copiar', + guideCopied: 'Copiado', + guideFootnotes: 'Notas de rodapé', + guideStandardIntro: 'Escolha o modo Padrão ao iniciar uma nova tarefa. Descreva o que você deseja realizar, indique os arquivos relevantes e explique como verificar o resultado.', + guideStandardExplanation: [ + '### Como funciona', + 'O agente chama ferramentas diretamente para ler e editar arquivos, pesquisar e executar comandos de terminal. Inclui habilidades, planejamento, metas, subagentes, fluxos de trabalho, gerenciamento de plugins e compactação de contexto.', + '### Quando escolher', + 'Comece aqui para codificação diária, trabalho de arquivo e pesquisa. O modo padrão também pode escrever scripts e processar arquivos em lotes. PTC altera a forma como as chamadas de ferramenta são organizadas; não é necessário para tarefas em lote.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Corrigir um bug', + '> Descubra por que enviar o formulário de pesquisa duas vezes faz com que os resultados desapareçam. Corrija-o e execute os testes relevantes. Explique a causa e o que mudou.', + 'Resultado esperado: uma alteração de código, os resultados de teste relevantes e uma explicação da causa.', + '### Organize notas do projeto', + '> Leia as Markdown notas deste projeto. Resuma as decisões acordadas e as questões abertas, com links para os arquivos originais.', + 'Resultado esperado: um resumo com referências que você pode comparar com as notas originais.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Escolha o modo PTC ao iniciar uma nova tarefa. Especifique os arquivos de entrada, regras de processamento e formato de saída. O agente escreve o código.', + guidePtcExplanation: [ + '### Como as ferramentas são chamadas', + 'PTC significa chamada de ferramenta programática. Nesta predefinição integrada, o agente usa run_code para escrever um programa TypeScript que chama ferramentas por meio de um SDK gerado. O programa pode usar loops, condições, tratamento de erros e chamadas simultâneas quando apropriado.', + '### O que chega ao modelo', + 'Os resultados da ferramenta chegam primeiro ao programa, que pode filtrá-los e combiná-los. O modelo recebe o que o programa imprime ou retorna; os resultados da imagem são anexados separadamente. As chamadas de ferramentas aninhadas ainda são gravadas e permanecem sujeitas às permissões da ferramenta.', + '### Comparado com o modo Padrão', + 'Ambos os modos podem lidar com tarefas de codificação e em lote. O modo padrão expõe ferramentas individuais diretamente; PTC organiza chamadas de ferramentas em código. A predefinição PTC atual deixa o fluxo de trabalho e as ferramentas de gerenciamento de plug-ins desativados. A velocidade e o uso do token dependem da tarefa e de como o programa lida com seus resultados.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Verifique um conjunto de arquivos de configuração', + '> Verifique todos os arquivos JSON em configs/. Liste os campos obrigatórios ausentes e os valores inválidos em relação ao schema.json. Salve um CSV com uma linha por problema. Inclua arquivos ilegíveis no relatório e continue verificando o restante. Deixe os arquivos originais inalterados.', + 'Resultado esperado: um resumo do problema e um relatório CSV. O programa pode repetir as mesmas verificações, tratar falhas individuais e coletar os resultados.', + '### Resumir logs de erros', + '> Analise os arquivos de log em logs/. Agrupe erros por serviço e tipo de erro. Mostre os dez grupos mais frequentes e um exemplo de cada. Salve as contagens completas em um CSV.', + 'Resultado esperado: os principais grupos de erros e uma tabela de contagem completa. Os dados intermediários podem ser agregados no programa antes que o resumo chegue ao modelo.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Escolha o modo Mínimo para uma nova tarefa. Para uma comparação, mantenha o modelo, as permissões, a entrada e o estado inicial do espaço de trabalho constantes em todas as execuções.', + guideMinimalExplanation: [ + '### O que está incluído', + 'Uma ferramenta de shell persistente, read_image, e um prompt de sistema fixo. A leitura de imagens requer um modelo com capacidade de imagem. O histórico mais antigo é compactado automaticamente sob pressão do contexto; use /compact enquanto o agente estiver ocioso para compactá-lo manualmente. A predefinição integrada não carrega habilidades, planejamento ou contexto de tempo de execução padrão.', + '### Quando escolher', + 'Use-o como base para experimentos e comparações. Ele ainda pode ler arquivos e executar scripts por meio de comandos shell, mas oferece menos maneiras integradas de gerenciar uma tarefa longa. Menos ferramentas não torna necessariamente mais fácil para um iniciante.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Compare o desempenho em uma pequena correção de bug', + '> Execute os testes para este projeto, encontre a causa da falha e faça a menor correção. Execute os testes relevantes novamente e relate o resultado.', + 'Execute a mesma tarefa separadamente nos modos Padrão e Mínimo a partir do mesmo estado inicial. Compare a conclusão da tarefa, as chamadas de ferramentas e as alterações resultantes. O modo mínimo executa o trabalho por meio de comandos de terminal.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Escolha o modo Criador para uma nova tarefa. Descreva o recurso desejado, onde ele deve aparecer e como você o verificará.', + guideCordisExplanation: [ + '### O que você pode criar', + 'O modo Criador inclui ferramentas de tarefas padrão, além de inspeção de tempo de execução e orientação para criação de Cordis plug-ins e predefinições de agente. Ele pode criar um plug-in que adicione um recurso ou UI, ou uma predefinição que combine ferramentas e prompts para um trabalho específico.', + '### Plug-ins e modos', + 'Um plug-in adiciona recursos a DSH, como uma ferramenta, uma conexão de serviço ou uma entrada de IU. Um modo é uma predefinição de agente que seleciona ferramentas e define como o agente trabalha em uma tarefa. Um plugin pode ser incluído em uma predefinição personalizada.', + '### Como o resultado entra em vigor', + 'Peça ao agente para instalar e verificar o resultado, não apenas gerar o código-fonte. Um plug-in pode carregar imediatamente ou exigir uma reinicialização, dependendo do que for alterado. Uma predefinição recém-criada é selecionada ao iniciar uma nova tarefa.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Adicionar uma IU', + '> Crie um plug-in DSH que adicione uma entrada de notas do projeto à barra lateral. Deixe-me navegar pelos arquivos Markdown nesta área de trabalho e visualizar uma nota selecionada. Instale-o e verifique se a página abre.', + 'Resultado esperado: um plug-in instalado com uma entrada funcional e uma página de visualização, além de quaisquer etapas de ativação restantes.', + '### Adicionar uma ferramenta', + '> Crie um plugin com uma ferramenta que leia o relatório de teste deste projeto e resuma os testes que falharam. Registre-o e verifique-o com um relatório de amostra.', + 'Resultado esperado: um plugin com uma ferramenta que pode ser chamada e uma chamada de amostra verificada.', + '### Crie meu próprio modo', + '> Crie um modo “Revisão de código” baseado no modo Padrão. Faça com que ele priorize possíveis bugs e teste lacunas, cite caminhos e linhas de arquivos e pergunte antes de modificar arquivos. Salve-o como uma predefinição selecionável.', + 'Resultado esperado: uma predefinição personalizada para novas tarefas. Estas instruções de revisão orientam o agente; as configurações de permissão determinam quais ações ele pode executar.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 0d9d61e621..b9e1363c6d 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -42,7 +42,7 @@ import { AgentPresetSection } from './AgentPresetSection.tsx' import type { AgentPresetSectionInjected } from './AgentPresetSection.tsx' import { AgentPresetSeatController, type AgentPresetStage } from './seat-store.ts' import { AgentPresetSectionController } from './section-store.ts' -import { en, ja, zh, type AgentPresetSettingsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AgentPresetSettingsKey } from './locales.ts' import { AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController } from './settings-store.ts' import { analyticsMode } from './analytics.ts' @@ -128,7 +128,7 @@ export function apply(ctx: ClientContext): void { return binding === undefined ? undefined : seatFor(binding) } - ctx.effect(() => ctx.locale.register('settings.agentPreset', { zh, en, ja }), 'ui-agent-preset: settings row dictionaries') + ctx.effect(() => ctx.locale.register('settings.agentPreset', { zh, en, ja, es, fr, de, ru, pt }), 'ui-agent-preset: settings row dictionaries') ctx.effect(() => { let requested = 0 diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index 00fce156ee..f5a3c8ed67 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -1,6 +1,6 @@ /** Locale bundles for the agent-preset hero chip, header label, and management section. */ -import { guideEn, guideJa, guideZh, type PresetGuideKey } from './guide-locales.ts' +import { guideEn, guideJa, guideEs, guideFr, guideDe, guideRu, guidePt, guideZh, type PresetGuideKey } from './guide-locales.ts' /** Locale keys these surfaces render. */ export type AgentPresetSettingsKey = @@ -158,3 +158,228 @@ export const ja = { 'createPluginUnavailable': '一時的に利用できません。このメニューを開き直して再試行してください', 'createPluginMissing': 'この構成にはクリエイターモードが含まれていません', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...guideEs, + builtInGroup: 'Incorporado', customGroup: 'personalizado', + sectionIntro: 'Elija las herramientas del agente y cómo funciona. Utilice el modo Estándar para las tareas diarias o el modo Creador para agregar capacidades a DSH.', + + seatHint: 'Elija el agente preestablecido para su nueva tarea', + headerHint: 'El agente preestablecido elegido cuando comenzó esta tarea', + nav: 'Ajustes preestablecidos del agente', + + setDefault: 'Establecer como nueva tarea predeterminada', + view: 'Ver configuración', + + presetStandardName: 'Modo estándar', + presetStandardDescription: + 'Trabaje con código, archivos e información. Adecuado para la mayoría de las tareas, con búsqueda, edición, comandos de terminal y otras herramientas disponibles según sea necesario.', + presetPtcName: 'Modo PTC', + presetPtcDescription: + 'Incluye todas las capacidades del modo Estándar. Más adecuado para tareas que llaman a herramientas en lotes y luego filtran, organizan, deduplican, cuentan o resumen los resultados.', + presetMinimalName: 'modo mínimo', + presetMinimalDescription: + 'El agente utiliza un terminal y lectura de imágenes, con compactación de contexto. Útil para probar y comparar su rendimiento básico.', + presetCordisName: 'Modo creador', + presetCordisDescription: + 'Personaliza DSH a través de la conversación. Deje que el agente escriba complementos que agreguen funciones o interfaz de usuario, o combine herramientas e indicaciones para crear su propio modo.', + + inUse: 'Nueva tarea predeterminada', + + noDescription: 'Sin descripción.', + brokenBadge: 'No se pudo cargar', + + switchRefused: 'No se pudo cambiar a {name}: {reason}', + standardUnavailable: 'El modo estándar no está disponible. Restáuralo o elige otro modo disponible.', + + close: 'Cerrar', + + creatorDraft: 'Deja que el agente me ayude a crear un preset', + createPlugin: 'Deje que el agente cree un complemento', + createPluginDescription: 'Ingrese al modo Creador y cree su propio complemento DSH', + createPluginChecking: 'Comprobando si el modo Creador está disponible', + createPluginUnavailable: 'Temporalmente no disponible. Vuelve a abrir este menú para volver a intentarlo.', + createPluginMissing: 'El modo creador no está incluido en esta configuración.', + +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...guideFr, + builtInGroup: 'Intégré', customGroup: 'Personnalisé', + sectionIntro: 'Choisissez les outils de l’agent et son fonctionnement. Utilisez le mode Standard pour les tâches quotidiennes ou le mode Créateur pour ajouter des fonctionnalités à DSH.', + + seatHint: "Choisissez le préréglage d'agent pour votre nouvelle tâche", + headerHint: "Le préréglage d'agent choisi au démarrage de cette tâche", + nav: "Préréglages d'agent", + + setDefault: 'Définir comme nouvelle tâche par défaut', + view: 'Afficher la configuration', + + presetStandardName: 'Mode standard', + presetStandardDescription: + 'Travaillez avec du code, des fichiers et des informations. Convient à la plupart des tâches, avec recherche, édition, commandes de terminal et autres outils disponibles selon les besoins.', + presetPtcName: 'Mode PTC', + presetPtcDescription: + 'Inclut toutes les fonctionnalités du mode Standard. Mieux adapté aux tâches qui appellent des outils par lots, puis filtrent, organisent, dédupliquent, comptent ou résument les résultats.', + presetMinimalName: 'Mode minimal', + presetMinimalDescription: + "L'agent utilise un terminal et une lecture d'image, avec compactage du contexte. Utile pour tester et comparer ses performances de base.", + presetCordisName: 'Mode Créateur', + presetCordisDescription: + "Personnalisez DSH via la conversation. Laissez l'agent écrire des plugins qui ajoutent des fonctionnalités ou une interface utilisateur, ou combinez des outils et des invites pour créer votre propre mode.", + + inUse: 'Nouvelle tâche par défaut', + + noDescription: 'Aucune description.', + brokenBadge: 'Échec du chargement', + + switchRefused: 'Impossible de passer à {name} : {reason}', + standardUnavailable: "Le mode standard n'est pas disponible. Restaurez-le ou choisissez un autre mode disponible.", + + close: 'Fermer', + + creatorDraft: "Laissez l'agent m'aider à créer un préréglage", + createPlugin: "Laissez l'agent créer un plugin", + createPluginDescription: 'Entrez en mode Créateur et créez votre propre plugin DSH', + createPluginChecking: 'Vérifier si le mode Créateur est disponible', + createPluginUnavailable: 'Temporairement indisponible. Rouvrez ce menu pour réessayer', + createPluginMissing: "Le mode Créateur n'est pas inclus dans cette configuration", + +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...guideDe, + builtInGroup: 'Eingebaut', customGroup: 'Benutzerdefiniert', + sectionIntro: 'Wählen Sie die Tools des Agenten und deren Funktionsweise aus. Verwenden Sie den Standardmodus für alltägliche Aufgaben oder den Creator-Modus, um DSH Funktionen hinzuzufügen.', + + seatHint: 'Wählen Sie die Agentenvoreinstellung für Ihre neue Aufgabe', + headerHint: 'Die Agentenvoreinstellung, die beim Start dieser Aufgabe ausgewählt wurde', + nav: 'Agentenvoreinstellungen', + + setDefault: 'Als Standard für neue Aufgabe festlegen', + view: 'Konfiguration anzeigen', + + presetStandardName: 'Standardmodus', + presetStandardDescription: + 'Arbeiten Sie mit Code, Dateien und Informationen. Geeignet für die meisten Aufgaben, da bei Bedarf Suche, Bearbeitung, Terminalbefehle und andere Tools verfügbar sind.', + presetPtcName: 'PTC-Modus', + presetPtcDescription: + 'Beinhaltet alle Funktionen des Standardmodus. Besser geeignet für Aufgaben, bei denen Tools stapelweise aufgerufen und die Ergebnisse dann gefiltert, organisiert, dedupliziert, gezählt oder zusammengefasst werden.', + presetMinimalName: 'Minimalmodus', + presetMinimalDescription: + 'Der Agent verwendet ein Terminal und das Lesen von Bildern mit Kontextkomprimierung. Nützlich zum Testen und Vergleichen der Grundleistung.', + presetCordisName: 'Erstellermodus', + presetCordisDescription: + 'Passen Sie DSH durch Gespräche an. Lassen Sie den Agenten Plugins schreiben, die Funktionen oder Benutzeroberflächen hinzufügen, oder kombinieren Sie Tools und Eingabeaufforderungen, um Ihren eigenen Modus zu erstellen.', + + inUse: 'Neuer Aufgabenstandard', + + noDescription: 'Keine Beschreibung.', + brokenBadge: 'Laden fehlgeschlagen', + + switchRefused: 'Konnte nicht zu {name} wechseln: {reason}', + standardUnavailable: 'Der Standardmodus ist nicht verfügbar. Stellen Sie es wieder her oder wählen Sie einen anderen verfügbaren Modus.', + + close: 'Schließen', + + creatorDraft: 'Lassen Sie sich vom Agenten beim Erstellen einer Voreinstellung helfen', + createPlugin: 'Lassen Sie den Agenten ein Plugin erstellen', + createPluginDescription: 'Wechseln Sie in den Erstellermodus und erstellen Sie Ihr eigenes DSH-Plugin', + createPluginChecking: 'Prüfen, ob der Creator-Modus verfügbar ist', + createPluginUnavailable: 'Vorübergehend nicht verfügbar. Öffnen Sie dieses Menü erneut, um es erneut zu versuchen', + createPluginMissing: 'Der Creator-Modus ist in dieser Konfiguration nicht enthalten', + +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...guideRu, + builtInGroup: 'Встроенный', customGroup: 'Пользовательский', + sectionIntro: 'Выбирайте инструменты агента и то, как они работают. Используйте стандартный режим для повседневных задач или режим Creator, чтобы расширить возможности DSH.', + + seatHint: 'Выберите предустановку агента для вашей новой задачи', + headerHint: 'Предварительная настройка агента, выбранная при запуске этой задачи', + nav: 'Настройки агента', + + setDefault: 'Установить в качестве новой задачи по умолчанию', + view: 'Посмотреть конфигурацию', + + presetStandardName: 'Стандартный режим', + presetStandardDescription: + 'Работайте с кодом, файлами и информацией. Подходит для большинства задач, с поиском, редактированием, командами терминала и другими инструментами, доступными по мере необходимости.', + presetPtcName: 'Режим PTC', + presetPtcDescription: + 'Включает все возможности стандартного режима. Лучше подходит для задач, которые вызывают инструменты в пакетном режиме, а затем фильтруют, организуют, дедуплицируют, подсчитывают или суммируют результаты.', + presetMinimalName: 'Минимальный режим', + presetMinimalDescription: + 'Агент использует терминал и чтение изображений со сжатием контекста. Полезно для тестирования и сравнения его базовой производительности.', + presetCordisName: 'Режим автора', + presetCordisDescription: + 'Настройте DSH через разговор. Позвольте агенту писать плагины, которые добавляют функции или пользовательский интерфейс, или комбинируйте инструменты и подсказки для создания собственного режима.', + + inUse: 'Новая задача по умолчанию', + + noDescription: 'Нет описания.', + brokenBadge: 'Не удалось загрузить', + + switchRefused: 'Не удалось переключиться на {name}: {reason}', + standardUnavailable: 'Стандартный режим недоступен. Восстановите его или выберите другой доступный режим.', + + close: 'Закрыть', + + creatorDraft: 'Позвольте агенту помочь мне создать пресет', + createPlugin: 'Позвольте агенту создать плагин', + createPluginDescription: 'Войдите в режим Creator и создайте свой собственный плагин DSH.', + createPluginChecking: 'Проверка доступности режима Creator', + createPluginUnavailable: 'Временно недоступен. Откройте это меню еще раз, чтобы повторить попытку.', + createPluginMissing: 'Режим Creator не включен в эту конфигурацию.', + +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...guidePt, + builtInGroup: 'Integrado', customGroup: 'Personalizado', + sectionIntro: 'Escolha as ferramentas do agente e como funciona. Use o modo Padrão para tarefas diárias ou o modo Criador para adicionar recursos ao DSH.', + + seatHint: 'Escolha a predefinição de agente para sua nova tarefa', + headerHint: 'A predefinição do agente escolhida quando esta tarefa foi iniciada', + nav: 'Predefinições de agente', + + setDefault: 'Definir como nova tarefa padrão', + view: 'Ver configuração', + + presetStandardName: 'Modo padrão', + presetStandardDescription: + 'Trabalhe com código, arquivos e informações. Adequado para a maioria das tarefas, com pesquisa, edição, comandos de terminal e outras ferramentas disponíveis conforme necessário.', + presetPtcName: 'modo PTC', + presetPtcDescription: + 'Inclui todos os recursos do modo Padrão. Mais adequado para tarefas que chamam ferramentas em lotes e depois filtram, organizam, desduplicam, contam ou resumem os resultados.', + presetMinimalName: 'Modo mínimo', + presetMinimalDescription: + 'O agente utiliza terminal e leitura de imagens, com compactação de contexto. Útil para testar e comparar seu desempenho básico.', + presetCordisName: 'Modo criador', + presetCordisDescription: + 'Personalize DSH por meio de conversa. Deixe o agente escrever plug-ins que adicionem recursos ou UI, ou combine ferramentas e prompts para criar seu próprio modo.', + + inUse: 'Novo padrão de tarefa', + + noDescription: 'Nenhuma descrição.', + brokenBadge: 'Falha ao carregar', + + switchRefused: 'Não foi possível mudar para {name}: {reason}', + standardUnavailable: 'O modo padrão não está disponível. Restaure-o ou escolha outro modo disponível.', + + close: 'Fechar', + + creatorDraft: 'Deixe o agente me ajudar a criar uma predefinição', + createPlugin: 'Deixe o agente criar um plugin', + createPluginDescription: 'Entre no modo Criador e crie seu próprio plug-in DSH', + createPluginChecking: 'Verificando se o modo Criador está disponível', + createPluginUnavailable: 'Temporariamente indisponível. Reabra este menu para tentar novamente', + createPluginMissing: 'O modo criador não está incluído nesta configuração', + +} satisfies Record diff --git a/packages/client/ui-approval/locale/de.json b/packages/client/ui-approval/locale/de.json new file mode 100644 index 0000000000..ee89d86fe1 --- /dev/null +++ b/packages/client/ui-approval/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Betriebsgenehmigung", + "description": "Zeigt Aktionen an, deren Genehmigung noch aussteht, und führt Benutzerauswahlen zu laufenden Aufgaben zurück." + } +} diff --git a/packages/client/ui-approval/locale/es.json b/packages/client/ui-approval/locale/es.json new file mode 100644 index 0000000000..115786cd1c --- /dev/null +++ b/packages/client/ui-approval/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de aprobación de operación", + "description": "Muestra acciones pendientes de aprobación y devuelve las selecciones del usuario a las tareas en ejecución." + } +} diff --git a/packages/client/ui-approval/locale/fr.json b/packages/client/ui-approval/locale/fr.json new file mode 100644 index 0000000000..69768c0bab --- /dev/null +++ b/packages/client/ui-approval/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'approbation des opérations", + "description": "Affiche les actions en attente d'approbation et renvoie les sélections de l'utilisateur aux tâches en cours d'exécution." + } +} diff --git a/packages/client/ui-approval/locale/pt.json b/packages/client/ui-approval/locale/pt.json new file mode 100644 index 0000000000..7fe00b6570 --- /dev/null +++ b/packages/client/ui-approval/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de aprovação de operação", + "description": "Exibe ações com aprovação pendente e retorna as seleções do usuário para tarefas em execução." + } +} diff --git a/packages/client/ui-approval/locale/ru.json b/packages/client/ui-approval/locale/ru.json new file mode 100644 index 0000000000..b656b6b2d5 --- /dev/null +++ b/packages/client/ui-approval/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс одобрения операций", + "description": "Отображает действия, ожидающие утверждения, и возвращает выбранные пользователем действия к выполняемым задачам." + } +} diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 13d49344cc..9ab90c9d66 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-approval/src/client/index.ts b/packages/client/ui-approval/src/client/index.ts index a667d507e9..4073a85aa2 100644 --- a/packages/client/ui-approval/src/client/index.ts +++ b/packages/client/ui-approval/src/client/index.ts @@ -11,7 +11,7 @@ import type { ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client import { ApprovalCard } from './ApprovalCard.tsx' import { ApprovalPanel } from './ApprovalPanel.tsx' import { PendingApproval } from './contract/slots.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { ApprovalCardInput, @@ -77,7 +77,7 @@ async function answerApproval( * @param ctx - Client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-approval: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-approval: dictionaries') ctx.inject(['shortcuts'], (scope) => { const t = ctx.locale.bind(NS) scope.effect(() => scope.shortcuts.registerFixed({ diff --git a/packages/client/ui-approval/src/client/locales.ts b/packages/client/ui-approval/src/client/locales.ts index c8e540fd49..99408815d9 100644 --- a/packages/client/ui-approval/src/client/locales.ts +++ b/packages/client/ui-approval/src/client/locales.ts @@ -35,3 +35,58 @@ export const ja = { 'allowOnce': '1 回だけ許可', 'allowAlways': '常に許可', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + waiting: 'Esperando aprobación', + 'detail.aria': 'Detalles de aprobación', + escalation: 'La herramienta {toolName} solicita ejecución privilegiada', + reject: 'Rechazar', + allow: 'Permitir', + allowOnce: 'permitir una vez', + allowAlways: 'Permitir siempre', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + waiting: "En attente d'approbation", + 'detail.aria': "Détails de l'approbation", + escalation: "L'outil {toolName} demande une exécution privilégiée", + reject: 'Rejeter', + allow: 'Autoriser', + allowOnce: 'Autoriser une fois', + allowAlways: 'Toujours autoriser', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + waiting: 'Warten auf Genehmigung', + 'detail.aria': 'Einzelheiten zur Genehmigung', + escalation: 'Tool {toolName} fordert privilegierte Ausführung an', + reject: 'Ablehnen', + allow: 'Erlauben', + allowOnce: 'Einmal zulassen', + allowAlways: 'Immer zulassen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + waiting: 'Ожидание одобрения', + 'detail.aria': 'Детали утверждения', + escalation: 'Инструмент {toolName} запрашивает привилегированное выполнение', + reject: 'Отклонить', + allow: 'Разрешить', + allowOnce: 'Разрешить один раз', + allowAlways: 'Всегда разрешать', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + waiting: 'Aguardando aprovação', + 'detail.aria': 'Detalhes de aprovação', + escalation: 'A ferramenta {toolName} solicita execução privilegiada', + reject: 'Rejeitar', + allow: 'Permitir', + allowOnce: 'Permitir uma vez', + allowAlways: 'Sempre permitir', +} satisfies Record diff --git a/packages/client/ui-attachment/locale/de.json b/packages/client/ui-attachment/locale/de.json new file mode 100644 index 0000000000..07c79593b0 --- /dev/null +++ b/packages/client/ui-attachment/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anhangsanzeige", + "description": "Zeigen Sie Anhänge und Bilder in Nachrichteneingaben, Konversationen und Ausführungsprotokollen an." + } +} diff --git a/packages/client/ui-attachment/locale/es.json b/packages/client/ui-attachment/locale/es.json new file mode 100644 index 0000000000..635cff5f3d --- /dev/null +++ b/packages/client/ui-attachment/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización de archivos adjuntos", + "description": "Muestre archivos adjuntos e imágenes en la entrada de mensajes, conversaciones y registros de ejecución." + } +} diff --git a/packages/client/ui-attachment/locale/fr.json b/packages/client/ui-attachment/locale/fr.json new file mode 100644 index 0000000000..c89a8bf6f9 --- /dev/null +++ b/packages/client/ui-attachment/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage des pièces jointes", + "description": "Affichez les pièces jointes et les images dans la saisie des messages, les conversations et les journaux d'exécution." + } +} diff --git a/packages/client/ui-attachment/locale/pt.json b/packages/client/ui-attachment/locale/pt.json new file mode 100644 index 0000000000..6295a88f6d --- /dev/null +++ b/packages/client/ui-attachment/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de anexo", + "description": "Exiba anexos e imagens em entradas de mensagens, conversas e registros de execução." + } +} diff --git a/packages/client/ui-attachment/locale/ru.json b/packages/client/ui-attachment/locale/ru.json new file mode 100644 index 0000000000..8f81ace343 --- /dev/null +++ b/packages/client/ui-attachment/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение вложений", + "description": "Отображение вложений и изображений при вводе сообщений, разговорах и журналах запуска." + } +} diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index 13b2018090..b63d7f9ce2 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/locale/de.json b/packages/client/ui-brand-official/locale/de.json new file mode 100644 index 0000000000..953f8dbf7e --- /dev/null +++ b/packages/client/ui-brand-official/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Markendarstellung", + "description": "Zeigen Sie offizielle Markenelemente an Orten wie der App-Seitenleiste an." + } +} diff --git a/packages/client/ui-brand-official/locale/es.json b/packages/client/ui-brand-official/locale/es.json new file mode 100644 index 0000000000..108c571dbe --- /dev/null +++ b/packages/client/ui-brand-official/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Visualización de marca", + "description": "Muestre elementos oficiales de la marca en lugares como la barra lateral de la aplicación." + } +} diff --git a/packages/client/ui-brand-official/locale/fr.json b/packages/client/ui-brand-official/locale/fr.json new file mode 100644 index 0000000000..5a47875c99 --- /dev/null +++ b/packages/client/ui-brand-official/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Affichage de la marque", + "description": "Affichez les éléments officiels de la marque dans des endroits tels que la barre latérale de l'application." + } +} diff --git a/packages/client/ui-brand-official/locale/pt.json b/packages/client/ui-brand-official/locale/pt.json new file mode 100644 index 0000000000..2b06cd6c7a --- /dev/null +++ b/packages/client/ui-brand-official/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Exibição da marca", + "description": "Exiba elementos oficiais da marca em locais como a barra lateral do aplicativo." + } +} diff --git a/packages/client/ui-brand-official/locale/ru.json b/packages/client/ui-brand-official/locale/ru.json new file mode 100644 index 0000000000..e3e9d5cead --- /dev/null +++ b/packages/client/ui-brand-official/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Дисплей бренда", + "description": "Отображайте официальные элементы бренда в таких местах, как боковая панель приложения." + } +} diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index e4aef838c4..e70b38f4d2 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index d3e5dc6506..7cc337dbb2 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -24,8 +24,8 @@ en: 41dcd70e14ebe9cc zh: ededead572b59037 /deepseek-ai-dsh-client-ui-chat/turn-token-usage: - en: 059eef868161efe4 - zh: 52ecc8e682d4c630 + en: cfdfe3d99d3f4e09 + zh: 9330bad3d3f7a689 /deepseek-ai-dsh-client-ui-chat/completed-turn-footer: en: 382821ca3ffa60dd zh: 9840ffa07c13082a diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index b5dc85a0f9..85b3e65c8d 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -70,7 +70,7 @@ A completed Turn shows an expandable usage row only when the loaded window inclu Settings → General → Performance & usage appears after Send behavior while busy and before Session Log upload, and stores `ui-chat.performanceUsage` as `detailed` (default) or `compact`. Compact shows only available output speed and cache-hit percentage beneath the composer, without interactive statistic dialogs or per-Turn usage. Detailed exposes session statistics and per-Turn token usage. Neither mode shows elapsed time in the completed-turn footer. The preference changes presentation only; accounting and Session events remain intact. -The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. +The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. When an ellipsis cuts off a reading, a tooltip shows the full reading. A dialog control shows it on hover or keyboard focus, but not while its dialog is open; a reading without a dialog shows it on hover, click, or tap. The session statistics dialog heads its rows with the turn and step counts. Explicit selections update every consumer immediately; accepted Host settings reconcile the live value, so a refused or pending write keeps the selected mode until the Host answers. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 06d1d924b2..486da291e6 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -70,7 +70,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node, 设置 → 通用设置 → 性能与用量位于繁忙时的发送行为之后、Session Log 上传之前,将 `ui-chat.performanceUsage` 保存为 `detailed`(默认)或 `compact`。简洁模式仅在输入框下方显示可用的输出速度和缓存命中率,不显示统计交互卡片或每轮用量。详细模式提供会话统计和每轮 token 用量。两种模式的已完成轮次页脚均不显示耗时。该偏好仅影响展示,记账和 Session 事件保持完整。 -输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。 +输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。读数被省略号截断时,提示气泡会显示完整读数。带对话框的控件在悬停或键盘聚焦时显示,对话框打开期间不显示;没有对话框的读数在悬停、点击或轻触时显示。会话统计对话框的标题行显示轮次数与步骤数。 明确选择会立即更新所有使用方;Host 接受的设置会校正实时值,因此写入被拒绝或尚未应答时保持所选模式,直到 Host 应答。 diff --git a/packages/client/ui-chat/locale/de.json b/packages/client/ui-chat/locale/de.json new file mode 100644 index 0000000000..15ae984c6b --- /dev/null +++ b/packages/client/ui-chat/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anzeige von Chat-Inhalten", + "description": "Rendern Sie Chat-Nachrichten, Tool-Inhalte und zugehörige Details." + } +} diff --git a/packages/client/ui-chat/locale/es.json b/packages/client/ui-chat/locale/es.json new file mode 100644 index 0000000000..eb088700ac --- /dev/null +++ b/packages/client/ui-chat/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización del contenido del chat", + "description": "Representa mensajes de chat, contenido de herramientas y detalles relacionados." + } +} diff --git a/packages/client/ui-chat/locale/fr.json b/packages/client/ui-chat/locale/fr.json new file mode 100644 index 0000000000..95efa01a16 --- /dev/null +++ b/packages/client/ui-chat/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage du contenu du chat", + "description": "Rendre les messages de discussion, le contenu de l'outil et les détails associés." + } +} diff --git a/packages/client/ui-chat/locale/pt.json b/packages/client/ui-chat/locale/pt.json new file mode 100644 index 0000000000..0a5efa2be2 --- /dev/null +++ b/packages/client/ui-chat/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de conteúdo de bate-papo", + "description": "Renderize mensagens de bate-papo, conteúdo de ferramentas e detalhes relacionados." + } +} diff --git a/packages/client/ui-chat/locale/ru.json b/packages/client/ui-chat/locale/ru.json new file mode 100644 index 0000000000..ffb6e15666 --- /dev/null +++ b/packages/client/ui-chat/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение содержимого чата", + "description": "Отображение сообщений чата, содержимого инструментов и связанных с ними деталей." + } +} diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 398661a4ff..ed088904bf 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index fcbc44d9be..fbabf95903 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -37,7 +37,7 @@ import { UserContent } from './chat/UserContent.tsx' import { ActivityPill, UsagePill } from './chat/StatsPills.tsx' import { registerConversationNodes } from './conversation-nodes/register.ts' import { QuotaNoticeHost } from './chat/QuotaNoticeHost.tsx' -import { en, ja, NS, zh } from './locale.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh } from './locale.ts' import { TranscriptViewRow, type TranscriptViewRowInjected } from './settings/TranscriptViewRow.tsx' import { createChatStore } from './stores.ts' import { TranscriptViewPolicy } from './transcript-view.ts' @@ -134,7 +134,7 @@ export function apply(ctx: Context): void { resolve: binding => ({ hooks: { chat: chatSource(binding) } }), }) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-chat: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-chat: dictionaries') const t = ctx.locale.bind(NS) const chatStore = createChatStore() const chatScrollPositions = new Map() diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.tsx b/packages/client/ui-chat/src/client/chat/StatsPills.tsx index 838effb768..7108c44719 100644 --- a/packages/client/ui-chat/src/client/chat/StatsPills.tsx +++ b/packages/client/ui-chat/src/client/chat/StatsPills.tsx @@ -6,7 +6,7 @@ import { memo, useMemo, type ReactNode } from 'react' import { createPortal } from 'react-dom' -import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' import type { InjectFace, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' // Type-only: merges the sessionStats key into SessionProjectionMap for useProjection. @@ -136,21 +136,27 @@ interface PillContent { stat: StatId icon: ReactNode label: ReactNode + /** The full reading as plain text: the accessible name and the tooltip shown while the label is cut off. */ + text: string } function exactCount(value: number, t: Translate): string { return t('message.turnUsage.count', { count: formatExactTokens(value, t) }) } -function joined(first: string, second: string | null): ReactNode { - if (second === null) return first - return ( - <> - {first} - · - {second} - - ) +/** One or two reading segments, visually joined by a separator glyph. */ +function reading(first: string, second: string | null): Pick { + if (second === null) return { label: first, text: first } + return { + label: ( + <> + {first} + · + {second} + + ), + text: `${first} · ${second}`, + } } // Every figure rides the durable sessionStats projection, so paging and @@ -169,14 +175,20 @@ function decodeSpeed(stats: WindowStats, t: Translate): string { }) } +/** The full reading above a pill, only while an ellipsis cuts its label; the delay matches the dock's context meter. */ +const FULL_READING_TOOLTIP = { side: 'top', portal: true, delayMs: 200, onlyWhenClipped: true } as const + /** A static reading: used when the pill has no dialog rows or the mode is Compact. */ -function PlainPill({ stat, icon, label }: PillContent) { +function PlainPill({ stat, icon, label, text }: PillContent) { return ( - - {icon} - {label} - + {/* No dialog carries a static reading, so a tap also pins the full text. */} + + + {icon} + {label} + + ) } @@ -186,8 +198,7 @@ function PlainPill({ stat, icon, label }: PillContent) { * state; useStatDialog closes it on Escape or an outside pointerdown or click, * so at most one dialog is open across the dock. */ -function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children }: PillContent & { - ariaLabel: string +function DialogPill({ stat, icon, label, text, title, titleValue, children }: PillContent & { title: string titleValue?: string children: ReactNode @@ -195,17 +206,20 @@ function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() return ( - + {/* The open dialog already shows every figure; a tooltip would cover it. */} + + + {open && createPortal(
0 ? decodeSpeed(stats, t) : null const icon = - if (mode === 'compact') return speed === null ? null : + if (mode === 'compact') return speed === null ? null : if (stats.steps === 0) return null const counts = t('stats.counts', { turns: stats.turns, steps: stats.steps }) - const content: PillContent = { stat: 'activity', icon, label: joined(counts, speed) } + const content: PillContent = { stat: 'activity', icon, ...reading(counts, speed) } // A window without one timed figure has no dialog rows to show, so the pill // stays a plain reading instead of a button opening an empty dialog. if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && speed === null) return return ( - + // The counts head the dialog, so a cut-off reading stays complete once the dialog opens. +
{stats.llmMs > 0 && ( <> @@ -289,7 +304,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance const cacheHitText = cacheHit !== null ? t('stats.cacheHit', { percent: cacheHit }) : null const icon = if (mode === 'compact') { - return cacheHitText === null ? null : + return cacheHitText === null ? null : } // Same aggregate as the Turn pill's totalTokens: every prompt-side billing bucket plus output. const total = billedInputTokens(usage) + usage.outputTokens @@ -298,8 +313,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index c5ffa1e87c..6f0e6eda52 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -602,3 +602,998 @@ export const ja = { 'clock.md': '{m}/{d}', 'clock.ymd': '{y}-{m}-{d}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'message.stepProcess.thinking': 'Analizando la solicitud', + 'message.stepProcess.read': 'Leyendo archivos', + 'message.stepProcess.readImage': 'Leyendo imágenes', + 'message.stepProcess.write': 'Escribir archivos', + 'message.stepProcess.search': 'código de búsqueda', + 'message.stepProcess.edit': 'Editar archivos', + 'message.stepProcess.commands': 'Ejecutar comandos', + 'message.stepProcess.code': 'Código en ejecución', + 'message.stepProcess.webSearch': 'buscando en la web', + 'message.stepProcess.webFetch': 'Visitar páginas web', + 'message.stepProcess.subagents': 'Subagentes coordinadores', + 'message.stepProcess.plan': 'Actualizando el plan', + 'message.stepProcess.questions': 'Esperando tu acción', + 'message.stepProcess.tools': 'herramientas de llamada', + 'message.stepProcess.prepare.read': 'Preparándose para leer archivos', + 'message.stepProcess.prepare.readImage': 'Preparándose para leer imágenes', + 'message.stepProcess.prepare.write': 'Preparándose para escribir archivos', + 'message.stepProcess.prepare.search': 'Preparándose para buscar el código', + 'message.stepProcess.prepare.edit': 'Preparándose para editar archivos', + 'message.stepProcess.prepare.commands': 'Preparándose para ejecutar comandos', + 'message.stepProcess.prepare.code': 'Preparándose para ejecutar código', + 'message.stepProcess.prepare.webSearch': 'Preparándose para buscar en la web', + 'message.stepProcess.prepare.webFetch': 'Preparándose para visitar páginas web', + 'message.stepProcess.prepare.subagents': 'Preparándose para coordinar subagentes', + 'message.stepProcess.prepare.plan': 'Preparándose para actualizar el plan', + 'message.stepProcess.prepare.questions': 'Preparando preguntas', + 'message.stepProcess.prepare.tools': 'Preparando llamadas a herramientas', + 'message.stepProcess.done.thinking': 'Análisis completado', + 'message.stepProcess.done.read': 'leer archivos', + 'message.stepProcess.done.readImage': 'Leer imágenes', + 'message.stepProcess.done.write': 'Escribió archivos', + 'message.stepProcess.done.search': 'código buscado', + 'message.stepProcess.done.edit': 'Archivos editados', + 'message.stepProcess.done.commands': 'Comandos ejecutados', + 'message.stepProcess.done.code': 'código ejecutado', + 'message.stepProcess.done.webSearch': 'Busqué en la web', + 'message.stepProcess.done.webFetch': 'Paginas web visitadas', + 'message.stepProcess.done.subagents': 'Subagentes coordinados', + 'message.stepProcess.done.plan': 'Actualizado el plan', + 'message.stepProcess.done.questions': 'preguntas hechas', + 'message.stepProcess.done.tools': 'herramientas llamadas', + 'message.stepProcess.joinTwo': '{first} y {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Ejecución solicitada', + 'message.trigger.goal': 'Objetivo continuo', + 'message.trigger.agent': 'Mensaje de tarea recibido', + 'message.trigger.team': 'Mensaje del equipo recibido', + 'message.trigger.subagent': 'Estado de subtarea actualizado', + 'message.trigger.github': 'Evento GitHub recibido', + 'message.trigger.webhook': 'Evento externo recibido', + 'message.trigger.schedule': 'Tarea de automatización', + 'message.trigger.job': 'Tarea en segundo plano actualizada', + 'message.trigger.plugin': 'Estado del complemento actualizado', + 'message.trigger.explanation': 'Esta notificación desencadenó esta respuesta.', + 'message.turnProcess.worked': 'Completado', + 'message.turnProcess.took': 'Completado en ', + 'message.turnProcess.failed': 'Fallido', + 'view.chat': 'Chat', + 'image.open': 'Ver imagen completa', + 'image.loading': 'Cargando imagen…', + 'image.failed': 'Vista previa de la imagen no disponible', + 'image.dialog': 'Vista previa de la imagen', + 'image.close': 'Cerrar vista previa de la imagen', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} turnos, {steps} pasos', + 'stats.cacheHit': 'Aciertos de caché: {percent}%', + 'stats.dialog.title': 'Estadísticas de sesión', + 'stats.dialog.usageTitle': 'Uso de tokens', + 'stats.dialog.llmTime': 'tiempo de maestría', + 'stats.dialog.toolTime': 'tiempo de herramienta', + 'stats.dialog.ttft': 'Tiempo promedio hasta el primer token (TTFT)', + 'stats.dialog.speed': 'Fichas por segundo (TPS)', + 'chat.loadingHistory': 'Cargando historial…', + 'chat.loadError': 'No se pudo cargar el historial: {message} ({code})', + 'chat.loadOlder': 'Cargar antes', + 'chat.toBottom': 'Volver al fondo', + 'chat.deepDiving': 'Razonamiento profundo', + 'chat.deepDivingFor': 'Razonando a fondo durante {duration} ···', + 'chat.turnNavigation.label': 'Navegación por turnos', + 'chat.turnNavigation.jump': 'Ir al turno {turn}', + 'chat.turnNavigation.jumpLoad': 'Cargar e ir al turno {turn}', + 'chat.turnNavigation.turn': 'Turno {turn}', + 'settings.performance.title': 'Rendimiento y uso', + 'settings.performance.description': 'Elija cuánta información de rendimiento y uso desea mostrar', + 'settings.performance.compact': 'Compacto', + 'settings.performance.detailed': 'Detallado', + 'settings.links.title': 'Abrir enlaces de chat en', + 'settings.links.description': 'Elija dónde abrir enlaces web', + 'settings.links.sidebar': 'Barra lateral en la aplicación', + 'settings.links.newTab': 'Navegador predeterminado', + 'settings.transcript.title': 'Detalles del trabajo', + 'settings.transcript.description': 'Elija cuántos detalles mostrar para las llamadas de herramientas', + 'settings.collapse.title': 'Cuándo contraer los detalles del trabajo', + 'settings.collapse.description': 'Elija cuándo contraer automáticamente los detalles del trabajo', + 'settings.collapse.completion': 'Al finalizar', + 'settings.collapse.nextInput': 'En el siguiente mensaje', + 'settings.foldedReplies.title': 'Respuestas después del plegado', + 'settings.foldedReplies.description': 'Elija qué respuestas del asistente permanecen visibles cuando los detalles del trabajo colapsan', + 'settings.foldedReplies.all': 'Todas las respuestas', + 'settings.foldedReplies.final': 'Solo respuesta final', + 'settings.transcript.compact': 'Compacto', + 'settings.transcript.standard': 'Estándar', + 'settings.transcript.detailed': 'Detallado', + 'settings.transcript.verbose': 'Detallado', + 'fileOpen.title': 'No se pudo abrir el archivo', + 'fileOpen.unknown': 'No se pudo abrir este archivo', + 'message.extraBlock': 'Bloque de contenido adicional', + 'message.systemPrompt': 'Aviso del sistema', + 'message.systemPromptUpdate': 'Actualización del mensaje del sistema', + 'message.toolAdded': 'Herramienta agregada: {name}', + 'message.toolRemoved': 'Herramienta eliminada: {name}', + 'message.toolsAdded': 'Añadido: {names}', + 'message.toolsAddedCount': '{count} añadido', + 'message.toolsChanged': '{added} agregado, {removed} eliminado', + 'message.toolsRemoved': 'Eliminado: {names}', + 'message.toolsRemovedCount': '{count} eliminado', + 'message.toolsUpdated': 'Herramientas actualizadas', + 'message.contextInjection': 'inyección de contexto', + 'message.contextRecall': 'Recuerdo de sesión', + 'message.referenceSummary': 'Sesión referenciada · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'cargado', + 'message.context.instructions.added': 'añadido', + 'message.context.instructions.updated': 'actualizado', + 'message.context.instructions.removed': 'eliminado', + 'message.context.catalog.replaced': 'Catálogo de repuestos', + 'message.context.catalog.more': '… {count} más', + 'message.context.snapshot.supersedes': 'Reemplaza instantáneas anteriores', + 'message.context.relay.from': 'De la sesión {session}', + 'message.context.recall.counts': '{retained} mantenido · {omitted} omitido', + 'message.context.recall.truncated': 'truncado', + 'message.compaction': 'Contexto compactado', + 'message.compaction.running': 'Contexto de compactación…', + 'message.compaction.completed': 'Elementos del historial {items} compactados (~tokens {tokens})', + 'message.compaction.expand': 'Ver resumen de compactación', + 'message.compaction.unavailable': 'Resumen de compactación no disponible', + 'message.compaction.commandTitle': 'compacto', + 'message.think': 'pensar', + 'message.unknownSurface': 'Evento de superficie desconocido: {type}', + 'message.unknownBlock': 'Bloque de contenido desconocido', + 'message.turnProcess.toolCalls.one': 'Llamada a la herramienta {count}', + 'message.turnProcess.toolCalls.other': 'Llamadas a herramientas {count}', + 'message.turnProcess.messages.one': 'Mensaje {count}', + 'message.turnProcess.messages.other': 'Mensajes {count}', + 'message.turnProcess.subagents.one': 'Subagente {count}', + 'message.turnProcess.subagents.other': 'Subagentes {count}', + 'message.turnProcess.thoughtForAWhile': 'Razonó durante un tiempo', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Detenido', + 'message.branch': 'Pasar a una nueva conversación', + 'message.branchUnavailable': 'Disponible solo en el último mensaje de un turno completado', + 'message.retry.active': 'Reintentando solicitud de modelo', + 'message.retry.cancelled': 'Reintento de solicitud de modelo cancelado', + 'message.retry.started': 'Solicitud de modelo reintentada', + 'message.retry.scheduled': 'Esperando para volver a intentar la solicitud de modelo', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Retraso de reintento: ', + 'message.retry.failure': 'Razón del fracaso: ', + 'message.failure.auth': 'La clave API no es válida', + 'message.taskStopped': 'Tarea detenida', + 'message.failure.accountSignedOut': 'Se detuvo porque cerró sesión en DeepSeek.', + 'message.failure.interrupted': 'El proceso DSH salió antes de que terminara este turno (por ejemplo, se detuvo, se estrelló o se agotó el tiempo de espera mientras se apagaba), por lo que el turno se cerró cuando se reinició DSH. Las llamadas a herramientas inacabadas tienen resultados desconocidos.', + 'message.failure.accountSignInRequired': 'Inicie sesión en DeepSeek y asegúrese de que el destino de la solicitud admita la autenticación de la cuenta.', + 'message.failure.quota': 'Solicitud de cupo agotado.', + 'message.turnError': 'Este turno falló', + 'message.maxTokens': 'Se alcanzó el límite del token de salida', + 'message.maxTokens.hint': 'La respuesta fue cortada; La salida anterior se conserva en la conversación. Envíe "continuar" para permitir que se reanude el modelo.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Uso del turno', + 'message.turnUsage.consumed': 'Uso {total}', + 'message.turnUsage.model': 'Proveedor / modelo', + 'message.turnUsage.cacheHit': 'Aciertos de caché', + 'message.turnUsage.input': 'Entrada sin caché', + 'message.turnUsage.cacheRead': 'Entrada en caché', + 'message.turnUsage.cacheWrite': 'escritura en caché', + 'message.turnUsage.output': 'Salida', + 'message.turnUsage.reasoning': ' (razonamiento {tokens})', + 'message.turnUsage.count': 'Tok {count}', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Corriendo…', + 'command.failed': 'El comando falló', + 'command.done': 'Completado', + 'command.title': 'Comando', + 'row.running': 'corriendo', + 'row.failed': 'Fallido', + 'json.truncated': '… truncado, caracteres {total} en total', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'message.stepProcess.thinking': 'Analyser la demande', + 'message.stepProcess.read': 'Lecture de fichiers', + 'message.stepProcess.readImage': 'Lire des images', + 'message.stepProcess.write': 'Écriture de fichiers', + 'message.stepProcess.search': 'Recherche de code', + 'message.stepProcess.edit': 'Modification de fichiers', + 'message.stepProcess.commands': 'Exécuter des commandes', + 'message.stepProcess.code': 'Exécution du code', + 'message.stepProcess.webSearch': 'Recherche sur le Web', + 'message.stepProcess.webFetch': 'Visiter des pages Web', + 'message.stepProcess.subagents': 'Sous-agents de coordination', + 'message.stepProcess.plan': 'Mise à jour du plan', + 'message.stepProcess.questions': 'En attendant votre action', + 'message.stepProcess.tools': "Outils d'appel", + 'message.stepProcess.prepare.read': 'Préparation à la lecture de fichiers', + 'message.stepProcess.prepare.readImage': 'Se préparer à lire des images', + 'message.stepProcess.prepare.write': "Préparation à l'écriture de fichiers", + 'message.stepProcess.prepare.search': 'Préparation à la recherche de code', + 'message.stepProcess.prepare.edit': 'Préparation à la modification de fichiers', + 'message.stepProcess.prepare.commands': "Préparation à l'exécution de commandes", + 'message.stepProcess.prepare.code': "Préparation à l'exécution du code", + 'message.stepProcess.prepare.webSearch': 'Se préparer à effectuer une recherche sur le Web', + 'message.stepProcess.prepare.webFetch': 'Se préparer à visiter des pages Web', + 'message.stepProcess.prepare.subagents': 'Se préparer à coordonner les sous-agents', + 'message.stepProcess.prepare.plan': 'Préparation de la mise à jour du plan', + 'message.stepProcess.prepare.questions': 'Préparation des questions', + 'message.stepProcess.prepare.tools': "Préparation des appels d'outils", + 'message.stepProcess.done.thinking': 'Analyse terminée', + 'message.stepProcess.done.read': 'Lire des fichiers', + 'message.stepProcess.done.readImage': 'Lire des images', + 'message.stepProcess.done.write': 'A écrit des fichiers', + 'message.stepProcess.done.search': 'Code recherché', + 'message.stepProcess.done.edit': 'Fichiers modifiés', + 'message.stepProcess.done.commands': 'Commandes exécutées', + 'message.stepProcess.done.code': 'Code exécuté', + 'message.stepProcess.done.webSearch': "J'ai cherché sur le Web", + 'message.stepProcess.done.webFetch': 'Pages Web visitées', + 'message.stepProcess.done.subagents': 'Sous-agents coordonnés', + 'message.stepProcess.done.plan': 'Plan mis à jour', + 'message.stepProcess.done.questions': 'Questions posées', + 'message.stepProcess.done.tools': 'Outils appelés', + 'message.stepProcess.joinTwo': '{first} et {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Exécution demandée', + 'message.trigger.goal': 'Objectif continu', + 'message.trigger.agent': 'Message de tâche reçu', + 'message.trigger.team': "Message d'équipe reçu", + 'message.trigger.subagent': 'Statut de la sous-tâche mis à jour', + 'message.trigger.github': 'Événement GitHub reçu', + 'message.trigger.webhook': 'Événement externe reçu', + 'message.trigger.schedule': "Tâche d'automatisation", + 'message.trigger.job': 'Tâche en arrière-plan mise à jour', + 'message.trigger.plugin': 'Statut du plugin mis à jour', + 'message.trigger.explanation': 'Cette notification a déclenché cette réponse.', + 'message.turnProcess.worked': 'Terminé', + 'message.turnProcess.took': 'Terminé en ', + 'message.turnProcess.failed': 'Échec', + 'view.chat': 'Discussion', + 'image.open': "Voir l'image complète", + 'image.loading': "Chargement de l'image…", + 'image.failed': "Aperçu de l'image indisponible", + 'image.dialog': "Aperçu de l'image", + 'image.close': "Fermer l'aperçu de l'image", + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} tours, {steps} étapes', + 'stats.cacheHit': 'Taux de réussite du cache : {percent} %', + 'stats.dialog.title': 'Statistiques de session', + 'stats.dialog.usageTitle': 'Utilisation des jetons', + 'stats.dialog.llmTime': 'Temps LLM', + 'stats.dialog.toolTime': "Temps d'outil", + 'stats.dialog.ttft': "Temps moyen jusqu'au premier jeton (TTFT)", + 'stats.dialog.speed': 'Jetons par seconde (TPS)', + 'chat.loadingHistory': "Chargement de l'historique…", + 'chat.loadError': "Échec du chargement de l'historique : {message} ({code})", + 'chat.loadOlder': 'Charger plus tôt', + 'chat.toBottom': 'Retour en bas', + 'chat.deepDiving': 'Réflexion approfondie', + 'chat.deepDivingFor': 'Réflexion approfondie depuis {duration} ···', + 'chat.turnNavigation.label': 'Navigation entre les tours', + 'chat.turnNavigation.jump': 'Aller au tour {turn}', + 'chat.turnNavigation.jumpLoad': 'Charger et aller au tour {turn}', + 'chat.turnNavigation.turn': 'Tour {turn}', + 'settings.performance.title': 'Performances et utilisation', + 'settings.performance.description': "Choisissez la quantité d'informations sur les performances et l'utilisation à afficher", + 'settings.performance.compact': 'Compacte', + 'settings.performance.detailed': 'Détaillé', + 'settings.links.title': 'Ouvrir les liens de discussion dans', + 'settings.links.description': 'Choisissez où ouvrir les liens Web', + 'settings.links.sidebar': "Barre latérale dans l'application", + 'settings.links.newTab': 'Navigateur par défaut', + 'settings.transcript.title': 'Détails du travail', + 'settings.transcript.description': "Choisissez le niveau de détails à afficher pour les appels d'outils", + 'settings.collapse.title': 'Quand réduire les détails du travail', + 'settings.collapse.description': 'Choisissez quand réduire automatiquement les détails du travail', + 'settings.collapse.completion': 'À la fin', + 'settings.collapse.nextInput': 'Au prochain message', + 'settings.foldedReplies.title': 'Réponses après le repli', + 'settings.foldedReplies.description': "Choisissez quelles réponses de l'assistant restent visibles lorsque les détails du travail sont réduits", + 'settings.foldedReplies.all': 'Toutes les réponses', + 'settings.foldedReplies.final': 'Réponse finale seulement', + 'settings.transcript.compact': 'Compacte', + 'settings.transcript.standard': 'Norme', + 'settings.transcript.detailed': 'Détaillé', + 'settings.transcript.verbose': 'Verbeux', + 'fileOpen.title': "Impossible d'ouvrir le fichier", + 'fileOpen.unknown': "Impossible d'ouvrir ce fichier", + 'message.extraBlock': 'Bloc de contenu supplémentaire', + 'message.systemPrompt': 'Invite système', + 'message.systemPromptUpdate': "Mise à jour de l'invite du système", + 'message.toolAdded': 'Outil ajouté : {name}', + 'message.toolRemoved': 'Outil supprimé : {name}', + 'message.toolsAdded': 'Ajouté : {names}', + 'message.toolsAddedCount': '{count} ajouté', + 'message.toolsChanged': '{added} ajouté, {removed} supprimé', + 'message.toolsRemoved': 'Supprimé : {names}', + 'message.toolsRemovedCount': '{count} supprimé', + 'message.toolsUpdated': 'Outils mis à jour', + 'message.contextInjection': 'Injection de contexte', + 'message.contextRecall': 'Rappel de session', + 'message.referenceSummary': 'Séance référencée · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'chargé', + 'message.context.instructions.added': 'ajouté', + 'message.context.instructions.updated': 'mis à jour', + 'message.context.instructions.removed': 'supprimé', + 'message.context.catalog.replaced': 'Catalogue de remplacement', + 'message.context.catalog.more': '… {count} plus', + 'message.context.snapshot.supersedes': 'Remplace les instantanés précédents', + 'message.context.relay.from': 'De la séance {session}', + 'message.context.recall.counts': '{retained} conservé · {omitted} omis', + 'message.context.recall.truncated': 'tronqué', + 'message.compaction': 'Contexte compacté', + 'message.compaction.running': 'Contexte de compactage…', + 'message.compaction.completed': "Éléments d'historique {items} compactés (~jetons {tokens})", + 'message.compaction.expand': 'Afficher le résumé du compactage', + 'message.compaction.unavailable': 'Résumé du compactage indisponible', + 'message.compaction.commandTitle': 'compacte', + 'message.think': 'Pensez', + 'message.unknownSurface': 'Événement de surface inconnu : {type}', + 'message.unknownBlock': 'Bloc de contenu inconnu', + 'message.turnProcess.toolCalls.one': "Appel d'outil {count}", + 'message.turnProcess.toolCalls.other': "Appels d'outil {count}", + 'message.turnProcess.messages.one': 'Message {count}', + 'message.turnProcess.messages.other': 'Messages {count}', + 'message.turnProcess.subagents.one': 'Sous-agent {count}', + 'message.turnProcess.subagents.other': 'Sous-agents {count}', + 'message.turnProcess.thoughtForAWhile': 'Réflexion pendant un moment', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Arrêté', + 'message.branch': 'Lancez-vous dans une nouvelle conversation', + 'message.branchUnavailable': "Disponible uniquement sur le dernier message d'un tour terminé", + 'message.retry.active': 'Nouvelle tentative de demande de modèle', + 'message.retry.cancelled': 'Nouvelle tentative de demande de modèle annulée', + 'message.retry.started': 'Demande de modèle réessayée', + 'message.retry.scheduled': 'En attente de réessayer la demande de modèle', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}', + 'message.retry.delay': 'Délai de nouvelle tentative : ', + 'message.retry.failure': "Raison de l'échec : ", + 'message.failure.auth': "La clé API n'est pas valide", + 'message.taskStopped': 'Tâche arrêtée', + 'message.failure.accountSignedOut': 'Arrêté car vous vous êtes déconnecté de DeepSeek.', + 'message.failure.interrupted': "Le processus DSH s'est terminé avant la fin de ce tour (par exemple, il a été arrêté, s'est écrasé ou a expiré lors de l'arrêt), de sorte que le tour a été fermé lorsque DSH a redémarré. Les appels d’outils inachevés ont des résultats inconnus.", + 'message.failure.accountSignInRequired': "Connectez-vous à DeepSeek et assurez-vous que la destination de la demande prend en charge l'authentification du compte.", + 'message.failure.quota': 'Quota de demandes épuisé.', + 'message.turnError': 'Ce tour a échoué', + 'message.maxTokens': 'Limite de jetons de sortie atteinte', + 'message.maxTokens.hint': 'La réponse fut coupée ; la sortie antérieure est conservée dans la conversation. Envoyez « continuer » pour laisser le modèle reprendre.', + 'message.tokensPerSecond': '{tps} jetons/s', + 'message.turnUsage.title': 'Consommation du tour', + 'message.turnUsage.consumed': 'Utilisation {total}', + 'message.turnUsage.model': 'Fournisseur / modèle', + 'message.turnUsage.cacheHit': 'Taux de réussite du cache', + 'message.turnUsage.input': 'Entrée non mise en cache', + 'message.turnUsage.cacheRead': 'Entrée en cache', + 'message.turnUsage.cacheWrite': 'Écriture en cache', + 'message.turnUsage.output': 'Sortie', + 'message.turnUsage.reasoning': ' (raisonnement {tokens})', + 'message.turnUsage.count': 'Jeton {count}', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Courir…', + 'command.failed': 'La commande a échoué', + 'command.done': 'Terminé', + 'command.title': 'Commande', + 'row.running': 'Courir', + 'row.failed': 'Échec', + 'json.truncated': '… tronqué, nombre total de caractères {total}', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'message.stepProcess.thinking': 'Analyse der Anfrage', + 'message.stepProcess.read': 'Dateien lesen', + 'message.stepProcess.readImage': 'Bilder lesen', + 'message.stepProcess.write': 'Dateien schreiben', + 'message.stepProcess.search': 'Code wird gesucht', + 'message.stepProcess.edit': 'Bearbeiten von Dateien', + 'message.stepProcess.commands': 'Befehle ausführen', + 'message.stepProcess.code': 'Ausführen von Code', + 'message.stepProcess.webSearch': 'Suche im Internet', + 'message.stepProcess.webFetch': 'Besuch von Webseiten', + 'message.stepProcess.subagents': 'Koordinierende Unteragenten', + 'message.stepProcess.plan': 'Aktualisierung des Plans', + 'message.stepProcess.questions': 'Ich warte auf Ihre Aktion', + 'message.stepProcess.tools': 'Aufrufende Werkzeuge', + 'message.stepProcess.prepare.read': 'Vorbereitung zum Lesen von Dateien', + 'message.stepProcess.prepare.readImage': 'Vorbereitung zum Lesen von Bildern', + 'message.stepProcess.prepare.write': 'Vorbereitung zum Schreiben von Dateien', + 'message.stepProcess.prepare.search': 'Vorbereiten der Codesuche', + 'message.stepProcess.prepare.edit': 'Vorbereitung zum Bearbeiten von Dateien', + 'message.stepProcess.prepare.commands': 'Vorbereitung zum Ausführen von Befehlen', + 'message.stepProcess.prepare.code': 'Vorbereiten, Code auszuführen', + 'message.stepProcess.prepare.webSearch': 'Bereiten Sie sich auf die Suche im Internet vor', + 'message.stepProcess.prepare.webFetch': 'Vorbereitung auf den Besuch von Webseiten', + 'message.stepProcess.prepare.subagents': 'Vorbereitung zur Koordinierung von Subagenten', + 'message.stepProcess.prepare.plan': 'Vorbereitung zur Aktualisierung des Plans', + 'message.stepProcess.prepare.questions': 'Fragen vorbereiten', + 'message.stepProcess.prepare.tools': 'Toolaufrufe vorbereiten', + 'message.stepProcess.done.thinking': 'Analyse abgeschlossen', + 'message.stepProcess.done.read': 'Dateien lesen', + 'message.stepProcess.done.readImage': 'Bilder lesen', + 'message.stepProcess.done.write': 'Dateien geschrieben', + 'message.stepProcess.done.search': 'Gesuchter Code', + 'message.stepProcess.done.edit': 'Bearbeitete Dateien', + 'message.stepProcess.done.commands': 'Befehle ausgeführt', + 'message.stepProcess.done.code': 'Code ausgeführt', + 'message.stepProcess.done.webSearch': 'Habe das Internet durchsucht', + 'message.stepProcess.done.webFetch': 'Besuchte Webseiten', + 'message.stepProcess.done.subagents': 'Koordinierte Unteragenten', + 'message.stepProcess.done.plan': 'Plan aktualisiert', + 'message.stepProcess.done.questions': 'Gestellte Fragen', + 'message.stepProcess.done.tools': 'Werkzeuge genannt', + 'message.stepProcess.joinTwo': '{first} und {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title} usw.', + 'message.trigger.request': 'Ausführung beantragt', + 'message.trigger.goal': 'Weiterführendes Ziel', + 'message.trigger.agent': 'Aufgabennachricht erhalten', + 'message.trigger.team': 'Teamnachricht erhalten', + 'message.trigger.subagent': 'Unteraufgabenstatus aktualisiert', + 'message.trigger.github': 'GitHub-Ereignis empfangen', + 'message.trigger.webhook': 'Externes Ereignis empfangen', + 'message.trigger.schedule': 'Automatisierungsaufgabe', + 'message.trigger.job': 'Hintergrundaufgabe aktualisiert', + 'message.trigger.plugin': 'Plugin-Status aktualisiert', + 'message.trigger.explanation': 'Diese Benachrichtigung hat diese Reaktion ausgelöst.', + 'message.turnProcess.worked': 'Abgeschlossen', + 'message.turnProcess.took': 'Fertiggestellt in ', + 'message.turnProcess.failed': 'Fehlgeschlagen', + 'view.chat': 'Chat', + 'image.open': 'Vollständiges Bild anzeigen', + 'image.loading': 'Bild wird geladen…', + 'image.failed': 'Bildvorschau nicht verfügbar', + 'image.dialog': 'Bildvorschau', + 'image.close': 'Bildvorschau schließen', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} Runden, {steps} Schritte', + 'stats.cacheHit': 'Cache-Treffer: {percent}%', + 'stats.dialog.title': 'Sitzungsstatistiken', + 'stats.dialog.usageTitle': 'Token-Nutzung', + 'stats.dialog.llmTime': 'LLM-Zeit', + 'stats.dialog.toolTime': 'Werkzeugzeit', + 'stats.dialog.ttft': 'Durchschnittliche Zeit bis zum ersten Token (TTFT)', + 'stats.dialog.speed': 'Token pro Sekunde (TPS)', + 'chat.loadingHistory': 'Verlauf wird geladen…', + 'chat.loadError': 'Fehler beim Laden des Verlaufs: {message} ({code})', + 'chat.loadOlder': 'Früher laden', + 'chat.toBottom': 'Zurück nach unten', + 'chat.deepDiving': 'Gründliches Nachdenken', + 'chat.deepDivingFor': 'Gründliches Nachdenken seit {duration} ···', + 'chat.turnNavigation.label': 'Rundennavigation', + 'chat.turnNavigation.jump': 'Zu Runde {turn} springen', + 'chat.turnNavigation.jumpLoad': 'Runde {turn} laden und öffnen', + 'chat.turnNavigation.turn': 'Runde {turn}', + 'settings.performance.title': 'Leistung und Nutzung', + 'settings.performance.description': 'Wählen Sie aus, wie viele Leistungs- und Nutzungsinformationen angezeigt werden sollen', + 'settings.performance.compact': 'Kompakt', + 'settings.performance.detailed': 'Ausführlich', + 'settings.links.title': 'Öffnen Sie Chat-Links in', + 'settings.links.description': 'Wählen Sie aus, wo Weblinks geöffnet werden sollen', + 'settings.links.sidebar': 'In-App-Seitenleiste', + 'settings.links.newTab': 'Standardbrowser', + 'settings.transcript.title': 'Arbeitsdetails', + 'settings.transcript.description': 'Wählen Sie aus, wie viele Details für Werkzeugaufrufe angezeigt werden sollen', + 'settings.collapse.title': 'Wann Arbeitsdetails minimiert werden sollten', + 'settings.collapse.description': 'Wählen Sie aus, wann Arbeitsdetails automatisch ausgeblendet werden sollen', + 'settings.collapse.completion': 'Nach Fertigstellung', + 'settings.collapse.nextInput': 'Bei der nächsten Nachricht', + 'settings.foldedReplies.title': 'Antworten nach dem Einklappen', + 'settings.foldedReplies.description': 'Wählen Sie aus, welche Assistentenantworten sichtbar bleiben, wenn die Arbeitsdetails ausgeblendet werden', + 'settings.foldedReplies.all': 'Alle Antworten', + 'settings.foldedReplies.final': 'Nur die endgültige Antwort', + 'settings.transcript.compact': 'Kompakt', + 'settings.transcript.standard': 'Standard', + 'settings.transcript.detailed': 'Ausführlich', + 'settings.transcript.verbose': 'Ausführlich', + 'fileOpen.title': 'Datei konnte nicht geöffnet werden', + 'fileOpen.unknown': 'Diese Datei konnte nicht geöffnet werden', + 'message.extraBlock': 'Zusätzlicher Inhaltsblock', + 'message.systemPrompt': 'Systemaufforderung', + 'message.systemPromptUpdate': 'Aktualisierung der Systemaufforderung', + 'message.toolAdded': 'Tool hinzugefügt: {name}', + 'message.toolRemoved': 'Entferntes Werkzeug: {name}', + 'message.toolsAdded': 'Hinzugefügt: {names}', + 'message.toolsAddedCount': '{count} hinzugefügt', + 'message.toolsChanged': '{added} hinzugefügt, {removed} entfernt', + 'message.toolsRemoved': 'Entfernt: {names}', + 'message.toolsRemovedCount': '{count} entfernt', + 'message.toolsUpdated': 'Werkzeuge aktualisiert', + 'message.contextInjection': 'Kontextinjektion', + 'message.contextRecall': 'Sitzungsrückruf', + 'message.referenceSummary': 'Referenzierte Sitzung · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'geladen', + 'message.context.instructions.added': 'hinzugefügt', + 'message.context.instructions.updated': 'aktualisiert', + 'message.context.instructions.removed': 'entfernt', + 'message.context.catalog.replaced': 'Ersatzkatalog', + 'message.context.catalog.more': '… {count} mehr', + 'message.context.snapshot.supersedes': 'Ersetzt frühere Schnappschüsse', + 'message.context.relay.from': 'Aus der Sitzung {session}', + 'message.context.recall.counts': '{retained} beibehalten · {omitted} weggelassen', + 'message.context.recall.truncated': 'abgeschnitten', + 'message.compaction': 'Kontext verdichtet', + 'message.compaction.running': 'Kontext verdichten…', + 'message.compaction.completed': 'Komprimierte {items}-Verlaufselemente (~{tokens}-Tokens)', + 'message.compaction.expand': 'Komprimierungszusammenfassung anzeigen', + 'message.compaction.unavailable': 'Komprimierungszusammenfassung nicht verfügbar', + 'message.compaction.commandTitle': 'kompakt', + 'message.think': 'Denken Sie nach', + 'message.unknownSurface': 'Unbekanntes Oberflächenereignis: {type}', + 'message.unknownBlock': 'Unbekannter Inhaltsblock', + 'message.turnProcess.toolCalls.one': 'Werkzeugaufruf {count}', + 'message.turnProcess.toolCalls.other': '{count} Toolaufrufe', + 'message.turnProcess.messages.one': '{count}-Nachricht', + 'message.turnProcess.messages.other': '{count}-Nachrichten', + 'message.turnProcess.subagents.one': '{count}-Subagent', + 'message.turnProcess.subagents.other': '{count}-Subagenten', + 'message.turnProcess.thoughtForAWhile': 'Eine Weile nachgedacht', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Angehalten', + 'message.branch': 'Verzweigen Sie in ein neues Gespräch', + 'message.branchUnavailable': 'Nur verfügbar bei der letzten Nachricht einer abgeschlossenen Runde', + 'message.retry.active': 'Modellanforderung wird erneut versucht', + 'message.retry.cancelled': 'Wiederholungsversuch der Modellanforderung abgebrochen', + 'message.retry.started': 'Modellanfrage erneut versucht', + 'message.retry.scheduled': 'Warten auf erneuten Versuch der Modellanforderung', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Wiederholungsverzögerung: ', + 'message.retry.failure': 'Fehlergrund: ', + 'message.failure.auth': 'Der API-Schlüssel ist ungültig', + 'message.taskStopped': 'Aufgabe gestoppt', + 'message.failure.accountSignedOut': 'Gestoppt, weil Sie sich von DeepSeek abgemeldet haben.', + 'message.failure.interrupted': 'Der DSH-Prozess wurde beendet, bevor dieser Zug beendet war (z. B. wurde er beendet, stürzte ab oder es kam zu einer Zeitüberschreitung beim Herunterfahren), sodass der Zug geschlossen wurde, als DSH neu gestartet wurde. Unvollendete Werkzeugaufrufe haben unbekannte Ergebnisse.', + 'message.failure.accountSignInRequired': 'Melden Sie sich bei DeepSeek an und stellen Sie sicher, dass das Anforderungsziel die Kontoauthentifizierung unterstützt.', + 'message.failure.quota': 'Anforderungskontingent ausgeschöpft.', + 'message.turnError': 'Diese Runde ist fehlgeschlagen', + 'message.maxTokens': 'Ausgabe-Token-Limit erreicht', + 'message.maxTokens.hint': 'Die Antwort wurde abgeschnitten; Frühere Ausgaben bleiben in der Konversation erhalten. Senden Sie „Weiter“, um das Modell fortzusetzen.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Rundenverbrauch', + 'message.turnUsage.consumed': 'Verwendung {total}', + 'message.turnUsage.model': 'Anbieter / Modell', + 'message.turnUsage.cacheHit': 'Cache-Treffer', + 'message.turnUsage.input': 'Nicht zwischengespeicherte Eingabe', + 'message.turnUsage.cacheRead': 'Zwischengespeicherte Eingabe', + 'message.turnUsage.cacheWrite': 'Cache-Schreiben', + 'message.turnUsage.output': 'Ausgabe', + 'message.turnUsage.reasoning': ' ({tokens}-Argumentation)', + 'message.turnUsage.count': '{count}-Token', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Laufen…', + 'command.failed': 'Der Befehl ist fehlgeschlagen', + 'command.done': 'Abgeschlossen', + 'command.title': 'Befehl', + 'row.running': 'Laufen', + 'row.failed': 'Fehlgeschlagen', + 'json.truncated': '… gekürzt, {total} Zeichen insgesamt', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'message.stepProcess.thinking': 'Анализ запроса', + 'message.stepProcess.read': 'Чтение файлов', + 'message.stepProcess.readImage': 'Чтение изображений', + 'message.stepProcess.write': 'Запись файлов', + 'message.stepProcess.search': 'Поиск кода', + 'message.stepProcess.edit': 'Редактирование файлов', + 'message.stepProcess.commands': 'Запуск команд', + 'message.stepProcess.code': 'Запуск кода', + 'message.stepProcess.webSearch': 'Поиск в Интернете', + 'message.stepProcess.webFetch': 'Посещение веб-страниц', + 'message.stepProcess.subagents': 'Координирующие субагенты', + 'message.stepProcess.plan': 'Обновление плана', + 'message.stepProcess.questions': 'Жду ваших действий', + 'message.stepProcess.tools': 'Вызов инструментов', + 'message.stepProcess.prepare.read': 'Подготовка к чтению файлов', + 'message.stepProcess.prepare.readImage': 'Подготовка к чтению изображений', + 'message.stepProcess.prepare.write': 'Подготовка к записи файлов', + 'message.stepProcess.prepare.search': 'Подготовка к поиску кода', + 'message.stepProcess.prepare.edit': 'Подготовка к редактированию файлов', + 'message.stepProcess.prepare.commands': 'Подготовка к выполнению команд', + 'message.stepProcess.prepare.code': 'Подготовка к запуску кода', + 'message.stepProcess.prepare.webSearch': 'Подготовка к поиску в Интернете', + 'message.stepProcess.prepare.webFetch': 'Подготовка к посещению веб-страниц', + 'message.stepProcess.prepare.subagents': 'Подготовка к координации субагентов', + 'message.stepProcess.prepare.plan': 'Подготовка к обновлению плана', + 'message.stepProcess.prepare.questions': 'Подготовка вопросов', + 'message.stepProcess.prepare.tools': 'Подготовка вызовов инструментов', + 'message.stepProcess.done.thinking': 'Анализ завершен', + 'message.stepProcess.done.read': 'Чтение файлов', + 'message.stepProcess.done.readImage': 'Чтение изображений', + 'message.stepProcess.done.write': 'Написал файлы', + 'message.stepProcess.done.search': 'Искомый код', + 'message.stepProcess.done.edit': 'Отредактированные файлы', + 'message.stepProcess.done.commands': 'Выполнение команд', + 'message.stepProcess.done.code': 'Выполненный код', + 'message.stepProcess.done.webSearch': 'Искал в Интернете', + 'message.stepProcess.done.webFetch': 'Посещенные веб-страницы', + 'message.stepProcess.done.subagents': 'Координированные субагенты', + 'message.stepProcess.done.plan': 'Обновлен план', + 'message.stepProcess.done.questions': 'Задаваемые вопросы', + 'message.stepProcess.done.tools': 'Вызываемые инструменты', + 'message.stepProcess.joinTwo': '{first} и {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title} и т. д.', + 'message.trigger.request': 'Запрошено выполнение', + 'message.trigger.goal': 'Постоянная цель', + 'message.trigger.agent': 'Сообщение о задаче получено', + 'message.trigger.team': 'Сообщение команды получено', + 'message.trigger.subagent': 'Статус подзадачи обновлен', + 'message.trigger.github': 'Событие GitHub получено', + 'message.trigger.webhook': 'Получено внешнее событие', + 'message.trigger.schedule': 'Задача автоматизации', + 'message.trigger.job': 'Фоновая задача обновлена.', + 'message.trigger.plugin': 'Статус плагина обновлен', + 'message.trigger.explanation': 'Это уведомление вызвало такой ответ.', + 'message.turnProcess.worked': 'Завершено', + 'message.turnProcess.took': 'Завершено в ', + 'message.turnProcess.failed': 'Не удалось', + 'view.chat': 'Чат', + 'image.open': 'Посмотреть полное изображение', + 'image.loading': 'Загрузка изображения…', + 'image.failed': 'Предварительный просмотр изображения недоступен.', + 'image.dialog': 'Предварительный просмотр изображения', + 'image.close': 'Закрыть предварительный просмотр изображения', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': 'Ходов: {turns}, шагов: {steps}', + 'stats.cacheHit': 'Попадание в кэш: {percent}%', + 'stats.dialog.title': 'Статистика сеансов', + 'stats.dialog.usageTitle': 'Использование токена', + 'stats.dialog.llmTime': 'время магистратуры', + 'stats.dialog.toolTime': 'Время работы инструмента', + 'stats.dialog.ttft': 'Среднее время до первого токена (TTFT)', + 'stats.dialog.speed': 'Токенов в секунду (TPS)', + 'chat.loadingHistory': 'Загрузка истории…', + 'chat.loadError': 'Не удалось загрузить историю: {message} ({code})', + 'chat.loadOlder': 'Загрузить раньше', + 'chat.toBottom': 'Вернуться к низу', + 'chat.deepDiving': 'Глубокое размышление', + 'chat.deepDivingFor': 'Глубокое размышление в течение {duration} ···', + 'chat.turnNavigation.label': 'Навигация по ходам', + 'chat.turnNavigation.jump': 'Перейти к ходу {turn}', + 'chat.turnNavigation.jumpLoad': 'Загрузить ход {turn} и перейти к нему', + 'chat.turnNavigation.turn': 'Ход {turn}', + 'settings.performance.title': 'Производительность и использование', + 'settings.performance.description': 'Выберите, какой объем информации о производительности и использовании показывать', + 'settings.performance.compact': 'Компактный', + 'settings.performance.detailed': 'Подробный', + 'settings.links.title': 'Открыть ссылки на чат в', + 'settings.links.description': 'Выберите, где открывать веб-ссылки', + 'settings.links.sidebar': 'Боковая панель в приложении', + 'settings.links.newTab': 'Браузер по умолчанию', + 'settings.transcript.title': 'Детали работы', + 'settings.transcript.description': 'Выберите, насколько подробно следует отображать вызовы инструментов.', + 'settings.collapse.title': 'Когда сворачивать детали работы', + 'settings.collapse.description': 'Выберите, когда автоматически сворачивать сведения о работе', + 'settings.collapse.completion': 'По завершении', + 'settings.collapse.nextInput': 'В следующем сообщении', + 'settings.foldedReplies.title': 'Ответы после сворачивания', + 'settings.foldedReplies.description': 'Выберите, какие ответы помощника останутся видимыми, когда сведения о работе будут свернуты.', + 'settings.foldedReplies.all': 'Все ответы', + 'settings.foldedReplies.final': 'Только окончательный ответ', + 'settings.transcript.compact': 'Компактный', + 'settings.transcript.standard': 'Стандартный', + 'settings.transcript.detailed': 'Подробный', + 'settings.transcript.verbose': 'Многословный', + 'fileOpen.title': 'Не удалось открыть файл', + 'fileOpen.unknown': 'Не удалось открыть этот файл', + 'message.extraBlock': 'Дополнительный блок контента', + 'message.systemPrompt': 'Системная подсказка', + 'message.systemPromptUpdate': 'Обновление системного запроса', + 'message.toolAdded': 'Добавлен инструмент: {name}.', + 'message.toolRemoved': 'Инструмент удален: {name}.', + 'message.toolsAdded': 'Добавлено: {names}', + 'message.toolsAddedCount': '{count} добавлен', + 'message.toolsChanged': '{added} добавлен, {removed} удален.', + 'message.toolsRemoved': 'Удалено: {names}.', + 'message.toolsRemovedCount': '{count} удален.', + 'message.toolsUpdated': 'Инструменты обновлены', + 'message.contextInjection': 'Внедрение контекста', + 'message.contextRecall': 'Отзыв сеанса', + 'message.referenceSummary': 'Ссылка на сессию · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'загружен', + 'message.context.instructions.added': 'добавлено', + 'message.context.instructions.updated': 'обновлено', + 'message.context.instructions.removed': 'удалено', + 'message.context.catalog.replaced': 'Каталог замены', + 'message.context.catalog.more': '… {count} ещё', + 'message.context.snapshot.supersedes': 'Заменяет предыдущие снимки', + 'message.context.relay.from': 'Из сеанса {session}', + 'message.context.recall.counts': '{retained} сохранен · {omitted} опущен', + 'message.context.recall.truncated': 'усеченный', + 'message.compaction': 'Контекст сжат', + 'message.compaction.running': 'Сжатие контекста…', + 'message.compaction.completed': 'Сжатые элементы истории {items} (токены ~{tokens})', + 'message.compaction.expand': 'Посмотреть сводку по уплотнению', + 'message.compaction.unavailable': 'Сводка по уплотнению недоступна.', + 'message.compaction.commandTitle': 'компактный', + 'message.think': 'Подумайте', + 'message.unknownSurface': 'Неизвестное событие на поверхности: {type}.', + 'message.unknownBlock': 'Неизвестный блок контента', + 'message.turnProcess.toolCalls.one': '{count} вызов инструмента', + 'message.turnProcess.toolCalls.other': '{count} вызовов инструментов', + 'message.turnProcess.messages.one': 'Сообщение {count}', + 'message.turnProcess.messages.other': 'Сообщения {count}', + 'message.turnProcess.subagents.one': 'Субагент {count}', + 'message.turnProcess.subagents.other': 'Субагенты {count}', + 'message.turnProcess.thoughtForAWhile': 'Размышлял некоторое время', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Остановлено', + 'message.branch': 'Перейти к новому разговору', + 'message.branchUnavailable': 'Доступно только в последнем сообщении о завершенном ходу.', + 'message.retry.active': 'Повторный запрос модели', + 'message.retry.cancelled': 'Повтор запроса модели отменен.', + 'message.retry.started': 'Повторный запрос модели', + 'message.retry.scheduled': 'Ожидание повторного запроса модели', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Задержка повтора: ', + 'message.retry.failure': 'Причина неисправности: ', + 'message.failure.auth': 'Ключ API недействителен.', + 'message.taskStopped': 'Задача остановлена', + 'message.failure.accountSignedOut': 'Остановлено, поскольку вы вышли из DeepSeek.', + 'message.failure.interrupted': 'Процесс DSH завершился до завершения этого хода (например, был завершен, произошел сбой или истекло время ожидания при завершении работы), поэтому ход был закрыт при перезапуске DSH. Незавершенные вызовы инструментов имеют неизвестные результаты.', + 'message.failure.accountSignInRequired': 'Войдите в DeepSeek и убедитесь, что пункт назначения запроса поддерживает аутентификацию учетной записи.', + 'message.failure.quota': 'Квота запросов исчерпана.', + 'message.turnError': 'Этот ход завершился ошибкой', + 'message.maxTokens': 'Достигнут предел выходных токенов', + 'message.maxTokens.hint': 'Ответ был прерван; более ранний вывод сохраняется в разговоре. Отправьте «продолжить», чтобы модель возобновила работу.', + 'message.tokensPerSecond': '{tps} ток/с', + 'message.turnUsage.title': 'Использование ресурсов за ход', + 'message.turnUsage.consumed': 'Использование {total}', + 'message.turnUsage.model': 'Поставщик/модель', + 'message.turnUsage.cacheHit': 'Попадание в кэш', + 'message.turnUsage.input': 'Некэшированный ввод', + 'message.turnUsage.cacheRead': 'Кэшированный ввод', + 'message.turnUsage.cacheWrite': 'Запись в кэш', + 'message.turnUsage.output': 'Выход', + 'message.turnUsage.reasoning': ' (рассуждение {tokens})', + 'message.turnUsage.count': '{count} ток', + 'duration.secondUnit': 'с', + 'duration.minuteUnit': 'м ', + 'duration.hourUnit': 'ч ', + 'command.running': 'Бег…', + 'command.failed': 'Команда не выполнена', + 'command.done': 'Завершено', + 'command.title': 'Команда', + 'row.running': 'Бег', + 'row.failed': 'Не удалось', + 'json.truncated': '… усечено, всего символов {total}', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'message.stepProcess.thinking': 'Analisando a solicitação', + 'message.stepProcess.read': 'Lendo arquivos', + 'message.stepProcess.readImage': 'Lendo imagens', + 'message.stepProcess.write': 'Gravando arquivos', + 'message.stepProcess.search': 'Pesquisando código', + 'message.stepProcess.edit': 'Editando arquivos', + 'message.stepProcess.commands': 'Executando comandos', + 'message.stepProcess.code': 'Executando código', + 'message.stepProcess.webSearch': 'Pesquisando na web', + 'message.stepProcess.webFetch': 'Visitando páginas da web', + 'message.stepProcess.subagents': 'Coordenando subagentes', + 'message.stepProcess.plan': 'Atualizando o plano', + 'message.stepProcess.questions': 'Esperando por sua ação', + 'message.stepProcess.tools': 'Ferramentas de chamada', + 'message.stepProcess.prepare.read': 'Preparando-se para ler arquivos', + 'message.stepProcess.prepare.readImage': 'Preparando-se para ler imagens', + 'message.stepProcess.prepare.write': 'Preparando-se para gravar arquivos', + 'message.stepProcess.prepare.search': 'Preparando-se para pesquisar o código', + 'message.stepProcess.prepare.edit': 'Preparando-se para editar arquivos', + 'message.stepProcess.prepare.commands': 'Preparando para executar comandos', + 'message.stepProcess.prepare.code': 'Preparando-se para executar o código', + 'message.stepProcess.prepare.webSearch': 'Preparando-se para pesquisar na web', + 'message.stepProcess.prepare.webFetch': 'Preparando-se para visitar páginas da web', + 'message.stepProcess.prepare.subagents': 'Preparando-se para coordenar subagentes', + 'message.stepProcess.prepare.plan': 'Preparando-se para atualizar o plano', + 'message.stepProcess.prepare.questions': 'Preparando perguntas', + 'message.stepProcess.prepare.tools': 'Preparando chamadas de ferramenta', + 'message.stepProcess.done.thinking': 'Análise concluída', + 'message.stepProcess.done.read': 'Ler arquivos', + 'message.stepProcess.done.readImage': 'Ler imagens', + 'message.stepProcess.done.write': 'Escreveu arquivos', + 'message.stepProcess.done.search': 'Código pesquisado', + 'message.stepProcess.done.edit': 'Arquivos editados', + 'message.stepProcess.done.commands': 'Comandos executados', + 'message.stepProcess.done.code': 'Código executado', + 'message.stepProcess.done.webSearch': 'Pesquisei na web', + 'message.stepProcess.done.webFetch': 'Páginas da web visitadas', + 'message.stepProcess.done.subagents': 'Subagentes coordenados', + 'message.stepProcess.done.plan': 'Atualizado o plano', + 'message.stepProcess.done.questions': 'Perguntas feitas', + 'message.stepProcess.done.tools': 'Ferramentas chamadas', + 'message.stepProcess.joinTwo': '{first} e {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Execução solicitada', + 'message.trigger.goal': 'Objetivo contínuo', + 'message.trigger.agent': 'Mensagem de tarefa recebida', + 'message.trigger.team': 'Mensagem da equipe recebida', + 'message.trigger.subagent': 'Status da subtarefa atualizado', + 'message.trigger.github': 'GitHub evento recebido', + 'message.trigger.webhook': 'Evento externo recebido', + 'message.trigger.schedule': 'Tarefa de automação', + 'message.trigger.job': 'Tarefa em segundo plano atualizada', + 'message.trigger.plugin': 'Status do plug-in atualizado', + 'message.trigger.explanation': 'Esta notificação desencadeou esta resposta.', + 'message.turnProcess.worked': 'Concluído', + 'message.turnProcess.took': 'Concluído em ', + 'message.turnProcess.failed': 'Falha', + 'view.chat': 'Chat', + 'image.open': 'Ver imagem completa', + 'image.loading': 'Carregando imagem…', + 'image.failed': 'Visualização da imagem indisponível', + 'image.dialog': 'Visualização da imagem', + 'image.close': 'Fechar visualização da imagem', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} turnos, {steps} etapas', + 'stats.cacheHit': 'Acertos no cache: {percent}%', + 'stats.dialog.title': 'Estatísticas da sessão', + 'stats.dialog.usageTitle': 'Uso de token', + 'stats.dialog.llmTime': 'Horário do LLM', + 'stats.dialog.toolTime': 'Tempo da ferramenta', + 'stats.dialog.ttft': 'Tempo médio para o primeiro token (TTFT)', + 'stats.dialog.speed': 'Tokens por segundo (TPS)', + 'chat.loadingHistory': 'Carregando histórico…', + 'chat.loadError': 'Falha ao carregar o histórico: {message} ({code})', + 'chat.loadOlder': 'Carregar mais cedo', + 'chat.toBottom': 'De volta ao fundo', + 'chat.deepDiving': 'Raciocínio profundo', + 'chat.deepDivingFor': 'Raciocínio profundo há {duration} ···', + 'chat.turnNavigation.label': 'Navegação entre turnos', + 'chat.turnNavigation.jump': 'Ir para o turno {turn}', + 'chat.turnNavigation.jumpLoad': 'Carregar e ir para o turno {turn}', + 'chat.turnNavigation.turn': 'Turno {turn}', + 'settings.performance.title': 'Desempenho e uso', + 'settings.performance.description': 'Escolha quantas informações de desempenho e uso serão exibidas', + 'settings.performance.compact': 'Compacto', + 'settings.performance.detailed': 'Detalhado', + 'settings.links.title': 'Abra links de bate-papo em', + 'settings.links.description': 'Escolha onde abrir links da web', + 'settings.links.sidebar': 'Barra lateral no aplicativo', + 'settings.links.newTab': 'Navegador padrão', + 'settings.transcript.title': 'Detalhes do trabalho', + 'settings.transcript.description': 'Escolha quantos detalhes serão exibidos nas chamadas de ferramenta', + 'settings.collapse.title': 'Quando recolher detalhes do trabalho', + 'settings.collapse.description': 'Escolha quando recolher automaticamente os detalhes do trabalho', + 'settings.collapse.completion': 'Na conclusão', + 'settings.collapse.nextInput': 'Na próxima mensagem', + 'settings.foldedReplies.title': 'Respostas após o recolhimento', + 'settings.foldedReplies.description': 'Escolha quais respostas do assistente permanecerão visíveis quando os detalhes do trabalho forem recolhidos', + 'settings.foldedReplies.all': 'Todas as respostas', + 'settings.foldedReplies.final': 'Apenas resposta final', + 'settings.transcript.compact': 'Compacto', + 'settings.transcript.standard': 'Padrão', + 'settings.transcript.detailed': 'Detalhado', + 'settings.transcript.verbose': 'Verboso', + 'fileOpen.title': 'Não foi possível abrir o arquivo', + 'fileOpen.unknown': 'Não foi possível abrir este arquivo', + 'message.extraBlock': 'Bloco de conteúdo extra', + 'message.systemPrompt': 'Alerta do sistema', + 'message.systemPromptUpdate': 'Atualização imediata do sistema', + 'message.toolAdded': 'Ferramenta adicionada: {name}', + 'message.toolRemoved': 'Ferramenta removida: {name}', + 'message.toolsAdded': 'Adicionado: {names}', + 'message.toolsAddedCount': '{count} adicionado', + 'message.toolsChanged': '{added} adicionado, {removed} removido', + 'message.toolsRemoved': 'Removido: {names}', + 'message.toolsRemovedCount': '{count} removido', + 'message.toolsUpdated': 'Ferramentas atualizadas', + 'message.contextInjection': 'Injeção de contexto', + 'message.contextRecall': 'Recuperação de sessão', + 'message.referenceSummary': 'Sessão referenciada · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'carregado', + 'message.context.instructions.added': 'adicionado', + 'message.context.instructions.updated': 'atualizado', + 'message.context.instructions.removed': 'removido', + 'message.context.catalog.replaced': 'Catálogo de substituição', + 'message.context.catalog.more': '… {count} mais', + 'message.context.snapshot.supersedes': 'Substitui instantâneos anteriores', + 'message.context.relay.from': 'Da sessão {session}', + 'message.context.recall.counts': '{retained} mantido · {omitted} omitido', + 'message.context.recall.truncated': 'truncado', + 'message.compaction': 'Contexto compactado', + 'message.compaction.running': 'Contexto compactado…', + 'message.compaction.completed': '{items} itens de histórico compactados (~{tokens} tokens)', + 'message.compaction.expand': 'Ver resumo de compactação', + 'message.compaction.unavailable': 'Resumo de compactação indisponível', + 'message.compaction.commandTitle': 'compacto', + 'message.think': 'Pense', + 'message.unknownSurface': 'Evento de superfície desconhecido: {type}', + 'message.unknownBlock': 'Bloco de conteúdo desconhecido', + 'message.turnProcess.toolCalls.one': '{count} chamada de ferramenta', + 'message.turnProcess.toolCalls.other': '{count} chamadas de ferramenta', + 'message.turnProcess.messages.one': '{count} mensagem', + 'message.turnProcess.messages.other': '{count} mensagens', + 'message.turnProcess.subagents.one': '{count} subagente', + 'message.turnProcess.subagents.other': '{count} subagentes', + 'message.turnProcess.thoughtForAWhile': 'Pensou por um tempo', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Parado', + 'message.branch': 'Ramifique para uma nova conversa', + 'message.branchUnavailable': 'Disponível apenas na última mensagem de um turno concluído', + 'message.retry.active': 'Tentando novamente a solicitação de modelo', + 'message.retry.cancelled': 'Nova tentativa de solicitação de modelo cancelada', + 'message.retry.started': 'Solicitação de modelo repetida', + 'message.retry.scheduled': 'Aguardando para tentar novamente a solicitação de modelo', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Atraso na nova tentativa: ', + 'message.retry.failure': 'Motivo da falha: ', + 'message.failure.auth': 'A chave API é inválida', + 'message.taskStopped': 'Tarefa interrompida', + 'message.failure.accountSignedOut': 'Interrompido porque você saiu de DeepSeek.', + 'message.failure.interrupted': 'O processo DSH foi encerrado antes do término deste turno (por exemplo, ele foi eliminado, travou ou expirou durante o desligamento), portanto, o turno foi encerrado quando DSH foi reiniciado. Chamadas de ferramentas inacabadas têm resultados desconhecidos.', + 'message.failure.accountSignInRequired': 'Faça login em DeepSeek e verifique se o destino da solicitação suporta autenticação de conta.', + 'message.failure.quota': 'Cota de solicitação esgotada.', + 'message.turnError': 'Este turno falhou', + 'message.maxTokens': 'Limite de token de saída atingido', + 'message.maxTokens.hint': 'A resposta foi cortada; a saída anterior é preservada na conversa. Envie "continuar" para permitir que o modelo seja retomado.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Uso do turno', + 'message.turnUsage.consumed': 'Uso {total}', + 'message.turnUsage.model': 'Provedor/modelo', + 'message.turnUsage.cacheHit': 'Acertos no cache', + 'message.turnUsage.input': 'Entrada sem cache', + 'message.turnUsage.cacheRead': 'Entrada em cache', + 'message.turnUsage.cacheWrite': 'Gravação em cache', + 'message.turnUsage.output': 'Saída', + 'message.turnUsage.reasoning': ' ({tokens} raciocínio)', + 'message.turnUsage.count': '{count} tok', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Correndo…', + 'command.failed': 'Falha no comando', + 'command.done': 'Concluído', + 'command.title': 'Comando', + 'row.running': 'Correndo', + 'row.failed': 'Falha', + 'json.truncated': '… truncado, {total} caracteres no total', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record diff --git a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx index 79281d39d1..8d63088052 100644 --- a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx @@ -268,7 +268,7 @@ describe('composer stats pills', () => { expect(dialog.getAttribute('aria-label')).toBe('Session statistics') // Portaled out of the composer dock. expect(dialog.parentElement).toBe(document.body) - expect(dialog.firstChild?.textContent).toBe('Session statistics') + expect(dialog.firstChild?.textContent).toBe('Session statistics1 turns 1 steps') const details = dialog.querySelector('[data-session-stats-details]') as HTMLElement expect(details).toBeTruthy() expect(details.textContent).toContain('LLM time3.8s') @@ -375,6 +375,53 @@ describe('composer stats pills', () => { expect(usageDialog.textContent).toContain('未缓存输入5 tok') }) + describe('full reading of a cut-off pill', () => { + const FULL = '105 tok · Cache hit 90%' + + function renderCut(overflow: boolean, mode: 'detailed' | 'compact' = 'detailed') { + vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(overflow ? 200 : 80) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + vi.useFakeTimers() + const { source } = makeSource({ nodes: [timedStep()] }) + return render( selector(mode)} />) + } + const tooltipText = (view: ReturnType) => view.queryByRole('tooltip')?.textContent ?? null + afterEach(() => { vi.useRealTimers() }) + + it.each([false, true])('shows on hover after the dock delay only while the label is cut off: %s', (overflow) => { + const view = renderCut(overflow) + fireEvent.pointerEnter(view.getAllByRole('button')[1]!) + expect(tooltipText(view)).toBeNull() + act(() => { vi.advanceTimersByTime(200) }) + expect(tooltipText(view)).toBe(overflow ? FULL : null) + }) + + it('shows on keyboard focus of a dialog pill', () => { + const view = renderCut(true) + fireEvent.keyDown(document.body, { key: 'Tab' }) + fireEvent.focus(view.getAllByRole('button')[1]!) + expect(tooltipText(view)).toBe(FULL) + }) + + it('yields to the open dialog, whose head carries the counts', () => { + const view = renderCut(true) + const [timePill] = view.getAllByRole('button') as [HTMLElement] + fireEvent.click(timePill) + fireEvent.pointerEnter(timePill) + act(() => { vi.advanceTimersByTime(200) }) + expect(tooltipText(view)).toBeNull() + expect(view.getByRole('dialog').firstChild?.textContent).toBe('Session statistics1 turns 1 steps') + }) + + it('pins a cut-off static reading on tap', () => { + const view = renderCut(true, 'compact') + const plainPill = view.getByText('Cache hit 90%').parentElement! + fireEvent.pointerDown(plainPill, { pointerType: 'touch' }) + fireEvent.click(plainPill) + expect(tooltipText(view)).toBe('Cache hit 90%') + }) + }) + it('keeps the durable usage pill after the visible step window is empty', () => { const { source } = makeSource() const view = render( { }) const fileDrag = { prepare: vi.fn(async () => ({ start: () => true, release: () => {} })), reportFailure: vi.fn() } + +it('formats Portuguese elapsed minutes with numeric units', () => { + expect(formatRunDuration(90_000, key => pt[key])).toBe('1m 30s') +}) diff --git a/packages/client/ui-commands/README.i18n.yaml b/packages/client/ui-commands/README.i18n.yaml index 2521e4cb81..eed1e982c7 100644 --- a/packages/client/ui-commands/README.i18n.yaml +++ b/packages/client/ui-commands/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-commands/use-this-package: - en: 01adce72791579fa - zh: 70eb55a1529038e7 + en: c325c9eaca4efe47 + zh: dbbc29cc9cec54bc /deepseek-ai-dsh-client-ui-commands/use-this-package/kinds-and-decorations: en: 57679a20574d8b3f zh: 98d5e5a7781e6de1 diff --git a/packages/client/ui-commands/README.md b/packages/client/ui-commands/README.md index fc5e7efe21..ef8781649e 100644 --- a/packages/client/ui-commands/README.md +++ b/packages/client/ui-commands/README.md @@ -25,7 +25,7 @@ Typing a `/` command opens a registered popup, a client action, a host command's ## Use this package -Mount this plugin alongside `ui-input-trigger` and `ui-conversation`; the `/` source then appears in the trigger menu, and business packages register their command surfaces through `ctx.commandUi`. Typing `/model` opens the registered popup; a host command with an argument claim opens its input or executes directly. The popup holds composer focus: typing filters the loaded rows locally, `↑`/`↓` walk them, Enter and `Tab` accept the highlighted row, and Escape and `Shift+Tab` return to the composer. The highlight opens on the row the options mark as the session's current value, so accepting on a freshly opened panel confirms it. The composer's `+` button and a typed `/` open the same menu: an Add section (File, Goal, Plan, Feedback) and a Commands section (Compact, Permission, Model, Export) in usage order, each row with a glyph, a localized title and description, and the command name as an alias where the localized title differs from it. +Mount this plugin alongside `ui-input-trigger` and `ui-conversation`; the `/` source then appears in the trigger menu, and business packages register their command surfaces through `ctx.commandUi`. Typing `/model` opens the registered popup; a host command with an argument claim opens its input or executes directly. The popup automatically focuses its search input on desktop. Phones and tablets require tapping the search input, following the shared [text-input focus policy](../ui-primitives/README.md#use-this-package). With search focused, typing filters the loaded rows locally, `↑`/`↓` walk them, Enter and `Tab` accept the highlighted row, and Escape and `Shift+Tab` return to the composer. The highlight opens on the row the options mark as the session's current value, so accepting on a freshly opened panel confirms it. The composer's `+` button and a typed `/` open the same menu: an Add section (File, Goal, Plan, Feedback) and a Commands section (Compact, Permission, Model, Export) in usage order, each row with a glyph, a localized title and description, and the command name as an alias where the localized title differs from it. ### Kinds and decorations diff --git a/packages/client/ui-commands/README.zh.md b/packages/client/ui-commands/README.zh.md index 7393d20329..7ad5b4f9a1 100644 --- a/packages/client/ui-commands/README.zh.md +++ b/packages/client/ui-commands/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -与 `ui-input-trigger` 及 `ui-conversation` 一起挂载本插件;`/` source 随即出现在触发菜单中,业务包经 `ctx.commandUi` 注册自己的命令表面。键入 `/model` 打开已注册的弹窗;带参数声明的宿主命令打开其输入或直接执行。弹窗持有 composer 焦点:键入即在已加载的行上本地筛选,`↑`/`↓` 在行间移动,回车与 `Tab` 接受高亮行,Escape 与 `Shift+Tab` 把焦点还给 composer。高亮落在选项标记为会话当前值的行上,因此在刚打开的弹窗上接受即确认当前值。composer 的 `+` 按钮与键入的 `/` 打开同一个菜单:「添加」小节(文件、目标、计划、反馈)与「指令」小节(压缩、权限、模型、下载日志)按使用频次排列,每行带图标、本地化的标题与说明,本地化标题与命令名不同时还显示命令名作为别名。 +与 `ui-input-trigger` 及 `ui-conversation` 一起挂载本插件;`/` source 随即出现在触发菜单中,业务包经 `ctx.commandUi` 注册自己的命令表面。键入 `/model` 打开已注册的弹窗;带参数声明的宿主命令打开其输入或直接执行。桌面端的弹窗自动聚焦搜索框。手机和平板需要点击搜索框,遵循共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。搜索框聚焦后,键入即在已加载的行上本地筛选,`↑`/`↓` 在行间移动,回车与 `Tab` 接受高亮行,Escape 与 `Shift+Tab` 把焦点还给 composer。高亮落在选项标记为会话当前值的行上,因此在刚打开的弹窗上接受即确认当前值。composer 的 `+` 按钮与键入的 `/` 打开同一个菜单:「添加」小节(文件、目标、计划、反馈)与「指令」小节(压缩、权限、模型、下载日志)按使用频次排列,每行带图标、本地化的标题与说明,本地化标题与命令名不同时还显示命令名作为别名。 ### 种类与装饰 diff --git a/packages/client/ui-commands/locale/de.json b/packages/client/ui-commands/locale/de.json new file mode 100644 index 0000000000..8fdc53edce --- /dev/null +++ b/packages/client/ui-commands/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Befehlsauswahlschnittstelle", + "description": "Bietet ein Slash-Befehlsmenü und eine zugehörige Bedienoberfläche im Eingabefeld." + } +} diff --git a/packages/client/ui-commands/locale/es.json b/packages/client/ui-commands/locale/es.json new file mode 100644 index 0000000000..9fb9b09c55 --- /dev/null +++ b/packages/client/ui-commands/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de comandos", + "description": "Proporciona un menú de comando de barra diagonal y una interfaz de operación relacionada en el cuadro de entrada." + } +} diff --git a/packages/client/ui-commands/locale/fr.json b/packages/client/ui-commands/locale/fr.json new file mode 100644 index 0000000000..e0e61417a6 --- /dev/null +++ b/packages/client/ui-commands/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de commandes", + "description": "Fournit un menu de commande slash et une interface d’opération associée dans la zone de saisie." + } +} diff --git a/packages/client/ui-commands/locale/pt.json b/packages/client/ui-commands/locale/pt.json new file mode 100644 index 0000000000..01fc89d3a8 --- /dev/null +++ b/packages/client/ui-commands/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de comandos", + "description": "Fornece um menu de comando de barra e interface de operação relacionada na caixa de entrada." + } +} diff --git a/packages/client/ui-commands/locale/ru.json b/packages/client/ui-commands/locale/ru.json new file mode 100644 index 0000000000..529ec9773b --- /dev/null +++ b/packages/client/ui-commands/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора команд", + "description": "Предоставляет меню команд с косой чертой и соответствующий рабочий интерфейс в поле ввода." + } +} diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index 0b3854264c..0f97864fff 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-commands/src/client/PopupSelectView.tsx b/packages/client/ui-commands/src/client/PopupSelectView.tsx index 4b0b4bf418..911a8f5d21 100644 --- a/packages/client/ui-commands/src/client/PopupSelectView.tsx +++ b/packages/client/ui-commands/src/client/PopupSelectView.tsx @@ -1,9 +1,10 @@ /** * Official popupSelect shell: renders one session's PopupSelectController * store into the conversation.input.overlay anchor. Unlike the slash menu - * (combobox — textarea keeps focus), this shell HOLDS focus while open: the - * inner search input takes focus, plain typing filters the loaded options - * locally, Enter and Tab accept the filtered highlight, ↑↓ walk it (wrapping, + * (combobox — textarea keeps focus), this shell focuses its search input on + * desktop; phones and tablets require explicit search focus. Typing filters + * the loaded options locally, Enter and Tab accept the filtered highlight, + * ↑↓ walk it (wrapping, * scrolled into view), and Escape and Shift+Tab dismiss back to the composer. * ←→ keep the search input's native caret. Any pointer interaction outside the * box dismisses (the click's own target takes focus). Closed state renders @@ -14,7 +15,7 @@ import { MenuGroup, MenuSurface, observeStickyMenuGroups } from '@deepseek-ai/ds import { Fragment, useEffect, useMemo, useRef } from 'react' import { useSyncExternalStore } from 'react' import clsx from 'clsx' -import { IconCheckOutlineRegular, RiskConfirmation, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconCheckOutlineRegular, isMobileDevice, RiskConfirmation, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import { filterOptions } from './popup.ts' import type { SelectOption } from './contract.ts' @@ -67,8 +68,7 @@ export function PopupSelectView({ popup, t }: PopupSelectViewProps) { cardRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: 'nearest' }) }, [active, rows, state.confirming]) - // Focus ownership: the search input grabs on open, and ANY outside - // pointer interaction dismisses — + // Any outside pointer interaction dismisses — // capture phase so a click landing anywhere else (textarea included) // closes the shell before its own handlers run; that click's target then // takes focus naturally, so no focusComposer here. @@ -84,7 +84,9 @@ export function PopupSelectView({ popup, t }: PopupSelectViewProps) { // Focus the search input after it mounts (separate effect so the ref is populated). useEffect(() => { - if (state.open && state.confirming === null) searchRef.current?.focus() + if (!state.open || state.confirming !== null) return + if (isMobileDevice()) return + searchRef.current?.focus() }, [state.open, state.confirming]) if (!state.open) return null diff --git a/packages/client/ui-commands/src/client/index.ts b/packages/client/ui-commands/src/client/index.ts index fc098764a4..f1e8d62bf2 100644 --- a/packages/client/ui-commands/src/client/index.ts +++ b/packages/client/ui-commands/src/client/index.ts @@ -18,7 +18,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { CommandUiRuntime } from './service.ts' import type { PopupSelectInjected } from './PopupSelectView.tsx' import { PopupSelectView } from './PopupSelectView.tsx' -import { en, ja, zh, type CommandKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type CommandKey } from './locales.ts' export { CommandUiRuntime } from './service.ts' export { CommandDirectory } from './directory.ts' @@ -56,7 +56,7 @@ export const inject = ['inputTriggers', 'sessions', 'remote', 'remote.commands', * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-commands: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-commands: dictionaries') ctx.plugin(CommandUiRuntime) ctx.inject(['slots', 'commandUi', 'sessions'], (scope: ClientContext) => { const command = scope.commandUi diff --git a/packages/client/ui-commands/src/client/locales.ts b/packages/client/ui-commands/src/client/locales.ts index de34e25cb7..ae0517b5f4 100644 --- a/packages/client/ui-commands/src/client/locales.ts +++ b/packages/client/ui-commands/src/client/locales.ts @@ -103,3 +103,163 @@ export const ja = { 'listbox.aria': '/{command} の一致項目', 'notice.attachmentsUnsupported': '/{command} は添付ファイルを受け付けません。先に削除してください', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'section.add': 'Añadir', + 'section.commands': 'Comandos', + 'label.goal': 'Objetivo', + 'label.plan': 'Planificar', + 'label.feedback': 'Comentarios', + 'label.compact': 'Compacto', + 'label.permission': 'Permiso', + 'label.export': 'Exportar', + 'description.goal': 'Establecer o ver el objetivo de una tarea de larga duración', + 'description.plan': 'Entrar o salir del modo plan', + 'description.feedback': 'Registrar comentarios sobre esta sesión', + 'description.compact': 'Historial de conversaciones antiguo compacto', + 'description.permission': 'Cambiar el ajuste preestablecido de permisos (modo sandbox + política de aprobación)', + 'description.export': 'Descargue este registro de sesión como un archivo ZIP', + 'token.goal': 'objetivo', + 'token.plan': 'planear', + 'token.feedback': 'retroalimentación', + 'token.compact': 'compacto', + 'token.permission': 'permiso', + 'token.export': 'exportar', + 'search.placeholder': 'Buscar…', + 'search.aria': 'Opciones de filtro', + 'status.loading': 'Cargando opciones…', + 'status.applying': 'Aplicando…', + 'status.empty': 'Sin opciones', + 'overlay.aria': 'Opciones de /{command}', + 'listbox.aria': '/{command} coincidencias', + 'notice.attachmentsUnsupported': '/{command} no acepta archivos adjuntos; eliminarlos primero', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'section.add': 'Ajouter', + 'section.commands': 'Commandes', + 'label.goal': 'Objectif', + 'label.plan': 'Planifier', + 'label.feedback': 'Commentaires', + 'label.compact': 'Compacte', + 'label.permission': 'Autorisation', + 'label.export': 'Exporter', + 'description.goal': "Définir ou afficher l'objectif d'une tâche de longue durée", + 'description.plan': 'Entrer ou quitter le mode Plan', + 'description.feedback': 'Enregistrer les commentaires sur cette session', + 'description.compact': 'Compacter l’historique des conversations anciennes', + 'description.permission': "Changer le préréglage d'autorisation (mode sandbox + politique d'approbation)", + 'description.export': "Téléchargez ce journal de session sous forme d'archive ZIP", + 'token.goal': 'objectif', + 'token.plan': 'planifier', + 'token.feedback': 'commentaires', + 'token.compact': 'compacte', + 'token.permission': 'autorisation', + 'token.export': 'exporter', + 'search.placeholder': 'Rechercher…', + 'search.aria': 'Options de filtrage', + 'status.loading': 'Options de chargement…', + 'status.applying': 'Application…', + 'status.empty': 'Aucune option', + 'overlay.aria': 'Options /{command}', + 'listbox.aria': '/{command} correspondances', + 'notice.attachmentsUnsupported': "/{command} n'accepte pas les pièces jointes ; retirez-les d'abord", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'section.add': 'Hinzufügen', + 'section.commands': 'Befehle', + 'label.goal': 'Ziel', + 'label.plan': 'Planen', + 'label.feedback': 'Rückmeldung', + 'label.compact': 'Kompakt', + 'label.permission': 'Erlaubnis', + 'label.export': 'Exportieren', + 'description.goal': 'Legen Sie das Ziel für eine Aufgabe mit langer Laufzeit fest oder zeigen Sie es an', + 'description.plan': 'Aktivieren oder verlassen Sie den Planmodus', + 'description.feedback': 'Zeichnen Sie Feedback zu dieser Sitzung auf', + 'description.compact': 'Kompakter älterer Gesprächsverlauf', + 'description.permission': 'Ändern Sie die Berechtigungsvoreinstellung (Sandbox-Modus + Genehmigungsrichtlinie).', + 'description.export': 'Laden Sie dieses Sitzungsprotokoll als ZIP-Archiv herunter', + 'token.goal': 'Ziel', + 'token.plan': 'planen', + 'token.feedback': 'Rückmeldung', + 'token.compact': 'kompakt', + 'token.permission': 'Erlaubnis', + 'token.export': 'exportieren', + 'search.placeholder': 'Suchen…', + 'search.aria': 'Filteroptionen', + 'status.loading': 'Ladeoptionen…', + 'status.applying': 'Wird angewendet…', + 'status.empty': 'Keine Optionen', + 'overlay.aria': '/{command}-Optionen', + 'listbox.aria': '/{command} stimmt überein', + 'notice.attachmentsUnsupported': '/{command} akzeptiert keine Anhänge; Entfernen Sie sie zuerst', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'section.add': 'Добавить', + 'section.commands': 'Команды', + 'label.goal': 'Цель', + 'label.plan': 'План', + 'label.feedback': 'Обратная связь', + 'label.compact': 'Компактный', + 'label.permission': 'Разрешение', + 'label.export': 'Экспорт', + 'description.goal': 'Установите или просмотрите цель для долгосрочной задачи', + 'description.plan': 'Войти в режим плана или выйти из него', + 'description.feedback': 'Запишите отзыв об этом сеансе', + 'description.compact': 'Сжать старую историю разговоров', + 'description.permission': 'Переключение настроек разрешений (режим песочницы + политика утверждения)', + 'description.export': 'Загрузите этот журнал сеанса в виде ZIP-архива.', + 'token.goal': 'цель', + 'token.plan': 'план', + 'token.feedback': 'обратная связь', + 'token.compact': 'компактный', + 'token.permission': 'разрешение', + 'token.export': 'экспорт', + 'search.placeholder': 'Искать…', + 'search.aria': 'Параметры фильтра', + 'status.loading': 'Загрузка параметров…', + 'status.applying': 'Применение…', + 'status.empty': 'Нет вариантов', + 'overlay.aria': 'Опции /{command}', + 'listbox.aria': '/{command} совпадений', + 'notice.attachmentsUnsupported': '/{command} не принимает вложения; сначала удали их', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'section.add': 'Adicionar', + 'section.commands': 'Comandos', + 'label.goal': 'Objetivo', + 'label.plan': 'Plano', + 'label.feedback': 'Comentários', + 'label.compact': 'Compacto', + 'label.permission': 'Permissão', + 'label.export': 'Exportar', + 'description.goal': 'Defina ou visualize a meta para uma tarefa de longa duração', + 'description.plan': 'Entrar ou sair do modo de plano', + 'description.feedback': 'Registre feedback sobre esta sessão', + 'description.compact': 'Histórico compacto de conversas antigas', + 'description.permission': 'Alterar a predefinição de permissão (modo sandbox + política de aprovação)', + 'description.export': 'Baixe este log de sessão como um arquivo ZIP', + 'token.goal': 'objetivo', + 'token.plan': 'planejar', + 'token.feedback': 'comentários', + 'token.compact': 'compacto', + 'token.permission': 'permissão', + 'token.export': 'exportar', + 'search.placeholder': 'Pesquisar…', + 'search.aria': 'Opções de filtro', + 'status.loading': 'Carregando opções…', + 'status.applying': 'Aplicando…', + 'status.empty': 'Sem opções', + 'overlay.aria': '/{command} opções', + 'listbox.aria': '/{command} correspondências', + 'notice.attachmentsUnsupported': '/{command} não aceita anexos; remova-os primeiro', +} satisfies Record diff --git a/packages/client/ui-commands/tests/popup-view.client.spec.tsx b/packages/client/ui-commands/tests/popup-view.client.spec.tsx index 3003a868d9..9b1d246927 100644 --- a/packages/client/ui-commands/tests/popup-view.client.spec.tsx +++ b/packages/client/ui-commands/tests/popup-view.client.spec.tsx @@ -1,7 +1,8 @@ // @vitest-environment jsdom /** * PopupSelectView interaction spec: the search input takes - * focus on open and plain typing filters locally, ↑↓ move the filtered + * focus on desktop open; mobile search requires explicit focus. Typing filters + * locally, ↑↓ move the filtered * highlight while ←→ stay native to the input, Enter and Tab select * single-flight, Escape and Shift+Tab dismiss back through focusComposer, * outside pointerdown dismisses @@ -9,7 +10,8 @@ * retry button, the highlighted row scrolls into view, and the card height * clamps to the space above the composer. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import * as uiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' +import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import type { SelectOption } from '../src/client/contract.ts' import type { PopupSpec, TokenSegment } from '../src/client/popup.ts' @@ -140,6 +142,40 @@ describe('PopupSelectView', () => { expect(rowLabels()).toEqual(['Dark', 'Light', 'Sepia']) }) + it.each([ + { name: 'iPhone', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1', platform: 'iPhone', maxTouchPoints: 5, automatic: false }, + { name: 'Android phone', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/119.0.0.0 Mobile Safari/537.36', platform: 'Linux armv8l', maxTouchPoints: 0, automatic: false }, + { name: 'Android tablet', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36', platform: 'Linux armv8l', maxTouchPoints: 5, automatic: false }, + { name: 'desktop-mode iPad', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 5, automatic: false }, + { name: 'Mac', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 0, automatic: true }, + { name: 'Windows touchscreen', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36', platform: 'Win32', maxTouchPoints: 5, automatic: true }, + ])('automatic search focus respects device classification: $name', async ({ userAgent, platform, maxTouchPoints, automatic }) => { + for (const [name, value] of Object.entries({ userAgent, platform, maxTouchPoints })) { + const descriptor = Object.getOwnPropertyDescriptor(navigator, name) + Object.defineProperty(navigator, name, { configurable: true, value }) + onTestFinished(() => { + if (descriptor === undefined) Reflect.deleteProperty(navigator, name) + else Object.defineProperty(navigator, name, descriptor) + }) + } + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const { popup, search } = await mountOpen() + expect(document.activeElement === search).toBe(automatic) + act(() => { search.focus() }) + fireEvent.change(search, { target: { value: 'dark' } }) + expect(document.activeElement).toBe(search) + expect(rowLabels()).toEqual(['Dark']) + act(() => { popup.dismiss() }) + await act(async () => { + popup.open('theme', spec(), 'ctx-A', SEGMENT) + await Promise.resolve() + }) + expect(document.activeElement === screen.getByRole('textbox', { name: '筛选选项' })).toBe(automatic) + }) + it('renders an optional option badge as a superscript marker', async () => { await mountOpen({ options: () => Promise.resolve([{ id: 'auto', label: 'Auto review', badge: 'EXP' }]), diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 268b7e59ae..7d9bf9f3c1 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,8 +21,8 @@ en: 7871f9fe6bcc4459 zh: 0d51565c600b3958 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 1d70d520d4a7537c - zh: 83febffaf90e0ac5 + en: e910ac7fec4fd72d + zh: 741a399436dc98d9 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index a905984576..86a1ffd236 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -70,18 +70,24 @@ The existing semantic `DraftSnapshot` retains quote text and clipboard-coordinat The shared image slot props keep display choices separate from durable references: `thumbnail` requests a contained attachment-list thumbnail, while `compact` requests a cropped gallery tile. An optional per-image `label` supplies the accessible display name; loading and cache identity still use the original attachment reference. `MessageImagesContentInput` adds optional complete `MessageImageLabels` for the root-scoped `conversation.images.content` Factory. Message, tool, and Factory inputs carry the view's authorized `loadImage` callback explicitly; loading and URL caching remain with the resource owner. [ui-attachment](../ui-attachment/README.md) provides this Factory and owns rendering and the lightbox. +The composer uses a minimum 16px draft font on coarse-pointer devices to avoid focus-triggered browser zoom; larger font preferences still apply. + The composer measures its expanded control groups after size, content, visibility, or font-loading changes. If they cannot share a line, the row sets `--dsh-composer-model-text-display: none` and `--dsh-composer-model-icon-display: block` for the model seat; their defaults are `block` and `none`. Wrapping remains available if even the icon cannot fit. The context meter follows the [statistics-control radius](../../../docs/ui-radius.md#radius-scale). +The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus. On these mobile devices, a submission that empties the focused draft moves focus to the non-text composer card, dismissing the keyboard while retaining its navigation position. Rejected submissions, retained newer draft text, and another control already holding focus preserve focus. The [Lexical patch](../../../patches/lexical@0.49.0.patch) keeps caret scrolling in the same coordinate system as DOM geometry when a mobile keyboard pans the page. + The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available. With Coding Tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Coding Tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility. -While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (available Views in registration order with `id`, `label`, and `pinned`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps one root list of unpinned View ids in browser `localStorage` under `dsh.slot.manual-order.conversation.view.v1`, stored as `{ "unpinned": string[] }` and shared by every Session; the tab bar omits unpinned Views, and Chat always shows. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View leaves its content selected and keeps its tab, with `aria-selected`, until another View is selected. Ids of Views that are not registered stay stored; stored content other than a string array shows every tab. +While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (Views outside the header in registration order with `id`, `label`, `pinned`, and `active`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps an in-memory list of unpinned View ids shared by every Session; the tab bar omits unpinned Views and places registered Chat first so it always remains outside More. Plugin reload resets pin choices, and legacy localStorage values are ignored. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View hides its tab immediately without changing the selected content. More lists only Views outside the header and identifies the current View through `active`. The header reads its available row width, reserves 48px for More and padding, and estimates 120px per tab. Remaining Views keep registration order in More. A single ResizeObserver watches the tab row so window resizing and sidebar expansion update the split; active selection does not change the order. The observer disconnects when the row is removed. If ResizeObserver is unavailable, the initial width calculation still applies without subsequent observation. The More button highlights and names the active View in its tooltip when that View is folded. Individual tabs are not measured. At least one preferred tab remains outside, and long labels truncate with a full-name tooltip. Unregistered View ids remain in the in-memory pin list until plugin reload. The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module. +The composer reads its Session's access answers through the Session-scope `useAccess` Hook: Send disappears while the access snapshot excludes `session/prompt`, Stop while it excludes `session/cancel`, and file intake follows `fileUploads/upload`. A composer without a Session has no answer and keeps its ordinary controls. Send and Stop are withheld from their buttons alone: a keyboard submission or the Esc-Esc sequence still reaches the Host, which refuses it and reports the failure. These UI answers never replace Host authorization. + `SessionInputShell` owns one Lexical editor per Session through its private [DraftEditorRuntime](src/client/input/editor/runtime.ts), while retaining submission, attachment selection, and recovery decisions. [DraftEditor](src/client/input/editor/DraftEditor.tsx) renders the borrowed editor; InputBar retains its Hooks and refs and installs DOM behavior through [view-binding](src/client/input/editor/view-binding.ts). Editor-facing types live in [draft-editor.ts](src/client/contract/draft-editor.ts), with shared input and submission types in [input.ts](src/client/contract/input.ts). Background editor updates retain the draft selection without changing the document selection or reclaiming focus from another control; explicit Composer focus restores its selection. This separation does not support simultaneous editable roots for one Session; [the two-stage isolation proposal](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) defines the remaining work. Drafts retain plain text and file, folder, and Session reference chips across Session switching and reload. Public input methods accept text; internal restoration and Workspace transfer preserve structured references. The input model imports saved content before its first published read; React only mounts that editor and binds the existing Conversation store writer. The original storage key accepts legacy strings and writes semantic snapshots without runtime node IDs. A pending skill catalog does not block editing: slash names remain ordinary text until a matching catalog entry arrives. Arrival rematches the current text without changing chips, selection, or Undo history. Browser-owned attachment File objects are not persisted by this mechanism. The [draft initialization decision](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md) defines ownership and restoration. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index feb91d7562..ebfbaff130 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -70,18 +70,24 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS 共享图片插槽属性将展示选择与持久化引用分开:`thumbnail` 请求完整缩放的附件列表缩略图,`compact` 请求裁剪的图片方块。每张图片可通过可选的 `label` 提供无障碍展示名称;加载和缓存标识仍使用原始附件引用。`MessageImagesContentInput` 为 root 作用域的 `conversation.images.content` Factory 增加可选的完整 `MessageImageLabels`。消息、工具及 Factory 输入显式携带视图提供的授权 `loadImage` 回调;读取和 URL 缓存仍由资源所有者负责。[ui-attachment](../ui-attachment/README.zh.md) 提供该 Factory,负责渲染与灯箱。 +在粗指针设备上,composer 的草稿字号至少为 16px,以避免获得焦点时浏览器自动放大;更大的字号偏好仍然生效。 + 控件组的尺寸、内容、可见性或字体加载状态变化后,composer 测量展开状态下的控件组。若无法排在同一行,控制栏为模型位设置 `--dsh-composer-model-text-display: none` 和 `--dsh-composer-model-icon-display: block`;两者默认值分别为 `block` 和 `none`。若连图标也放不下,仍允许换行。 上下文占用率入口遵循[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。 +composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。在这些移动设备上,提交清空正在聚焦的草稿后,焦点移到不接受文字输入的 composer 卡片,收起键盘并保留键盘导航位置。被拒绝的提交、保留的新草稿文字,以及已经聚焦的其他控件均保留原有焦点。移动端键盘平移页面时,[Lexical 补丁](../../../patches/lexical@0.49.0.patch)让光标滚动计算与 DOM 几何使用同一坐标系。 + 上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。 关闭代码工作工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭代码工作工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。 -root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可用 View,含 `id`、`label` 与 `pinned`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在浏览器 `localStorage` 的 `dsh.slot.manual-order.conversation.view.v1` 下保存一份 root 级的已收起 View id 列表,格式为 `{ "unpinned": string[] }`,所有 Session 共用;标签栏不显示已收起的 View,Chat 始终显示。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 不会切走其内容,其标签及 `aria-selected` 保留到选中其他 View 为止。未注册 View 的 id 保留在列表中;保存内容不是字符串数组时,所有标签都显示。 +root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的头部之外的 View,含 `id`、`label`、`pinned` 与 `active`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在内存中保存所有 Session 共用的已收起 View id 列表;标签栏不显示已收起的 View,并将已注册的 Chat 放在首位,确保它始终留在更多菜单外。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 会立即隐藏其标签,但不会切走其内容。更多菜单仅包含头部之外的 View,通过 `active` 标识当前 View。头部读取当前行可用宽度,为更多按钮与内边距预留 48px,每个标签估算 120px,其余 View 按注册顺序进入更多。单个 ResizeObserver 观察标签行,使窗口缩放和侧栏展开能够更新分组;当前选中项变化不改变顺序。标签行移除时断开观察。如果环境没有 ResizeObserver,仍按初始宽度计算,但不继续观察。当前 View 收起时,更多按钮高亮并在提示框中显示其名称。不逐项测量标签。至少保留一个优先展示的外部标签,长标题截断并通过提示框提供全名。未注册 View 的 id 保留在内存 Pin 列表中,直到插件重新加载。 输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。 +输入框通过会话作用域的 `useAccess` 钩子读取所在 Session 的访问答案:访问快照不含 `session/prompt` 时隐藏发送,不含 `session/cancel` 时隐藏停止,附件接收跟随 `fileUploads/upload`。没有 Session 的输入框尚无答案,保留普通控件。发送与停止都只对按钮生效——键盘提交或 Esc-Esc 序列仍会到达 Host,由 Host 拒绝并上报失败。这些界面判断不替代 Host 授权。 + `SessionInputShell` 通过私有 [DraftEditorRuntime](src/client/input/editor/runtime.ts) 为每个 Session 持有一个 Lexical editor,同时保留提交、附件选择和恢复决策。[DraftEditor](src/client/input/editor/DraftEditor.tsx) 呈现借用的 editor;InputBar 保留钩子与 refs,并通过 [view-binding](src/client/input/editor/view-binding.ts) 安装 DOM 行为。编辑器类型位于 [draft-editor.ts](src/client/contract/draft-editor.ts),共享输入和提交类型位于 [input.ts](src/client/contract/input.ts)。编辑器后台更新会保留草稿选区,不修改文档选区或从其他控件夺回焦点;显式聚焦 Composer 时恢复其选区。这一拆分不支持同一 Session 同时挂载多个可编辑 root;[两阶段隔离提案](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md) 定义剩余工作。 草稿中的普通文字及文件、目录、Session 引用胶囊在切换 Session 和刷新后保留。公开输入方法接受文本;内部恢复和工作区携带保留结构化引用。输入模型在首次对外读取前导入保存内容;React 只挂载同一编辑器并绑定现有 Conversation store 写入回调。原存储 key 兼容旧字符串,新写入保存不含运行时节点 ID 的语义快照。等待 skill 目录不会阻塞编辑:斜杠名称保持普通文字,直到匹配的目录项到达。目录到达只重匹配当前文字,不改变胶囊、选区或 Undo 历史。浏览器持有的附件 File 对象不由这一机制持久化。[草稿初始化决策](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md)规定所有权与恢复流程。 diff --git a/packages/client/ui-conversation/locale/de.json b/packages/client/ui-conversation/locale/de.json new file mode 100644 index 0000000000..def54f49a5 --- /dev/null +++ b/packages/client/ui-conversation/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Konversationsschnittstellen-Framework", + "description": "Organisieren Sie Dialoginhalte, Eingabefelder, Nachrichtenwarteschlangen und zeigen Sie die Navigation an." + } +} diff --git a/packages/client/ui-conversation/locale/es.json b/packages/client/ui-conversation/locale/es.json new file mode 100644 index 0000000000..89664f8f6a --- /dev/null +++ b/packages/client/ui-conversation/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "marco de interfaz conversacional", + "description": "Organice el contenido del diálogo, los cuadros de entrada, las colas de mensajes y vea la navegación." + } +} diff --git a/packages/client/ui-conversation/locale/fr.json b/packages/client/ui-conversation/locale/fr.json new file mode 100644 index 0000000000..a24689adf7 --- /dev/null +++ b/packages/client/ui-conversation/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "cadre d'interface conversationnelle", + "description": "Organisez le contenu des boîtes de dialogue, les zones de saisie, les files d'attente de messages et la navigation dans les vues." + } +} diff --git a/packages/client/ui-conversation/locale/pt.json b/packages/client/ui-conversation/locale/pt.json new file mode 100644 index 0000000000..25642a6dc7 --- /dev/null +++ b/packages/client/ui-conversation/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "estrutura de interface conversacional", + "description": "Organize o conteúdo da caixa de diálogo, caixas de entrada, filas de mensagens e visualize a navegação." + } +} diff --git a/packages/client/ui-conversation/locale/ru.json b/packages/client/ui-conversation/locale/ru.json new file mode 100644 index 0000000000..5e527b7f33 --- /dev/null +++ b/packages/client/ui-conversation/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "структура диалогового интерфейса", + "description": "Упорядочивайте содержимое диалогов, поля ввода, очереди сообщений и навигацию по просмотрам." + } +} diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 09c12d692d..050319168e 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 6a004c8740..897347729f 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -46,7 +46,7 @@ import { todoDockEntry } from './skeleton/TodoPanel.tsx' import { installStopShortcut } from './stop-shortcut.ts' import { TRAJECTORY_VIEW_ID, resolveActiveView } from './view-selection.ts' import { createViewPins } from './view-pins.ts' -import { en, ja, NS, zh, type ConversationKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ConversationKey } from './locales.ts' import { CONVERSATION_SETTINGS_NAMESPACE, type ConversationSettings } from '../submission-settings.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -158,7 +158,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { const slots = ctx.slots // Schemastery's field default is materialized before Cordis calls apply. const maxConcurrentFileUploads = config.maxConcurrentFileUploads as number - const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation + const workspaceNavigation = ctx.get('uiWorkspace') as WorkspaceNavigation const uiConversation = new UiConversation(ctx, sessions) uiConversation.userText.register(sessionReferenceRule) uiConversation.userText.register(collapseWhitespaceRule) @@ -185,7 +185,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { yield slots.register({ name: 'conversation.user.text.node', key: 'session', priority: 100 }, SessionReference) }) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-conversation: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-conversation: dictionaries') const t = ctx.locale.bind(NS) const conversationStore = createConversationStore() const submissionPolicy = new ComposerSubmissionPolicy( diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index f8d86187d3..3bf918336e 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -448,13 +448,15 @@ export interface ConversationViewActionEntry { readonly id: string /** Resolved View label. */ readonly label: string - /** False while the tab is unpinned from the header. */ + /** False while the View is manually unpinned. A pinned View still enters More when the header's width budget cannot hold it. */ readonly pinned: boolean + /** Whether this View is selected in this Session, independently of its pin state. */ + readonly active?: boolean } /** Owner share of the View tab row's More menu rows. */ export interface ConversationViewActionsOwnerProps { - /** Available Views in registration order; Chat is never listed. */ + /** Views outside the header in registration order. */ readonly entries: readonly ConversationViewActionEntry[] /** * Select a View in this Session without changing its pin state. @@ -462,9 +464,9 @@ export interface ConversationViewActionsOwnerProps { */ readonly onOpen: (id: string) => void /** - * Show (true) or unpin (false) a View tab in every Session. + * Pin or unpin a View in every Session. * @param id - Listed View id. - * @param pinned - Whether the tab shows in the header. + * @param pinned - Whether the View is manually pinned; the width budget can still fold it into More. */ readonly onPinnedChange: (id: string, pinned: boolean) => void /** Close the More menu. */ @@ -476,15 +478,15 @@ export interface ConversationSessionHeaderInjected { /** Package-owned View roster source bound only for the Conversation header. */ readonly hooks: { readonly conversationViews: ObservableSnapshot - /** Ids of unpinned View tabs, persisted across reloads and shared by every Session. */ + /** Ids of unpinned View tabs held in memory and shared by every Session until plugin reload. */ readonly entryPins: ObservableSnapshot /** Whether `conversation.session.header.views.actions` has registrations. */ readonly entryActions: ObservableSnapshot } /** - * Show or unpin one View tab in every Session; Chat is ignored. + * Pin or unpin one View tab in every Session; Chat is ignored. * @param id - View id. - * @param pinned - Whether the tab shows in the header. + * @param pinned - Whether the View is manually pinned; the width budget can still fold it into More. */ setPinned: (id: string, pinned: boolean) => void /** Select a Session through the Session Controller. */ diff --git a/packages/client/ui-conversation/src/client/input/editor/runtime.ts b/packages/client/ui-conversation/src/client/input/editor/runtime.ts index 698ef7bda6..f162943b6f 100644 --- a/packages/client/ui-conversation/src/client/input/editor/runtime.ts +++ b/packages/client/ui-conversation/src/client/input/editor/runtime.ts @@ -62,6 +62,9 @@ function $appendParagraphs(text: string): void { /** Undo merge window for contiguous typing, in ms (the old machine's mergeWindowMs). */ const HISTORY_MERGE_DELAY_MS = 1000 +/** Editor update that removes submitted content while retaining any newer draft suffix. */ +export const DRAFT_SUBMITTED_TAG = 'dsh-draft-submitted' + /** One model-owned editor; registration and disposal remain with its model. */ export class DraftEditorRuntime { /** The editor bound by the Composer's contenteditable host. */ @@ -412,7 +415,7 @@ export class DraftEditorRuntime { const root = $getRoot() root.clear() root.selectEnd() - }, { discrete: true, tag: HISTORY_MERGE_TAG }) + }, { discrete: true, tag: [HISTORY_MERGE_TAG, DRAFT_SUBMITTED_TAG] }) } /** diff --git a/packages/client/ui-conversation/src/client/input/editor/view-binding.ts b/packages/client/ui-conversation/src/client/input/editor/view-binding.ts index 524c8a37ea..9018592b8b 100644 --- a/packages/client/ui-conversation/src/client/input/editor/view-binding.ts +++ b/packages/client/ui-conversation/src/client/input/editor/view-binding.ts @@ -1,10 +1,31 @@ /** DOM and keymap bindings installed by the InputBar's existing effects. */ import type { MouseEvent, MutableRefObject, RefObject } from 'react' import type { LexicalEditor } from 'lexical' +import { $getRoot } from 'lexical' +import { focusWithoutRing, isMobileDevice } from '@deepseek-ai/dsh-client-ui-primitives' import type { ComposerKeyboard } from '../../contract/draft-editor.ts' import type { BusyEnterBehavior } from '../../contract/composer-submission.ts' import { resolveSubmitMode } from '../submission-policy.ts' import { registerComposerKeymap } from './keymap.ts' +import { DRAFT_SUBMITTED_TAG } from './runtime.ts' + +/** + * Transfer mobile editor focus after a submission empties the draft. + * A control the user already focused retains focus. + * @param editor - the Session-owned editor. + * @param destination - stable, focusable non-text element that retains the composer's keyboard position. + * @returns the update-listener cleanup. + */ +export function installDraftKeyboardDismissal(editor: LexicalEditor, destination: RefObject): () => void { + return editor.registerUpdateListener(({ editorState, tags }) => { + if (!tags.has(DRAFT_SUBMITTED_TAG) || !isMobileDevice()) return + const input = editor.getRootElement() + if (input === null || input.ownerDocument.activeElement !== input) return + if (editorState.read(() => $getRoot().getTextContentSize() === 0) && destination.current !== null) { + focusWithoutRing(destination.current, { preventScroll: true }) + } + }) +} interface DraftViewGate { locked: boolean diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index cde0c51e72..45f11ed7f7 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -80,6 +80,7 @@ export const zh = { 'hero.chooseWorkspace': '选择工作区', 'session.hierarchy': '会话层级', 'session.moreViews': '更多会话视图', + 'session.moreViewsCurrent': '更多会话视图 · {name}', 'todo.title': '任务', 'todo.progress.done': '{done} 已完成', 'todo.progress.active': '{active} 进行中', @@ -462,6 +463,7 @@ export const en = { 'hero.chooseWorkspace': 'Choose workspace', 'session.hierarchy': 'Session hierarchy', 'session.moreViews': 'More conversation views', + 'session.moreViewsCurrent': 'More conversation views · {name}', 'todo.title': 'To-dos', 'todo.progress.done': '{done} completed', 'todo.progress.active': '{active} in progress', @@ -843,6 +845,7 @@ export const ja = { 'hero.chooseWorkspace': 'ワークスペースを選択', 'session.hierarchy': 'セッション階層', 'session.moreViews': 'その他の会話ビュー', + 'session.moreViewsCurrent': 'その他の会話ビュー · {name}', 'todo.title': 'ToDo', 'todo.progress.done': '{done}件完了', 'todo.progress.active': '{active}件進行中', @@ -1148,3 +1151,1913 @@ export const ja = { 'terminal.sendInput': '(入力を送信)', 'terminal.session': 'ターミナル {sessionId}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'quote.remove': 'Eliminar cotización', + 'shortcut.newline': 'Nueva linea', + 'shortcut.complementary': 'Usar la acción alternativa de poner en cola o redirigir', + 'shortcut.slash': 'Abrir menú de comandos', + 'shortcut.mention': 'Abrir menú de referencia', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'describir el objetivo de una tarea de larga duración', + 'hint.goal.active': 'Objetivo activo: editar/pausar/reanudar/borrar', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envía mensajes o ejecuta una tarea, / comandos, @ archivos o sesiones', + 'placeholder.unavailable': 'Sesión no disponible', + 'placeholder.parentOffline': 'Sesión para padres sin conexión; El envío no está disponible pero aún puedes detener la ejecución.', + 'placeholder.hero': 'Describe lo que quieres construir, / comandos, @ archivos o sesiones', + 'placeholder.workspace': 'Elija un espacio de trabajo para comenzar', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter dirige todos los mensajes en cola', + 'input.commands': 'Agregar archivos o ejecutar comandos', + 'input.file': 'Archivo', + 'input.stop': 'Detener generación', + 'input.send': 'Enviar mensaje', + 'input.send.queue': 'mensaje de cola', + 'input.send.steer': 'Redirigir mensaje', + 'attachment.pending': 'Anexos pendientes', + 'attachment.scrollLeft': 'Desplazar archivos adjuntos hacia la izquierda', + 'attachment.scrollRight': 'Desplazar archivos adjuntos hacia la derecha', + 'attachment.dropTitle': 'Arrastre archivos o imágenes aquí para agregarlos', + 'attachment.dropDesc': 'Límite de imágenes: hasta imágenes {count}, {size} cada una', + 'attachment.dropBlocked': 'No se pueden agregar archivos e imágenes en este momento', + 'attachment.directoryDesktopOnly': 'Las carpetas sólo se pueden agregar en la aplicación de escritorio; agregar archivos individuales en el navegador', + 'attachment.pathUnavailable': 'No se pudo obtener la ruta de la carpeta; arrástralo de nuevo', + 'attachment.pathUnsupported': 'La ruta contiene caracteres que una referencia no puede contener; cámbiale el nombre y vuelve a intentarlo.', + 'image.pending': 'Imágenes pendientes', + 'image.openOriginal': 'Ver original', + 'image.openOriginalLabel': '{label}, haga clic para ver el original', + 'image.remove': 'Eliminar imagen {name}', + 'image.original': 'imagen original', + 'image.label': 'Imagen', + 'image.loadFailed': 'La imagen no se pudo cargar; haga clic para volver a intentar', + 'image.loading': 'Cargando imagen…', + 'image.preview': 'Vista previa de la imagen original', + 'image.closePreview': 'Cerrar vista previa de la imagen original', + 'image.unsupportedType': 'Solo se admiten imágenes PNG, JPG, WebP y GIF', + 'image.tooMany': 'Un mensaje puede incluir hasta imágenes {count}.', + 'image.fileTooLarge': 'Cada imagen debe ser más pequeña que {size}.', + 'image.totalTooLarge': 'Las imágenes superan {size} en total; elimina algunos y vuelve a intentarlo', + 'image.tooManyPixels': 'La resolución de la imagen es demasiado alta; comprímelo y vuelve a intentarlo.', + 'image.dimensionTooLarge': 'Los lados de la imagen deben ser como máximo {size}px; reduce la escala y vuelve a intentarlo', + 'image.modelUnsupported': 'El modelo actual no admite imágenes; cambiar a un modelo que lo haga', + 'image.sendFailed': 'Error al enviar imágenes ({reason}); Vuelve a agregarlos y vuelve a intentarlo.', + 'file.pending': 'Archivos pendientes', + 'file.remove': 'Eliminar archivo {name}', + 'file.uploading': 'Subiendo…', + 'file.uploadFailed': 'La carga falló; haga clic para volver a intentar', + 'file.retry': 'Reintente cargar {name}', + 'file.stillUploading': 'Los archivos todavía se están cargando; enviar después de que terminen', + 'file.sessionUnavailable': 'Sesión no disponible; los archivos no se pueden cargar', + 'file.notStaged': 'El archivo no ha terminado de cargarse; vuelve a agregarlo y vuelve a intentarlo', + 'file.label': 'Archivo', + 'context.aria': '{percent} de contexto utilizado', + 'context.used': 'de contexto utilizado', + 'context.system': 'Aviso del sistema', + 'context.tools': 'Definiciones de herramientas', + 'context.messages': 'Mensajes', + 'settings.enter.title': 'Enviar comportamiento mientras está ocupado', + 'settings.enter.description': 'Qué hacen los botones Intro y Enviar mientras el agente se está ejecutando; Cmd/Ctrl+Enter usa el otro comportamiento', + 'settings.enter.queue': 'cola', + 'settings.enter.steer': 'Redirigir', + 'hero.headline': 'Hacia lo desconocido', + 'hero.preview': 'Vista previa', + 'hero.chooseWorkspace': 'Elige espacio de trabajo', + 'session.hierarchy': 'Jerarquía de sesiones', + 'session.moreViews': 'Más vistas de conversación', + 'session.moreViewsCurrent': 'Más vistas de conversación · {name}', + 'todo.title': 'Tareas pendientes', + 'todo.progress.done': '{done} completado', + 'todo.progress.active': '{active} en progreso', + 'todo.progress.pending': '{pending} pendiente', + 'todo.status.completed': 'Completado', + 'todo.status.inProgress': 'En progreso', + 'todo.status.pending': 'Pendiente', + 'todo.rowTitle': 'Actualizar lista de tareas pendientes', + 'tool.title.createGoal': 'Crear objetivo', + 'tool.title.getGoal': 'Ver objetivo', + 'tool.title.updateGoal': 'Actualizar objetivo', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Crear recordatorio', + 'tool.title.listSchedules': 'Recordatorios de lista', + 'tool.title.deleteSchedule': 'Eliminar recordatorio', + 'tool.title.updateSchedule': 'Recordatorio de actualización', + 'detail.state': 'Estado', + 'detail.todo.completed': 'Completado', + 'detail.todo.in_progress': 'En progreso', + 'detail.todo.pending': 'Pendiente', + 'detail.todo.empty': 'La lista de tareas pendientes está vacía', + 'todo.diff.initial': 'lista inicial', + 'todo.diff.compare': 'Cambios desde la lista anterior', + 'todo.diff.unavailable': 'Lista anterior no disponible', + 'todo.diff.noChanges': 'No hay cambios en la lista.', + 'todo.diff.added': '{count} añadido', + 'todo.diff.updated': '{count} actualizado', + 'todo.diff.removed': '{count} eliminado', + 'todo.diff.unchanged': '{count} sin cambios', + 'todo.diff.addedItem': 'Añadido', + 'todo.diff.updatedItem': 'Estado cambiado', + 'todo.diff.movedItem': 'Reordenado', + 'todo.diff.removedItem': 'Eliminado', + 'detail.goal.empty': 'Sin gol', + 'detail.goal.active': 'Activo', + 'detail.goal.disarmed': 'En espera de continuación', + 'detail.goal.paused': 'En pausa', + 'detail.goal.blocked': 'Bloqueado', + 'detail.goal.complete': 'Completado', + 'detail.goal.rounds': 'Rondas', + 'detail.goal.reason': 'bloqueador', + 'detail.days': '{count} d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} mín.', + 'detail.seconds': '{count}', + 'detail.schedule.once': 'una vez', + 'detail.schedule.every': 'Cada {interval}', + 'detail.schedule.when': 'Programado para', + 'detail.schedule.frequency': 'repetir', + 'detail.schedule.scheduled': 'Programado', + 'detail.schedule.overdue': 'Atrasado, a la espera de reanudar la sesión.', + 'detail.schedule.empty': 'Sin recordatorios', + 'detail.schedule.deleted': 'Eliminado', + 'detail.schedule.count': 'Recordatorios de {count}', + 'detail.schedule.daily': 'Todos los días en {time} ({zone})', + 'detail.schedule.weekly': 'Semanalmente en {days} en {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'lun', + 'detail.weekday.2': 'mar', + 'detail.weekday.3': 'mié', + 'detail.weekday.4': 'jueves', + 'detail.weekday.5': 'viernes', + 'detail.weekday.6': 'sábado', + 'detail.weekday.7': 'sol', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspeccionar proveedores', + 'tool.title.queryRuntime': 'Tiempo de ejecución de consultas', + 'tool.title.inspectPlugins': 'Inspeccionar complementos', + 'tool.title.workflow': 'Ejecutar flujo de trabajo', + 'tool.title.ralph': 'Ejecutar bucle ralph', + 'tool.title.readEvent': 'Leer evento', + 'tool.title.searchEvents': 'Buscar eventos', + 'tool.title.traceEvent': 'Evento de seguimiento', + 'tool.title.searchSessions': 'Buscar sesiones', + 'tool.title.traceSession': 'Sesión de seguimiento', + 'tool.title.listModels': 'Listar modelos', + 'tool.title.subagent': 'Crear subagente', + 'tool.title.listAgents': 'Listar subagentes', + 'tool.title.sendMessage': 'Enviar mensaje', + 'tool.title.interruptAgent': 'agente de interrupción', + 'tool.title.listJobs': 'Listar trabajos en segundo plano', + 'tool.title.readJob': 'Leer la salida del trabajo', + 'tool.title.killJob': 'Cancelar trabajo en segundo plano', + 'tool.title.openTerminal': 'terminal abierta', + 'tool.title.readTerminal': 'Leer terminal', + 'tool.title.listTerminals': 'Lista de terminales', + 'tool.title.signalTerminal': 'terminal de señal', + 'tool.title.closeTerminal': 'Cerrar terminal', + 'tool.title.lsp': 'Símbolos de código de consulta', + 'tool.title.findDefinition': 'encontrar definición', + 'tool.title.findReferences': 'encontrar referencias', + 'tool.title.findImplementation': 'Encontrar implementación', + 'tool.title.hoverSymbol': 'Símbolo de inspección', + 'tool.title.spawnTeammate': 'Crear compañero de equipo', + 'tool.title.createTeamTask': 'Crear tarea de equipo', + 'tool.title.getTeamTask': 'Leer tarea de equipo', + 'tool.title.updateTeamTask': 'Actualizar tarea de equipo', + 'tool.title.listTeamTasks': 'Listar tareas del equipo', + 'tool.title.waitAgent': 'Espere al subagente', + 'detail.recordedResult': 'Resultado registrado', + 'detail.empty': 'Sin resultados', + 'detail.none': 'Ninguno', + 'detail.yes': 'si', + 'detail.no': 'No', + 'detail.moreInInspect': '{count} más artículos disponibles en Inspeccionar', + 'detail.status.running': 'corriendo', + 'detail.status.idle': 'inactivo', + 'detail.status.ready': 'Listo', + 'detail.status.inactive': 'Inactivo', + 'detail.status.provisioning': 'Aprovisionamiento', + 'detail.status.failed': 'Fallido', + 'detail.status.completed': 'Completado', + 'detail.status.deleted': 'Eliminado', + 'detail.status.killed': 'Cancelado', + 'detail.status.accepted': 'Aceptado', + 'detail.status.queued': 'En cola', + 'detail.status.exited': 'Salido', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Revisión', + 'detail.field.platform': 'Plataforma', + 'detail.field.provider': 'Proveedor', + 'detail.field.model': 'modelo', + 'detail.field.role': 'Rol', + 'detail.field.context': 'Contexto', + 'detail.field.owner': 'propietario', + 'detail.field.ready': 'Listo', + 'detail.field.dependencies': 'Dependencias', + 'detail.field.writeScopes': 'Escribir alcances', + 'detail.field.warnings': 'Advertencias', + 'detail.field.diagnostics': 'Diagnóstico', + 'detail.field.methods': 'Métodos', + 'detail.field.inputSchema': 'Esquema de entrada', + 'detail.field.outputSchema': 'Esquema de salida', + 'detail.field.currentPackage': 'Paquete actual', + 'detail.field.nextPackage': 'Siguiente paquete', + 'detail.field.latestRun': 'Última ejecución', + 'detail.field.packages': 'Paquetes', + 'detail.field.registrations': 'Inscripciones', + 'detail.field.props': 'Accesorios', + 'detail.field.data': 'Datos', + 'detail.field.source': 'Fuente', + 'detail.field.content': 'Contenido', + 'detail.field.message': 'Mensaje', + 'detail.field.messageId': 'ID de mensaje', + 'detail.field.root': 'raíz', + 'detail.field.pid': 'ID de proceso', + 'detail.field.type': 'Tipo', + 'detail.field.time': 'tiempo', + 'detail.field.seq': 'Secuencia de eventos', + 'detail.field.turn': 'Turno', + 'detail.field.step': 'paso', + 'detail.field.callId': 'ID de llamada', + 'detail.field.agents': 'Los agentes comenzaron', + 'detail.field.result': 'Resultado', + 'detail.field.parent': 'Padre', + 'detail.field.depth': 'Profundidad', + 'detail.field.exitCode': 'código de salida', + 'detail.field.signal': 'señal', + 'detail.field.previousStatus': 'Estado anterior', + 'detail.field.agent': 'ID del agente', + 'detail.field.job': 'ID de trabajo', + 'detail.field.task': 'Tarea', + 'detail.field.processGroup': 'grupo de procesos', + 'detail.field.availability': 'Disponibilidad', + 'detail.field.bestMatch': 'mejor partido', + 'detail.field.target': 'Evento objetivo', + 'detail.field.surface': 'Estado de registro', + 'detail.agents.count': 'Agentes {count}', + 'detail.jobs.count': 'Trabajos en segundo plano {count}', + 'detail.terminals.count': 'Terminales {count}', + 'detail.tasks.count': 'Tareas del equipo {count}', + 'detail.tasks.nextPage': 'Más tareas disponibles; el siguiente cursor es {cursor}', + 'detail.locations.count': 'Ubicaciones de {count}', + 'detail.location': 'Línea {line}, columna {column}', + 'detail.receipt.delivered': 'Mensaje entregado', + 'detail.receipt.interrupt': 'Interrupción solicitada', + 'detail.receipt.started': 'iniciado', + 'detail.receipt.cancel': 'Cancelación solicitada', + 'detail.receipt.alreadyFinished': 'ya terminé', + 'detail.receipt.signal': 'Señal entregada', + 'detail.receipt.closed': 'Cerrado', + 'detail.receipt.closing': 'Cierre', + 'detail.wait.noProgress': 'No hay subagentes activos', + 'detail.wait.title': 'Actividad del subagente', + 'detail.wait.timeout': 'Se agotó el tiempo de espera', + 'detail.wait.changed': 'Cambio detectado', + 'detail.agent.reply': 'Respuesta del agente', + 'detail.models.title': 'Modelos disponibles', + 'detail.output.lines': 'Líneas {begin}–{end} de {total}', + 'detail.output.truncated': 'Salida truncada', + 'detail.providers.count': '{count} inspeccionar proveedores', + 'detail.plugins.count': 'Complementos dinámicos {count}', + 'detail.workflow.script': 'Guión de flujo de trabajo', + 'detail.ralph.reportedComplete': 'finalización reportada por el trabajador', + 'detail.ralph.reportedBlocker': 'Trabajador reportó un bloqueador', + 'detail.ralph.limit': 'Límite de ronda alcanzado', + 'detail.report.nextSteps': 'Trabajo restante', + 'detail.trace.replacedBy': 'Reemplazado por', + 'detail.trace.replacementChain': 'Cadena de repuesto', + 'detail.trace.replaces': 'Eventos reemplazados', + 'detail.trace.sources': 'Eventos de origen', + 'detail.trace.derived': 'Eventos derivados', + 'detail.trace.ancestors': 'Sesiones de antepasados', + 'detail.trace.descendants': 'Sesiones de descendientes', + 'detail.matches.count': 'Partidos {count}', + 'detail.matches.capped': 'Límite de resultados alcanzado; acotar la búsqueda para más', + 'detail.event.neighbors': 'Eventos circundantes', + 'todo.completed': '{done}/{total} completado', + 'command.attachmentsUnsupported': '/{command} no acepta archivos adjuntos; eliminarlos primero', + 'ask.rowTitle': 'hacer pregunta', + 'ask.waiting': 'esperando', + 'ask.pending': 'continuó; respuesta aún disponible', + 'ask.pendingDetail': 'Estas cuestiones pendientes siguen siendo responsabilidad del compositor.', + 'ask.reopen': 'respuesta', + 'ask.review': 'Ver respuestas', + 'ask.closed': 'cerrado', + 'ask.closedDetail': 'Esta pregunta está cerrada; su resultado está en la conversación a continuación.', + 'ask.cancelled': 'cancelado', + 'ask.cancelledDetail': 'Este conjunto de preguntas se canceló antes de enviar las respuestas.', + 'ask.interrupted': 'interrumpido', + 'ask.interruptedDetail': 'Este conjunto de preguntas se interrumpió antes de que se enviaran las respuestas.', + 'ask.answered': '{answered}/{total} respondió', + 'ask.skipped': 'No respondido', + 'bash.running': 'corriendo', + 'bash.failed': 'Fallido', + 'bash.stopped': 'Detenido', + 'row.running': 'corriendo', + 'row.preparing': 'Preparando la llamada a la herramienta', + 'row.failed': 'Fallido', + 'row.stopped': 'Detenido', + 'row.commandArguments': 'Argumentos de comando', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'inspeccionar', + 'row.imageSize': '{width}x{height}px, {bytes} bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (reducido de {originalWidth}x{originalHeight}px), {bytes} bytes', + 'tool.title.search': 'Buscar', + 'tool.title.read': 'leer', + 'tool.title.bash': 'golpe', + 'tool.title.write': 'escribir', + 'tool.title.edit': 'Editar', + 'tool.title.code': 'Código', + 'tool.title.generic': 'llamada de herramienta', + 'tool.title.inspect': 'Consulta entorno Cordis', + 'tool.title.runCordis': 'Ejecute el complemento Cordis', + 'tool.title.stopCordis': 'Detener el complemento Cordis', + 'tool.title.removeCordis': 'Eliminar el complemento Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Leer imagen', + 'tool.title.grep': 'grep', + 'tool.title.glob': 'globo', + 'tool.title.webSearch': 'Buscar', + 'tool.title.webFetch': 'buscar', + 'tool.autoReviewRejected': 'Rechazado por Auto review', + 'tool.autoReviewNotExecuted': 'La herramienta no fue ejecutada. Razón: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review no autorizó esta acción', + 'diff.collapseAria': 'Contraer diferencia', + 'diff.expandAria': 'Expanda {count} más líneas diferenciales', + 'diff.expandRest': '… {count} más líneas', + 'read.window': 'Mostrando {shown} de líneas {total}', + 'read.collapseAria': 'Contraer contenido', + 'read.expandAria': 'Ampliar {count} más líneas', + 'read.expandRest': '… {count} más líneas', + 'search.paths': 'Rutas {shown}', + 'search.paths.truncated': 'Mostrando {shown} de las rutas {total}', + 'search.matches': 'Coincidencias {shown} · Archivos {files}', + 'search.matches.truncated': 'Mostrando {shown} de coincidencias {total} · Archivos {files}', + 'search.noResults': 'Sin resultados', + 'search.collapseAria': 'Contraer resultados', + 'search.expandAria': 'Expandir {count} más líneas de resultados', + 'search.expandRest': '… {count} más líneas', + 'web.noResults': 'No se encontraron resultados', + 'web.sourcesTruncated': 'Lista de fuentes truncada', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Contenido truncado', + 'details.running': 'Corriendo…', + 'queue.count': 'Mensajes en cola {n}', + 'queue.sending': 'Enviando…', + 'queue.image': 'Imagen de mensaje en cola', + 'queue.file': 'Archivo en cola {name}', + 'queue.edit': 'Editar mensaje en cola', + 'queue.edit.unsupported': 'Contiene contenido que no es texto; la edición aún no es compatible', + 'queue.save': 'Guardar mensaje en cola', + 'queue.cancelEdit': 'Cancelar edición', + 'queue.remove': 'Eliminar mensaje en cola', + 'queue.steer': 'Redirigir mensaje en cola', + 'queue.steer.unavailable': 'La dirección solo está disponible mientras el agente está en ejecución', + 'error.sessionInUse': 'Esta sesión ya está en uso, posiblemente por otra instancia de DSH en ejecución (como la web dsh o la aplicación de escritorio). Salga de otras instancias de DSH en ejecución y vuelva a intentarlo.', + 'queue.editFailed': 'Error de edición: es posible que este mensaje ya haya comenzado a enviarse.', + 'queue.removeFailed': 'Error al eliminar: es posible que este mensaje ya haya comenzado a enviarse.', + 'queue.steerFailed': 'La dirección falló. Intentar otra vez.', + 'terminal.signal': 'señal {signal}', + 'terminal.exitCode': 'código de salida {code}', + 'terminal.noExitCode': 'sin código de salida', + 'terminal.commandLine': 'Línea de comando {n}', + 'terminal.running': 'corriendo', + 'terminal.failed': 'Fallido', + 'terminal.done': 'hecho', + 'terminal.noOutput': 'Sin salida', + 'terminal.collapseAria': 'Contraer salida', + 'terminal.expandAria': 'Expanda las líneas de salida {n} restantes', + 'terminal.expandRest': '… {n} más líneas', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(enviar entrada)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'quote.remove': 'Supprimer le devis', + 'shortcut.newline': 'Nouvelle ligne', + 'shortcut.complementary': 'Utiliser l’action alternative de mise en attente ou de guidage', + 'shortcut.slash': 'Ouvrir le menu des commandes', + 'shortcut.mention': 'Ouvrir le menu de référence', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': "décrire l'objectif d'une tâche de longue durée", + 'hint.goal.active': 'objectif actif — modifier/mettre en pause/reprendre/effacer', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envoyez un message ou exécutez une tâche, des commandes /, des fichiers @ ou des sessions', + 'placeholder.unavailable': 'Séance indisponible', + 'placeholder.parentOffline': "Session parent hors ligne ; l'envoi n'est pas disponible mais vous pouvez toujours arrêter l'exécution", + 'placeholder.hero': 'Décrivez ce que vous souhaitez construire, / commandes, @ fichiers ou sessions', + 'placeholder.workspace': 'Choisissez un espace de travail pour commencer', + 'placeholder.steerQueue': "Cmd/Ctrl+Entrée dirige tous les messages en file d'attente", + 'input.commands': 'Ajouter des fichiers ou exécuter des commandes', + 'input.file': 'Fichier', + 'input.stop': 'Arrêter la génération', + 'input.send': 'Envoyer le message', + 'input.send.queue': "Message de file d'attente", + 'input.send.steer': 'Guider avec ce message', + 'attachment.pending': 'Pièces jointes en attente', + 'attachment.scrollLeft': 'Faire défiler les pièces jointes vers la gauche', + 'attachment.scrollRight': 'Faire défiler les pièces jointes vers la droite', + 'attachment.dropTitle': 'Faites glisser des fichiers ou des images ici pour les ajouter', + 'attachment.dropDesc': "Limite d'images : jusqu'à des images {count}, {size} chacune", + 'attachment.dropBlocked': 'Les fichiers et les images ne peuvent pas être ajoutés pour le moment', + 'attachment.directoryDesktopOnly': "Les dossiers ne peuvent être ajoutés que dans l'application de bureau ; ajouter des fichiers individuels dans le navigateur", + 'attachment.pathUnavailable': "Impossible d'obtenir le chemin du dossier ; fais-le glisser à nouveau", + 'attachment.pathUnsupported': "Le chemin contient des caractères qu'une référence ne peut pas transporter ; renommez-le et réessayez", + 'image.pending': 'Images en attente', + 'image.openOriginal': "Voir l'original", + 'image.openOriginalLabel': "{label}, cliquez pour voir l'original", + 'image.remove': "Supprimer l'image {name}", + 'image.original': 'Image originale', + 'image.label': 'Images', + 'image.loadFailed': "L'image n'a pas pu être chargée ; cliquez pour réessayer", + 'image.loading': "Chargement de l'image…", + 'image.preview': "Aperçu de l'image originale", + 'image.closePreview': "Fermer l'aperçu de l'image d'origine", + 'image.unsupportedType': 'Seules les images PNG, JPG, WebP et GIF sont prises en charge', + 'image.tooMany': "Un message peut inclure jusqu'à des images {count}", + 'image.fileTooLarge': 'Chaque image doit être plus petite que {size}', + 'image.totalTooLarge': 'Les images dépassent {size} au total ; enlève-en un peu et réessaye', + 'image.tooManyPixels': "La résolution de l'image est trop élevée ; compressez-le et réessayez", + 'image.dimensionTooLarge': "Les côtés de l'image doivent être au maximum {size}px ; réduisez-le et réessayez", + 'image.modelUnsupported': 'Le modèle actuel ne prend pas en charge les images ; passer à un modèle qui le fait', + 'image.sendFailed': "L'envoi des images a échoué ({reason}) ; rajoutez-les et réessayez", + 'file.pending': 'Fichiers en attente', + 'file.remove': 'Supprimer le fichier {name}', + 'file.uploading': 'Téléchargement…', + 'file.uploadFailed': 'Échec du téléchargement ; cliquez pour réessayer', + 'file.retry': 'Réessayez de télécharger {name}', + 'file.stillUploading': 'Les fichiers sont toujours en cours de téléchargement ; envoyer après avoir fini', + 'file.sessionUnavailable': 'Séance indisponible ; les fichiers ne peuvent pas être téléchargés', + 'file.notStaged': "Le téléchargement du fichier n'est pas terminé ; rajoutez-le et réessayez", + 'file.label': 'Fichier', + 'context.aria': '{percent} de contexte utilisé', + 'context.used': 'du contexte utilisé', + 'context.system': 'Invite système', + 'context.tools': "Définitions d'outils", + 'context.messages': 'Messages', + 'settings.enter.title': "Comportement d'envoi lorsque vous êtes occupé", + 'settings.enter.description': "Ce que font Entrée et le bouton Envoyer pendant l'exécution de l'agent ; Cmd/Ctrl+Entrée utilise l'autre comportement", + 'settings.enter.queue': "File d'attente", + 'settings.enter.steer': 'Guider', + 'hero.headline': "Dans l'inconnu", + 'hero.preview': 'Aperçu', + 'hero.chooseWorkspace': 'Choisir un espace de travail', + 'session.hierarchy': 'Hiérarchie des sessions', + 'session.moreViews': 'Plus de vues de conversations', + 'session.moreViewsCurrent': 'Plus de vues de conversations · {name}', + 'todo.title': 'À faire', + 'todo.progress.done': '{done} terminé', + 'todo.progress.active': '{active} en cours', + 'todo.progress.pending': '{pending} en attente', + 'todo.status.completed': 'Terminé', + 'todo.status.inProgress': 'En cours', + 'todo.status.pending': 'En attente', + 'todo.rowTitle': 'Mettre à jour la liste de tâches', + 'tool.title.createGoal': 'Créer un objectif', + 'tool.title.getGoal': "Afficher l'objectif", + 'tool.title.updateGoal': 'Objectif de mise à jour', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Créer un rappel', + 'tool.title.listSchedules': 'Liste des rappels', + 'tool.title.deleteSchedule': 'Supprimer le rappel', + 'tool.title.updateSchedule': 'Rappel de mise à jour', + 'detail.state': 'Statut', + 'detail.todo.completed': 'Terminé', + 'detail.todo.in_progress': 'En cours', + 'detail.todo.pending': 'En attente', + 'detail.todo.empty': 'La liste de tâches est vide', + 'todo.diff.initial': 'Liste initiale', + 'todo.diff.compare': 'Changements depuis la liste précédente', + 'todo.diff.unavailable': 'Liste précédente indisponible', + 'todo.diff.noChanges': 'Aucun changement dans la liste', + 'todo.diff.added': '{count} ajouté', + 'todo.diff.updated': '{count} mis à jour', + 'todo.diff.removed': '{count} supprimé', + 'todo.diff.unchanged': '{count} inchangé', + 'todo.diff.addedItem': 'Ajouté', + 'todo.diff.updatedItem': 'Statut modifié', + 'todo.diff.movedItem': 'Réorganisé', + 'todo.diff.removedItem': 'Supprimé', + 'detail.goal.empty': 'Aucun but', + 'detail.goal.active': 'Actif', + 'detail.goal.disarmed': 'En attente de suite', + 'detail.goal.paused': 'En pause', + 'detail.goal.blocked': 'Bloqué', + 'detail.goal.complete': 'Terminé', + 'detail.goal.rounds': 'Tours', + 'detail.goal.reason': 'Bloqueur', + 'detail.days': '{count}d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} min.', + 'detail.seconds': '{count}s', + 'detail.schedule.once': 'Une fois', + 'detail.schedule.every': 'Chaque {interval}', + 'detail.schedule.when': 'Prévu pour', + 'detail.schedule.frequency': 'Répéter', + 'detail.schedule.scheduled': 'Programmé', + 'detail.schedule.overdue': 'En retard, en attente de reprise de la session', + 'detail.schedule.empty': 'Aucun rappel', + 'detail.schedule.deleted': 'Supprimé', + 'detail.schedule.count': 'Rappels {count}', + 'detail.schedule.daily': 'Tous les jours à {time} ({zone})', + 'detail.schedule.weekly': 'Hebdomadaire sur {days} à {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'lundi', + 'detail.weekday.2': 'mar.', + 'detail.weekday.3': 'mer', + 'detail.weekday.4': 'jeu.', + 'detail.weekday.5': 'vendredi', + 'detail.weekday.6': 'Samedi', + 'detail.weekday.7': 'Soleil', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspecter les fournisseurs', + 'tool.title.queryRuntime': 'Exécution de la requête', + 'tool.title.inspectPlugins': 'Inspecter les plugins', + 'tool.title.workflow': 'Exécuter le flux de travail', + 'tool.title.ralph': 'Exécuter la boucle Ralph', + 'tool.title.readEvent': "Lire l'événement", + 'tool.title.searchEvents': 'Rechercher des événements', + 'tool.title.traceEvent': 'Événement de trace', + 'tool.title.searchSessions': 'Rechercher des sessions', + 'tool.title.traceSession': 'Session de traçage', + 'tool.title.listModels': 'Liste des modèles', + 'tool.title.subagent': 'Créer un sous-agent', + 'tool.title.listAgents': 'Liste des sous-agents', + 'tool.title.sendMessage': 'Envoyer le message', + 'tool.title.interruptAgent': "Agent d'interruption", + 'tool.title.listJobs': 'Répertorier les tâches en arrière-plan', + 'tool.title.readJob': 'Lire le résultat de la tâche', + 'tool.title.killJob': 'Annuler le travail en arrière-plan', + 'tool.title.openTerminal': 'Terminal ouvert', + 'tool.title.readTerminal': 'Lire le terminal', + 'tool.title.listTerminals': 'Liste des terminaux', + 'tool.title.signalTerminal': 'Borne de signal', + 'tool.title.closeTerminal': 'Fermer le terminal', + 'tool.title.lsp': 'Symboles du code de requête', + 'tool.title.findDefinition': 'Trouver une définition', + 'tool.title.findReferences': 'Trouver des références', + 'tool.title.findImplementation': 'Trouver la mise en œuvre', + 'tool.title.hoverSymbol': 'Inspecter le symbole', + 'tool.title.spawnTeammate': 'Créer un coéquipier', + 'tool.title.createTeamTask': "Créer une tâche d'équipe", + 'tool.title.getTeamTask': "Lire la tâche d'équipe", + 'tool.title.updateTeamTask': "Mettre à jour la tâche de l'équipe", + 'tool.title.listTeamTasks': "Répertorier les tâches de l'équipe", + 'tool.title.waitAgent': 'Attendez le sous-agent', + 'detail.recordedResult': 'Résultat enregistré', + 'detail.empty': 'Aucun résultat', + 'detail.none': 'Aucun', + 'detail.yes': 'Oui', + 'detail.no': 'Non', + 'detail.moreInInspect': "{count} plus d'articles disponibles dans Inspecter", + 'detail.status.running': 'Courir', + 'detail.status.idle': 'Inactif', + 'detail.status.ready': 'Prêt', + 'detail.status.inactive': 'Inactif', + 'detail.status.provisioning': 'Approvisionnement', + 'detail.status.failed': 'Échec', + 'detail.status.completed': 'Terminé', + 'detail.status.deleted': 'Supprimé', + 'detail.status.killed': 'Annulé', + 'detail.status.accepted': 'Accepté', + 'detail.status.queued': "En file d'attente", + 'detail.status.exited': 'Quitté', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Révision', + 'detail.field.platform': 'Plateforme', + 'detail.field.provider': 'Fournisseur', + 'detail.field.model': 'Modèle', + 'detail.field.role': 'Rôle', + 'detail.field.context': 'Contexte', + 'detail.field.owner': 'Propriétaire', + 'detail.field.ready': 'Prêt', + 'detail.field.dependencies': 'Dépendances', + 'detail.field.writeScopes': 'Écrire des étendues', + 'detail.field.warnings': 'Avertissements', + 'detail.field.diagnostics': 'Diagnostic', + 'detail.field.methods': 'Méthodes', + 'detail.field.inputSchema': "Schéma d'entrée", + 'detail.field.outputSchema': 'Schéma de sortie', + 'detail.field.currentPackage': 'Forfait actuel', + 'detail.field.nextPackage': 'Forfait suivant', + 'detail.field.latestRun': 'Dernière course', + 'detail.field.packages': 'Forfaits', + 'detail.field.registrations': 'Inscriptions', + 'detail.field.props': 'Accessoires', + 'detail.field.data': 'Données', + 'detail.field.source': 'Source', + 'detail.field.content': 'Contenu', + 'detail.field.message': 'Message', + 'detail.field.messageId': 'Identifiant du message', + 'detail.field.root': 'Racine', + 'detail.field.pid': 'ID de processus', + 'detail.field.type': 'Tapez', + 'detail.field.time': 'Temps', + 'detail.field.seq': "Séquence d'événements", + 'detail.field.turn': 'Tour', + 'detail.field.step': 'Étape', + 'detail.field.callId': "Identifiant d'appel", + 'detail.field.agents': 'Les agents ont commencé', + 'detail.field.result': 'Résultat', + 'detail.field.parent': 'Parent', + 'detail.field.depth': 'Profondeur', + 'detail.field.exitCode': 'Code de sortie', + 'detail.field.signal': 'Signal', + 'detail.field.previousStatus': 'Statut précédent', + 'detail.field.agent': "ID d'agent", + 'detail.field.job': 'ID de travail', + 'detail.field.task': 'Tâche', + 'detail.field.processGroup': 'Groupe de processus', + 'detail.field.availability': 'Disponibilité', + 'detail.field.bestMatch': 'Meilleure correspondance', + 'detail.field.target': 'Événement cible', + 'detail.field.surface': "Statut d'enregistrement", + 'detail.agents.count': 'Agents {count}', + 'detail.jobs.count': "Travaux d'arrière-plan {count}", + 'detail.terminals.count': 'Bornes {count}', + 'detail.tasks.count': "Tâches de l'équipe {count}", + 'detail.tasks.nextPage': 'Plus de tâches disponibles ; le curseur suivant est {cursor}', + 'detail.locations.count': 'Localisations {count}', + 'detail.location': 'Ligne {line}, colonne {column}', + 'detail.receipt.delivered': 'Message délivré', + 'detail.receipt.interrupt': 'Interruption demandée', + 'detail.receipt.started': 'Commencé', + 'detail.receipt.cancel': 'Annulation demandée', + 'detail.receipt.alreadyFinished': 'Déjà fini', + 'detail.receipt.signal': 'Signal délivré', + 'detail.receipt.closed': 'Fermé', + 'detail.receipt.closing': 'Clôture', + 'detail.wait.noProgress': 'Aucun sous-agent actif', + 'detail.wait.title': 'Activité du sous-agent', + 'detail.wait.timeout': "Le délai d'attente a expiré", + 'detail.wait.changed': 'Changement détecté', + 'detail.agent.reply': "Réponse de l'agent", + 'detail.models.title': 'Modèles disponibles', + 'detail.output.lines': 'Lignes {begin}–{end} de {total}', + 'detail.output.truncated': 'Sortie tronquée', + 'detail.providers.count': '{count} inspecte les fournisseurs', + 'detail.plugins.count': 'Plugins dynamiques {count}', + 'detail.workflow.script': 'Script de flux de travail', + 'detail.ralph.reportedComplete': "Le travailleur a signalé l'achèvement", + 'detail.ralph.reportedBlocker': 'Le travailleur a signalé un bloqueur', + 'detail.ralph.limit': 'Limite de tours atteinte', + 'detail.report.nextSteps': 'Travail restant', + 'detail.trace.replacedBy': 'Remplacé par', + 'detail.trace.replacementChain': 'Chaîne de remplacement', + 'detail.trace.replaces': 'Événements remplacés', + 'detail.trace.sources': 'Événements sources', + 'detail.trace.derived': 'Événements dérivés', + 'detail.trace.ancestors': "Séances d'ancêtres", + 'detail.trace.descendants': 'Séances descendantes', + 'detail.matches.count': 'Correspondances {count}', + 'detail.matches.capped': 'Limite de résultat atteinte ; affiner la recherche pour en savoir plus', + 'detail.event.neighbors': 'Événements environnants', + 'todo.completed': '{done}/{total} terminé', + 'command.attachmentsUnsupported': "/{command} n'accepte pas les pièces jointes ; retirez-les d'abord", + 'ask.rowTitle': 'Poser une question', + 'ask.waiting': 'en attendant', + 'ask.pending': 'suite ; réponse toujours disponible', + 'ask.pendingDetail': 'Ces questions en suspens restent à la charge du compositeur.', + 'ask.reopen': 'Réponse', + 'ask.review': 'Afficher les réponses', + 'ask.closed': 'fermé', + 'ask.closedDetail': 'Cette question est fermée ; son résultat est dans la conversation ci-dessous.', + 'ask.cancelled': 'annulé', + 'ask.cancelledDetail': 'Cet ensemble de questions a été annulé avant que les réponses ne soient soumises.', + 'ask.interrupted': 'interrompu', + 'ask.interruptedDetail': 'Cette série de questions a été interrompue avant que les réponses ne soient soumises.', + 'ask.answered': '{answered}/{total} répondu', + 'ask.skipped': 'Pas de réponse', + 'bash.running': 'Courir', + 'bash.failed': 'Échec', + 'bash.stopped': 'Arrêté', + 'row.running': 'Courir', + 'row.preparing': "Préparation de l'appel à l'outil", + 'row.failed': 'Échec', + 'row.stopped': 'Arrêté', + 'row.commandArguments': 'Arguments de commande', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspecter', + 'row.imageSize': '{width}x{height}px, {bytes} octets', + 'row.imageSizeDownscaled': '{width}x{height}px (réduction de {originalWidth}x{originalHeight}px), {bytes} octets', + 'tool.title.search': 'Rechercher', + 'tool.title.read': 'Lire', + 'tool.title.bash': 'Frapper', + 'tool.title.write': 'Écrire', + 'tool.title.edit': 'Modifier', + 'tool.title.code': 'Coder', + 'tool.title.generic': "Appel d'outil", + 'tool.title.inspect': "Interroger l'environnement Cordis", + 'tool.title.runCordis': 'Exécuter le plugin Cordis', + 'tool.title.stopCordis': 'Arrêter le plug-in Cordis', + 'tool.title.removeCordis': 'Supprimer le plug-in Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': "Lire l'image", + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'Glob', + 'tool.title.webSearch': 'Rechercher', + 'tool.title.webFetch': 'Récupérer', + 'tool.autoReviewRejected': 'Rejeté par Auto review', + 'tool.autoReviewNotExecuted': "L'outil n'a pas été exécuté. Raison : {reason}", + 'tool.autoReviewReasonFallback': "Auto review n'a pas autorisé cette action", + 'diff.collapseAria': 'Réduire la différence', + 'diff.expandAria': 'Développez {count} plus de lignes de différence', + 'diff.expandRest': '… {count} plus de lignes', + 'read.window': 'Affichage des lignes {shown} des lignes {total}', + 'read.collapseAria': 'Réduire le contenu', + 'read.expandAria': 'Développez {count} plus de lignes', + 'read.expandRest': '… {count} plus de lignes', + 'search.paths': 'Chemins {shown}', + 'search.paths.truncated': 'Affichage des chemins {shown} sur {total}', + 'search.matches': 'Correspondances {shown} · Fichiers {files}', + 'search.matches.truncated': 'Affichage des correspondances {shown} des correspondances {total} · Fichiers {files}', + 'search.noResults': 'Aucun résultat', + 'search.collapseAria': 'Réduire les résultats', + 'search.expandAria': 'Développez {count} plus de lignes de résultats', + 'search.expandRest': '… {count} plus de lignes', + 'web.noResults': 'Aucun résultat trouvé', + 'web.sourcesTruncated': 'Liste source tronquée', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Contenu tronqué', + 'details.running': 'Courir…', + 'queue.count': "Messages en file d'attente {n}", + 'queue.sending': 'Envoi…', + 'queue.image': "Image du message en file d'attente", + 'queue.file': "Fichier en file d'attente {name}", + 'queue.edit': "Modifier le message en file d'attente", + 'queue.edit.unsupported': "Contient du contenu non textuel ; l'édition n'est pas encore prise en charge", + 'queue.save': "Enregistrer le message en file d'attente", + 'queue.cancelEdit': 'Annuler la modification', + 'queue.remove': "Supprimer le message en file d'attente", + 'queue.steer': 'Guider avec le message en attente', + 'queue.steer.unavailable': "Le pilotage est disponible uniquement lorsque l'agent est en cours d'exécution", + 'error.sessionInUse': "Cette session est déjà utilisée, éventuellement par une autre instance DSH en cours d'exécution (telle que le Web dsh ou l'application de bureau). Quittez les autres instances DSH en cours d'exécution et réessayez.", + 'queue.editFailed': "Échec de la modification : l'envoi de ce message a peut-être déjà commencé.", + 'queue.removeFailed': "Échec de la suppression : l'envoi de ce message a peut-être déjà commencé.", + 'queue.steerFailed': 'La direction a échoué. Essayer à nouveau.', + 'terminal.signal': 'signal {signal}', + 'terminal.exitCode': 'code de sortie {code}', + 'terminal.noExitCode': 'pas de code de sortie', + 'terminal.commandLine': 'Ligne de commande {n}', + 'terminal.running': 'Courir', + 'terminal.failed': 'Échec', + 'terminal.done': 'Terminé', + 'terminal.noOutput': 'Aucune sortie', + 'terminal.collapseAria': 'Réduire la sortie', + 'terminal.expandAria': 'Développez les lignes de sortie {n} restantes', + 'terminal.expandRest': '… {n} plus de lignes', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(envoyer une entrée)', + 'terminal.session': 'Borne {sessionId}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'quote.remove': 'Zitat entfernen', + 'shortcut.newline': 'Neue Zeile', + 'shortcut.complementary': 'Die alternative Aktion zum Vormerken oder Lenken verwenden', + 'shortcut.slash': 'Befehlsmenü öffnen', + 'shortcut.mention': 'Referenzmenü öffnen', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'Beschreiben Sie das Ziel einer lang andauernden Aufgabe', + 'hint.goal.active': 'Ziel aktiv – Bearbeiten / Anhalten / Fortsetzen / Löschen', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Senden Sie eine Nachricht oder führen Sie eine Aufgabe, /-Befehle, @-Dateien oder Sitzungen aus', + 'placeholder.unavailable': 'Sitzung nicht verfügbar', + 'placeholder.parentOffline': 'Elternsitzung offline; Das Senden ist nicht möglich, aber Sie können den Lauf trotzdem stoppen', + 'placeholder.hero': 'Beschreiben Sie, was Sie erstellen möchten: /-Befehle, @-Dateien oder Sitzungen', + 'placeholder.workspace': 'Wählen Sie einen Arbeitsbereich zum Starten aus', + 'placeholder.steerQueue': 'Cmd/Strg+Enter steuert alle Nachrichten in der Warteschlange', + 'input.commands': 'Fügen Sie Dateien hinzu oder führen Sie Befehle aus', + 'input.file': 'Datei', + 'input.stop': 'Generierung stoppen', + 'input.send': 'Nachricht senden', + 'input.send.queue': 'Warteschlangennachricht', + 'input.send.steer': 'Mit Nachricht lenken', + 'attachment.pending': 'Ausstehende Anhänge', + 'attachment.scrollLeft': 'Anhänge nach links scrollen', + 'attachment.scrollRight': 'Anhänge nach rechts scrollen', + 'attachment.dropTitle': 'Ziehen Sie Dateien oder Bilder hierher, um sie hinzuzufügen', + 'attachment.dropDesc': 'Bildbeschränkung: bis zu {count} Bilder, jeweils {size}', + 'attachment.dropBlocked': 'Dateien und Bilder können derzeit nicht hinzugefügt werden', + 'attachment.directoryDesktopOnly': 'Ordner können nur in der Desktop-App hinzugefügt werden; einzelne Dateien im Browser hinzufügen', + 'attachment.pathUnavailable': 'Der Ordnerpfad konnte nicht abgerufen werden; Ziehen Sie es erneut hinein', + 'attachment.pathUnsupported': 'Der Pfad enthält Zeichen, die eine Referenz nicht enthalten kann; Benennen Sie es um und versuchen Sie es erneut', + 'image.pending': 'Ausstehende Bilder', + 'image.openOriginal': 'Original ansehen', + 'image.openOriginalLabel': '{label}, klicken Sie, um das Original anzuzeigen', + 'image.remove': 'Bild {name} entfernen', + 'image.original': 'Originalbild', + 'image.label': 'Bild', + 'image.loadFailed': 'Bild konnte nicht geladen werden; Klicken Sie, um es noch einmal zu versuchen', + 'image.loading': 'Bild wird geladen…', + 'image.preview': 'Originalbildvorschau', + 'image.closePreview': 'Originalbildvorschau schließen', + 'image.unsupportedType': 'Es werden nur PNG-, JPG-, WebP- und GIF-Bilder unterstützt', + 'image.tooMany': 'Eine Nachricht kann bis zu {count} Bilder enthalten', + 'image.fileTooLarge': 'Jedes Bild muss kleiner als {size} sein', + 'image.totalTooLarge': 'Die Anzahl der Bilder übersteigt insgesamt {size}; Entfernen Sie einige davon und versuchen Sie es erneut', + 'image.tooManyPixels': 'Die Bildauflösung ist zu hoch; Komprimieren Sie es und versuchen Sie es erneut', + 'image.dimensionTooLarge': 'Bildseiten dürfen höchstens {size}px sein; Skalieren Sie es herunter und versuchen Sie es erneut', + 'image.modelUnsupported': 'Das aktuelle Modell unterstützt keine Bilder; Wechseln Sie zu einem Modell, das dies tut', + 'image.sendFailed': 'Das Senden von Bildern ist fehlgeschlagen ({reason}); Fügen Sie sie erneut hinzu und versuchen Sie es erneut', + 'file.pending': 'Ausstehende Dateien', + 'file.remove': 'Datei {name} entfernen', + 'file.uploading': 'Hochladen…', + 'file.uploadFailed': 'Hochladen fehlgeschlagen; Klicken Sie, um es noch einmal zu versuchen', + 'file.retry': 'Versuchen Sie erneut, {name} hochzuladen', + 'file.stillUploading': 'Dateien werden noch hochgeladen; senden, nachdem sie fertig sind', + 'file.sessionUnavailable': 'Sitzung nicht verfügbar; Dateien können nicht hochgeladen werden', + 'file.notStaged': 'Der Upload der Datei wurde noch nicht abgeschlossen. Fügen Sie es erneut hinzu und versuchen Sie es erneut', + 'file.label': 'Datei', + 'context.aria': '{percent} des verwendeten Kontexts', + 'context.used': 'des verwendeten Kontexts', + 'context.system': 'Systemaufforderung', + 'context.tools': 'Werkzeugdefinitionen', + 'context.messages': 'Nachrichten', + 'settings.enter.title': 'Sendeverhalten, während beschäftigt ist', + 'settings.enter.description': 'Was die Eingabetaste und die Schaltfläche „Senden“ bewirken, während der Agent ausgeführt wird; Cmd/Strg+Enter verwendet das andere Verhalten', + 'settings.enter.queue': 'Warteschlange', + 'settings.enter.steer': 'Lenken', + 'hero.headline': 'Ins Unbekannte', + 'hero.preview': 'Vorschau', + 'hero.chooseWorkspace': 'Wählen Sie einen Arbeitsbereich', + 'session.hierarchy': 'Sitzungshierarchie', + 'session.moreViews': 'Weitere Konversationsansichten', + 'session.moreViewsCurrent': 'Weitere Konversationsansichten · {name}', + 'todo.title': 'Aufgaben', + 'todo.progress.done': '{done} abgeschlossen', + 'todo.progress.active': '{active} in Bearbeitung', + 'todo.progress.pending': '{pending} ausstehend', + 'todo.status.completed': 'Abgeschlossen', + 'todo.status.inProgress': 'In Bearbeitung', + 'todo.status.pending': 'Ausstehend', + 'todo.rowTitle': 'To-Do-Liste aktualisieren', + 'tool.title.createGoal': 'Ziel erstellen', + 'tool.title.getGoal': 'Ziel ansehen', + 'tool.title.updateGoal': 'Ziel aktualisieren', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Erinnerung erstellen', + 'tool.title.listSchedules': 'Listen Sie Erinnerungen auf', + 'tool.title.deleteSchedule': 'Erinnerung löschen', + 'tool.title.updateSchedule': 'Update-Erinnerung', + 'detail.state': 'Status', + 'detail.todo.completed': 'Abgeschlossen', + 'detail.todo.in_progress': 'In Bearbeitung', + 'detail.todo.pending': 'Ausstehend', + 'detail.todo.empty': 'Die To-Do-Liste ist leer', + 'todo.diff.initial': 'Erste Liste', + 'todo.diff.compare': 'Änderungen gegenüber der vorherigen Liste', + 'todo.diff.unavailable': 'Vorherige Liste nicht verfügbar', + 'todo.diff.noChanges': 'Keine Änderungen an der Liste', + 'todo.diff.added': '{count} hinzugefügt', + 'todo.diff.updated': '{count} aktualisiert', + 'todo.diff.removed': '{count} entfernt', + 'todo.diff.unchanged': '{count} unverändert', + 'todo.diff.addedItem': 'Hinzugefügt', + 'todo.diff.updatedItem': 'Status geändert', + 'todo.diff.movedItem': 'Nachbestellt', + 'todo.diff.removedItem': 'Entfernt', + 'detail.goal.empty': 'Kein Ziel', + 'detail.goal.active': 'Aktiv', + 'detail.goal.disarmed': 'Warten auf Fortsetzung', + 'detail.goal.paused': 'Angehalten', + 'detail.goal.blocked': 'Blockiert', + 'detail.goal.complete': 'Abgeschlossen', + 'detail.goal.rounds': 'Runden', + 'detail.goal.reason': 'Blocker', + 'detail.days': '{count} d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} min', + 'detail.seconds': '{count} s', + 'detail.schedule.once': 'Einmal', + 'detail.schedule.every': 'Jeder {interval}', + 'detail.schedule.when': 'Geplant für', + 'detail.schedule.frequency': 'Wiederholen', + 'detail.schedule.scheduled': 'Geplant', + 'detail.schedule.overdue': 'Überfällig, ich warte auf die Fortsetzung der Sitzung', + 'detail.schedule.empty': 'Keine Erinnerungen', + 'detail.schedule.deleted': 'Gelöscht', + 'detail.schedule.count': '{count} Erinnerungen', + 'detail.schedule.daily': 'Täglich um {time} ({zone})', + 'detail.schedule.weekly': 'Wöchentlich auf {days} um {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'Mo', + 'detail.weekday.2': 'Di', + 'detail.weekday.3': 'Mi', + 'detail.weekday.4': 'Do', + 'detail.weekday.5': 'Fr', + 'detail.weekday.6': 'Sa', + 'detail.weekday.7': 'Sonne', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Anbieter prüfen', + 'tool.title.queryRuntime': 'Abfragelaufzeit', + 'tool.title.inspectPlugins': 'Plugins prüfen', + 'tool.title.workflow': 'Workflow ausführen', + 'tool.title.ralph': 'Führen Sie die Ralph-Schleife aus', + 'tool.title.readEvent': 'Veranstaltung lesen', + 'tool.title.searchEvents': 'Suchen Sie nach Ereignissen', + 'tool.title.traceEvent': 'Trace-Ereignis', + 'tool.title.searchSessions': 'Sitzungen suchen', + 'tool.title.traceSession': 'Trace-Sitzung', + 'tool.title.listModels': 'Modelle auflisten', + 'tool.title.subagent': 'Subagent erstellen', + 'tool.title.listAgents': 'Subagenten auflisten', + 'tool.title.sendMessage': 'Nachricht senden', + 'tool.title.interruptAgent': 'Interrupt-Agent', + 'tool.title.listJobs': 'Hintergrundjobs auflisten', + 'tool.title.readJob': 'Jobausgabe lesen', + 'tool.title.killJob': 'Hintergrundjob abbrechen', + 'tool.title.openTerminal': 'Terminal öffnen', + 'tool.title.readTerminal': 'Terminal lesen', + 'tool.title.listTerminals': 'Terminals auflisten', + 'tool.title.signalTerminal': 'Signalklemme', + 'tool.title.closeTerminal': 'Terminal schließen', + 'tool.title.lsp': 'Codesymbole abfragen', + 'tool.title.findDefinition': 'Definition finden', + 'tool.title.findReferences': 'Finden Sie Referenzen', + 'tool.title.findImplementation': 'Umsetzung finden', + 'tool.title.hoverSymbol': 'Symbol prüfen', + 'tool.title.spawnTeammate': 'Teamkollege erstellen', + 'tool.title.createTeamTask': 'Teamaufgabe erstellen', + 'tool.title.getTeamTask': 'Teamaufgabe lesen', + 'tool.title.updateTeamTask': 'Teamaufgabe aktualisieren', + 'tool.title.listTeamTasks': 'Teamaufgaben auflisten', + 'tool.title.waitAgent': 'Warten Sie auf den Subagenten', + 'detail.recordedResult': 'Aufgezeichnetes Ergebnis', + 'detail.empty': 'Keine Ergebnisse', + 'detail.none': 'Keine', + 'detail.yes': 'Ja', + 'detail.no': 'Nein', + 'detail.moreInInspect': '{count} weitere Artikel in Inspect verfügbar', + 'detail.status.running': 'Laufen', + 'detail.status.idle': 'Leerlauf', + 'detail.status.ready': 'Bereit', + 'detail.status.inactive': 'Inaktiv', + 'detail.status.provisioning': 'Bereitstellung', + 'detail.status.failed': 'Fehlgeschlagen', + 'detail.status.completed': 'Abgeschlossen', + 'detail.status.deleted': 'Gelöscht', + 'detail.status.killed': 'Abgesagt', + 'detail.status.accepted': 'Akzeptiert', + 'detail.status.queued': 'In der Warteschlange', + 'detail.status.exited': 'Ausgegangen', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Überarbeitung', + 'detail.field.platform': 'Plattform', + 'detail.field.provider': 'Anbieter', + 'detail.field.model': 'Modell', + 'detail.field.role': 'Rolle', + 'detail.field.context': 'Kontext', + 'detail.field.owner': 'Besitzer', + 'detail.field.ready': 'Bereit', + 'detail.field.dependencies': 'Abhängigkeiten', + 'detail.field.writeScopes': 'Bereiche schreiben', + 'detail.field.warnings': 'Warnungen', + 'detail.field.diagnostics': 'Diagnose', + 'detail.field.methods': 'Methoden', + 'detail.field.inputSchema': 'Eingabeschema', + 'detail.field.outputSchema': 'Ausgabeschema', + 'detail.field.currentPackage': 'Aktuelles Paket', + 'detail.field.nextPackage': 'Nächstes Paket', + 'detail.field.latestRun': 'Letzter Lauf', + 'detail.field.packages': 'Pakete', + 'detail.field.registrations': 'Anmeldungen', + 'detail.field.props': 'Requisiten', + 'detail.field.data': 'Daten', + 'detail.field.source': 'Quelle', + 'detail.field.content': 'Inhalt', + 'detail.field.message': 'Nachricht', + 'detail.field.messageId': 'Nachrichten-ID', + 'detail.field.root': 'Wurzel', + 'detail.field.pid': 'Prozess-ID', + 'detail.field.type': 'Typ', + 'detail.field.time': 'Zeit', + 'detail.field.seq': 'Ereignissequenz', + 'detail.field.turn': 'Runde', + 'detail.field.step': 'Schritt', + 'detail.field.callId': 'Aufruf-ID', + 'detail.field.agents': 'Agenten begannen', + 'detail.field.result': 'Ergebnis', + 'detail.field.parent': 'Eltern', + 'detail.field.depth': 'Tiefe', + 'detail.field.exitCode': 'Exit-Code', + 'detail.field.signal': 'Signal', + 'detail.field.previousStatus': 'Vorheriger Stand', + 'detail.field.agent': 'Agenten-ID', + 'detail.field.job': 'Job-ID', + 'detail.field.task': 'Aufgabe', + 'detail.field.processGroup': 'Prozessgruppe', + 'detail.field.availability': 'Verfügbarkeit', + 'detail.field.bestMatch': 'Beste Übereinstimmung', + 'detail.field.target': 'Zielereignis', + 'detail.field.surface': 'Aufnahmestatus', + 'detail.agents.count': '{count}-Agenten', + 'detail.jobs.count': '{count} Hintergrundjobs', + 'detail.terminals.count': '{count}-Klemmen', + 'detail.tasks.count': '{count} Teamaufgaben', + 'detail.tasks.nextPage': 'Weitere Aufgaben verfügbar; Der nächste Cursor ist {cursor}', + 'detail.locations.count': '{count}-Standorte', + 'detail.location': 'Zeile {line}, Spalte {column}', + 'detail.receipt.delivered': 'Nachricht zugestellt', + 'detail.receipt.interrupt': 'Unterbrechung angefordert', + 'detail.receipt.started': 'Begonnen', + 'detail.receipt.cancel': 'Stornierung beantragt', + 'detail.receipt.alreadyFinished': 'Schon fertig', + 'detail.receipt.signal': 'Signal geliefert', + 'detail.receipt.closed': 'Geschlossen', + 'detail.receipt.closing': 'Schließung', + 'detail.wait.noProgress': 'Keine aktiven Subagenten', + 'detail.wait.title': 'Aktivität des Subagenten', + 'detail.wait.timeout': 'Wartezeit abgelaufen', + 'detail.wait.changed': 'Änderung erkannt', + 'detail.agent.reply': 'Antwort des Agenten', + 'detail.models.title': 'Verfügbare Modelle', + 'detail.output.lines': 'Linien {begin}–{end} von {total}', + 'detail.output.truncated': 'Ausgabe abgeschnitten', + 'detail.providers.count': '{count} prüft Anbieter', + 'detail.plugins.count': 'Dynamische {count}-Plugins', + 'detail.workflow.script': 'Workflow-Skript', + 'detail.ralph.reportedComplete': 'Der Arbeiter hat die Fertigstellung gemeldet', + 'detail.ralph.reportedBlocker': 'Der Arbeiter hat einen Blocker gemeldet', + 'detail.ralph.limit': 'Rundenlimit erreicht', + 'detail.report.nextSteps': 'Verbleibende Arbeit', + 'detail.trace.replacedBy': 'Ersetzt durch', + 'detail.trace.replacementChain': 'Ersatzkette', + 'detail.trace.replaces': 'Ereignisse ersetzt', + 'detail.trace.sources': 'Quellereignisse', + 'detail.trace.derived': 'Abgeleitete Ereignisse', + 'detail.trace.ancestors': 'Ahnensitzungen', + 'detail.trace.descendants': 'Nachkommende Sitzungen', + 'detail.matches.count': '{count} Übereinstimmungen', + 'detail.matches.capped': 'Ergebnisgrenze erreicht; Grenzen Sie die Suche auf mehr ein', + 'detail.event.neighbors': 'Umliegende Veranstaltungen', + 'todo.completed': '{done}/{total} abgeschlossen', + 'command.attachmentsUnsupported': '/{command} akzeptiert keine Anhänge; Entfernen Sie sie zuerst', + 'ask.rowTitle': 'Frage stellen', + 'ask.waiting': 'warten', + 'ask.pending': 'Fortsetzung; Antwort noch verfügbar', + 'ask.pendingDetail': 'Diese offenen Fragen bleiben vom Komponisten zu beantworten.', + 'ask.reopen': 'Antwort', + 'ask.review': 'Antworten anzeigen', + 'ask.closed': 'geschlossen', + 'ask.closedDetail': 'Diese Frage ist geschlossen; Das Ergebnis finden Sie im folgenden Gespräch.', + 'ask.cancelled': 'abgesagt', + 'ask.cancelledDetail': 'Dieser Fragensatz wurde gelöscht, bevor die Antworten übermittelt wurden.', + 'ask.interrupted': 'unterbrochen', + 'ask.interruptedDetail': 'Dieser Fragensatz wurde unterbrochen, bevor Antworten übermittelt wurden.', + 'ask.answered': '{answered}/{total} antwortete', + 'ask.skipped': 'Nicht beantwortet', + 'bash.running': 'Laufen', + 'bash.failed': 'Fehlgeschlagen', + 'bash.stopped': 'Angehalten', + 'row.running': 'Laufen', + 'row.preparing': 'Werkzeugaufruf wird vorbereitet', + 'row.failed': 'Fehlgeschlagen', + 'row.stopped': 'Angehalten', + 'row.commandArguments': 'Befehlsargumente', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspizieren', + 'row.imageSize': '{width}x{height}px, {bytes} Bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (herunterskaliert von {originalWidth}x{originalHeight}px), {bytes} Bytes', + 'tool.title.search': 'Suchen', + 'tool.title.read': 'Lesen', + 'tool.title.bash': 'Bash', + 'tool.title.write': 'Schreiben', + 'tool.title.edit': 'Bearbeiten', + 'tool.title.code': 'Code', + 'tool.title.generic': 'Werkzeugaufruf', + 'tool.title.inspect': 'Fragen Sie die Cordis-Umgebung ab', + 'tool.title.runCordis': 'Führen Sie das Cordis-Plugin aus', + 'tool.title.stopCordis': 'Stoppen Sie das Cordis-Plugin', + 'tool.title.removeCordis': 'Entfernen Sie das Cordis-Plugin', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Bild lesen', + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'Globus', + 'tool.title.webSearch': 'Suchen', + 'tool.title.webFetch': 'Holen', + 'tool.autoReviewRejected': 'Abgelehnt von Auto review', + 'tool.autoReviewNotExecuted': 'Werkzeug wurde nicht ausgeführt. Grund: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review hat diese Aktion nicht autorisiert', + 'diff.collapseAria': 'Differenz ausblenden', + 'diff.expandAria': 'Erweitern Sie {count} um weitere Diff-Linien', + 'diff.expandRest': '… {count} weitere Zeilen', + 'read.window': 'Zeigt {shown} der {total}-Linien', + 'read.collapseAria': 'Inhalt reduzieren', + 'read.expandAria': 'Erweitern Sie {count} um weitere Zeilen', + 'read.expandRest': '… {count} weitere Zeilen', + 'search.paths': '{shown}-Pfade', + 'search.paths.truncated': 'Zeigt {shown} von {total}-Pfaden', + 'search.matches': '{shown} entspricht · {files}-Dateien', + 'search.matches.truncated': 'Zeigt {shown} von {total}-Übereinstimmungen · {files}-Dateien', + 'search.noResults': 'Keine Ergebnisse', + 'search.collapseAria': 'Ergebnisse reduzieren', + 'search.expandAria': 'Erweitern Sie {count} um weitere Ergebniszeilen', + 'search.expandRest': '… {count} weitere Zeilen', + 'web.noResults': 'Keine Ergebnisse gefunden', + 'web.sourcesTruncated': 'Quellenliste gekürzt', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Inhalt abgeschnitten', + 'details.running': 'Laufen…', + 'queue.count': '{n} Nachrichten in der Warteschlange', + 'queue.sending': 'Senden…', + 'queue.image': 'Nachrichtenbild in der Warteschlange', + 'queue.file': 'Datei in der Warteschlange {name}', + 'queue.edit': 'Bearbeiten Sie die Nachricht in der Warteschlange', + 'queue.edit.unsupported': 'Enthält Nicht-Textinhalte; Bearbeitung wird noch nicht unterstützt', + 'queue.save': 'Speichern Sie die Nachricht in der Warteschlange', + 'queue.cancelEdit': 'Bearbeitung abbrechen', + 'queue.remove': 'Entfernen Sie die Nachricht in der Warteschlange', + 'queue.steer': 'Mit vorgemerkter Nachricht lenken', + 'queue.steer.unavailable': 'Die Steuerung ist nur verfügbar, während der Agent ausgeführt wird', + 'error.sessionInUse': 'Diese Sitzung wird bereits verwendet, möglicherweise von einer anderen laufenden DSH-Instanz (z. B. dsh Web oder der Desktop-App). Beenden Sie andere laufende DSH-Instanzen und versuchen Sie es erneut.', + 'queue.editFailed': 'Bearbeitung fehlgeschlagen: Der Versand dieser Nachricht wurde möglicherweise bereits gestartet.', + 'queue.removeFailed': 'Fehler beim Entfernen: Der Versand dieser Nachricht wurde möglicherweise bereits gestartet.', + 'queue.steerFailed': 'Lenkung versagt. Versuchen Sie es erneut.', + 'terminal.signal': 'Signal {signal}', + 'terminal.exitCode': 'Exit-Code {code}', + 'terminal.noExitCode': 'kein Exit-Code', + 'terminal.commandLine': 'Befehlszeile {n}', + 'terminal.running': 'Laufen', + 'terminal.failed': 'Fehlgeschlagen', + 'terminal.done': 'Fertig', + 'terminal.noOutput': 'Keine Ausgabe', + 'terminal.collapseAria': 'Ausgabe reduzieren', + 'terminal.expandAria': 'Erweitern Sie die verbleibenden {n}-Ausgabezeilen', + 'terminal.expandRest': '… {n} weitere Zeilen', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(Eingabe senden)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'quote.remove': 'Удалить цитату', + 'shortcut.newline': 'Новая линия', + 'shortcut.complementary': 'Использовать альтернативное действие: поставить в очередь или направить', + 'shortcut.slash': 'Открыть командное меню', + 'shortcut.mention': 'Открыть справочное меню', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'описать цель долгосрочной задачи', + 'hint.goal.active': 'цель активна — изменить/приостановить/возобновить/очистить', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Отправьте сообщение или запустите задачу, /команды, @файлы или сеансы', + 'placeholder.unavailable': 'Сеанс недоступен', + 'placeholder.parentOffline': 'Родительская сессия в автономном режиме; отправка недоступна, но вы все равно можете остановить запуск', + 'placeholder.hero': 'Опишите, что вы хотите построить: /команды, @файлы или сеансы.', + 'placeholder.workspace': 'Выберите рабочее пространство для начала', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter управляет всеми сообщениями в очереди.', + 'input.commands': 'Добавляйте файлы или запускайте команды', + 'input.file': 'Файл', + 'input.stop': 'Остановить генерацию', + 'input.send': 'Отправить сообщение', + 'input.send.queue': 'Сообщение в очереди', + 'input.send.steer': 'Направить сообщение', + 'attachment.pending': 'Ожидающие вложения', + 'attachment.scrollLeft': 'Прокрутить вложения влево', + 'attachment.scrollRight': 'Прокрутить вложения вправо', + 'attachment.dropTitle': 'Перетащите файлы или изображения сюда, чтобы добавить их.', + 'attachment.dropDesc': 'Ограничение изображений: до изображений {count}, по {size} каждое.', + 'attachment.dropBlocked': 'Файлы и изображения не могут быть добавлены прямо сейчас.', + 'attachment.directoryDesktopOnly': 'Папки можно добавлять только в настольном приложении; добавлять отдельные файлы в браузере', + 'attachment.pathUnavailable': 'Не удалось получить путь к папке; перетащите это снова', + 'attachment.pathUnsupported': 'Путь содержит символы, которые не может содержать ссылка; переименуй его и попробуй еще раз', + 'image.pending': 'Ожидающие изображения', + 'image.openOriginal': 'Посмотреть оригинал', + 'image.openOriginalLabel': '{label}, нажмите, чтобы посмотреть оригинал', + 'image.remove': 'Удалить изображение {name}', + 'image.original': 'Исходное изображение', + 'image.label': 'Изображение', + 'image.loadFailed': 'Изображение не удалось загрузить; нажмите, чтобы повторить попытку', + 'image.loading': 'Загрузка изображения…', + 'image.preview': 'Предварительный просмотр исходного изображения', + 'image.closePreview': 'Закрыть предварительный просмотр исходного изображения', + 'image.unsupportedType': 'Поддерживаются только изображения PNG, JPG, WebP и GIF.', + 'image.tooMany': 'Сообщение может содержать до {count} изображений.', + 'image.fileTooLarge': 'Каждое изображение должно быть меньше {size}.', + 'image.totalTooLarge': 'Общее количество изображений превышает {size}; удалите некоторые и попробуйте еще раз', + 'image.tooManyPixels': 'Разрешение изображения слишком высокое; сожми его и попробуй еще раз', + 'image.dimensionTooLarge': 'Стороны изображения должны быть не более {size}px; уменьшите масштаб и попробуйте еще раз', + 'image.modelUnsupported': 'Текущая модель не поддерживает изображения; переключиться на модель, которая делает', + 'image.sendFailed': 'Отправка изображений не удалась ({reason}); добавьте их повторно и попробуйте еще раз', + 'file.pending': 'Ожидающие файлы', + 'file.remove': 'Удалить файл {name}', + 'file.uploading': 'Загрузка…', + 'file.uploadFailed': 'Загрузка не удалась; нажмите, чтобы повторить попытку', + 'file.retry': 'Повторите загрузку {name}.', + 'file.stillUploading': 'Файлы все еще загружаются; отправить после того, как они закончат', + 'file.sessionUnavailable': 'Сессия недоступна; файлы не могут быть загружены', + 'file.notStaged': 'Загрузка файла не завершена; добавьте его еще раз и попробуйте еще раз', + 'file.label': 'Файл', + 'context.aria': '{percent} используемого контекста', + 'context.used': 'используемого контекста', + 'context.system': 'Системная подсказка', + 'context.tools': 'Определения инструментов', + 'context.messages': 'Сообщения', + 'settings.enter.title': 'Отправлять поведение во время занятости', + 'settings.enter.description': 'Что делают кнопки «Ввод» и «Отправить» во время работы агента; Cmd/Ctrl+Enter использует другое поведение.', + 'settings.enter.queue': 'Очередь', + 'settings.enter.steer': 'Направить', + 'hero.headline': 'В неизвестность', + 'hero.preview': 'Предварительный просмотр', + 'hero.chooseWorkspace': 'Выберите рабочее пространство', + 'session.hierarchy': 'Иерархия сеансов', + 'session.moreViews': 'Больше просмотров беседы', + 'session.moreViewsCurrent': 'Больше просмотров беседы · {name}', + 'todo.title': 'Дела', + 'todo.progress.done': '{done} завершено', + 'todo.progress.active': '{active} в процессе', + 'todo.progress.pending': '{pending} в ожидании', + 'todo.status.completed': 'Завершено', + 'todo.status.inProgress': 'В процессе', + 'todo.status.pending': 'Ожидается', + 'todo.rowTitle': 'Обновить список дел', + 'tool.title.createGoal': 'Создать цель', + 'tool.title.getGoal': 'Посмотреть цель', + 'tool.title.updateGoal': 'Обновить цель', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Создать напоминание', + 'tool.title.listSchedules': 'Список напоминаний', + 'tool.title.deleteSchedule': 'Удалить напоминание', + 'tool.title.updateSchedule': 'Обновить напоминание', + 'detail.state': 'Статус', + 'detail.todo.completed': 'Завершено', + 'detail.todo.in_progress': 'В процессе', + 'detail.todo.pending': 'Ожидается', + 'detail.todo.empty': 'Список дел пуст', + 'todo.diff.initial': 'Первоначальный список', + 'todo.diff.compare': 'Изменения по сравнению с предыдущим списком', + 'todo.diff.unavailable': 'Предыдущий список недоступен', + 'todo.diff.noChanges': 'Никаких изменений в списке', + 'todo.diff.added': '{count} добавлен', + 'todo.diff.updated': '{count} обновлен', + 'todo.diff.removed': '{count} удален.', + 'todo.diff.unchanged': '{count} без изменений', + 'todo.diff.addedItem': 'Добавлено', + 'todo.diff.updatedItem': 'Статус изменен', + 'todo.diff.movedItem': 'Переупорядочен', + 'todo.diff.removedItem': 'Удален', + 'detail.goal.empty': 'Нет цели', + 'detail.goal.active': 'Активный', + 'detail.goal.disarmed': 'Ожидание продолжения', + 'detail.goal.paused': 'Приостановлено', + 'detail.goal.blocked': 'Заблокировано', + 'detail.goal.complete': 'Завершено', + 'detail.goal.rounds': 'Раунды', + 'detail.goal.reason': 'Блокатор', + 'detail.days': '{count} д', + 'detail.hours': '{count} ч', + 'detail.minutes': '{count} мин.', + 'detail.seconds': '{count} с', + 'detail.schedule.once': 'Однажды', + 'detail.schedule.every': 'Каждый {interval}', + 'detail.schedule.when': 'Запланировано на', + 'detail.schedule.frequency': 'Повторить', + 'detail.schedule.scheduled': 'Запланировано', + 'detail.schedule.overdue': 'Просрочено, ждём возобновления сеанса', + 'detail.schedule.empty': 'Нет напоминаний', + 'detail.schedule.deleted': 'Удалено', + 'detail.schedule.count': '{count} напоминания', + 'detail.schedule.daily': 'Каждый день в {time} ({zone})', + 'detail.schedule.weekly': 'Еженедельно на {days} по адресу {time} ({zone})', + 'detail.schedule.cron': 'Крон {expression} ({zone})', + 'detail.weekday.1': 'Пн.', + 'detail.weekday.2': 'Вт', + 'detail.weekday.3': 'ср.', + 'detail.weekday.4': 'Чт', + 'detail.weekday.5': 'Пт', + 'detail.weekday.6': 'Суббота', + 'detail.weekday.7': 'Солнце', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Проверка поставщиков', + 'tool.title.queryRuntime': 'Время выполнения запроса', + 'tool.title.inspectPlugins': 'Проверьте плагины', + 'tool.title.workflow': 'Запустить рабочий процесс', + 'tool.title.ralph': 'Запустить цикл Ральфа', + 'tool.title.readEvent': 'Чтение события', + 'tool.title.searchEvents': 'Поиск событий', + 'tool.title.traceEvent': 'Отследить событие', + 'tool.title.searchSessions': 'Поиск сеансов', + 'tool.title.traceSession': 'Трассировка сеанса', + 'tool.title.listModels': 'Список моделей', + 'tool.title.subagent': 'Создать субагент', + 'tool.title.listAgents': 'Список субагентов', + 'tool.title.sendMessage': 'Отправить сообщение', + 'tool.title.interruptAgent': 'Агент прерываний', + 'tool.title.listJobs': 'Список фоновых заданий', + 'tool.title.readJob': 'Чтение результатов задания', + 'tool.title.killJob': 'Отменить фоновое задание', + 'tool.title.openTerminal': 'Открыть терминал', + 'tool.title.readTerminal': 'Чтение терминала', + 'tool.title.listTerminals': 'Список терминалов', + 'tool.title.signalTerminal': 'Сигнальный терминал', + 'tool.title.closeTerminal': 'Закрыть терминал', + 'tool.title.lsp': 'Символы кода запроса', + 'tool.title.findDefinition': 'Найти определение', + 'tool.title.findReferences': 'Найдите ссылки', + 'tool.title.findImplementation': 'Найти реализацию', + 'tool.title.hoverSymbol': 'Символ проверки', + 'tool.title.spawnTeammate': 'Создать товарища по команде', + 'tool.title.createTeamTask': 'Создать командную задачу', + 'tool.title.getTeamTask': 'Прочитать командное задание', + 'tool.title.updateTeamTask': 'Обновить групповую задачу', + 'tool.title.listTeamTasks': 'Перечислить командные задачи', + 'tool.title.waitAgent': 'Подождите субагента', + 'detail.recordedResult': 'Записанный результат', + 'detail.empty': 'Нет результатов', + 'detail.none': 'Нет', + 'detail.yes': 'Да', + 'detail.no': 'Нет', + 'detail.moreInInspect': '{count} больше элементов доступно в Inspect', + 'detail.status.running': 'Бег', + 'detail.status.idle': 'Простой', + 'detail.status.ready': 'Готово', + 'detail.status.inactive': 'Неактивный', + 'detail.status.provisioning': 'обеспечение', + 'detail.status.failed': 'Не удалось', + 'detail.status.completed': 'Завершено', + 'detail.status.deleted': 'Удалено', + 'detail.status.killed': 'Отменено', + 'detail.status.accepted': 'Принято', + 'detail.status.queued': 'В очереди', + 'detail.status.exited': 'Вышел', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Редакция', + 'detail.field.platform': 'Платформа', + 'detail.field.provider': 'Поставщик', + 'detail.field.model': 'Модель', + 'detail.field.role': 'Роль', + 'detail.field.context': 'Контекст', + 'detail.field.owner': 'Владелец', + 'detail.field.ready': 'Готово', + 'detail.field.dependencies': 'Зависимости', + 'detail.field.writeScopes': 'Запись областей', + 'detail.field.warnings': 'Предупреждения', + 'detail.field.diagnostics': 'Диагностика', + 'detail.field.methods': 'Методы', + 'detail.field.inputSchema': 'Входная схема', + 'detail.field.outputSchema': 'Выходная схема', + 'detail.field.currentPackage': 'Текущий пакет', + 'detail.field.nextPackage': 'Следующий пакет', + 'detail.field.latestRun': 'Последний запуск', + 'detail.field.packages': 'Пакеты', + 'detail.field.registrations': 'Регистрации', + 'detail.field.props': 'Реквизит', + 'detail.field.data': 'Данные', + 'detail.field.source': 'Источник', + 'detail.field.content': 'Содержание', + 'detail.field.message': 'Сообщение', + 'detail.field.messageId': 'Идентификатор сообщения', + 'detail.field.root': 'Корень', + 'detail.field.pid': 'Идентификатор процесса', + 'detail.field.type': 'Тип', + 'detail.field.time': 'Время', + 'detail.field.seq': 'Последовательность событий', + 'detail.field.turn': 'Ход', + 'detail.field.step': 'Шаг', + 'detail.field.callId': 'Идентификатор вызова', + 'detail.field.agents': 'Агенты запущены', + 'detail.field.result': 'Результат', + 'detail.field.parent': 'Родитель', + 'detail.field.depth': 'Глубина', + 'detail.field.exitCode': 'Код выхода', + 'detail.field.signal': 'Сигнал', + 'detail.field.previousStatus': 'Предыдущий статус', + 'detail.field.agent': 'Идентификатор агента', + 'detail.field.job': 'Идентификатор вакансии', + 'detail.field.task': 'Задача', + 'detail.field.processGroup': 'Группа процессов', + 'detail.field.availability': 'Доступность', + 'detail.field.bestMatch': 'Лучший матч', + 'detail.field.target': 'Целевое событие', + 'detail.field.surface': 'Статус записи', + 'detail.agents.count': '{count} агенты', + 'detail.jobs.count': '{count} фоновые задания', + 'detail.terminals.count': 'Клеммы {count}', + 'detail.tasks.count': '{count} командные задачи', + 'detail.tasks.nextPage': 'Доступно больше задач; следующий курсор — {cursor}', + 'detail.locations.count': '{count} локации', + 'detail.location': 'Строка {line}, столбец {column}', + 'detail.receipt.delivered': 'Сообщение доставлено', + 'detail.receipt.interrupt': 'Запрошено прерывание', + 'detail.receipt.started': 'Началось', + 'detail.receipt.cancel': 'Запрос на отмену', + 'detail.receipt.alreadyFinished': 'Уже закончено', + 'detail.receipt.signal': 'Сигнал доставлен', + 'detail.receipt.closed': 'Закрыто', + 'detail.receipt.closing': 'Закрытие', + 'detail.wait.noProgress': 'Нет активных субагентов', + 'detail.wait.title': 'Субагентская деятельность', + 'detail.wait.timeout': 'Время ожидания истекло', + 'detail.wait.changed': 'Обнаружено изменение', + 'detail.agent.reply': 'Ответ агента', + 'detail.models.title': 'Доступные модели', + 'detail.output.lines': 'Линии {begin} – {end} из {total}', + 'detail.output.truncated': 'Вывод усечен', + 'detail.providers.count': '{count} проверяет поставщиков', + 'detail.plugins.count': 'Динамические плагины {count}', + 'detail.workflow.script': 'Сценарий рабочего процесса', + 'detail.ralph.reportedComplete': 'Работник сообщил о завершении', + 'detail.ralph.reportedBlocker': 'Работник сообщил о блокировщике', + 'detail.ralph.limit': 'Достигнут лимит раундов', + 'detail.report.nextSteps': 'Оставшаяся работа', + 'detail.trace.replacedBy': 'Заменено на', + 'detail.trace.replacementChain': 'Замена цепи', + 'detail.trace.replaces': 'Замененные события', + 'detail.trace.sources': 'Исходные события', + 'detail.trace.derived': 'Производные события', + 'detail.trace.ancestors': 'Сеансы предков', + 'detail.trace.descendants': 'Потомки сеансов', + 'detail.matches.count': '{count} совпадений', + 'detail.matches.capped': 'Достигнут предел результатов; сузить поиск для большего', + 'detail.event.neighbors': 'Сопутствующие события', + 'todo.completed': '{done}/{total} завершено', + 'command.attachmentsUnsupported': '/{command} не принимает вложения; сначала удали их', + 'ask.rowTitle': 'Задать вопрос', + 'ask.waiting': 'ожидание', + 'ask.pending': 'продолжение; ответ еще доступен', + 'ask.pendingDetail': 'На эти нерешенные вопросы остается ответ композитора.', + 'ask.reopen': 'Ответ', + 'ask.review': 'Посмотреть ответы', + 'ask.closed': 'закрыто', + 'ask.closedDetail': 'Этот вопрос закрыт; его результат в разговоре ниже.', + 'ask.cancelled': 'отменен', + 'ask.cancelledDetail': 'Этот набор вопросов был отменен до того, как были отправлены ответы.', + 'ask.interrupted': 'прерванный', + 'ask.interruptedDetail': 'Этот набор вопросов был прерван до того, как были представлены ответы.', + 'ask.answered': '{answered}/{total} ответил', + 'ask.skipped': 'Не ответил', + 'bash.running': 'Бег', + 'bash.failed': 'Не удалось', + 'bash.stopped': 'Остановлено', + 'row.running': 'Бег', + 'row.preparing': 'Подготовка вызова инструмента', + 'row.failed': 'Не удалось', + 'row.stopped': 'Остановлено', + 'row.commandArguments': 'Аргументы команды', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Осмотреть', + 'row.imageSize': '{width}x{height}px, {bytes} байт', + 'row.imageSizeDownscaled': '{width}x{height}px (уменьшено с {originalWidth}x{originalHeight}px), {bytes} байт', + 'tool.title.search': 'Поиск', + 'tool.title.read': 'Читать', + 'tool.title.bash': 'Баш', + 'tool.title.write': 'Написать', + 'tool.title.edit': 'Редактировать', + 'tool.title.code': 'Код', + 'tool.title.generic': 'Вызов инструмента', + 'tool.title.inspect': 'Запрос среды Cordis', + 'tool.title.runCordis': 'Запустите плагин Cordis', + 'tool.title.stopCordis': 'Остановить плагин Cordis', + 'tool.title.removeCordis': 'Удалить плагин Cordis', + 'tool.title.pwsh': 'Пшш', + 'tool.title.readImage': 'Читать изображение', + 'tool.title.grep': 'Греп', + 'tool.title.glob': 'Глоб', + 'tool.title.webSearch': 'Поиск', + 'tool.title.webFetch': 'Получить', + 'tool.autoReviewRejected': 'Отклонено Auto review', + 'tool.autoReviewNotExecuted': 'Инструмент не был выполнен. Причина: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review не санкционировал это действие', + 'diff.collapseAria': 'Свернуть разницу', + 'diff.expandAria': 'Разверните {count} больше строк различий', + 'diff.expandRest': '… {count} еще строки', + 'read.window': 'Показаны линии {shown} {total}.', + 'read.collapseAria': 'Свернуть содержимое', + 'read.expandAria': 'Разверните {count} еще строки', + 'read.expandRest': '… {count} еще строки', + 'search.paths': '{shown} пути', + 'search.paths.truncated': 'Отображение путей {shown} из {total}', + 'search.matches': '{shown} соответствует · файлам {files}', + 'search.matches.truncated': 'Отображение {shown} совпадений {total} · Файлы {files}', + 'search.noResults': 'Нет результатов', + 'search.collapseAria': 'Свернуть результаты', + 'search.expandAria': 'Разверните {count} больше строк результатов', + 'search.expandRest': '… {count} еще строки', + 'web.noResults': 'Результаты не найдены', + 'web.sourcesTruncated': 'Список источников сокращен', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Содержание усечено', + 'details.running': 'Бег…', + 'queue.count': 'Сообщения в очереди {n}', + 'queue.sending': 'Отправка…', + 'queue.image': 'Изображение сообщения в очереди', + 'queue.file': 'Файл в очереди {name}', + 'queue.edit': 'Изменить сообщение в очереди', + 'queue.edit.unsupported': 'Содержит нетекстовый контент; редактирование пока не поддерживается', + 'queue.save': 'Сохранить сообщение в очереди', + 'queue.cancelEdit': 'Отменить редактирование', + 'queue.remove': 'Удалить сообщение из очереди', + 'queue.steer': 'Направить сообщение из очереди', + 'queue.steer.unavailable': 'Управление доступно только во время работы агента.', + 'error.sessionInUse': 'Этот сеанс уже используется, возможно, другим работающим экземпляром DSH (например, веб-сайтом dsh или настольным приложением). Закройте другие работающие экземпляры DSH и повторите попытку.', + 'queue.editFailed': 'Не удалось изменить: возможно, это сообщение уже начало отправляться.', + 'queue.removeFailed': 'Удаление не удалось: возможно, это сообщение уже начало отправляться.', + 'queue.steerFailed': 'Рулевое управление не удалось. Попробуйте еще раз.', + 'terminal.signal': 'сигнал {signal}', + 'terminal.exitCode': 'код выхода {code}', + 'terminal.noExitCode': 'нет кода выхода', + 'terminal.commandLine': 'Командная строка {n}', + 'terminal.running': 'Бег', + 'terminal.failed': 'Не удалось', + 'terminal.done': 'Готово', + 'terminal.noOutput': 'Нет вывода', + 'terminal.collapseAria': 'Свернуть вывод', + 'terminal.expandAria': 'Разверните оставшиеся выходные строки {n}.', + 'terminal.expandRest': '… {n} еще строки', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(отправить ввод)', + 'terminal.session': 'Терминал {sessionId}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'quote.remove': 'Remover cotação', + 'shortcut.newline': 'Nova linha', + 'shortcut.complementary': 'Usar a ação alternativa de enfileirar ou orientar', + 'shortcut.slash': 'Abrir menu de comando', + 'shortcut.mention': 'Abrir menu de referência', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'descrever o objetivo de uma tarefa de longa duração', + 'hint.goal.active': 'meta ativa — editar/pausar/resumir/limpar', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envie mensagens ou execute uma tarefa, / comandos, @ arquivos ou sessões', + 'placeholder.unavailable': 'Sessão indisponível', + 'placeholder.parentOffline': 'Sessão pai offline; o envio não está disponível, mas você ainda pode interromper a execução', + 'placeholder.hero': 'Descreva o que você deseja construir, / comandos, @ arquivos ou sessões', + 'placeholder.workspace': 'Escolha um espaço de trabalho para começar', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter direciona todas as mensagens na fila', + 'input.commands': 'Adicione arquivos ou execute comandos', + 'input.file': 'Arquivo', + 'input.stop': 'Parar geração', + 'input.send': 'Enviar mensagem', + 'input.send.queue': 'Mensagem de fila', + 'input.send.steer': 'Orientar com mensagem', + 'attachment.pending': 'Anexos pendentes', + 'attachment.scrollLeft': 'Rolar anexos para a esquerda', + 'attachment.scrollRight': 'Rolar os anexos para a direita', + 'attachment.dropTitle': 'Arraste arquivos ou imagens aqui para adicioná-los', + 'attachment.dropDesc': 'Limite de imagens: até {count} imagens, {size} cada', + 'attachment.dropBlocked': 'Arquivos e imagens não podem ser adicionados no momento', + 'attachment.directoryDesktopOnly': 'As pastas só podem ser adicionadas no aplicativo de desktop; adicione arquivos individuais no navegador', + 'attachment.pathUnavailable': 'Não foi possível obter o caminho da pasta; arraste-o novamente', + 'attachment.pathUnsupported': 'O caminho contém caracteres que uma referência não pode transportar; renomeie-o e tente novamente', + 'image.pending': 'Imagens pendentes', + 'image.openOriginal': 'Ver original', + 'image.openOriginalLabel': '{label}, clique para ver o original', + 'image.remove': 'Remover imagem {name}', + 'image.original': 'Imagem original', + 'image.label': 'Imagem', + 'image.loadFailed': 'A imagem não foi carregada; clique para tentar novamente', + 'image.loading': 'Carregando imagem…', + 'image.preview': 'Visualização da imagem original', + 'image.closePreview': 'Fechar visualização da imagem original', + 'image.unsupportedType': 'Somente imagens PNG, JPG, WebP e GIF são suportadas', + 'image.tooMany': 'Uma mensagem pode incluir até {count} imagens', + 'image.fileTooLarge': 'Cada imagem deve ser menor que {size}', + 'image.totalTooLarge': 'As imagens excedem {size} no total; remova alguns e tente novamente', + 'image.tooManyPixels': 'A resolução da imagem é muito alta; comprima e tente novamente', + 'image.dimensionTooLarge': 'Os lados da imagem devem ter no máximo {size}px; diminua a escala e tente novamente', + 'image.modelUnsupported': 'O modelo atual não suporta imagens; mude para um modelo que faça', + 'image.sendFailed': 'Falha no envio de imagens ({reason}); adicione-os novamente e tente novamente', + 'file.pending': 'Arquivos pendentes', + 'file.remove': 'Remover arquivo {name}', + 'file.uploading': 'Fazendo upload…', + 'file.uploadFailed': 'Falha no upload; clique para tentar novamente', + 'file.retry': 'Tente fazer upload novamente {name}', + 'file.stillUploading': 'Os arquivos ainda estão sendo carregados; enviar depois que terminarem', + 'file.sessionUnavailable': 'Sessão indisponível; os arquivos não podem ser carregados', + 'file.notStaged': 'O upload do arquivo não foi concluído; adicione novamente e tente novamente', + 'file.label': 'Arquivo', + 'context.aria': '{percent} de contexto usado', + 'context.used': 'do contexto usado', + 'context.system': 'Alerta do sistema', + 'context.tools': 'Definições de ferramentas', + 'context.messages': 'Mensagens', + 'settings.enter.title': 'Enviar comportamento enquanto estiver ocupado', + 'settings.enter.description': 'O que o botão Enter e o botão Enviar fazem enquanto o agente está em execução; Cmd/Ctrl+Enter usa o outro comportamento', + 'settings.enter.queue': 'Fila', + 'settings.enter.steer': 'Orientar', + 'hero.headline': 'Para o desconhecido', + 'hero.preview': 'Visualização', + 'hero.chooseWorkspace': 'Escolha o espaço de trabalho', + 'session.hierarchy': 'Hierarquia de sessão', + 'session.moreViews': 'Mais visualizações de conversas', + 'session.moreViewsCurrent': 'Mais visualizações de conversas · {name}', + 'todo.title': 'Tarefas', + 'todo.progress.done': '{done} concluído', + 'todo.progress.active': '{active} em andamento', + 'todo.progress.pending': '{pending} pendente', + 'todo.status.completed': 'Concluído', + 'todo.status.inProgress': 'Em andamento', + 'todo.status.pending': 'Pendente', + 'todo.rowTitle': 'Atualizar lista de tarefas', + 'tool.title.createGoal': 'Criar meta', + 'tool.title.getGoal': 'Ver meta', + 'tool.title.updateGoal': 'Atualizar meta', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Criar lembrete', + 'tool.title.listSchedules': 'Listar lembretes', + 'tool.title.deleteSchedule': 'Excluir lembrete', + 'tool.title.updateSchedule': 'Lembrete de atualização', + 'detail.state': 'Estado', + 'detail.todo.completed': 'Concluído', + 'detail.todo.in_progress': 'Em andamento', + 'detail.todo.pending': 'Pendente', + 'detail.todo.empty': 'A lista de tarefas está vazia', + 'todo.diff.initial': 'Lista inicial', + 'todo.diff.compare': 'Mudanças desde a lista anterior', + 'todo.diff.unavailable': 'Lista anterior indisponível', + 'todo.diff.noChanges': 'Sem alterações na lista', + 'todo.diff.added': '{count} adicionado', + 'todo.diff.updated': '{count} atualizado', + 'todo.diff.removed': '{count} removido', + 'todo.diff.unchanged': '{count} inalterado', + 'todo.diff.addedItem': 'Adicionado', + 'todo.diff.updatedItem': 'Status alterado', + 'todo.diff.movedItem': 'Reordenado', + 'todo.diff.removedItem': 'Removido', + 'detail.goal.empty': 'Sem objetivo', + 'detail.goal.active': 'Ativo', + 'detail.goal.disarmed': 'Aguardando continuação', + 'detail.goal.paused': 'Pausado', + 'detail.goal.blocked': 'Bloqueado', + 'detail.goal.complete': 'Concluído', + 'detail.goal.rounds': 'Rodadas', + 'detail.goal.reason': 'Bloqueador', + 'detail.days': '{count}d', + 'detail.hours': '{count}h', + 'detail.minutes': '{count} min', + 'detail.seconds': '{count} segundos', + 'detail.schedule.once': 'Uma vez', + 'detail.schedule.every': 'A cada {interval}', + 'detail.schedule.when': 'Agendado para', + 'detail.schedule.frequency': 'Repetir', + 'detail.schedule.scheduled': 'Agendado', + 'detail.schedule.overdue': 'Atrasado, aguardando retomada da sessão', + 'detail.schedule.empty': 'Sem lembretes', + 'detail.schedule.deleted': 'Excluído', + 'detail.schedule.count': '{count} lembretes', + 'detail.schedule.daily': 'Todos os dias às {time} ({zone})', + 'detail.schedule.weekly': 'Semanalmente em {days} às {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'segunda-feira', + 'detail.weekday.2': 'Terça', + 'detail.weekday.3': 'Quarta', + 'detail.weekday.4': 'Qui', + 'detail.weekday.5': 'Sexta-feira', + 'detail.weekday.6': 'Sábado', + 'detail.weekday.7': 'Sol', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspecionar fornecedores', + 'tool.title.queryRuntime': 'Tempo de execução da consulta', + 'tool.title.inspectPlugins': 'Inspecionar plug-ins', + 'tool.title.workflow': 'Executar fluxo de trabalho', + 'tool.title.ralph': 'Execute o circuito Ralph', + 'tool.title.readEvent': 'Ler evento', + 'tool.title.searchEvents': 'Pesquisar eventos', + 'tool.title.traceEvent': 'Evento de rastreamento', + 'tool.title.searchSessions': 'Buscar sessões', + 'tool.title.traceSession': 'Sessão de rastreamento', + 'tool.title.listModels': 'Listar modelos', + 'tool.title.subagent': 'Criar subagente', + 'tool.title.listAgents': 'Listar subagentes', + 'tool.title.sendMessage': 'Enviar mensagem', + 'tool.title.interruptAgent': 'Agente de interrupção', + 'tool.title.listJobs': 'Listar trabalhos em segundo plano', + 'tool.title.readJob': 'Ler saída do trabalho', + 'tool.title.killJob': 'Cancelar trabalho em segundo plano', + 'tool.title.openTerminal': 'Terminal aberto', + 'tool.title.readTerminal': 'Ler terminal', + 'tool.title.listTerminals': 'Listar terminais', + 'tool.title.signalTerminal': 'Terminal de sinal', + 'tool.title.closeTerminal': 'Fechar terminal', + 'tool.title.lsp': 'Símbolos de código de consulta', + 'tool.title.findDefinition': 'Encontre a definição', + 'tool.title.findReferences': 'Encontre referências', + 'tool.title.findImplementation': 'Encontre implementação', + 'tool.title.hoverSymbol': 'Símbolo de inspeção', + 'tool.title.spawnTeammate': 'Criar companheiro de equipe', + 'tool.title.createTeamTask': 'Criar tarefa de equipe', + 'tool.title.getTeamTask': 'Ler tarefa da equipe', + 'tool.title.updateTeamTask': 'Atualizar tarefa da equipe', + 'tool.title.listTeamTasks': 'Listar tarefas da equipe', + 'tool.title.waitAgent': 'Aguarde o subagente', + 'detail.recordedResult': 'Resultado registrado', + 'detail.empty': 'Nenhum resultado', + 'detail.none': 'Nenhum', + 'detail.yes': 'Sim', + 'detail.no': 'Não', + 'detail.moreInInspect': 'Mais {count} itens disponíveis no Inspecionar', + 'detail.status.running': 'Correndo', + 'detail.status.idle': 'Inativo', + 'detail.status.ready': 'Pronto', + 'detail.status.inactive': 'Inativo', + 'detail.status.provisioning': 'Provisionamento', + 'detail.status.failed': 'Falha', + 'detail.status.completed': 'Concluído', + 'detail.status.deleted': 'Excluído', + 'detail.status.killed': 'Cancelado', + 'detail.status.accepted': 'Aceito', + 'detail.status.queued': 'Na fila', + 'detail.status.exited': 'Saiu', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Revisão', + 'detail.field.platform': 'Plataforma', + 'detail.field.provider': 'Provedor', + 'detail.field.model': 'Modelo', + 'detail.field.role': 'Função', + 'detail.field.context': 'Contexto', + 'detail.field.owner': 'Proprietário', + 'detail.field.ready': 'Pronto', + 'detail.field.dependencies': 'Dependências', + 'detail.field.writeScopes': 'Escrever escopos', + 'detail.field.warnings': 'Avisos', + 'detail.field.diagnostics': 'Diagnóstico', + 'detail.field.methods': 'Métodos', + 'detail.field.inputSchema': 'Esquema de entrada', + 'detail.field.outputSchema': 'Esquema de saída', + 'detail.field.currentPackage': 'Pacote atual', + 'detail.field.nextPackage': 'Próximo pacote', + 'detail.field.latestRun': 'Última execução', + 'detail.field.packages': 'Pacotes', + 'detail.field.registrations': 'Inscrições', + 'detail.field.props': 'Adereços', + 'detail.field.data': 'Dados', + 'detail.field.source': 'Fonte', + 'detail.field.content': 'Conteúdo', + 'detail.field.message': 'Mensagem', + 'detail.field.messageId': 'ID da mensagem', + 'detail.field.root': 'Raiz', + 'detail.field.pid': 'ID do processo', + 'detail.field.type': 'Tipo', + 'detail.field.time': 'Hora', + 'detail.field.seq': 'Sequência de eventos', + 'detail.field.turn': 'Turno', + 'detail.field.step': 'Passo', + 'detail.field.callId': 'ID de chamada', + 'detail.field.agents': 'Agentes iniciados', + 'detail.field.result': 'Resultado', + 'detail.field.parent': 'Pai', + 'detail.field.depth': 'Profundidade', + 'detail.field.exitCode': 'Código de saída', + 'detail.field.signal': 'Sinal', + 'detail.field.previousStatus': 'Estado anterior', + 'detail.field.agent': 'ID do agente', + 'detail.field.job': 'ID do trabalho', + 'detail.field.task': 'Tarefa', + 'detail.field.processGroup': 'Grupo de processos', + 'detail.field.availability': 'Disponibilidade', + 'detail.field.bestMatch': 'Melhor correspondência', + 'detail.field.target': 'Evento alvo', + 'detail.field.surface': 'Status do registro', + 'detail.agents.count': '{count} agentes', + 'detail.jobs.count': '{count} trabalhos em segundo plano', + 'detail.terminals.count': '{count} terminais', + 'detail.tasks.count': '{count} tarefas de equipe', + 'detail.tasks.nextPage': 'Mais tarefas disponíveis; o próximo cursor é {cursor}', + 'detail.locations.count': '{count} locais', + 'detail.location': 'Linha {line}, coluna {column}', + 'detail.receipt.delivered': 'Mensagem entregue', + 'detail.receipt.interrupt': 'Interrupção solicitada', + 'detail.receipt.started': 'Iniciado', + 'detail.receipt.cancel': 'Cancelamento solicitado', + 'detail.receipt.alreadyFinished': 'Já terminei', + 'detail.receipt.signal': 'Sinal entregue', + 'detail.receipt.closed': 'Fechado', + 'detail.receipt.closing': 'Fechando', + 'detail.wait.noProgress': 'Nenhum subagente ativo', + 'detail.wait.title': 'Atividade do subagente', + 'detail.wait.timeout': 'A espera expirou', + 'detail.wait.changed': 'Alteração detectada', + 'detail.agent.reply': 'Resposta do agente', + 'detail.models.title': 'Modelos disponíveis', + 'detail.output.lines': 'Linhas {begin}–{end} de {total}', + 'detail.output.truncated': 'Saída truncada', + 'detail.providers.count': '{count} inspecionar provedores', + 'detail.plugins.count': '{count} plug-ins dinâmicos', + 'detail.workflow.script': 'Roteiro de fluxo de trabalho', + 'detail.ralph.reportedComplete': 'Conclusão relatada pelo trabalhador', + 'detail.ralph.reportedBlocker': 'Trabalhador relatou um bloqueador', + 'detail.ralph.limit': 'Limite de rodadas atingido', + 'detail.report.nextSteps': 'Trabalho restante', + 'detail.trace.replacedBy': 'Substituído por', + 'detail.trace.replacementChain': 'Cadeia de substituição', + 'detail.trace.replaces': 'Eventos substituídos', + 'detail.trace.sources': 'Eventos de origem', + 'detail.trace.derived': 'Eventos derivados', + 'detail.trace.ancestors': 'Sessões ancestrais', + 'detail.trace.descendants': 'Sessões de descendentes', + 'detail.matches.count': '{count} correspondências', + 'detail.matches.capped': 'Limite de resultados atingido; restringir a busca por mais', + 'detail.event.neighbors': 'Eventos circundantes', + 'todo.completed': '{done}/{total} concluído', + 'command.attachmentsUnsupported': '/{command} não aceita anexos; remova-os primeiro', + 'ask.rowTitle': 'Faça uma pergunta', + 'ask.waiting': 'esperando', + 'ask.pending': 'continuou; resposta ainda disponível', + 'ask.pendingDetail': 'Essas questões pendentes permanecem respondidas pelo compositor.', + 'ask.reopen': 'Resposta', + 'ask.review': 'Ver respostas', + 'ask.closed': 'fechado', + 'ask.closedDetail': 'Esta questão está encerrada; seu resultado está na conversa abaixo.', + 'ask.cancelled': 'cancelado', + 'ask.cancelledDetail': 'Este conjunto de perguntas foi cancelado antes do envio das respostas.', + 'ask.interrupted': 'interrompido', + 'ask.interruptedDetail': 'Este conjunto de perguntas foi interrompido antes do envio das respostas.', + 'ask.answered': '{answered}/{total} respondeu', + 'ask.skipped': 'Não respondido', + 'bash.running': 'Correndo', + 'bash.failed': 'Falha', + 'bash.stopped': 'Parado', + 'row.running': 'Correndo', + 'row.preparing': 'Preparando chamada de ferramenta', + 'row.failed': 'Falha', + 'row.stopped': 'Parado', + 'row.commandArguments': 'Argumentos de comando', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspecionar', + 'row.imageSize': '{width}x{height}px, {bytes} bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (reduzido de {originalWidth}x{originalHeight}px), {bytes} bytes', + 'tool.title.search': 'Pesquisar', + 'tool.title.read': 'Leia', + 'tool.title.bash': 'Bash', + 'tool.title.write': 'Escreva', + 'tool.title.edit': 'Editar', + 'tool.title.code': 'Código', + 'tool.title.generic': 'Chamada de ferramenta', + 'tool.title.inspect': 'Consultar ambiente Cordis', + 'tool.title.runCordis': 'Execute o plug-in Cordis', + 'tool.title.stopCordis': 'Parar o plug-in Cordis', + 'tool.title.removeCordis': 'Remover plug-in Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Ler imagem', + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'globo', + 'tool.title.webSearch': 'Pesquisar', + 'tool.title.webFetch': 'Buscar', + 'tool.autoReviewRejected': 'Rejeitado por Auto review', + 'tool.autoReviewNotExecuted': 'A ferramenta não foi executada. Motivo: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review não autorizou esta ação', + 'diff.collapseAria': 'Recolher diferença', + 'diff.expandAria': 'Expanda mais {count} linhas de diferenças', + 'diff.expandRest': '… mais {count} linhas', + 'read.window': 'Mostrando {shown} de {total} linhas', + 'read.collapseAria': 'Recolher conteúdo', + 'read.expandAria': 'Expanda mais {count} linhas', + 'read.expandRest': '… mais {count} linhas', + 'search.paths': '{shown} caminhos', + 'search.paths.truncated': 'Mostrando {shown} de {total} caminhos', + 'search.matches': '{shown} correspondências · {files} arquivos', + 'search.matches.truncated': 'Mostrando {shown} de {total} correspondências · {files} arquivos', + 'search.noResults': 'Nenhum resultado', + 'search.collapseAria': 'Recolher resultados', + 'search.expandAria': 'Expanda mais {count} linhas de resultados', + 'search.expandRest': '… mais {count} linhas', + 'web.noResults': 'Nenhum resultado encontrado', + 'web.sourcesTruncated': 'Lista de fontes truncada', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Conteúdo truncado', + 'details.running': 'Correndo…', + 'queue.count': '{n} mensagens na fila', + 'queue.sending': 'Enviando…', + 'queue.image': 'Imagem de mensagem na fila', + 'queue.file': 'Arquivo na fila {name}', + 'queue.edit': 'Editar mensagem na fila', + 'queue.edit.unsupported': 'Contém conteúdo não textual; a edição ainda não é suportada', + 'queue.save': 'Salvar mensagem na fila', + 'queue.cancelEdit': 'Cancelar edição', + 'queue.remove': 'Remover mensagem na fila', + 'queue.steer': 'Orientar com mensagem da fila', + 'queue.steer.unavailable': 'A direção está disponível apenas enquanto o agente está em execução', + 'error.sessionInUse': 'Esta sessão já está em uso, possivelmente por outra instância DSH em execução (como dsh web ou aplicativo de desktop). Saia de outras instâncias DSH em execução e tente novamente.', + 'queue.editFailed': 'Falha na edição: esta mensagem pode já ter começado a ser enviada.', + 'queue.removeFailed': 'Falha na remoção: esta mensagem pode já ter começado a ser enviada.', + 'queue.steerFailed': 'A direção falhou. Tente novamente.', + 'terminal.signal': 'sinal {signal}', + 'terminal.exitCode': 'código de saída {code}', + 'terminal.noExitCode': 'sem código de saída', + 'terminal.commandLine': 'Linha de comando {n}', + 'terminal.running': 'Correndo', + 'terminal.failed': 'Falha', + 'terminal.done': 'Concluído', + 'terminal.noOutput': 'Sem saída', + 'terminal.collapseAria': 'Recolher saída', + 'terminal.expandAria': 'Expanda as {n} linhas de saída restantes', + 'terminal.expandRest': '… mais {n} linhas', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(enviar entrada)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index cf92656162..1f0380680c 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -198,6 +198,9 @@ z-index: 1; display: flex; gap: 16px 20px; + width: 100%; + min-width: 0; + box-sizing: border-box; align-items: flex-start; flex-wrap: wrap; margin-top: 10px; @@ -212,12 +215,20 @@ min-height: 25px; } -/* The 16px text line retains 9px below it. The active bar reaches 1px past - the tab box to sit over the header rule. */ +/* The 16px text line retains 9px below it. The press target extends 10px + above the text into the strip's top margin and 10px to each side, matching + the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative + margins keep the layout box at its 25px row and text width. The active bar + spans the text and reaches 1px past the tab box to sit over the header + rule. */ .tab { + --dsh-tab-hit-inline: 10px; + position: relative; - min-height: 25px; - padding: 0 0 9px; + box-sizing: border-box; + min-height: 35px; + margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0; + padding: 10px var(--dsh-tab-hit-inline) 9px; border: none; background: transparent; font-size: 13px; @@ -230,9 +241,9 @@ .tab::after { content: ''; position: absolute; - right: 0; + right: var(--dsh-tab-hit-inline); bottom: -1px; - left: 0; + left: var(--dsh-tab-hit-inline); height: 2px; border-radius: 2px; background: transparent; @@ -262,6 +273,10 @@ color: var(--dsw-alias-label-secondary); } +.viewsMoreActive { color: var(--dsw-alias-state-business-primary); } + +.tabLabel { display: block; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .viewsMore[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); } diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx index f4ef1f342a..3089b083df 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx @@ -1,6 +1,6 @@ /** Strict per-session header/body content inserted into the resident conversation layout. */ -import { useState } from 'react' +import { useLayoutEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { Button, IconEllipsisOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client' @@ -22,17 +22,17 @@ export type ConversationSessionHeaderProps = ConversationSessionHeaderSlotProps type ViewsMoreMenuProps = Omit & Pick - & { readonly label: string } + & { readonly label: string; readonly current: boolean } /** The View tab row's More launcher; its menu rows come from `conversation.session.header.views.actions`. */ -function ViewsMoreMenu({ label, entries, onOpen, onPinnedChange, renderSlot }: ViewsMoreMenuProps) { +function ViewsMoreMenu({ label, current, entries, onOpen, onPinnedChange, renderSlot }: ViewsMoreMenuProps) { const [open, setOpen] = useState(false) const onDismiss = (): void => { setOpen(false) } return ( - @@ -93,6 +93,31 @@ export function ConversationSessionHeader({ const unpinned = useEntryPins(ids => ids) const hasEntryActions = useEntryActions(registered => registered) const showTabs = !hideChrome && tabs.length > 1 + const tabRow = useRef(null) + const [visibleViewLimit, setVisibleViewLimit] = useState(1) + // Observe only the available row width; individual tabs are not measured. + useLayoutEffect(() => { + const row = tabRow.current + if (row === null || !hasEntryActions) return + const updateLimit = (): void => { + setVisibleViewLimit(Math.max(1, Math.floor((row.clientWidth - 48) / 120))) + } + updateLimit() + if (typeof ResizeObserver === 'undefined') return + const observer = new ResizeObserver(updateLimit) + observer.observe(row) + return () => { observer.disconnect() } + }, [showTabs, hasEntryActions]) + const { visibleTabs, moreTabs } = useMemo(() => { + const preferred = [ + ...tabs.filter(tab => tab.id === FIXED_VIEW_ID), + ...tabs.filter(tab => tab.id !== FIXED_VIEW_ID && !unpinned.includes(tab.id)), + ] + const visibleTabs = hasEntryActions ? preferred.slice(0, visibleViewLimit) : preferred + const visibleIds = new Set(visibleTabs.map(tab => tab.id)) + return { visibleTabs, moreTabs: tabs.filter(tab => !visibleIds.has(tab.id)) } + }, [tabs, unpinned, hasEntryActions, visibleViewLimit]) + const currentInMore = moreTabs.some(tab => tab.id === active?.id) return ( <>
@@ -169,10 +194,9 @@ export function ConversationSessionHeader({ {showTabs && ( // data-conversation-tabs: marks the tab strip, which the window-chrome // geometry and the browser coverage lane anchor on. -
+
- {tabs.filter(viewTab => viewTab.id === FIXED_VIEW_ID || viewTab.id === active?.id - || !unpinned.includes(viewTab.id)).map(viewTab => ( + {visibleTabs.map(viewTab => ( ))}
{hasEntryActions && ( viewTab.id !== FIXED_VIEW_ID) - .map(viewTab => ({ id: viewTab.id, label: viewTab.label, pinned: !unpinned.includes(viewTab.id) }))} + label={currentInMore && active !== undefined ? t('session.moreViewsCurrent', { name: active.label }) : t('session.moreViews')} + current={currentInMore} + entries={moreTabs.map(viewTab => ({ + id: viewTab.id, label: viewTab.label, + pinned: !unpinned.includes(viewTab.id), active: viewTab.id === active?.id, + }))} onOpen={selectView} onPinnedChange={setPinned} renderSlot={renderSlot} diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index 40df87269b..b46db926da 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -138,12 +138,13 @@ export function HeroShell({ t, renderSlot, children }: HeroShellProps) { {/* figma 34:10412: fish 34×25 leading the headline, gap 10. */} { - if (window.matchMedia('(hover: hover) and (prefers-reduced-motion: no-preference)').matches) { + onPointerEnter={(event) => { + if (event.pointerType !== 'touch' + && window.matchMedia('(hover: hover) and (prefers-reduced-motion: no-preference)').matches) { setHovering(true) } }} - onMouseLeave={() => { setHovering(false) }} + onPointerLeave={() => { setHovering(false) }} > {renderSlot('conversation.hero.brand.mark', { size: 34, className: css.fish }, { fallback: , diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 8e9041c151..67ad49f958 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -76,6 +76,12 @@ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); } +@media (pointer: coarse) { + .card { + font-size: max(16px, var(--dsh-content-font-size, 14px)); + } +} + /* No-workspace trigger state: an inset 1px dashed outline marks the card as a workspace picker rather than a live composer. Keeping the complete stroke inside the rounded box prevents clipped corner pixels from reading heavier. */ diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index e53e666864..fd0d9190ec 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -17,7 +17,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat import type { ChangeEvent, KeyboardEvent, MouseEvent } from 'react' import clsx from 'clsx' import { - IconPlusOutlineMedium, IconWarningOutlineRegular, Toast, Tooltip, + IconPlusOutlineMedium, IconWarningOutlineRegular, isMobileDevice, Toast, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' // Type-only: the `plan` projection key merge (the TodoDock posture — the // composer reads a host-computed value; the domain owns the key). @@ -31,7 +31,7 @@ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots' import type { ComposerBarProps } from '../contract/slots.ts' import { DraftEditor } from '../input/editor/DraftEditor.tsx' import { - focusDraftEditor, installDraftFilePicker, installDraftKeymap, installDraftWheel, + focusDraftEditor, installDraftFilePicker, installDraftKeyboardDismissal, installDraftKeymap, installDraftWheel, keepDraftFocus, revealDraftSelection, } from '../input/editor/view-binding.ts' import { resolveSubmitMode } from '../input/submission-policy.ts' @@ -48,6 +48,7 @@ export const InputBar = memo(function InputBar({ retryFileUpload, toggleCommandMenu, stop, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useStopShortcut, + useAccess, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, @@ -56,6 +57,13 @@ export const InputBar = memo(function InputBar({ const notice = useNotices(s => s) const busyEnter = useBusyEnter(s => s) const stopKeys = useStopShortcut(keys => keys) + // A page whose access snapshot excludes prompting, stopping, or uploading to this Session is offered + // no Send, no Stop, or no file intake rather than a disabled control. `useAccess` is undefined only + // without a Session, where the bar is an inert placeholder with no keyboard, stop, or file intake. + const promptDenied = useAccess('session/prompt') === false + const cancelDenied = useAccess('session/cancel') === false + const uploadDenied = useAccess('fileUploads/upload') === false + const stopAction = cancelDenied ? undefined : stop void useLexicon // hook seat stays bound by the inject compartment; text-ref decoration rides the shell's editor transforms const commandMenuOpen = useMenuLauncher(source => source === 'command') const [activity, setActivity] = useState(false) @@ -178,13 +186,11 @@ export const InputBar = memo(function InputBar({ revealDraftSelection(scrollRef) } - // Unlock (mount / session switch) returns focus to the box, and owns the - // reveal that comes with it. Lexical's focus() suppresses the browser's - // scroll walk (preventScroll inside), so the reveal in our own scrollport - // is ours to perform — switching to a longer draft otherwise leaves the - // caret (restored at the draft's end) off screen. + // Automatic desktop focus restores and reveals the draft selection without + // scrolling the conversation. useEffect(() => { if (locked || editor === null) return + if (isMobileDevice()) return focusDraftEditor(editor, revealSelection) }, [locked, sessionId, editor]) @@ -216,7 +222,8 @@ export const InputBar = memo(function InputBar({ // The host enforces the same image limits at submit for callers that bypass // this composer. const intakeFiles = useCallback((files: readonly File[], directories?: ReadonlySet): void => { - if (locked || machineBusy || (subagent !== null && !continuable) || addFiles === undefined || files.length === 0) return + if (uploadDenied || locked || machineBusy || (subagent !== null && !continuable) + || addFiles === undefined || files.length === 0) return const rejected = ((): string | null => { // Child prompts support images, but not generic file receipts or the // Desktop path-reference fallback. Refuse mixed batches before intake. @@ -242,9 +249,9 @@ export const InputBar = memo(function InputBar({ return addFiles(files, directories) })() if (rejected !== null) showToast(rejected) - }, [locked, machineBusy, subagent, continuable, addFiles, attachments, imageLimits, showToast, t]) + }, [uploadDenied, locked, machineBusy, subagent, continuable, addFiles, attachments, imageLimits, showToast, t]) - const canAcceptDrop = (subagent === null || continuable) && !locked && !machineBusy && addFiles !== undefined + const canAcceptDrop = !uploadDenied && (subagent === null || continuable) && !locked && !machineBusy && addFiles !== undefined const fileInputRef = useRef(null) const onPickFiles = (e: ChangeEvent): void => { @@ -275,6 +282,11 @@ export const InputBar = memo(function InputBar({ return installDraftKeymap(editor, keyboard, gate) }, [editor, keyboard]) + useEffect(() => { + if (editor === null) return + return installDraftKeyboardDismissal(editor, cardRef) + }, [editor]) + // Button presses steal focus from the editor; suppress at mousedown so // typing continues seamlessly. Lexical's focus() carries preventScroll and // restores the previous selection, so no reveal is needed: the caret has @@ -290,7 +302,7 @@ export const InputBar = memo(function InputBar({ if (target.closest('[data-composer-overlay]') !== null) return if (scrollRef.current?.contains(target)) return const control = target.closest('button, a, input, select, textarea, [role="button"], [contenteditable], [tabindex]') - if (control !== null && e.currentTarget.contains(control)) return + if (control !== null && control !== e.currentTarget && e.currentTarget.contains(control)) return e.preventDefault() // Lexical's stored selection can lag behind native caret moves, so calling // editor.focus() on a focused editor can restore an older caret. @@ -328,7 +340,7 @@ export const InputBar = memo(function InputBar({ // exposes Stop independently. const primaryStops = running && subagent === null && (empty || blocked !== undefined) // Disabled native buttons may omit mouseleave; their tooltip must close from state. - const primaryDisabled = primaryStops ? stop === undefined : empty || disabled || machineBusy || uploadsPending + const primaryDisabled = primaryStops ? stopAction === undefined : empty || disabled || machineBusy || uploadsPending const interruptible = running && continuable const primarySubmitMode = resolveSubmitMode(busyEnter, running, 'enter', steeringAvailable) const plainMessageDraft = !empty && input?.phase === 'plain' && !draft.trimStart().startsWith('/') @@ -339,7 +351,7 @@ export const InputBar = memo(function InputBar({ : t('input.send') const onPrimary = (): void => { if (primaryStops) { - stop?.() + stopAction?.() return } if (keyboard === undefined) return // absent machine: the button is disabled @@ -400,6 +412,7 @@ export const InputBar = memo(function InputBar({ click's reopen (close-then-open flickers the chip's open echo). */}
diff --git a/packages/client/ui-conversation/src/client/view-pins.ts b/packages/client/ui-conversation/src/client/view-pins.ts index aebf97470a..d3d4e1f408 100644 --- a/packages/client/ui-conversation/src/client/view-pins.ts +++ b/packages/client/ui-conversation/src/client/view-pins.ts @@ -1,11 +1,9 @@ -/** Persisted pin choices for the Conversation View tabs. */ +/** In-memory pin choices for the Conversation View tabs. */ import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' /** View whose tab always shows and never enters the customization list. */ export const FIXED_VIEW_ID = 'chat' -/** Root-level key shared with the slot layer's future per-list preferences; Sessions share one list. */ -const STORAGE_KEY = 'dsh.slot.manual-order.conversation.view.v1' const NO_VIEWS: readonly string[] = [] /** Unpinned View ids and their only mutation. */ @@ -13,30 +11,24 @@ export interface ViewPins { /** Unpinned View ids; ids of Views that are not registered stay listed. */ readonly unpinned: ObservableSnapshot /** - * Show or hide one View tab; the fixed View is ignored. + * Pin or unpin one View tab; the fixed View is ignored. A pinned tab still + * enters More when the header's width budget cannot hold it. * @param id - View id. - * @param pinned - Whether the tab shows in the Conversation header. + * @param pinned - Whether the tab is manually pinned. */ readonly setPinned: (id: string, pinned: boolean) => void } -/** Stored content is unchecked; anything but a string array leaves every View pinned. */ -function unpinnedViews(value: unknown): readonly string[] { - if (typeof value !== 'object' || value === null || !('unpinned' in value)) return NO_VIEWS - const { unpinned } = value - return Array.isArray(unpinned) && unpinned.every((id): id is string => typeof id === 'string') ? unpinned : NO_VIEWS -} - /** - * Create the persisted View pin list; the snapshot keeps its reference until the list changes. - * @returns Validated unpinned View ids and the pin mutation. + * Create the in-memory View pin list; the snapshot keeps its reference until the list changes. + * @returns Unpinned View ids and the pin mutation. */ export function createViewPins(): ViewPins { - const store = createSnapshotStore({ unpinned: NO_VIEWS }, { persist: { name: STORAGE_KEY } }) + const store = createSnapshotStore<{ readonly unpinned: readonly string[] }>({ unpinned: NO_VIEWS }) return { - unpinned: { getSnapshot: () => unpinnedViews(store.getSnapshot()), subscribe: listener => store.subscribe(listener) }, + unpinned: { getSnapshot: () => store.getSnapshot().unpinned, subscribe: listener => store.subscribe(listener) }, setPinned: (id, pinned) => { - const current = unpinnedViews(store.getSnapshot()) + const current = store.getSnapshot().unpinned if (id === FIXED_VIEW_ID || pinned !== current.includes(id)) return store.set({ unpinned: pinned ? current.filter(entry => entry !== id) : [...current, id] }) }, diff --git a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx index fa7f2fc4ef..a7b0bfdbdf 100644 --- a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx @@ -44,7 +44,7 @@ function entry( } describe('target-neutral Conversation apply wiring', () => { - it('hands every Session header the same persisted View pins', async () => { + it('hands every Session header the same in-memory View pins without restoring saved choices', async () => { localStorage.setItem('dsh.slot.manual-order.conversation.view.v1', JSON.stringify({ unpinned: ['trajectory'] })) try { const b = await bench() @@ -56,10 +56,10 @@ describe('target-neutral Conversation apply wiring', () => { const inject = header.inject as (sessionId: SessionId) => PinHooks const injected = inject(SID) const first = injected.hooks.entryPins - expect(first.getSnapshot()).toEqual(['trajectory']) + expect(first.getSnapshot()).toEqual([]) expect(inject('session-2' as SessionId).hooks.entryPins).toBe(first) injected.setPinned('timeline', false) - expect(first.getSnapshot()).toEqual(['trajectory', 'timeline']) + expect(first.getSnapshot()).toEqual(['timeline']) const actions = injected.hooks.entryActions const changed = vi.fn() actions.subscribe(changed) diff --git a/packages/client/ui-conversation/tests/editor-focus.client.spec.ts b/packages/client/ui-conversation/tests/editor-focus.client.spec.ts index a75bdf01aa..9f7cc0a878 100644 --- a/packages/client/ui-conversation/tests/editor-focus.client.spec.ts +++ b/packages/client/ui-conversation/tests/editor-focus.client.spec.ts @@ -1,9 +1,10 @@ // @vitest-environment jsdom /** Composer selection survives focus transfers and background editor reconciliation. */ +import * as uiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' import { expect, it, onTestFinished, vi } from 'vitest' import { $getRoot, $getSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, UNDO_COMMAND } from 'lexical' import { DraftEditorRuntime } from '../src/client/input/editor/runtime.ts' -import { focusDraftEditor } from '../src/client/input/editor/view-binding.ts' +import { focusDraftEditor, installDraftKeyboardDismissal } from '../src/client/input/editor/view-binding.ts' function fixture(start = 1, end = 4) { // jsdom has no Range geometry; these cases observe selection and focus, not scrolling. @@ -16,9 +17,12 @@ function fixture(start = 1, end = 4) { const root = document.createElement('div') root.setAttribute('contenteditable', 'true') root.tabIndex = 0 + const card = document.createElement('div') + card.tabIndex = -1 + card.append(root) const outside = document.createElement('button') - document.body.append(root, outside) - onTestFinished(() => { root.remove(); outside.remove() }) + document.body.append(card, outside) + onTestFinished(() => { card.remove(); outside.remove() }) // jsdom collapses document Selection on every focus(); Chromium retains the // editor range here. Keep native focus events and activeElement changes. for (const element of [root, outside]) { @@ -66,7 +70,7 @@ function fixture(start = 1, end = 4) { editor.read(() => {}) return reveal } - return { runtime, editor, root, outside, onUpdate, dispose, selection, domSelection, focusComposer } + return { runtime, editor, root, card, outside, onUpdate, dispose, selection, domSelection, focusComposer } } it.each([ @@ -138,6 +142,48 @@ it('restores a pending selection when blur and explicit focus share one update b expect(reveal).toHaveBeenCalledOnce() }) +it('hands mobile submission focus to the composer without stealing focus from another control', async () => { + const userAgent = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1') + onTestFinished(() => { userAgent.mockRestore() }) + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const f = fixture() + const dispose = installDraftKeyboardDismissal(f.editor, { current: f.card }) + onTestFinished(dispose) + + f.runtime.clearCommittedDraft(() => null) + expect(f.root.textContent).toBe('') + expect(document.activeElement).toBe(f.card) + + f.runtime.setDraft('next draft') + f.outside.focus() + f.runtime.clearCommittedDraft(() => null) + expect(document.activeElement).toBe(f.outside) +}) + +it('retains mobile focus for a newer draft suffix and removes dismissal with the view', async () => { + const userAgent = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1') + onTestFinished(() => { userAgent.mockRestore() }) + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const f = fixture() + const dispose = installDraftKeyboardDismissal(f.editor, { current: f.card }) + onTestFinished(dispose) + + f.runtime.clearCommittedDraft(() => 6) + expect(f.root.textContent).toBe('beta') + expect(document.activeElement).toBe(f.root) + + dispose() + f.runtime.clearCommittedDraft(() => null) + expect(f.root.textContent).toBe('') + expect(document.activeElement).toBe(f.root) +}) + it('removes focus preservation and update subscriptions when registration is disposed', () => { const f = fixture() f.dispose() diff --git a/packages/client/ui-conversation/tests/editor-viewport.client.spec.ts b/packages/client/ui-conversation/tests/editor-viewport.client.spec.ts new file mode 100644 index 0000000000..b000350885 --- /dev/null +++ b/packages/client/ui-conversation/tests/editor-viewport.client.spec.ts @@ -0,0 +1,73 @@ +// @vitest-environment jsdom +/** Composer caret visibility across page scrolling and virtual-keyboard viewport changes. */ +import { expect, it, onTestFinished, vi } from 'vitest' +import { DraftEditorRuntime } from '../src/client/input/editor/runtime.ts' + +function fixture(viewport: { height: number; offsetTop: number; pageTop: number } | undefined, pageScroll: number) { + // jsdom has no visual viewport or Range geometry; use the browser measurements + // while leaving Lexical's DOM selection and scrolling decisions intact. + vi.stubGlobal('visualViewport', viewport) + vi.stubGlobal('scrollY', pageScroll) + vi.stubGlobal('innerHeight', 600) + const scrollBy = vi.spyOn(window, 'scrollBy').mockImplementation(() => {}) + const geometry = Object.getOwnPropertyDescriptor(Range.prototype, 'getBoundingClientRect') + let caret = new DOMRect(20, 169, 0, 26) + Object.defineProperty(Range.prototype, 'getBoundingClientRect', { + configurable: true, + value: () => caret, + }) + const root = document.createElement('div') + root.setAttribute('contenteditable', 'true') + root.tabIndex = 0 + root.getBoundingClientRect = () => new DOMRect(0, 0, 400, 1000) + document.body.append(root) + const runtime: DraftEditorRuntime = new DraftEditorRuntime({ + onUpdate: () => { runtime.refreshProjection() }, + quoteLabels: () => ({ remove: 'Remove quote' }), + openReference: () => false, + lexicon: () => new Map(), resolveLexicon: () => undefined, + }) + const dispose = runtime.register() + onTestFinished(() => { + dispose() + root.remove() + scrollBy.mockRestore() + if (geometry === undefined) Reflect.deleteProperty(Range.prototype, 'getBoundingClientRect') + else Object.defineProperty(Range.prototype, 'getBoundingClientRect', geometry) + vi.unstubAllGlobals() + }) + runtime.editor.setRootElement(root) + root.focus({ preventScroll: true }) + scrollBy.mockClear() + return { + scrollBy, + typeAt(top: number): void { + caret = new DOMRect(20, top, 0, 26) + runtime.setDraft('a') + }, + } +} + +it('keeps a visible caret still after Safari pans the page above the keyboard', () => { + const f = fixture({ height: 376, offsetTop: 196, pageTop: 196 }, 196) + f.typeAt(169) + expect(f.scrollBy).not.toHaveBeenCalled() +}) + +it('reveals a caret below the keyboard after the page has scrolled', () => { + const f = fixture({ height: 376, offsetTop: 196, pageTop: 196 }, 196) + f.typeAt(410) + expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, 60) +}) + +it('retains visual viewport panning relative to an already scrolled page', () => { + const f = fixture({ height: 376, offsetTop: 150, pageTop: 350 }, 200) + f.typeAt(100) + expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, -50) +}) + +it('uses the window height when the browser has no visual viewport', () => { + const f = fixture(undefined, 200) + f.typeAt(630) + expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, 56) +}) diff --git a/packages/client/ui-conversation/tests/header-styles.client.spec.ts b/packages/client/ui-conversation/tests/header-styles.client.spec.ts index c3abd2bcf4..3c964950d0 100644 --- a/packages/client/ui-conversation/tests/header-styles.client.spec.ts +++ b/packages/client/ui-conversation/tests/header-styles.client.spec.ts @@ -8,8 +8,10 @@ describe('conversation header geometry', () => { it('keeps the single-line header at the shared 76px height', () => { expect(css.match(/\.header\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 76px') expect(css.match(/\.viewTabs\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px') - expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px') - expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 0 0 9px') + // The tab's 35px press target sits 10px above its 25px row through a negative top margin. + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 35px') + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0') + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 10px var(--dsh-tab-hit-inline) 9px') }) it('uses 32px between view labels without changing the action gap', () => { diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index e308d326da..a6968a7055 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -8,6 +8,7 @@ // root listener routes them through the keymap commands); draft writes drive // the shell (jsdom's beforeinput lacks the ranges Lexical needs). +import * as uiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' import './control-row-dom.ts' import type { InboxState } from '@deepseek-ai/dsh-agent/types' import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots' @@ -105,6 +106,8 @@ interface BenchOptions { attachments?: readonly ComposerAttachment[] /** Upload states served for file-kind drafts (absent = every file is ready). */ fileUploads?: DraftFileUploads + /** Endpoints the Session's access snapshot excludes (absent = every endpoint is allowed). */ + deniedEndpoints?: readonly string[] addFiles?: (files: readonly File[], directories?: ReadonlySet) => string | null commandMenuOpen?: boolean busyEnter?: 'queue' | 'steer' @@ -192,6 +195,7 @@ function bench(over?: BenchOptions) { items: [], archivedSessionIds: [], pinnedSessionIds: [], state: 'idle', phase: 'ready', error: null, })), ...allowAllAccess, + useAccess: endpoint => !(over?.deniedEndpoints ?? []).includes(endpoint), useProjection: ((key: string, selector?: (v: unknown) => unknown) => (selector ?? (v => v))(key === 'plan' ? over?.plan @@ -707,6 +711,23 @@ describe('image draft rail', () => { } }) + it('keeps text editable but refuses attachment-slot, picker and paste intake without permission', () => { + const added = vi.fn(() => null) + const result = bench({ deniedEndpoints: ['fileUploads/upload'], addFiles: added, draft: 'text stays editable' }) + const attachment = attachmentOwner(result.slotCalls) + expect(attachment.canAcceptDrop).toBe(false) + const file = new File(['note'], 'note.txt') + attachment.onAddFiles([file]) + const picker = result.view.container.querySelector('input[type=file]')! + expect(picker.disabled).toBe(true) + fireEvent.change(picker, { target: { files: [file] } }) + fireEvent.paste(result.textarea, { clipboardData: { + items: [{ kind: 'file', type: 'text/plain', getAsFile: () => file }], getData: () => '', + } }) + expect(added).not.toHaveBeenCalled() + expect(result.textarea.getAttribute('contenteditable')).toBe('true') + }) + it('announces a rejected attachment-slot intake through the same toast', () => { const addFiles = vi.fn(() => '图片读取服务不可用') const result = bench({ addFiles }) @@ -1196,6 +1217,41 @@ describe('running and lock semantics', () => { expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer', expect.any(AbortSignal)) }) + it('a running composer whose access excludes prompting and stopping offers neither Stop nor Send', () => { + const { view, stop } = bench({ + running: true, + // The bar is handed a live Stop handler all the same, so what withholds the action is the access + // answer and not a missing face. + deniedEndpoints: ['session/prompt', 'session/cancel'], + blocked: { reason: '只读查看:本页由 owner 分享,无法发送消息' }, + placeholder: '只读查看:本页由 owner 分享,无法发送消息', + }) + expect(view.container.querySelector('button[aria-label="停止生成"]')).toBeNull() + expect(view.container.querySelector('button[aria-label="发送消息"]')).toBeNull() + expect(stop).not.toHaveBeenCalled() + }) + + it('a running composer whose access excludes only prompting keeps Stop but offers no busy Send', () => { + const empty = bench({ running: true, deniedEndpoints: ['session/prompt'] }) + expect(empty.view.getByLabelText('停止生成')).toBeTruthy() + cleanup() + // A draft would make the primary action the busy Send, which the snapshot excludes. + const drafted = bench({ running: true, deniedEndpoints: ['session/prompt'], draft: '排到下一轮' }) + expect(drafted.view.container.querySelector('button[aria-label="排队发送"]')).toBeNull() + }) + + it('an idle composer whose access excludes prompting offers no Send', () => { + const { view } = bench({ deniedEndpoints: ['session/prompt'], draft: 'hello' }) + expect(view.container.querySelector('button[aria-label="发送消息"]')).toBeNull() + }) + + it('a composer whose access has no answer yet keeps its ordinary Send', () => { + const { view, props } = bench({ draft: 'hello' }) + // Without a bound Session the Session-scope Hook answers undefined, which denies nothing. + view.rerender( undefined} />) + expect(view.getByLabelText('发送消息')).toBeTruthy() + }) + it('running Cmd/Ctrl+Enter uses the opposite of the busy-state Enter preference', () => { const meta = bench({ running: true, busyEnter: 'steer', draft: '排到下一轮' }) fireEvent.keyDown(meta.textarea, { key: 'Enter', metaKey: true }) @@ -1468,6 +1524,81 @@ describe('running and lock semantics', () => { expect(focused).toEqual([true]) }) + it.each([ + { name: 'iPhone', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1', platform: 'iPhone', maxTouchPoints: 5, automatic: false }, + { name: 'Android phone', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/119.0.0.0 Mobile Safari/537.36', platform: 'Linux armv8l', maxTouchPoints: 0, automatic: false }, + { name: 'Android tablet', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36', platform: 'Linux armv8l', maxTouchPoints: 5, automatic: false }, + { name: 'desktop-mode iPad', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 5, automatic: false }, + { name: 'Mac', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 0, automatic: true }, + { name: 'Windows touchscreen', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36', platform: 'Win32', maxTouchPoints: 5, automatic: true }, + ])('composer focus and submission respect device classification: $name', async ({ userAgent, platform, maxTouchPoints, automatic }) => { + for (const [name, value] of Object.entries({ userAgent, platform, maxTouchPoints })) { + const descriptor = Object.getOwnPropertyDescriptor(navigator, name) + Object.defineProperty(navigator, name, { configurable: true, value }) + onTestFinished(() => { + if (descriptor === undefined) Reflect.deleteProperty(navigator, name) + else Object.defineProperty(navigator, name, descriptor) + }) + } + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const outside = document.createElement('input') + document.body.append(outside) + onTestFinished(() => { outside.remove() }) + outside.focus() + + const { view, textarea, props, shell, button, sink } = bench({ draft: 'draft' }) + expect(document.activeElement).toBe(automatic ? textarea : outside) + act(() => { outside.focus() }) + act(() => { view.rerender() }) + expect(document.activeElement).toBe(automatic ? textarea : outside) + act(() => { outside.focus() }) + act(() => { view.rerender() }) + act(() => { view.rerender() }) + expect(document.activeElement).toBe(automatic ? textarea : outside) + + act(() => { outside.focus() }) + fireEvent.mouseDown(view.container.querySelector('[data-composer-card]')!, { button: 0 }) + expect(document.activeElement).toBe(textarea) + + writeDraft(shell, '') + expect(document.activeElement).toBe(textarea) + fireEvent.keyDown(textarea, { key: 'Enter' }) + expect(sink).not.toHaveBeenCalled() + expect(document.activeElement).toBe(textarea) + + for (const gesture of ['click', 'enter'] as const) { + writeDraft(shell, `send by ${gesture}`) + act(() => { textarea.focus() }) + if (gesture === 'click') { + fireEvent.mouseDown(button, { button: 0 }) + fireEvent.click(button) + } else { + fireEvent.keyDown(textarea, { key: 'Enter' }) + } + await act(async () => {}) + expect(sink).toHaveBeenLastCalledWith(`send by ${gesture}`, [], 'queue', expect.any(AbortSignal)) + expect(shell.snapshot.draft).toBe('') + expect(document.activeElement === textarea).toBe(automatic) + } + + writeDraft(shell, '/goal ') + act(() => { + shell.beginCommand( + { name: 'goal', token: '/goal ', submit: () => Promise.resolve({ kind: 'error', text: 'Missing goal' }) }, + { start: 0, end: 6, draftRev: shell.snapshot.draftRev }, + ) + textarea.focus() + }) + fireEvent.keyDown(textarea, { key: 'Enter' }) + await act(async () => {}) + expect(shell.snapshot.phase).toBe('claimed') + expect(shell.snapshot.draft).toBe('/goal ') + expect(document.activeElement).toBe(textarea) + }) + it('a persisted draft adopted after mount does not steal focus from another control', () => { const { shell } = bench() const other = document.createElement('input') @@ -1883,6 +2014,21 @@ describe('command launcher chrome and control seats', () => { expect(launcher.getAttribute('aria-expanded')).toBe('true') }) + it('follows the access answers over a running Session, and restores the actions when they return', () => { + const { view, props } = bench({ running: true }) + // While the page may stop the running Session, the primary action is its Stop. + expect(view.getByLabelText('停止生成')).toBeTruthy() + + // The access snapshot now excludes the Session's writes: the bar offers neither Stop nor Send. + view.rerender( false} />) + expect(view.queryByLabelText('停止生成')).toBeNull() + expect(view.queryByLabelText('发送消息')).toBeNull() + + // The answers return: the bar offers the action the page holds again. + view.rerender() + expect(view.getByLabelText('停止生成')).toBeTruthy() + }) + it('opening the command menu from the button puts the keyboard in the editor first', () => { const toggleCommandMenu = vi.fn() const { view, textarea } = bench({ toggleCommandMenu }) diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index b278797d5d..bda16c8a08 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -85,6 +85,7 @@ function fireResize(el: Element): void { afterEach(() => { cleanup() + vi.restoreAllMocks() vi.unstubAllGlobals() resizeObservers.length = 0 }) @@ -144,6 +145,8 @@ function mount( unpinnedViews?: readonly string[] /** Whether the View actions list has registrations. */ entryActions?: boolean + /** Isolate header capability checks from the composer's required layout observers. */ + headerOnly?: boolean } = {}, ) { const sessionId = 'sessionId' in options ? options.sessionId : SID @@ -340,6 +343,7 @@ function mount( const renderFactorySlot = ((_name: string, input: ConversationContentInputProps, factoryOptions?: { slots?: Record ReactNode> }) => { + if (options.headerOnly) return null const common: ConversationViewsProps = { sessionId: SID, SessionProvider, @@ -681,7 +685,30 @@ describe('ConversationRoot resident composer', () => { expect(b.view.getByRole('tablist').hasAttribute('data-conversation-tabs')).toBe(true) }) - it('keeps the selected View tab while it is unpinned and hides it once another View is selected', () => { + it('keeps Chat outside at the narrowest width even when it registers after other Views', () => { + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(180) + const viewTabs = ['trajectory', 'timeline', 'chat', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)).toEqual(['chat']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['trajectory', 'timeline', 'notes']) + act(() => { b.actionsOwner().onOpen('trajectory') }) + expect(b.view.getByRole('tab', { name: 'chat' })).toBeTruthy() + expect(b.view.queryByRole('tab', { name: 'trajectory' })).toBeNull() + expect(b.actionsOwner().entries.find(entry => entry.id === 'trajectory')?.active).toBe(true) + }) + + it('calculates the initial tab budget when ResizeObserver is unavailable', () => { + vi.stubGlobal('ResizeObserver', undefined) + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(300) + const viewTabs = ['chat', 'trajectory', 'timeline'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true, headerOnly: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)).toEqual(['chat', 'trajectory']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['timeline']) + }) + + it('keeps an unpinned View selected without exposing its tab', () => { const viewTabs: ViewTab[] = [ { id: 'chat', label: 'Chat' }, { id: 'trajectory', label: 'Trajectory' }, @@ -691,7 +718,7 @@ describe('ConversationRoot resident composer', () => { act(() => { b.store.actions.setView('trajectory') }) const tabs = (): HTMLElement[] => within(b.view.getByRole('tablist')).getAllByRole('tab') expect(tabs().map(tab => [tab.textContent, tab.getAttribute('aria-selected')])).toEqual([ - ['Chat', 'false'], ['Trajectory', 'true'], ['Timeline', 'false'], + ['Chat', 'false'], ['Timeline', 'false'], ]) expect(b.view.getByTestId('view-trajectory')).toBeTruthy() act(() => { b.store.actions.setView('chat') }) @@ -708,6 +735,75 @@ describe('ConversationRoot resident composer', () => { expect(b.view.queryByRole('menu')).toBeNull() }) + it('reuses the width budget on View roster and selection changes without a window resize listener', () => { + let width = 480 + const readWidth = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => width) + const viewTabs: ViewTab[] = [ + { id: 'chat', label: 'Chat' }, + { id: 'trajectory', label: 'Trajectory' }, + { id: 'timeline', label: 'Timeline' }, + { id: 'inspector', label: 'Inspector' }, + ] + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + const external = () => within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['inspector']) + act(() => { b.actionsOwner().onOpen('inspector') }) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + expect(b.actionsOwner().entries.find(entry => entry.id === 'inspector')?.active).toBe(true) + expect(b.view.getByRole('button', { name: '更多会话视图 · Inspector' })).toBeTruthy() + const reads = readWidth.mock.calls.length + width = 1000 + fireEvent(window, new Event('resize')) + act(() => { b.store.actions.setView('timeline') }) + expect(readWidth).toHaveBeenCalledTimes(reads) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + viewTabs.splice(1, 1) + act(() => { b.store.actions.setView('chat') }) + expect(external()).toEqual(['Chat', 'Timeline', 'Inspector']) + expect(readWidth).toHaveBeenCalledTimes(reads) + expect(b.actionsOwner().entries).toEqual([]) + expect(b.setPinned).not.toHaveBeenCalled() + }) + + it('folds and restores Views on container resize without changing selection, and disconnects on unmount', () => { + let width = 1000 + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => width) + const viewTabs = ['chat', 'trajectory', 'timeline', 'inspector', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + const row = b.view.getByRole('tablist').parentElement! + const external = () => within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent) + expect(external()).toEqual(viewTabs.map(tab => tab.label)) + act(() => { b.store.actions.setView('notes') }) + width = 300 + act(() => { fireResize(row) }) + expect(external()).toEqual(['chat', 'trajectory']) + fireEvent.click(b.view.getByRole('button', { name: '更多会话视图 · notes' })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['timeline', 'inspector', 'notes']) + expect(b.actionsOwner().entries.find(entry => entry.id === 'notes')?.active).toBe(true) + width = 1000 + act(() => { fireResize(row) }) + expect(external()).toEqual(viewTabs.map(tab => tab.label)) + expect(b.view.getByRole('tab', { name: 'notes' }).getAttribute('aria-selected')).toBe('true') + expect(b.actionsOwner().entries).toEqual([]) + expect(b.setPinned).not.toHaveBeenCalled() + const observers = resizeObservers.filter(observer => observer.targets.includes(row)) + expect(observers).toHaveLength(1) + b.view.unmount() + expect(observers[0]!.targets).toEqual([]) + }) + + it('keeps all five Views outside when the sampled width budget is sufficient', () => { + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(1000) + const viewTabs = ['chat', 'trajectory', 'timeline', 'inspector', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)) + .toEqual(viewTabs.map(tab => tab.label)) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries).toEqual([]) + }) + it('omits More while the View actions list has no registrations', () => { const b = mount(sessionSnapshotOf()) expect(b.view.getByRole('tablist')).toBeTruthy() @@ -726,11 +822,13 @@ describe('ConversationRoot resident composer', () => { fireEvent.click(more) expect(more.getAttribute('aria-expanded')).toBe('true') expect(within(b.view.getByRole('menu')).getByRole('menuitem', { name: 'Header view action' })).toBeTruthy() - expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false }]) + expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false, active: false }]) b.actionsOwner().onPinnedChange('trajectory', true) expect(b.setPinned).toHaveBeenCalledExactlyOnceWith('trajectory', true) act(() => { b.actionsOwner().onOpen('trajectory') }) expect(b.view.getByTestId('view-trajectory')).toBeTruthy() + expect(b.view.queryByRole('tab', { name: 'Trajectory' })).toBeNull() + expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false, active: true }]) fireEvent.click(b.view.getByRole('menuitem', { name: 'Header view action' })) expect(b.view.queryByRole('menu')).toBeNull() expect(more.getAttribute('aria-expanded')).toBe('false') diff --git a/packages/client/ui-conversation/tests/view-pins.client.spec.ts b/packages/client/ui-conversation/tests/view-pins.client.spec.ts index b42e6171ee..fa4b42d53d 100644 --- a/packages/client/ui-conversation/tests/view-pins.client.spec.ts +++ b/packages/client/ui-conversation/tests/view-pins.client.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** Conversation View pins persist one root list for every Session and ignore Chat. */ +/** Conversation View pins share one in-memory root list for every Session and ignore Chat. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { createViewPins } from '../src/client/view-pins.ts' @@ -7,15 +7,15 @@ const KEY = 'dsh.slot.manual-order.conversation.view.v1' afterEach(() => { localStorage.clear() }) describe('Conversation View pins', () => { - it('persists unpinned View ids and restores them in a new instance', () => { + it('keeps unpinned View ids in memory without restoring them in a new instance', () => { const pins = createViewPins() const initial = pins.unpinned.getSnapshot() expect(initial).toEqual([]) expect(pins.unpinned.getSnapshot()).toBe(initial) pins.setPinned('trajectory', false) pins.setPinned('timeline', false) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['trajectory', 'timeline'] }) - expect(createViewPins().unpinned.getSnapshot()).toEqual(['trajectory', 'timeline']) + expect(localStorage.getItem(KEY)).toBeNull() + expect(createViewPins().unpinned.getSnapshot()).toEqual([]) pins.setPinned('trajectory', true) expect(pins.unpinned.getSnapshot()).toEqual(['timeline']) }) @@ -34,20 +34,14 @@ describe('Conversation View pins', () => { }) it('keeps ids of Views that are not registered', () => { - localStorage.setItem(KEY, JSON.stringify({ unpinned: ['removed-view'] })) const pins = createViewPins() + pins.setPinned('removed-view', false) pins.setPinned('timeline', false) expect(pins.unpinned.getSnapshot()).toEqual(['removed-view', 'timeline']) }) - it.each([ - ['a non-array list', { unpinned: { timeline: true } }], - ['non-string ids', { unpinned: [null] }], - ['a missing list', { pinned: [] }], - ['null', null], - ['an array root', ['timeline']], - ])('reads %s as no unpinned Views with a stable snapshot', (_name, stored) => { - localStorage.setItem(KEY, JSON.stringify(stored)) + it('ignores a legacy stored list instead of restoring it', () => { + localStorage.setItem(KEY, JSON.stringify({ unpinned: ['trajectory'] })) const pins = createViewPins() const snapshot = pins.unpinned.getSnapshot() expect(snapshot).toEqual([]) diff --git a/packages/client/ui-deepseek-model-label-1017/README.i18n.yaml b/packages/client/ui-deepseek-model-label-1017/README.i18n.yaml index 9bd85f7de1..b9d71b2818 100644 --- a/packages/client/ui-deepseek-model-label-1017/README.i18n.yaml +++ b/packages/client/ui-deepseek-model-label-1017/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-deepseek-model-label-1017/use-this-package: - en: c241d810553094d8 - zh: 9102b83b2aa5e032 + en: a6d5fcecfa93935b + zh: 8ae2809a06910c52 /deepseek-ai-dsh-client-ui-deepseek-model-label-1017/understand-the-implementation: - en: 368aae60ad92d90e - zh: 67ac74f0351f1b5e + en: ca1d0c5db1b819a0 + zh: 960e8db56fa5987e /deepseek-ai-dsh-client-ui-deepseek-model-label-1017/further-exploration: en: 2885099078047d38 zh: d7de1db3f1ba0a50 diff --git a/packages/client/ui-deepseek-model-label-1017/README.md b/packages/client/ui-deepseek-model-label-1017/README.md index fc55f834f6..51c83c266a 100644 --- a/packages/client/ui-deepseek-model-label-1017/README.md +++ b/packages/client/ui-deepseek-model-label-1017/README.md @@ -27,7 +27,7 @@ This package shows `1017` beside `DeepSeek-V4.1-Flash` in the composer model sel The Web bundle includes this plugin, including when Desktop uses that bundle. It has no configuration fields. -The badge appears only when the provider is `deepseek-official` or `deepseek-account`, the model ID is `deepseek-flash`, and the catalog name is exactly `DeepSeek-V4.1-Flash`. Other names and providers keep their catalog text. The input button, current-model menu row, and model options share the blue plain-text release marker. It uses the model name’s font size and baseline, with no capsule; when space is limited, the selector fades out the end of the combined text, so the marker may be hidden. +The badge appears only when the provider is `deepseek-official` or `deepseek-account`, the model ID is `deepseek-flash`, and the catalog name is exactly `DeepSeek-V4.1-Flash`. Other names and providers keep their catalog text. The input button, current-model menu row, and model options share the badge. Disable the following row through a profile patch to restore the catalog name: @@ -44,7 +44,7 @@ Disable the following row through a profile patch to restore the catalog name:
Implementation internals — click to expand -The browser entry contributes to the existing `conversation.input.model.label` slot through `ctx.slots.inject()`. Its component derives the label from owner props and renders the release marker as an inline span; it owns no cache, subscription, network request, or model-selection state. Registration and dictionaries leave with the plugin. The Host entry only makes the browser package available to the Loader. +The browser entry contributes to the existing `conversation.input.model.label` slot through `ctx.slots.inject()`. Its component derives the label from owner props and uses the shared `Tag` primitive; it owns no cache, subscription, network request, or model-selection state. Registration and dictionaries leave with the plugin. The Host entry only makes the browser package available to the Loader.
diff --git a/packages/client/ui-deepseek-model-label-1017/README.zh.md b/packages/client/ui-deepseek-model-label-1017/README.zh.md index c6064f5e30..554f99c8e1 100644 --- a/packages/client/ui-deepseek-model-label-1017/README.zh.md +++ b/packages/client/ui-deepseek-model-label-1017/README.zh.md @@ -27,7 +27,7 @@ kind: "package-reference" Web bundle 包含本插件,桌面端使用该 bundle 时也会加载。本包没有配置字段。 -只有提供方为 `deepseek-official` 或 `deepseek-account`、模型 ID 为 `deepseek-flash`,且目录名称恰好为 `DeepSeek-V4.1-Flash` 时才显示角标。其他名称和提供方保留目录文字。输入框按钮、当前模型菜单行和模型选项共用蓝色纯文字版本标记。标记与模型名称字号相同、基线对齐,不使用胶囊;空间不足时由选择器将整段文字末尾渐隐,版本标记可能被隐藏。 +只有提供方为 `deepseek-official` 或 `deepseek-account`、模型 ID 为 `deepseek-flash`,且目录名称恰好为 `DeepSeek-V4.1-Flash` 时才显示角标。其他名称和提供方保留目录文字。输入框按钮、当前模型菜单行和模型选项共用这个角标。 通过 profile patch 停用以下条目即可恢复目录名称: @@ -44,7 +44,7 @@ Web bundle 包含本插件,桌面端使用该 bundle 时也会加载。本包
内部实现 — 点击展开 -浏览器入口通过 `ctx.slots.inject()` 向已有的 `conversation.input.model.label` slot 注册组件。组件根据 owner props 派生标签,以行内 span 显示版本标记,不拥有缓存、订阅、网络请求或模型选择状态。注册和字典随插件卸载。Host 入口仅让 Loader 能加载浏览器包。 +浏览器入口通过 `ctx.slots.inject()` 向已有的 `conversation.input.model.label` slot 注册组件。组件根据 owner props 派生标签,使用共享 `Tag` 基元,不拥有缓存、订阅、网络请求或模型选择状态。注册和字典随插件卸载。Host 入口仅让 Loader 能加载浏览器包。
diff --git a/packages/client/ui-deepseek-model-label-1017/locale/de.json b/packages/client/ui-deepseek-model-label-1017/locale/de.json new file mode 100644 index 0000000000..97f572f313 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek 1017 Modellplakette", + "description": "Fügen Sie den offiziellen DeepSeek Flash-Modellnamen ein temporäres 1017-Release-Abzeichen hinzu." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/es.json b/packages/client/ui-deepseek-model-label-1017/locale/es.json new file mode 100644 index 0000000000..39db24f093 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Insignia del modelo DeepSeek 1017", + "description": "Agregue una insignia de lanzamiento temporal 1017 a los nombres oficiales de los modelos Flash DeepSeek." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/fr.json b/packages/client/ui-deepseek-model-label-1017/locale/fr.json new file mode 100644 index 0000000000..a0d47622b6 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Badge modèle DeepSeek 1017", + "description": "Ajoutez un badge de version 1017 temporaire aux noms de modèles Flash DeepSeek officiels." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/pt.json b/packages/client/ui-deepseek-model-label-1017/locale/pt.json new file mode 100644 index 0000000000..10be9f4c03 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek selo do modelo 1017", + "description": "Adicione um selo de versão temporário 1017 aos nomes oficiais dos modelos do DeepSeek Flash." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/ru.json b/packages/client/ui-deepseek-model-label-1017/locale/ru.json new file mode 100644 index 0000000000..1d116a03be --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Значок модели 1017", + "description": "Добавьте временный значок выпуска 1017 к официальным названиям моделей Flash DeepSeek." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/package.json b/packages/client/ui-deepseek-model-label-1017/package.json index 4a44b82db6..ea4d6f1b60 100644 --- a/packages/client/ui-deepseek-model-label-1017/package.json +++ b/packages/client/ui-deepseek-model-label-1017/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deepseek-model-label-1017", "description": "Temporary 1017 release badge for official DeepSeek Flash model labels", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.module.css b/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.module.css index 058c0ac44e..2b24c72d1a 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.module.css +++ b/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.module.css @@ -1,7 +1,7 @@ .label { + display: inline-flex; + align-items: center; + gap: 4px; + vertical-align: baseline; white-space: nowrap; } - -.release { - color: var(--dsw-alias-state-business-primary); -} diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.tsx b/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.tsx index f3e00b598c..2c876319bd 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.tsx +++ b/packages/client/ui-deepseek-model-label-1017/src/client/DeepSeekModelLabel.tsx @@ -1,6 +1,7 @@ /** Display-only release marking; the catalog and selected route remain unchanged. */ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client' +import { Tag } from '@deepseek-ai/dsh-client-ui-primitives' import css from './DeepSeekModelLabel.module.css' const MODEL_NAME = 'DeepSeek-V4.1-Flash' @@ -8,7 +9,7 @@ const MODEL_NAME = 'DeepSeek-V4.1-Flash' /** * Render the fixed 1017 release label for recognized official Flash entries. * @param props - Provider and model identity plus the badge dictionary. - * @returns The base name with a blue release marker, or the untouched catalog name. + * @returns The base name with a badge, or the untouched catalog name. */ export function DeepSeekModelLabel({ provider, model, t }: PropsRuntime<'conversation.input.model.label'> & PropsLocale<'deepseek-model-label-1017'>, @@ -16,6 +17,6 @@ export function DeepSeekModelLabel({ provider, model, t }: const official = provider.id === 'deepseek-account' || provider.id === 'deepseek-official' if (!official || model.id !== 'deepseek-flash' || model.name !== MODEL_NAME) return model.name return - {model.name}{' '}{t('badge.label')} + {model.name}{' '}{t('badge.label')} } diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/index.ts b/packages/client/ui-deepseek-model-label-1017/src/client/index.ts index 5a38858688..7e35d4e14c 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/index.ts +++ b/packages/client/ui-deepseek-model-label-1017/src/client/index.ts @@ -4,7 +4,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import { DeepSeekModelLabel } from './DeepSeekModelLabel.tsx' -import { en, ja, zh, type LabelKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type LabelKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -22,7 +22,7 @@ export const inject = ['slots', 'locale'] */ export function apply(ctx: Context): void { const namespace = 'deepseek-model-label-1017' - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'deepseek-model-label-1017: dictionaries') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'deepseek-model-label-1017: dictionaries') ctx.slots.inject('conversation.input.model.label', () => ctx.slots.register({ name: 'conversation.input.model.label', locale: namespace, }, DeepSeekModelLabel)) diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts b/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts index 95ca2cb796..d23df8c829 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts +++ b/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts @@ -13,3 +13,18 @@ export const en: Record = { 'badge.label': '1017' } export const ja = { 'badge.label': '1017', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { 'badge.label': '1017' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { 'badge.label': '1017' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { 'badge.label': '1017' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { 'badge.label': '1017' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { 'badge.label': '1017' } satisfies Record diff --git a/packages/client/ui-deliverables/locale/de.json b/packages/client/ui-deliverables/locale/de.json new file mode 100644 index 0000000000..a87831a167 --- /dev/null +++ b/packages/client/ui-deliverables/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Dokumentänderungen und Lieferpräsentation", + "description": "Anzeige geänderter Dateien, Inhaltsvergleich und Eingabe der Lieferdatei." + } +} diff --git a/packages/client/ui-deliverables/locale/es.json b/packages/client/ui-deliverables/locale/es.json new file mode 100644 index 0000000000..930c363d05 --- /dev/null +++ b/packages/client/ui-deliverables/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cambios de documentos y presentación de entrega.", + "description": "Muestra archivos modificados, comparación de contenido y entrada de archivos de entrega." + } +} diff --git a/packages/client/ui-deliverables/locale/fr.json b/packages/client/ui-deliverables/locale/fr.json new file mode 100644 index 0000000000..965bcb788d --- /dev/null +++ b/packages/client/ui-deliverables/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modifications du document et présentation de la livraison", + "description": "Afficher les fichiers modifiés, la comparaison du contenu et la saisie du fichier de livraison." + } +} diff --git a/packages/client/ui-deliverables/locale/pt.json b/packages/client/ui-deliverables/locale/pt.json new file mode 100644 index 0000000000..caef83a2ab --- /dev/null +++ b/packages/client/ui-deliverables/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Alterações de documentos e apresentação de entrega", + "description": "Exibir arquivos modificados, comparação de conteúdo e entrada de arquivo de entrega." + } +} diff --git a/packages/client/ui-deliverables/locale/ru.json b/packages/client/ui-deliverables/locale/ru.json new file mode 100644 index 0000000000..b940a1eba1 --- /dev/null +++ b/packages/client/ui-deliverables/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Изменения документов и представление поставки", + "description": "Отображение измененных файлов, сравнение содержимого и запись файла доставки." + } +} diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index c8f9724f93..c224a85d36 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deliverables/src/client/index.ts b/packages/client/ui-deliverables/src/client/index.ts index ef5bc4b06d..5750f585f3 100644 --- a/packages/client/ui-deliverables/src/client/index.ts +++ b/packages/client/ui-deliverables/src/client/index.ts @@ -27,7 +27,7 @@ import { DeliverablesTail, type DeliverablesInjected } from './Deliverables.tsx' import { ReviewTab, type ReviewInjected } from './ReviewTab.tsx' import { CHANGES_REVIEW_ID, changesReviewDefinition } from './review-definition.ts' import { createReviewStore } from './review-store.ts' -import { en, ja, NS, zh, type DeliverablesKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type DeliverablesKey } from './locales.ts' import { deliverablesDefinition, presentedForClosing, producedFileMentions, selectProducedFiles, } from './turn-deliverables.ts' @@ -57,7 +57,7 @@ export function apply(ctx: ClientContext): void { diffs.reset() }) ctx.uiConversation.events.register(deliverablesDefinition) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-deliverables: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-deliverables: dictionaries') ctx.slots.inject( 'conversation.chat.turnTail', () => ctx.slots.register({ diff --git a/packages/client/ui-deliverables/src/client/locales.ts b/packages/client/ui-deliverables/src/client/locales.ts index d5c67949ca..e25cc96c43 100644 --- a/packages/client/ui-deliverables/src/client/locales.ts +++ b/packages/client/ui-deliverables/src/client/locales.ts @@ -197,3 +197,323 @@ export const ja = { 'diff.coarse': '行比較がタイムアウトしたため、ファイル全体の置換として表示しています', 'diff.truncated': '最初の{count}行を表示', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'presented.nativeUnavailable': 'Este archivo no tiene una ruta de host disponible. Vista previa en la barra lateral.', + 'presented.revealError': 'No se pudo mostrar en el administrador de archivos. Intentar otra vez.', + 'presented.directoryError': 'No se pudo abrir la carpeta que lo contiene. Intentar otra vez.', + 'presented.directoryOpening': 'Abriendo la carpeta que contiene...', + 'presented.directoryOpened': 'Apertura solicitada que contiene la carpeta', + 'presented.revealed': 'Visualización solicitada en el administrador de archivos.', + 'presented.revealing': 'Mostrando en el administrador de archivos…', + 'presented.unavailable': 'Este Host no tiene un escritorio disponible para abrir archivos o carpetas en aplicaciones externas. Los archivos aún se pueden obtener una vista previa en la barra lateral.', + 'presented.retry': 'Reintentar', + 'presented.hostError': 'No se pudo leer la información del escritorio del host', + 'presented.preview': 'Vista previa en la barra lateral', + 'presented.previewButton': 'Abra {name} en la barra lateral', + 'presented.previewCard': 'Vista previa de {name} en la barra lateral', + 'presented.all': 'Todos los archivos {count}', + 'presented.expandAria': 'Mostrar todos los archivos entregados {count}', + 'presented.collapse': 'Contraer', + 'presented.collapseAria': 'Contraer archivos entregados', + 'presented.opening': 'Abriendo…', + 'presented.opened': 'Abierto solicitado', + 'presented.error': 'No se pudo abrir. Haga clic para volver a intentarlo.', + 'presented.file': 'Archivo', + 'row.title': 'Presentar archivos', + 'row.running': 'Entregando', + 'row.preparing': 'Preparando entregables', + 'row.ok': 'Entregado', + 'row.error': 'Entrega fallida', + 'row.stopped': 'interrumpido', + 'row.inspect': 'inspeccionar llamada', + 'changes.title': 'Archivos {count} editados', + 'changes.singleTitle': 'Editado {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binario', + 'changes.openReview': 'Revisa los cambios de este turno en la barra lateral.', + 'changes.all': 'Todos los archivos {count}', + 'changes.expandAria': 'Mostrar todos los archivos modificados {count}', + 'changes.collapse': 'Contraer', + 'changes.collapseAria': 'Contraer archivos modificados', + 'changes.oversized': 'demasiado grande', + 'changes.viewDiff': 'Ver cambios en {name}', + 'review.title': 'Revisión · turno {turn}', + 'review.selectFile': 'Elija el archivo para revisar', + 'review.split': 'Cambiar a vista dividida', + 'review.unified': 'Cambiar a vista unificada', + 'review.splitAria': 'Vista dividida', + 'review.wrap': 'Habilitar ajuste de línea', + 'review.nowrap': 'Desactivar ajuste de línea', + 'review.wrapAria': 'Ajuste de línea', + 'review.openFile': 'Abra el archivo completo en la barra lateral.', + 'review.openFileAria': 'Abra {name} en la barra lateral', + 'diff.loading': 'Cambios de lectura…', + 'diff.missing': 'El contenido de los cambios de este turno ya no está disponible.', + 'diff.error': 'No se pudieron leer los cambios.', + 'diff.binary': 'Archivo binario; los cambios no se pueden mostrar', + 'diff.oversized': 'Archivo demasiado grande; los cambios no se pueden mostrar', + 'diff.created': 'Creado en este turno', + 'diff.deleted': 'Eliminado en este turno', + 'diff.unchanged': 'Ambos lados mantienen las mismas líneas.', + 'diff.coarse': 'Se agotó el tiempo de espera para la comparación de líneas; se muestra como un reemplazo de archivo completo', + 'diff.truncated': 'Mostrando las primeras líneas {count}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'presented.nativeUnavailable': "Ce fichier n'a pas de chemin d'hôte disponible. Prévisualisez-le dans la barre latérale.", + 'presented.revealError': "Impossible d'afficher dans le gestionnaire de fichiers. Essayer à nouveau.", + 'presented.directoryError': "Impossible d'ouvrir le dossier contenant. Essayer à nouveau.", + 'presented.directoryOpening': 'Ouverture du dossier contenant…', + 'presented.directoryOpened': 'Ouverture demandée contenant le dossier', + 'presented.revealed': 'Affichage demandé dans le gestionnaire de fichiers', + 'presented.revealing': 'Affichage dans le gestionnaire de fichiers…', + 'presented.unavailable': 'Cet hôte ne dispose pas de bureau disponible pour ouvrir des fichiers ou des dossiers dans des applications externes. Les fichiers peuvent toujours être prévisualisés dans la barre latérale.', + 'presented.retry': 'Réessayer', + 'presented.hostError': 'Impossible de lire les informations du bureau hôte', + 'presented.preview': 'Aperçu dans la barre latérale', + 'presented.previewButton': 'Ouvrir {name} dans la barre latérale', + 'presented.previewCard': 'Aperçu de {name} dans la barre latérale', + 'presented.all': 'Tous les fichiers {count}', + 'presented.expandAria': 'Afficher tous les fichiers livrés {count}', + 'presented.collapse': 'Réduire', + 'presented.collapseAria': 'Réduire les fichiers livrés', + 'presented.opening': 'Ouverture…', + 'presented.opened': 'Ouvert demandé', + 'presented.error': "Impossible d'ouvrir. Cliquez pour réessayer.", + 'presented.file': 'Fichier', + 'row.title': 'Présenter des fichiers', + 'row.running': 'Livrer', + 'row.preparing': 'Préparation des livrables', + 'row.ok': 'Livré', + 'row.error': 'La livraison a échoué', + 'row.stopped': 'Interrompu', + 'row.inspect': "Inspecter l'appel", + 'changes.title': 'Fichiers {count} modifiés', + 'changes.singleTitle': 'Modifié {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binaire', + 'changes.openReview': 'Consultez les modifications de ce tour dans la barre latérale', + 'changes.all': 'Tous les fichiers {count}', + 'changes.expandAria': 'Afficher tous les fichiers modifiés {count}', + 'changes.collapse': 'Réduire', + 'changes.collapseAria': 'Réduire les fichiers modifiés', + 'changes.oversized': 'trop grand', + 'changes.viewDiff': 'Afficher les modifications apportées à {name}', + 'review.title': 'Révision · tour {turn}', + 'review.selectFile': 'Choisissez le fichier à examiner', + 'review.split': 'Passer à la vue fractionnée', + 'review.unified': 'Passer à la vue unifiée', + 'review.splitAria': 'Vue fractionnée', + 'review.wrap': 'Activer le retour à la ligne', + 'review.nowrap': 'Désactiver le retour à la ligne', + 'review.wrapAria': 'Retour à la ligne', + 'review.openFile': "Ouvrez l'intégralité du fichier dans la barre latérale", + 'review.openFileAria': 'Ouvrir {name} dans la barre latérale', + 'diff.loading': 'La lecture change…', + 'diff.missing': "Le contenu des modifications de ce tour n'est plus disponible", + 'diff.error': 'Impossible de lire les modifications', + 'diff.binary': 'Fichier binaire ; les modifications ne peuvent pas être affichées', + 'diff.oversized': 'Fichier trop volumineux ; les modifications ne peuvent pas être affichées', + 'diff.created': 'Créé à ce tour', + 'diff.deleted': 'Supprimé à ce tour', + 'diff.unchanged': 'Les deux côtés tiennent les mêmes lignes', + 'diff.coarse': "La comparaison de lignes a expiré ; affiché en remplacement de l'ensemble du fichier", + 'diff.truncated': 'Affichage des premières lignes {count}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'presented.nativeUnavailable': 'Für diese Datei ist kein Hostpfad verfügbar. Sehen Sie sich die Vorschau in der Seitenleiste an.', + 'presented.revealError': 'Konnte im Dateimanager nicht angezeigt werden. Versuchen Sie es erneut.', + 'presented.directoryError': 'Der enthaltende Ordner konnte nicht geöffnet werden. Versuchen Sie es erneut.', + 'presented.directoryOpening': 'Öffnen des enthaltenden Ordners…', + 'presented.directoryOpened': 'Angeforderte Öffnung mit Ordner', + 'presented.revealed': 'Angeforderte Anzeige im Dateimanager', + 'presented.revealing': 'Wird im Dateimanager angezeigt…', + 'presented.unavailable': 'Auf diesem Host steht kein Desktop zum Öffnen von Dateien oder Ordnern in externen Apps zur Verfügung. Dateien können weiterhin in der Seitenleiste in der Vorschau angezeigt werden.', + 'presented.retry': 'Erneut versuchen', + 'presented.hostError': 'Die Host-Desktop-Informationen konnten nicht gelesen werden', + 'presented.preview': 'Vorschau in der Seitenleiste', + 'presented.previewButton': 'Öffnen Sie {name} in der Seitenleiste', + 'presented.previewCard': 'Vorschau von {name} in der Seitenleiste', + 'presented.all': 'Alle {count}-Dateien', + 'presented.expandAria': 'Alle von {count} gelieferten Dateien anzeigen', + 'presented.collapse': 'Einklappen', + 'presented.collapseAria': 'Gelieferte Dateien ausblenden', + 'presented.opening': 'Eröffnung…', + 'presented.opened': 'Öffnung erbeten', + 'presented.error': 'Konnte nicht geöffnet werden. Klicken Sie, um es erneut zu versuchen.', + 'presented.file': 'Datei', + 'row.title': 'Vorhandene Akten', + 'row.running': 'Liefern', + 'row.preparing': 'Vorbereiten von Liefergegenständen', + 'row.ok': 'Geliefert', + 'row.error': 'Die Lieferung ist fehlgeschlagen', + 'row.stopped': 'Unterbrochen', + 'row.inspect': 'Aufruf prüfen', + 'changes.title': 'Bearbeitete {count}-Dateien', + 'changes.singleTitle': 'Bearbeitet {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binär', + 'changes.openReview': 'Überprüfen Sie die Änderungen dieser Runde in der Seitenleiste', + 'changes.all': 'Alle {count}-Dateien', + 'changes.expandAria': 'Alle von {count} geänderten Dateien anzeigen', + 'changes.collapse': 'Einklappen', + 'changes.collapseAria': 'Geänderte Dateien reduzieren', + 'changes.oversized': 'zu groß', + 'changes.viewDiff': 'Änderungen an {name} anzeigen', + 'review.title': 'Überprüfung · Runde {turn}', + 'review.selectFile': 'Wählen Sie die zu überprüfende Datei aus', + 'review.split': 'Wechseln Sie zur geteilten Ansicht', + 'review.unified': 'Wechseln Sie zur einheitlichen Ansicht', + 'review.splitAria': 'Geteilte Ansicht', + 'review.wrap': 'Zeilenumbruch aktivieren', + 'review.nowrap': 'Zeilenumbruch deaktivieren', + 'review.wrapAria': 'Zeilenumbruch', + 'review.openFile': 'Öffnen Sie die gesamte Datei in der Seitenleiste', + 'review.openFileAria': 'Öffnen Sie {name} in der Seitenleiste', + 'diff.loading': 'Änderungen lesen…', + 'diff.missing': 'Der Inhalt der Änderungen dieser Runde ist nicht mehr verfügbar', + 'diff.error': 'Die Änderungen konnten nicht gelesen werden', + 'diff.binary': 'Binärdatei; Änderungen können nicht angezeigt werden', + 'diff.oversized': 'Datei zu groß; Änderungen können nicht angezeigt werden', + 'diff.created': 'In dieser Runde erstellt', + 'diff.deleted': 'In dieser Runde gelöscht', + 'diff.unchanged': 'Beide Seiten vertreten die gleichen Linien', + 'diff.coarse': 'Zeitüberschreitung beim Leitungsvergleich; wird als vollständige Dateiersetzung angezeigt', + 'diff.truncated': 'Zeigt die ersten {count}-Zeilen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'presented.nativeUnavailable': 'У этого файла нет доступного пути к хосту. Предварительный просмотр на боковой панели.', + 'presented.revealError': 'Не удалось отобразить в файловом менеджере. Попробуйте еще раз.', + 'presented.directoryError': 'Не удалось открыть содержащую папку. Попробуйте еще раз.', + 'presented.directoryOpening': 'Открытие папки, содержащей…', + 'presented.directoryOpened': 'Запрошенное открытие, содержащее папку', + 'presented.revealed': 'Запрошенное отображение в файловом менеджере', + 'presented.revealing': 'Отображается в файловом менеджере…', + 'presented.unavailable': 'У этого хоста нет рабочего стола, на котором можно было бы открывать файлы или папки во внешних приложениях. Файлы по-прежнему можно просматривать на боковой панели.', + 'presented.retry': 'Повторить', + 'presented.hostError': 'Не удалось прочитать информацию о рабочем столе хоста.', + 'presented.preview': 'Предварительный просмотр в боковой панели', + 'presented.previewButton': 'Откройте {name} на боковой панели.', + 'presented.previewCard': 'Предварительный просмотр {name} на боковой панели', + 'presented.all': 'Все файлы {count}', + 'presented.expandAria': 'Показать все доставленные файлы {count}', + 'presented.collapse': 'Свернуть', + 'presented.collapseAria': 'Свернуть доставленные файлы', + 'presented.opening': 'Открытие…', + 'presented.opened': 'Открыть запрос', + 'presented.error': 'Не удалось открыть. Нажмите, чтобы повторить попытку.', + 'presented.file': 'Файл', + 'row.title': 'Представление файлов', + 'row.running': 'Доставка', + 'row.preparing': 'Подготовка результатов', + 'row.ok': 'Доставлено', + 'row.error': 'Доставка не удалась', + 'row.stopped': 'Прерванный', + 'row.inspect': 'Проверить вызов', + 'changes.title': 'Отредактированные файлы {count}.', + 'changes.singleTitle': 'Отредактировано {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'двоичный', + 'changes.openReview': 'Просмотрите изменения этого хода на боковой панели.', + 'changes.all': 'Все файлы {count}', + 'changes.expandAria': 'Показать все измененные файлы {count}', + 'changes.collapse': 'Свернуть', + 'changes.collapseAria': 'Свернуть измененные файлы', + 'changes.oversized': 'слишком большой', + 'changes.viewDiff': 'Посмотреть изменения в {name}', + 'review.title': 'Проверка · ход {turn}', + 'review.selectFile': 'Выберите файл для просмотра', + 'review.split': 'Переключиться на разделенный вид', + 'review.unified': 'Переключиться на единый вид', + 'review.splitAria': 'Разделенный вид', + 'review.wrap': 'Включить перенос строк', + 'review.nowrap': 'Отключить перенос строк', + 'review.wrapAria': 'Перенос строки', + 'review.openFile': 'Открыть весь файл на боковой панели', + 'review.openFileAria': 'Откройте {name} на боковой панели.', + 'diff.loading': 'Чтение изменений…', + 'diff.missing': 'Содержимое изменений этого хода больше не доступно.', + 'diff.error': 'Не удалось прочитать изменения', + 'diff.binary': 'Бинарный файл; изменения не могут быть показаны', + 'diff.oversized': 'Файл слишком большой; изменения не могут быть показаны', + 'diff.created': 'Создано в этом ходу', + 'diff.deleted': 'Удален на этом ходу', + 'diff.unchanged': 'Обе стороны держат одни и те же линии', + 'diff.coarse': 'Время сравнения строк истекло; отображается как замена всего файла', + 'diff.truncated': 'Показаны первые строки {count}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'presented.nativeUnavailable': 'Este arquivo não possui caminho de host disponível. Visualize-o na barra lateral.', + 'presented.revealError': 'Não foi possível mostrar no gerenciador de arquivos. Tente novamente.', + 'presented.directoryError': 'Não foi possível abrir a pasta que contém. Tente novamente.', + 'presented.directoryOpening': 'Abrindo a pasta que contém…', + 'presented.directoryOpened': 'Abertura solicitada contendo pasta', + 'presented.revealed': 'Exibição solicitada no gerenciador de arquivos', + 'presented.revealing': 'Mostrando no gerenciador de arquivos…', + 'presented.unavailable': 'Este Host não possui área de trabalho disponível para abrir arquivos ou pastas em aplicativos externos. Os arquivos ainda podem ser visualizados na barra lateral.', + 'presented.retry': 'Tentar novamente', + 'presented.hostError': 'Não foi possível ler as informações da área de trabalho do Host', + 'presented.preview': 'Pré-visualização na barra lateral', + 'presented.previewButton': 'Abra {name} na barra lateral', + 'presented.previewCard': 'Visualizar {name} na barra lateral', + 'presented.all': 'Todos os {count} arquivos', + 'presented.expandAria': 'Mostrar todos os {count} arquivos entregues', + 'presented.collapse': 'Recolher', + 'presented.collapseAria': 'Recolher arquivos entregues', + 'presented.opening': 'Abrindo…', + 'presented.opened': 'Aberto solicitado', + 'presented.error': 'Não foi possível abrir. Clique para tentar novamente.', + 'presented.file': 'Arquivo', + 'row.title': 'Apresentar arquivos', + 'row.running': 'Entregando', + 'row.preparing': 'Preparando resultados', + 'row.ok': 'Entregue', + 'row.error': 'Falha na entrega', + 'row.stopped': 'Interrompido', + 'row.inspect': 'Inspecionar chamada', + 'changes.title': '{count} arquivos editados', + 'changes.singleTitle': 'Editado {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binário', + 'changes.openReview': 'Revise as mudanças deste turno na barra lateral', + 'changes.all': 'Todos os {count} arquivos', + 'changes.expandAria': 'Mostrar todos os {count} arquivos alterados', + 'changes.collapse': 'Recolher', + 'changes.collapseAria': 'Recolher arquivos alterados', + 'changes.oversized': 'muito grande', + 'changes.viewDiff': 'Ver alterações em {name}', + 'review.title': 'Revisão · turno {turn}', + 'review.selectFile': 'Escolha o arquivo para revisar', + 'review.split': 'Mudar para visualização dividida', + 'review.unified': 'Mudar para visualização unificada', + 'review.splitAria': 'Visualização dividida', + 'review.wrap': 'Ativar quebra de linha', + 'review.nowrap': 'Desativar quebra de linha', + 'review.wrapAria': 'Quebra de linha', + 'review.openFile': 'Abra o arquivo inteiro na barra lateral', + 'review.openFileAria': 'Abra {name} na barra lateral', + 'diff.loading': 'A leitura muda…', + 'diff.missing': 'O conteúdo das alterações deste turno não está mais disponível', + 'diff.error': 'Não foi possível ler as alterações', + 'diff.binary': 'Arquivo binário; as alterações não podem ser mostradas', + 'diff.oversized': 'Arquivo muito grande; as alterações não podem ser mostradas', + 'diff.created': 'Criado neste turno', + 'diff.deleted': 'Excluído neste turno', + 'diff.unchanged': 'Ambos os lados mantêm as mesmas linhas', + 'diff.coarse': 'A comparação de linha expirou; mostrado como uma substituição de arquivo inteiro', + 'diff.truncated': 'Mostrando as primeiras {count} linhas', +} satisfies Record diff --git a/packages/client/ui-directory-picker-browse/locale/de.json b/packages/client/ui-directory-picker-browse/locale/de.json new file mode 100644 index 0000000000..0ae1c16fad --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur In-App-Verzeichnisauswahl", + "description": "Durchsuchen, erstellen und wählen Sie ein Arbeitsbereichsverzeichnis innerhalb der App aus." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/es.json b/packages/client/ui-directory-picker-browse/locale/es.json new file mode 100644 index 0000000000..1ae27d3528 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de directorio en la aplicación", + "description": "Busque, cree y seleccione un directorio de espacio de trabajo dentro de la aplicación." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/fr.json b/packages/client/ui-directory-picker-browse/locale/fr.json new file mode 100644 index 0000000000..c68b6dfdcd --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de répertoire dans l'application", + "description": "Parcourez, créez et sélectionnez un répertoire d'espace de travail dans l'application." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/pt.json b/packages/client/ui-directory-picker-browse/locale/pt.json new file mode 100644 index 0000000000..236a105e59 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de diretório no aplicativo", + "description": "Navegue, crie e selecione um diretório de espaço de trabalho no aplicativo." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/ru.json b/packages/client/ui-directory-picker-browse/locale/ru.json new file mode 100644 index 0000000000..5c2a88cff4 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора каталога в приложении", + "description": "Просмотрите, создайте и выберите каталог рабочей области в приложении." + } +} diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index 63526ccec8..745b17dd9e 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-browse/src/client/index.ts b/packages/client/ui-directory-picker-browse/src/client/index.ts index bc78900e0b..d3cff16b31 100644 --- a/packages/client/ui-directory-picker-browse/src/client/index.ts +++ b/packages/client/ui-directory-picker-browse/src/client/index.ts @@ -80,6 +80,81 @@ export function apply(ctx: ClientContext): void { 'browser.truncated': 'フォルダが多すぎて一覧表示できません。先頭のみ表示しています。', 'browser.showHidden': '隠しファイルを表示', }], + ['es', { + 'browser.title': 'Seleccionar carpeta de trabajo', + 'browser.home': 'Inicio', + 'browser.newFolder': 'Nueva carpeta', + 'browser.folderName': 'Nombre de la carpeta', + 'browser.createIn': 'Nueva carpeta en "{name}"', + 'browser.untitledFolder': 'Carpeta sin título', + 'browser.create': 'Crear', + 'browser.cancel': 'Cancelar', + 'browser.open': 'Abrir', + 'browser.editPath': 'Editar ruta', + 'browser.loading': 'Cargando…', + 'browser.truncated': 'Hay demasiadas carpetas; solo se muestra el principio.', + 'browser.showHidden': 'Mostrar archivos ocultos', + }], + ['fr', { + 'browser.title': 'Choisir le dossier de travail', + 'browser.home': 'Dossier personnel', + 'browser.newFolder': 'Nouveau dossier', + 'browser.folderName': 'Nom du dossier', + 'browser.createIn': 'Nouveau dossier dans « {name} »', + 'browser.untitledFolder': 'Dossier sans titre', + 'browser.create': 'Créer', + 'browser.cancel': 'Annuler', + 'browser.open': 'Ouvrir', + 'browser.editPath': 'Modifier le chemin', + 'browser.loading': 'Chargement…', + 'browser.truncated': 'Trop de dossiers à afficher ; seul le début est affiché.', + 'browser.showHidden': 'Afficher les fichiers cachés', + }], + ['de', { + 'browser.title': 'Arbeitsverzeichnis auswählen', + 'browser.home': 'Persönlicher Ordner', + 'browser.newFolder': 'Neuer Ordner', + 'browser.folderName': 'Ordnername', + 'browser.createIn': 'Neuer Ordner in „{name}“', + 'browser.untitledFolder': 'Unbenannter Ordner', + 'browser.create': 'Erstellen', + 'browser.cancel': 'Abbrechen', + 'browser.open': 'Öffnen', + 'browser.editPath': 'Pfad bearbeiten', + 'browser.loading': 'Laden…', + 'browser.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.', + 'browser.showHidden': 'Versteckte Dateien anzeigen', + }], + ['ru', { + 'browser.title': 'Выбрать рабочую папку', + 'browser.home': 'Домашняя папка', + 'browser.newFolder': 'Новая папка', + 'browser.folderName': 'Имя папки', + 'browser.createIn': 'Новая папка в «{name}»', + 'browser.untitledFolder': 'Папка без названия', + 'browser.create': 'Создать', + 'browser.cancel': 'Отмена', + 'browser.open': 'Открыть', + 'browser.editPath': 'Изменить путь', + 'browser.loading': 'Загрузка…', + 'browser.truncated': 'Папок слишком много; показана только начальная часть списка.', + 'browser.showHidden': 'Показать скрытые файлы', + }], + ['pt', { + 'browser.title': 'Selecionar pasta de trabalho', + 'browser.home': 'Pasta pessoal', + 'browser.newFolder': 'Nova pasta', + 'browser.folderName': 'Nome da pasta', + 'browser.createIn': 'Nova pasta em "{name}"', + 'browser.untitledFolder': 'Pasta sem nome', + 'browser.create': 'Criar', + 'browser.cancel': 'Cancelar', + 'browser.open': 'Abrir', + 'browser.editPath': 'Editar caminho', + 'browser.loading': 'Carregando…', + 'browser.truncated': 'Há pastas demais; apenas o início é exibido.', + 'browser.showHidden': 'Mostrar arquivos ocultos', + }], ] try { for (const [locale, dict] of dictionaries) disposers.push(ctx.locale.register(LOCALE_NS, locale, dict)) diff --git a/packages/client/ui-directory-picker-native/locale/de.json b/packages/client/ui-directory-picker-native/locale/de.json new file mode 100644 index 0000000000..d0bd0ff6aa --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Auswahl des Systemverzeichnisses", + "description": "Verbinden Sie das Arbeitsbereich-Auswahlportal und das Systemverzeichnis-Auswahldialogfeld." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/es.json b/packages/client/ui-directory-picker-native/locale/es.json new file mode 100644 index 0000000000..67dfd34b96 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección del directorio del sistema", + "description": "Conecte el portal de selección del espacio de trabajo y el cuadro de diálogo de selección del directorio del sistema." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/fr.json b/packages/client/ui-directory-picker-native/locale/fr.json new file mode 100644 index 0000000000..87bff91413 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection du répertoire système", + "description": "Connectez le portail de sélection de l'espace de travail et la boîte de dialogue de sélection du répertoire système." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/pt.json b/packages/client/ui-directory-picker-native/locale/pt.json new file mode 100644 index 0000000000..908a8e4105 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de diretório do sistema", + "description": "Conecte o portal de seleção de espaço de trabalho e a caixa de diálogo de seleção de diretório do sistema." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/ru.json b/packages/client/ui-directory-picker-native/locale/ru.json new file mode 100644 index 0000000000..e57e07e0d0 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора системного каталога", + "description": "Соедините портал выбора рабочего пространства и диалоговое окно выбора системного каталога." + } +} diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index ff25c9ae17..e9d140b9d3 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the local Desktop or Host OS chooser", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/README.i18n.yaml b/packages/client/ui-dockkit/README.i18n.yaml index 2f06976029..f631fbecc2 100644 --- a/packages/client/ui-dockkit/README.i18n.yaml +++ b/packages/client/ui-dockkit/README.i18n.yaml @@ -15,8 +15,8 @@ en: 42038fdc402b9c2a zh: 99890f8f908624ce /deepseek-ai-dsh-client-ui-dockkit/the-two-layers: - en: 99356ea8e3d1e7aa - zh: 870f746730d0e6cf + en: 718f52cfce426009 + zh: e8282e2e3713d242 /deepseek-ai-dsh-client-ui-dockkit/embedding-it: en: c4ba08038bfa6f30 zh: 95db30618b52babe diff --git a/packages/client/ui-dockkit/README.md b/packages/client/ui-dockkit/README.md index 0ba54c2c8e..976148bde2 100644 --- a/packages/client/ui-dockkit/README.md +++ b/packages/client/ui-dockkit/README.md @@ -37,10 +37,12 @@ A docking layout kit: a split tree of tabbed panes with invertible operations, a - `planSettle` is the opt-in rule that keeps every docked pane populated after an intent: panes an intent emptied are merged away, and an emptied root pane is reseeded through the embedder's factory — withholding the factory keeps the merge and leaves the root pane empty. An embedder that wants empty panes simply does not call it, or calls it without a factory. `planDropTab` takes the same factory: with one, a sole tab released on its own pane's edge splits and the factory's tab backfills the pane it vacates (the dragged tab stays focused); without one that release changes nothing. - `DockController` is the intent layer and an observable source (`subscribe` + `getSnapshot`, whose reference only changes when the layout does). -**The components** render a layout snapshot and report settled intents — one per gesture, never a drag frame. A drag previews in local state while the gesture's own facts stay in its closure; on release the net result leaves through one `DockIntents` call — a strip release reports the caret slot as drawn, the dragged chip counted, and `planPlaceTab` turns that into the reorder or the move. That is what lets an embedder record exactly one history entry per gesture. The strip follows the WAI-ARIA tabs pattern with manual activation: the selected chip is in the tab order; Left and Right (wrapping), Home, and End move focus between chips without selecting; Enter or Space selects the focused chip through the same intent as a click. A chip is a capsule carrying one control, its close; the context menu (a secondary press on the chip) carries the same close plus the embedder's items — a menu that would hold no item at all never shows — and renders in a portal positioned against the chip because the chip box clips its overflow on purpose (see below). After the chips sits the add control, which asks the embedder (`DockIntents.addTab`) to seat its seeded tab; the embedder's `canAddTab(paneId)` decides per pane whether the control is drawn at all. Copying a tab has no kit control — it is the embedder's API — and floating is the drag released clear of the surface. +**The components** render a layout snapshot and report settled intents — one per gesture, never a drag frame. A drag previews in local state while the gesture's own facts stay in its closure; on release the net result leaves through one `DockIntents` call — a strip release reports a placement only when a caret is drawn, the dragged chip counted, and `planPlaceTab` turns that into the reorder or the move. That is what lets an embedder record exactly one history entry per gesture. The strip follows the WAI-ARIA tabs pattern with manual activation: the selected chip is in the tab order; Left and Right (wrapping), Home, and End move focus between chips without selecting; Enter or Space selects the focused chip through the same intent as a click. A chip is a capsule carrying one control, its close; the context menu (a secondary press on the chip) carries the same close plus the embedder's items — a menu that would hold no item at all never shows — and renders in a portal positioned against the chip because the chip box clips its overflow on purpose (see below). After the chips sits the add control, which asks the embedder (`DockIntents.addTab`) to seat its seeded tab; the embedder's `canAddTab(paneId)` decides per pane whether the control is drawn at all. Copying a tab has no kit control — it is the embedder's API — and floating is the drag released clear of the surface. Middle-clicking anywhere on a closable tab chip, including its close control, closes it through the same intent as its close button, without selecting it first. The chip owns the middle click and suppresses nested title content’s default action. Closing a background tab preserves the selected tab. Only the primary button starts tab dragging; a middle click while another mouse button is held does not close the tab. +Strip slots that preserve the dragged tab’s position show no insertion caret and report no placement; the tab can still be dragged to split a pane or float. + Tab context menus show the close action name without a shortcut hint: they act on the clicked tab, while shortcuts can target another focused pane. An unmodified Escape dismisses the foreground tab menu without closing the tab and returns focus from a menu item to its tab without drawing a focus outline. Tab and arrow navigation retain the tab’s visible focus indicator. Composition and held-key repeats do not dismiss it. diff --git a/packages/client/ui-dockkit/README.zh.md b/packages/client/ui-dockkit/README.zh.md index 39acc02792..85ad53eb45 100644 --- a/packages/client/ui-dockkit/README.zh.md +++ b/packages/client/ui-dockkit/README.zh.md @@ -37,10 +37,12 @@ kind: "package-reference" - `planSettle` 是可选加入的规则,保证意图之后每个停靠格都有内容:被意图清空的格会被并掉,被清空的根格通过嵌入方的工厂重新播种——不传工厂则只并格、让根格保持为空。想要空格的嵌入方只需不调用它,或不带工厂调用。`planDropTab` 接受同一工厂:带工厂时,唯一 tab 放到本格边缘会分栏,工厂的 tab 回填它腾出的格(被拖的 tab 保持聚焦);不带工厂时这种释放不改变任何东西。 - `DockController` 是意图层,也是一个可观察源(`subscribe` + `getSnapshot`,其引用只在布局变化时才变)。 -**组件**渲染布局快照并上报已落定的意图——每次手势一条,绝不上报拖动帧。拖动过程中在本地状态里预览,手势自身的事实留在它的闭包里;松手时净结果通过一次 `DockIntents` 调用离开——在标签条上松手上报的是按绘制顺序数出的插入槽位(被拖的 chip 也计入),由 `planPlaceTab` 换算成重排或移动。正是这一点让嵌入方能为每次手势记录恰好一条历史。标签条遵循 WAI-ARIA tabs 模式的手动激活:选中的 chip 在 Tab 键序里;左右方向键(循环)、Home、End 只在 chip 之间移动焦点而不选中;Enter 或空格选中当前聚焦的 chip,走与点击相同的意图。chip 是一个胶囊,携带唯一的控件——它的关闭按钮;上下文菜单(在 chip 上的次键按下)携带同样的关闭项加上嵌入方的条目——一个连一项都没有的菜单绝不展示——并渲染在按 chip 定位的 portal 里,因为 chip 盒会故意裁掉溢出(见下文)。chip 之后是添加控件,它请嵌入方(`DockIntents.addTab`)安放其种子 tab;嵌入方的 `canAddTab(paneId)` 按格决定是否绘制该控件。复制 tab 没有套件控件——那是嵌入方的 API——而浮出就是把拖动松手在停靠区之外。 +**组件**渲染布局快照并上报已落定的意图——每次手势一条,绝不上报拖动帧。拖动过程中在本地状态里预览,手势自身的事实留在它的闭包里;松手时净结果通过一次 `DockIntents` 调用离开——在标签条上松手时,仅当定位条可见才上报插入槽位(被拖的 chip 也计入),由 `planPlaceTab` 换算成重排或移动。正是这一点让嵌入方能为每次手势记录恰好一条历史。标签条遵循 WAI-ARIA tabs 模式的手动激活:选中的 chip 在 Tab 键序里;左右方向键(循环)、Home、End 只在 chip 之间移动焦点而不选中;Enter 或空格选中当前聚焦的 chip,走与点击相同的意图。chip 是一个胶囊,携带唯一的控件——它的关闭按钮;上下文菜单(在 chip 上的次键按下)携带同样的关闭项加上嵌入方的条目——一个连一项都没有的菜单绝不展示——并渲染在按 chip 定位的 portal 里,因为 chip 盒会故意裁掉溢出(见下文)。chip 之后是添加控件,它请嵌入方(`DockIntents.addTab`)安放其种子 tab;嵌入方的 `canAddTab(paneId)` 按格决定是否绘制该控件。复制 tab 没有套件控件——那是嵌入方的 API——而浮出就是把拖动松手在停靠区之外。 中键点击可关闭标签页 chip 的任意位置(包括关闭控件)时,通过与关闭按钮相同的意图关闭,无需先选中。chip 接管中键点击,并阻止嵌套标题内容的默认动作。关闭后台标签页会保留当前选中的标签页。只有主键会开始拖动标签页;其它鼠标按键仍按住时,中键点击不会关闭标签页。 +不改变所拖标签位置的标签条落点不显示插入定位条,也不上报放置操作;标签仍可拖出分栏或浮动。 + 标签上下文菜单只显示关闭操作名,不显示快捷键:菜单操作针对被点击的标签,快捷键则可能作用于其他焦点分栏。无修饰键的 Esc 关闭前台标签菜单而不关闭标签,并将菜单项的焦点交还标签,不绘制焦点外轮廓线。Tab 和方向键导航保留标签页的可见焦点提示。输入法组合输入和长按重复事件不关闭菜单。 diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index 52a4921602..c3c999c314 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/src/components/DockSurface.tsx b/packages/client/ui-dockkit/src/components/DockSurface.tsx index eea2ea1724..846a4b75e8 100644 --- a/packages/client/ui-dockkit/src/components/DockSurface.tsx +++ b/packages/client/ui-dockkit/src/components/DockSurface.tsx @@ -5,8 +5,8 @@ * * A gesture only previews until it ends, then leaves through one intent, so the * embedder's operation sequence stays the single source of truth. Releasing a tab - * clear of this surface floats it; releasing inside it but on no pane is not a - * move at all. + * clear of this surface floats it; releasing inside it without an effective + * drop target changes nothing. */ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' @@ -110,12 +110,15 @@ const ALWAYS = (): boolean => true /** * Resolve where a pointer sits inside the docked surface. An edge zone is only * offered where the split it would make is allowed: within the pane budget and - * with room for two halves; otherwise the release is not a move at all. + * with room for two halves. Strip slots that preserve the dragged tab's position + * offer no caret or placement intent. An undefined target inside the surface + * leaves the layout unchanged on release. */ function hitTest( root: HTMLElement, x: number, y: number, + tabId: TabId, canSplit: boolean, fits: ReadonlyMap, dropZones: 'edges' | 'horizontal', @@ -126,7 +129,10 @@ function hitTest( const strip = pane.querySelector('[data-dockkit-strip]') if (strip !== null && containsPoint(strip.getBoundingClientRect(), x, y)) { const tabs = [...strip.querySelectorAll('[data-dockkit-tab]')] - return { kind: 'strip', paneId, index: insertionIndex(tabs.map(tab => tab.getBoundingClientRect()), x) } + const index = insertionIndex(tabs.map(tab => tab.getBoundingClientRect()), x) + const sourceIndex = tabs.findIndex(tab => tab.dataset.dockkitTab === tabId) + if (sourceIndex >= 0 && (index === sourceIndex || index === sourceIndex + 1)) return undefined + return { kind: 'strip', paneId, index } } const zone = dropZones === 'horizontal' ? canSplit && fitOf(fits, paneId).row @@ -229,12 +235,12 @@ function Surface({ setPreview({ ...NO_PREVIEW, draggingTabId: tabId, - dropTarget: hitTest(root, moved.clientX, moved.clientY, canSplit, fits, dropZones), + dropTarget: hitTest(root, moved.clientX, moved.clientY, tabId, canSplit, fits, dropZones), }) }, up: (released) => { if (!dragging) return - const target = hitTest(root, released.clientX, released.clientY, canSplit, fits, dropZones) + const target = hitTest(root, released.clientX, released.clientY, tabId, canSplit, fits, dropZones) if (target === undefined) { if (containsPoint(root.getBoundingClientRect(), released.clientX, released.clientY)) return intents.floatTab(tabId, floatRectAt(released.clientX, released.clientY, FLOAT_DEFAULT_SIZE)) diff --git a/packages/client/ui-dockkit/tests/components.client.spec.tsx b/packages/client/ui-dockkit/tests/components.client.spec.tsx index 85640ed768..9a44d625f4 100644 --- a/packages/client/ui-dockkit/tests/components.client.spec.tsx +++ b/packages/client/ui-dockkit/tests/components.client.spec.tsx @@ -999,21 +999,42 @@ describe('tab drags', () => { expect(document.querySelector('[data-dockkit-caret]')).toBeNull() }) - it('previews a caret at the slot under the pointer and reports that slot on release', () => { + it('previews only strip slots that change the tab order', () => { const { intents, first, fileTabId, chip } = twoPanes() // Left of the first chip's midpoint: slot 0. drag(chip(fileTabId), FILE_CHIP, [30, 18], false) expect(document.querySelector('[data-dockkit-caret]')?.getAttribute('data-dockkit-caret')).toBe('0') - // Past both chips: the slot after the last one, drawn at the strip's end. fireEvent.pointerMove(window, { pointerId: 7, clientX: 300, clientY: 18 }) - expect(document.querySelector('[data-dockkit-caret]')?.getAttribute('data-dockkit-caret')).toBe('2') - fireEvent.pointerUp(window, { pointerId: 7, clientX: 300, clientY: 18 }) + expect(document.querySelector('[data-dockkit-caret]')).toBeNull() + fireEvent.pointerMove(window, { pointerId: 7, clientX: 120, clientY: 18 }) + expect(document.querySelector('[data-dockkit-caret]')).toBeNull() + fireEvent.pointerMove(window, { pointerId: 7, clientX: 30, clientY: 18 }) + fireEvent.pointerUp(window, { pointerId: 7, clientX: 30, clientY: 18 }) // The slot counts the dragged chip itself; the planner turns it into a reorder. - expect(intents.placeTab).toHaveBeenCalledWith(fileTabId, first, 2) + expect(intents.placeTab).toHaveBeenCalledWith(fileTabId, first, 0) expect(gestureIntents(intents)).toEqual(['placeTab']) expect(document.querySelector('[data-dockkit-caret]')).toBeNull() }) + it.each([30, 300])('does not offer a strip placement for the lone Start tab at x=%s', (x) => { + const controller = seededController() + controller.setExpanded(true) + const panes = dockPaneIds(controller.getSnapshot().state) + layOut(panes, 520) + const intents = spyIntents() + renderSurface(controller, intents) + const chip = screen.getByRole('tab') + drag(chip, [51, 18], [x, 18], false) + expect(document.querySelector('[data-dockkit-caret]')).toBeNull() + fireEvent.pointerUp(window, { pointerId: 7, clientX: x, clientY: 18 }) + expect(gestureIntents(intents)).toEqual([]) + + drag(chip, [51, 18], [260, 590], false) + expect(document.querySelector('[data-dockkit-drop-active]')?.getAttribute('data-dockkit-dock-zone')).toBe('bottom') + fireEvent.pointerUp(window, { pointerId: 7, clientX: 260, clientY: 590 }) + expect(intents.dropTab).toHaveBeenCalledExactlyOnceWith(chip.getAttribute('data-dockkit-tab'), panes[0], 'bottom') + }) + it('reports a slot in another pane\'s strip as a placement there', () => { const { intents, second, fileTabId, chip } = twoPanes() drag(chip(fileTabId), FILE_CHIP, [420 + 30, 18]) diff --git a/packages/client/ui-entry-customization/README.i18n.yaml b/packages/client/ui-entry-customization/README.i18n.yaml index 021b70ca20..acd7ff8b16 100644 --- a/packages/client/ui-entry-customization/README.i18n.yaml +++ b/packages/client/ui-entry-customization/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-entry-customization/README.md /: - en: 200adf0dd8c9757c - zh: 3d10d49a71289702 + en: 57416280d387ea2b + zh: f9b97e610116bf59 /deepseek-ai-dsh-client-ui-entry-customization: en: ab35194f4a9dafce zh: 7e9e57fde518c3d7 /deepseek-ai-dsh-client-ui-entry-customization/summary: - en: d53e7d71da9fd4d6 - zh: a32b635ec1e572f3 + en: 589199344fe3dd70 + zh: e5e3003c5c4d9479 /deepseek-ai-dsh-client-ui-entry-customization/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-client-ui-entry-customization/use-this-package: - en: e642513bcfd42d68 - zh: 38b9109ec017900d + en: 795f14554e3ff1b0 + zh: ed6d8702c9cfa4e7 /deepseek-ai-dsh-client-ui-entry-customization/understand-the-implementation: - en: 2f1a873bebc098c8 - zh: c2ec33e50ff2ccc0 + en: 65738fbe4d77e47a + zh: 201acc369942cab4 /deepseek-ai-dsh-client-ui-entry-customization/model-experience: en: b891f311c09d536f zh: 68c0f41fbfed0d73 @@ -27,8 +27,8 @@ en: dc50ebf94183efa2 zh: 617803c558367430 /deepseek-ai-dsh-client-ui-entry-customization/known-limitations-and-deferred-work: - en: 2e582c76ffe9d663 - zh: 7e5e49ad3d6f0f83 + en: 5b4914a3ef05fa9c + zh: 9cbe8a17a88ee4d6 /deepseek-ai-dsh-client-ui-entry-customization/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-entry-customization/README.md b/packages/client/ui-entry-customization/README.md index c05efa4379..148fd6862d 100644 --- a/packages/client/ui-entry-customization/README.md +++ b/packages/client/ui-entry-customization/README.md @@ -1,5 +1,5 @@ --- -description: "Fill the More menus of three navigation regions with pin controls, plugin browsing, and a Creator draft." +description: "Offer plugin browsing and a Creator draft in three navigation regions." kind: "package-reference" --- @@ -11,7 +11,7 @@ Desktop product events use the optional [product analytics service](../product-a ## Summary -Fill the More menus of the left sidebar, Conversation View tabs, and right-sidebar guide with each region's customizable entries, pin buttons, plugin browsing, and a reviewable Creator draft. The Web app loads it by default. Each region draws its own More button and stores its own pin choices; this package stores nothing. +Browse plugins and prepare a reviewable Creator draft from the More menus of the left sidebar, Conversation View tabs, and right-sidebar guide. The Web app loads this package by default. Each region draws its own More button; this package stores nothing. ## Table of Contents @@ -28,13 +28,13 @@ Fill the More menus of the left sidebar, Conversation View tabs, and right-sideb The `@deepseek-ai/dsh-web-app` bundle loads this package by default; it needs no Plugins switch or profile selection. A region shows its More button only while its actions slot has a registration, so unloading this package removes all three buttons. -Open **More** beneath the left navigation entries, **More conversation views** after the Conversation View tabs, or **More tools** after the right-sidebar guide cards. Each menu lists the region's customizable entries in registration order; Plugins, Chat, and Workspace files always show and are not listed. Selecting an entry's name closes the menu and opens the entry without changing its pin state. The separate pin button pins or unpins the region's launcher and keeps the menu open; a filled pin marks a pinned entry. Unpinned entries stay reachable through the menu, and pin choices persist across reloads. Long entry names wrap, except in the More tools menu, where they truncate to one line. +Open **More** beneath the left navigation entries, **More conversation views** after the Conversation View tabs, or **More tools** after the right-sidebar guide cards. Left-sidebar and guide menus offer only **Browse other plugins** (when available) and **Create your plugin**. The Conversation menu also lists folded Views before those actions and marks the active View with a check on the right. None of these menus provide pin controls or sorting. -**Browse other plugins** opens the Plugins main list without opening an installation dialog. **Create your plugin** opens a new or reusable blank Session with the Creator preset and adds a short localized request for a plugin in that region to its draft: an empty draft takes the request as its text, and an existing draft keeps its text, references, and attachments with the request appended as a new paragraph. It never sends a message. Review and send the draft yourself. Both actions remain available when a region has no customizable entries; an empty list adds no separator. +**Browse other plugins** opens the Plugins main list without opening an installation dialog. **Create your plugin** opens a new or reusable blank Session with the Creator preset and adds a short localized request for a plugin in that region to its draft: an empty draft takes the request as its text, and an existing draft keeps its text, references, and attachments with the request appended as a new paragraph. It never sends a message. Review and send the draft yourself. Creation remains available regardless of the region’s entries; a separator appears only between folded Views and the actions, before browsing when available or before creation otherwise. -On Desktop, **Browse other plugins** and **Create your plugin** report `more_menu_item_click` before they act, with `action=browse_plugins` or `action=create_plugin` and the menu's `region`: `sidebar` for **More**, `conversation` for **More conversation views**, and `rightbar` for **More tools**. Opening an entry and changing a pin report nothing, and the menus report no exposure events. Without the analytics service, both actions behave the same and report nothing. +On Desktop, **Browse other plugins** and **Create your plugin** report `more_menu_item_click` before they act, with `action=browse_plugins` or `action=create_plugin` and the menu's `region`: `sidebar` for **More**, `conversation` for **More conversation views**, and `rightbar` for **More tools**. The menus report no exposure events. Without the analytics service, both actions behave the same and report nothing. -The browse-plugins row appears only while the `pluginNavigation` service is present; the plugin starts without it. +The browse-plugins row appears only while the `pluginNavigation` service is present; the plugin starts without it. Its absence removes only browsing, not More buttons, folded Views, or creation. ----- @@ -44,7 +44,7 @@ The browse-plugins row appears only while the `pluginNavigation` service is pres
Implementation internals — click to expand -[`apply`](src/client/index.ts) registers [`EntryMenuContent`](src/client/EntryMenuContent.tsx) under id `entry-customization` into `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` through `ctx.slots.inject`, so each registration follows its region's declaration lifetime. The regions render these slots inside their own `Menu` and supply the entries, their pin state, `onOpen`, `onPinnedChange`, and `onDismiss` as owner props. The rows call `onDismiss` before opening an entry or running a footer action; pin changes leave the menu open. +[`apply`](src/client/index.ts) registers [`EntryMenuContent`](src/client/EntryMenuContent.tsx) under id `entry-customization` into `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` through `ctx.slots.inject`, so each registration follows its region's declaration lifetime. The regions render these slots inside their own `Menu`; the rows call `onDismiss` before browsing or creating. Only the Conversation menu consumes its owner’s folded entry list and navigation callback; pin callbacks remain unused. Each registration's `inject` supplies its region and two callbacks. Each callback first calls `ctx.get('productAnalytics')?.track('more_menu_item_click', { region, action })`. Browsing then calls `ctx.get('pluginNavigation')?.openList()`; ui-plugin-manager provides that service only while the Plugins page has access, so this package reads it optionally and never lists it in `inject`. Creation then calls `uiWorkspace.startSession(undefined, { agentPreset: 'cordis', prompt, whenDraftExists: 'append' })` with the region's localized prompt; it does not go through `ui-agent-preset`, so the preset chip plays no introduction. The [`dsh-web-app` bundle patch](../../bundle/web-app/cordis.patch.yml) mounts the browser plugin. @@ -65,12 +65,12 @@ None; the package neither assembles nor sends model requests. -- If this package is disabled or fails to load after entries were unpinned, those launchers stay hidden and no More menu can restore them. Re-enable the package or clear the region's `dsh.slot.manual-order.*.v1` localStorage key. -- Pin choices live in browser localStorage, which is isolated by protocol, host, and port: changing the Web port or switching between Web and Desktop does not carry them over, and they do not sync across devices. +- More menus provide no visibility controls. Region pin callbacks remain available to other registrations and change only in-memory choices; plugin reload restores defaults. +- Legacy pin values in localStorage are neither read nor written. - **Create your plugin** does not check whether the Creator preset exists. Without it, Session creation fails, the Workspace shows its creation-failure notice, and no draft is prepared. Draft preparation also requires a resolvable Workspace; without one, the existing flow reports that a Workspace is required. -- Entries keep registration order; the menus offer no reordering, and entries cannot move between regions. +- Conversation owns the estimated tab split; these menus provide no sorting. - Conversation controls follow the existing nonblank-Session header and Coding Tools eligibility. Right-sidebar controls require a selected Session and appear on its guide page. -- Only active registered launchers appear; the menus are not an inventory of disabled or uninstalled plugins. +- The menus do not list plugins; use the browsing action to open the Plugins page. ### Dev Note diff --git a/packages/client/ui-entry-customization/README.zh.md b/packages/client/ui-entry-customization/README.zh.md index daa91719b3..4bb7cf5379 100644 --- a/packages/client/ui-entry-customization/README.zh.md +++ b/packages/client/ui-entry-customization/README.zh.md @@ -1,5 +1,5 @@ --- -description: "为三个导航区域的“更多”菜单提供 Pin 控件、插件浏览与创造模式草稿。" +description: "为三个导航区域的更多菜单提供插件浏览与创造模式草稿。" kind: "package-reference" --- @@ -11,7 +11,7 @@ kind: "package-reference" ## 概述 -为左侧栏、会话 View 标签和右侧栏起始页的“更多”菜单提供内容:各区域可自定义的入口、Pin 按钮、插件浏览与可审阅的创造模式草稿。Web 应用默认加载本包。各区域自己画“更多”按钮并保存自己的 Pin 状态;本包不保存状态。 +为左侧栏、会话 View 标签和右侧栏起始页的“更多”菜单提供插件浏览与可审阅的创造模式草稿。Web 应用默认加载本包。各区域自己画“更多”按钮;本包不保存状态。 ## 目录 @@ -28,13 +28,13 @@ kind: "package-reference" `@deepseek-ai/dsh-web-app` 组合包默认加载本包,无需在插件页开启或由 profile 选择。区域只在其 actions slot 有注册项时显示“更多”按钮,因此卸载本包会移除三个按钮。 -打开左侧导航入口下方的**更多**、会话 View 标签之后的**更多会话视图**,或右侧栏起始页卡片之后的**更多工具**。每个菜单按注册顺序列出该区域可自定义的入口;插件管理、Chat 和工作区文件始终显示,不列入菜单。点击入口名称会关闭菜单并打开该入口,不改变 Pin 状态。独立的 Pin 按钮在区域中固定或收起该入口,并保持菜单打开;实心 Pin 表示已固定。已收起的入口仍可从菜单打开,Pin 状态在刷新后保留。过长的入口名称会换行,更多工具菜单中则截断为单行。 +打开左侧导航入口下方的**更多**、会话 View 标签之后的**更多会话视图**,或右侧栏起始页卡片之后的**更多工具**。左侧栏和起始页菜单仅提供**浏览更多插件**(可用时)与**创建你的插件**。会话菜单在这些操作之前列出收起的 View,并在当前 View 右侧显示勾选标记。所有菜单都不提供 Pin 或排序操作。 -**浏览更多插件**进入插件页主列表,不打开安装弹窗。**创建你的插件**用 Creator 预设打开新 Session 或可复用的空白 Session,并把对应区域的简短本地化插件创建请求加入草稿:空草稿直接以该请求为文字,已有草稿保留其文字、引用和附件,请求另起一段追加在后面。它绝不发送消息。请自行审阅并发送草稿。当前区域没有可自定义入口时,两项操作仍然可用;空列表不添加分隔线。 +**浏览更多插件**进入插件页主列表,不打开安装弹窗。**创建你的插件**用 Creator 预设打开新 Session 或可复用的空白 Session,并把对应区域的简短本地化插件创建请求加入草稿:空草稿直接以该请求为文字,已有草稿保留其文字、引用和附件,请求另起一段追加在后面。它绝不发送消息。请自行审阅并发送草稿。无论区域是否有入口,创建操作始终可用;分隔线仅显示在收起的 View 与操作之间,浏览可用时位于浏览之前,否则位于创建之前。 -在桌面端,**浏览更多插件**与**创建你的插件**在执行前上报 `more_menu_item_click`,`action` 为 `browse_plugins` 或 `create_plugin`,`region` 为菜单所在区域:**更多**为 `sidebar`,**更多会话视图**为 `conversation`,**更多工具**为 `rightbar`。打开入口和更改 Pin 状态不上报,菜单也不上报曝光事件。没有埋点服务时,两项操作行为不变且不上报。 +在桌面端,**浏览更多插件**与**创建你的插件**在执行前上报 `more_menu_item_click`,`action` 为 `browse_plugins` 或 `create_plugin`,`region` 为菜单所在区域:**更多**为 `sidebar`,**更多会话视图**为 `conversation`,**更多工具**为 `rightbar`。菜单不上报曝光事件。没有埋点服务时,两项操作行为不变且不上报。 -「浏览插件」一行只在 `pluginNavigation` 服务存在时出现;没有该服务时插件照常启动。 +「浏览插件」一行只在 `pluginNavigation` 服务存在时出现;没有该服务时插件照常启动。该服务缺失时仅移除浏览操作,不影响“更多”按钮、收起的 View 或创建操作。 ----- @@ -44,7 +44,7 @@ kind: "package-reference"
实现细节——点击展开 -[`apply`](src/client/index.ts) 通过 `ctx.slots.inject` 以 id `entry-customization` 把 [`EntryMenuContent`](src/client/EntryMenuContent.tsx) 注册到 `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions`,每个注册项跟随所在区域声明的生命周期。各区域在自己的 `Menu` 中渲染这些 slot,并以 owner props 提供入口、Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。菜单行先调用 `onDismiss`,再打开入口或执行底部操作;Pin 变化不关闭菜单。 +[`apply`](src/client/index.ts) 通过 `ctx.slots.inject` 以 id `entry-customization` 把 [`EntryMenuContent`](src/client/EntryMenuContent.tsx) 注册到 `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions`,每个注册项跟随所在区域声明的生命周期。各区域在自己的 `Menu` 中渲染这些 slot;菜单行先调用 `onDismiss`,再执行浏览或创建操作。仅会话菜单消费 owner 提供的收起入口列表及导航回调;Pin 回调不被消费。 每个注册项的 `inject` 提供所属区域与两个回调。每个回调先调用 `ctx.get('productAnalytics')?.track('more_menu_item_click', { region, action })`。浏览随后调用 `ctx.get('pluginNavigation')?.openList()`;ui-plugin-manager 只在插件页有权限时提供该服务,因此本包按可选方式读取,不把它写进 `inject`。创建随后按区域取本地化提示语,调用 `uiWorkspace.startSession(undefined, { agentPreset: 'cordis', prompt, whenDraftExists: 'append' })`;该调用不经过 `ui-agent-preset`,预设标签不播放介绍动画。[`dsh-web-app` bundle patch](../../bundle/web-app/cordis.patch.yml) 挂载浏览器插件。 @@ -65,12 +65,12 @@ kind: "package-reference" -- 用户收起入口后,若本包被停用或加载失败,被收起的入口保持隐藏,且没有“更多”菜单可以恢复。重新启用本包或清除对应区域的 `dsh.slot.manual-order.*.v1` localStorage 键即可恢复。 -- Pin 状态保存在浏览器 localStorage,按协议、主机和端口隔离:更换 Web 端口或在 Web 与 Desktop 之间切换时不共享,也不跨设备同步。 +- 更多菜单不提供入口显隐管理。其他注册项仍可调用区域 Pin 回调,仅修改内存中的选择;插件重新加载后恢复默认值。 +- 不读取或写入 localStorage 中的旧 Pin 值。 - **创建你的插件**不检查 Creator 预设是否存在。预设缺失时 Session 创建失败,Workspace 显示创建失败提示,不准备草稿。草稿准备还要求可确定目标 Workspace;没有目标时,现有流程提示需要 Workspace。 -- 入口保持注册顺序;菜单不提供排序,入口也不能跨区域移动。 +- 会话区域负责按估算值分组;菜单不提供排序。 - 会话控件遵循现有非空 Session 头部与代码工作工具的可用性规则。右侧栏控件要求已选择 Session,并显示在其起始页。 -- 仅显示已激活、已注册的入口;菜单不是已停用或未安装插件的清单。 +- 菜单不列出插件清单;使用浏览操作进入插件页。 ### 开发备注 diff --git a/packages/client/ui-entry-customization/locale/de.json b/packages/client/ui-entry-customization/locale/de.json new file mode 100644 index 0000000000..b00350d66a --- /dev/null +++ b/packages/client/ui-entry-customization/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anpassung der Benutzeroberfläche", + "description": "Passen Sie Plugin-Einträge in der linken Seitenleiste, den Konversationsansichten und der rechten Seitenleiste an und erstellen Sie schnell neue Plugins." + } +} diff --git a/packages/client/ui-entry-customization/locale/es.json b/packages/client/ui-entry-customization/locale/es.json new file mode 100644 index 0000000000..c91bc568a9 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personalización de la interfaz", + "description": "Personalice las entradas de complementos en la barra lateral izquierda, las vistas de conversación y la barra lateral derecha, y cree rápidamente nuevos complementos." + } +} diff --git a/packages/client/ui-entry-customization/locale/fr.json b/packages/client/ui-entry-customization/locale/fr.json new file mode 100644 index 0000000000..b5209dccd1 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personnalisation de l'interface", + "description": "Personnalisez les entrées de plugin dans la barre latérale gauche, les vues de conversation et la barre latérale droite, et créez rapidement de nouveaux plugins." + } +} diff --git a/packages/client/ui-entry-customization/locale/pt.json b/packages/client/ui-entry-customization/locale/pt.json new file mode 100644 index 0000000000..d0171b4721 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personalização da interface", + "description": "Personalize as entradas de plug-ins na barra lateral esquerda, nas visualizações de conversas e na barra lateral direita e crie novos plug-ins rapidamente." + } +} diff --git a/packages/client/ui-entry-customization/locale/ru.json b/packages/client/ui-entry-customization/locale/ru.json new file mode 100644 index 0000000000..52ba475bb5 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройка интерфейса", + "description": "Настраивайте записи плагинов на левой боковой панели, в представлениях бесед и на правой боковой панели и быстро создавайте новые плагины." + } +} diff --git a/packages/client/ui-entry-customization/package.json b/packages/client/ui-entry-customization/package.json index f98c8ed620..94cac01670 100644 --- a/packages/client/ui-entry-customization/package.json +++ b/packages/client/ui-entry-customization/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-entry-customization", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "description": "Customize plugin entries in the left sidebar, conversation views, and right sidebar, and quickly create new plugins.", "publishConfig": { "access": "public" }, "repository": { diff --git a/packages/client/ui-entry-customization/src/client/EntryMenu.module.css b/packages/client/ui-entry-customization/src/client/EntryMenu.module.css index 82ce8f3f88..d7d1c3a298 100644 --- a/packages/client/ui-entry-customization/src/client/EntryMenu.module.css +++ b/packages/client/ui-entry-customization/src/client/EntryMenu.module.css @@ -1,12 +1,3 @@ -.entryRow { position: relative; } -.entryName { display: flex; align-items: center; gap: 8px; padding-right: 28px; } +.entryName { display: flex; align-items: center; gap: 8px; } .entryTitle { min-width: 0; overflow-wrap: anywhere; white-space: normal; } -.singleLine .entryTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.pinAnchor { position: absolute; right: 6px; top: 0; bottom: 0; display: flex; align-items: center; } -.pinButton { width: 22px; height: 22px; padding: 0; color: var(--dsw-alias-label-tertiary); } - -@media (hover: hover) { - .pinButton:hover { color: var(--dsw-alias-label-primary); } - .pinAnchor[data-pinned='false'] { opacity: 0; } - .entryRow:hover .pinAnchor, .entryRow:focus-within .pinAnchor { opacity: 1; } -} +.activeMark { flex: none; margin-left: auto; } diff --git a/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx b/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx index 22a73babc1..5ab4fa91cf 100644 --- a/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx +++ b/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx @@ -1,17 +1,17 @@ -/** Rows of a navigation region's More menu: customizable entries, plugin discovery while reachable, and creation. */ +/** Folded Conversation Views, plugin discovery while reachable, and creation. */ import { - IconPluginPinwheelOutlineRegular, IconPlusOutlineRegular, MenuItemButton, + IconCheckOutlineRegular, IconPluginPinwheelOutlineRegular, IconPlusOutlineRegular, MenuItemButton, } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' -import { EntryPickerRow } from './EntryPickerRow.tsx' +import css from './EntryMenu.module.css' /** Navigation region whose More menu hosts one registration. */ export type EntryRegion = 'sidebar' | 'conversation' | 'rightbar' /** Region identity and footer actions injected into each registration. */ export interface EntryMenuInjected { - /** Region of this registration; selects pin copy and the Creator prompt. */ + /** Region of this registration; selects analytics attributes and the Creator prompt. */ readonly region: EntryRegion readonly hooks: { /** Whether the Plugins page navigation service is present; the discovery row shows only while it is. */ @@ -28,27 +28,28 @@ type EntryMenuContentProps = PropsRuntime<'sidebar.panellist.actions'> & PropsLocale<'entryCustomization'> & InjectFace /** - * Render the region's entries with pin actions, then the discovery and Creator rows. - * @param props - Owner entries and callbacks, injected region actions, and localized copy. + * Render folded Conversation Views followed by discovery and Creator actions, without pin controls. + * @param props - Owner dismissal callback, injected actions, and localized copy. * @returns Menu rows for the region's More menu. */ export function EntryMenuContent({ - entries, onOpen, onPinnedChange, onDismiss, region, usePluginNavigation, browsePlugins, createPlugin, t, + entries, onOpen, region, onDismiss, usePluginNavigation, browsePlugins, createPlugin, t, }: EntryMenuContentProps) { + const moreEntries = region === 'conversation' ? entries : [] const browsable = usePluginNavigation(present => present) return <> - {entries.map((entry) => { - const pinLabel = t(`${entry.pinned ? 'unpin' : 'pin'}.${region}`) - return { onDismiss(); onOpen(entry.id) }} - onPin={() => { onPinnedChange(entry.id, !entry.pinned) }} /> - })} - {browsable && } separatorBefore={entries.length > 0} + {moreEntries.map(entry => { onDismiss(); onOpen(entry.id) }}> + + {entry.label} + {entry.active === true && } + + )} + {browsable && 0} icon={} onSelect={() => { onDismiss(); browsePlugins() }}> {t('install')} } - } separatorBefore={!browsable && entries.length > 0} + } separatorBefore={!browsable && moreEntries.length > 0} onSelect={() => { onDismiss(); createPlugin() }}> {t('create')} diff --git a/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx b/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx deleted file mode 100644 index 4148de08f9..0000000000 --- a/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** A launcher row separates navigation from its independently focusable pin action. */ -import clsx from 'clsx' -import { - Button, IconPinFillRegular, IconPinOutlineRegular, MenuItemButton, Tooltip, -} from '@deepseek-ai/dsh-client-ui-primitives' -import css from './EntryMenu.module.css' - -interface EntryPickerRowProps { - readonly label: string - readonly pinned: boolean - readonly pinLabel: string - readonly pinAccessibleLabel: string - /** Truncate the label to one line instead of wrapping it. */ - readonly singleLine: boolean - readonly onOpen: () => void - readonly onPin: () => void -} - -/** - * Render two sibling controls so pinning never opens the entry. - * @param props - Entry label, pin state, localized action names and independent callbacks. - * @returns A shared menu row with a trailing pin button. - */ -export function EntryPickerRow({ - label, pinned, pinLabel, pinAccessibleLabel, singleLine, onOpen, onPin, -}: EntryPickerRowProps) { - return
- - - {label} - - - - - - - -
-} diff --git a/packages/client/ui-entry-customization/src/client/index.ts b/packages/client/ui-entry-customization/src/client/index.ts index ba41d131a9..689279b22c 100644 --- a/packages/client/ui-entry-customization/src/client/index.ts +++ b/packages/client/ui-entry-customization/src/client/index.ts @@ -10,11 +10,11 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' import { EntryMenuContent, type EntryMenuInjected, type EntryRegion } from './EntryMenuContent.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { - /** More menu rows: pin actions, footer actions, and Creator draft prompts. */ + /** More menu rows: navigation actions, and Creator draft prompts. */ entryCustomization: keyof typeof en } } @@ -27,7 +27,7 @@ export const inject = ['slots', 'locale', 'uiWorkspace'] * @param ctx - Browser context with slot, locale, and Workspace services, and the optional plugin navigation. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('entryCustomization', { en, zh, ja })) + ctx.effect(() => ctx.locale.register('entryCustomization', { en, zh, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('entryCustomization') // The Plugins page provides its navigation only while the current Peer may use it. const pluginNavigation = createSnapshotStore(false) diff --git a/packages/client/ui-entry-customization/src/client/locales.ts b/packages/client/ui-entry-customization/src/client/locales.ts index 6693ed6fc2..c0a0e2cccc 100644 --- a/packages/client/ui-entry-customization/src/client/locales.ts +++ b/packages/client/ui-entry-customization/src/client/locales.ts @@ -1,12 +1,5 @@ -/** Copy for the More menu rows: pin actions, footer actions, and Creator draft prompts per region. */ +/** Copy for the More menu rows: navigation actions, and Creator draft prompts per region. */ export const en = { - 'pin.sidebar': 'Pin to left sidebar', - 'unpin.sidebar': 'Unpin from left sidebar', - 'pin.conversation': 'Pin to conversation tabs', - 'unpin.conversation': 'Unpin from conversation tabs', - 'pin.rightbar': 'Pin to right sidebar', - 'unpin.rightbar': 'Unpin from right sidebar', - pinLabel: '{action}: {name}', install: 'Browse other plugins', create: 'Create your plugin', 'createPrompt.sidebar': 'Create a DSH plugin with a left sidebar entry.', @@ -16,13 +9,6 @@ export const en = { /** Chinese counterpart of the entry-customization dictionary. */ export const zh: Record = { - 'pin.sidebar': '固定到左侧栏', - 'unpin.sidebar': '从左侧栏取消固定', - 'pin.conversation': '固定到会话标签栏', - 'unpin.conversation': '从会话标签栏取消固定', - 'pin.rightbar': '固定到右侧栏', - 'unpin.rightbar': '从右侧栏取消固定', - pinLabel: '{action}:{name}', install: '浏览更多插件', create: '创建你的插件', 'createPrompt.sidebar': '创建一个带左侧栏入口的 DSH 插件。', @@ -32,16 +18,54 @@ export const zh: Record = { /** Japanese copy, checked against the English keys. */ export const ja = { - 'pin.sidebar': '左サイドバーに固定', - 'unpin.sidebar': '左サイドバーから固定解除', - 'pin.conversation': '会話タブに固定', - 'unpin.conversation': '会話タブから固定解除', - 'pin.rightbar': '右サイドバーに固定', - 'unpin.rightbar': '右サイドバーから固定解除', - 'pinLabel': '{action}: {name}', 'install': '他のプラグインを探す', 'create': 'プラグインを作成', 'createPrompt.sidebar': '左サイドバーにエントリを持つ DSH プラグインを作成します。', 'createPrompt.conversation': 'DSH 会話ビュープラグインを作成します。', 'createPrompt.rightbar': '右サイドバーにエントリを持つ DSH プラグインを作成します。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + install: 'Explorar otros complementos', + create: 'Crea tu complemento', + 'createPrompt.sidebar': 'Cree un complemento DSH con una entrada en la barra lateral izquierda.', + 'createPrompt.conversation': 'Cree un complemento de vista de conversación DSH.', + 'createPrompt.rightbar': 'Cree un complemento DSH con una entrada en la barra lateral derecha.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + install: "Parcourir d'autres plugins", + create: 'Créez votre plugin', + 'createPrompt.sidebar': 'Créez un plugin DSH avec une entrée dans la barre latérale gauche.', + 'createPrompt.conversation': "Créez un plugin d'affichage de conversation DSH.", + 'createPrompt.rightbar': 'Créez un plugin DSH avec une entrée dans la barre latérale droite.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + install: 'Durchsuchen Sie andere Plugins', + create: 'Erstellen Sie Ihr Plugin', + 'createPrompt.sidebar': 'Erstellen Sie ein DSH-Plugin mit einem Eintrag in der linken Seitenleiste.', + 'createPrompt.conversation': 'Erstellen Sie ein DSH-Plugin für die Konversationsansicht.', + 'createPrompt.rightbar': 'Erstellen Sie ein DSH-Plugin mit einem Eintrag in der rechten Seitenleiste.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + install: 'Просмотрите другие плагины', + create: 'Создайте свой плагин', + 'createPrompt.sidebar': 'Создайте плагин DSH с записью на левой боковой панели.', + 'createPrompt.conversation': 'Создайте плагин просмотра диалога DSH.', + 'createPrompt.rightbar': 'Создайте плагин DSH с записью на правой боковой панели.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + install: 'Procure outros plug-ins', + create: 'Crie seu plug-in', + 'createPrompt.sidebar': 'Crie um plug-in DSH com uma entrada na barra lateral esquerda.', + 'createPrompt.conversation': 'Crie um plug-in de visualização de conversa DSH.', + 'createPrompt.rightbar': 'Crie um plug-in DSH com uma entrada na barra lateral direita.', +} satisfies Record diff --git a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx index 1a38e84430..ff77c49ca7 100644 --- a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx +++ b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** The plugin fills each region's More menu with entry rows and the discovery and Creator actions. */ +/** The plugin fills each region's More menu with the discovery and Creator actions. */ import type { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, within } from '@testing-library/react' @@ -30,10 +30,9 @@ function content(overrides: Partial = {}) { onDismiss: vi.fn(), browsePlugins: vi.fn(), createPlugin: vi.fn(), } - // Framework hooks stay unused stubs; this isolated row set reads only its owner, inject, and locale shares. const props = { entries: [{ id: 'notes', label: 'Notes', pinned: true }, { id: 'tasks', label: 'Tasks', pinned: false }], - region: 'sidebar', t: makeTranslate(en), renderFactorySlot: unused, + region: 'sidebar', t: makeTranslate(en), usePluginNavigation: (select: (present: boolean) => T): T => select(true), usePanelInfo: unused, useSessions: unused, useSessionStatus: unused, useSessionRetainInfo: unused, useResource: unused, useWorkspaces: unused, @@ -45,54 +44,59 @@ function content(overrides: Partial = {}) { } describe('More menu rows', () => { - it('separates opening from pinning and reports the inverted pin state', () => { - const b = content() - expect(b.view.getAllByRole('menuitem').map(row => row.getAttribute('aria-label') ?? row.textContent)).toEqual([ - 'Notes', 'Unpin from left sidebar: Notes', 'Tasks', 'Pin to left sidebar: Tasks', 'Browse other plugins', 'Create your plugin', - ]) - expect(b.view.getAllByRole('separator')).toHaveLength(1) - fireEvent.click(b.view.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - fireEvent.click(b.view.getByRole('menuitem', { name: 'Pin to left sidebar: Tasks' })) - expect(b.onPinnedChange).toHaveBeenNthCalledWith(1, 'notes', false) - expect(b.onPinnedChange).toHaveBeenNthCalledWith(2, 'tasks', true) - expect(b.onDismiss).not.toHaveBeenCalled() - expect(b.onOpen).not.toHaveBeenCalled() - fireEvent.click(b.view.getByRole('menuitem', { name: 'Tasks' })) - expect(b.onDismiss).toHaveBeenCalledOnce() - expect(b.onOpen).toHaveBeenCalledExactlyOnceWith('tasks') - expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.onOpen.mock.invocationCallOrder[0]!) - }) - - it.each([ - ['conversation', 'Pin to conversation tabs: Tasks'], - ['rightbar', 'Pin to right sidebar: Tasks'], - ] as const)('names the %s pin destination', (region, name) => { - const { view } = content({ region }) - expect(view.getByRole('menuitem', { name })).toBeTruthy() - }) - - it('keeps only the discovery and Creator actions, without a separator, for an empty roster', () => { - const b = content({ entries: [], region: 'rightbar' }) + it.each(['sidebar', 'rightbar'] as const)('shows only discovery and creation in %s regardless of entries', (region) => { + const b = content({ region, entries: [ + { id: 'notes', label: 'Notes', pinned: true, active: true }, + { id: 'tasks', label: 'Tasks', pinned: false }, + ] }) expect(b.view.getAllByRole('menuitem').map(row => row.textContent)).toEqual(['Browse other plugins', 'Create your plugin']) expect(b.view.queryByRole('separator')).toBeNull() fireEvent.click(b.view.getByRole('menuitem', { name: 'Browse other plugins' })) expect(b.browsePlugins).toHaveBeenCalledExactlyOnceWith() + expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.browsePlugins.mock.invocationCallOrder[0]!) fireEvent.click(b.view.getByRole('menuitem', { name: 'Create your plugin' })) expect(b.createPlugin).toHaveBeenCalledExactlyOnceWith() - expect(b.onDismiss).toHaveBeenCalledTimes(2) + expect(b.onDismiss.mock.invocationCallOrder[1]).toBeLessThan(b.createPlugin.mock.invocationCallOrder[0]!) + expect(b.onOpen).not.toHaveBeenCalled() + expect(b.onPinnedChange).not.toHaveBeenCalled() }) - it('shows the pin hint after the shared tooltip delay outside the menu card', () => { - vi.stubGlobal('matchMedia', vi.fn((query: string) => ({ matches: query === '(any-hover: hover)' }))) - const { view } = content() - const unpin = view.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' }) - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) - fireEvent.pointerEnter(unpin, { pointerType: 'mouse' }) - act(() => { vi.advanceTimersByTime(499) }) - expect(view.queryByRole('tooltip', { hidden: true })).toBeNull() - act(() => { vi.advanceTimersByTime(1) }) - expect(view.getByRole('tooltip', { hidden: true }).closest('[role="menu"]')).toBeNull() - fireEvent.pointerLeave(unpin, { pointerType: 'mouse' }) + it.each([true, false])('opens folded Views and marks the active View with plugin navigation present=%s', (browsable) => { + const b = content({ region: 'conversation', + usePluginNavigation: select => select(browsable), + entries: [ + { id: 'notes', label: 'Notes', pinned: true, active: true }, + { id: 'tasks', label: 'Tasks', pinned: true, active: false }, + ], + }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)) + .toEqual(['Notes', 'Tasks', ...(browsable ? ['Browse other plugins'] : []), 'Create your plugin']) + const currentRow = b.view.getByRole('menuitem', { name: 'Notes' }) + expect(currentRow.getAttribute('aria-current')).toBe('page') + expect(currentRow.querySelector('svg')?.previousElementSibling?.textContent).toBe('Notes') + expect(b.view.getByRole('menuitem', { name: 'Tasks' }).hasAttribute('aria-current')).toBe(false) + expect(b.view.getAllByRole('separator')).toHaveLength(1) + fireEvent.click(b.view.getByRole('menuitem', { name: 'Tasks' })) + expect(b.onOpen).toHaveBeenCalledExactlyOnceWith('tasks') + expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.onOpen.mock.invocationCallOrder[0]!) + expect(b.onPinnedChange).not.toHaveBeenCalled() + }) + + it.each([true, false])('keeps actions unseparated for an empty roster with plugin navigation present=%s', (browsable) => { + const b = content({ region: 'conversation', entries: [], usePluginNavigation: select => select(browsable) }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)) + .toEqual([...(browsable ? ['Browse other plugins'] : []), 'Create your plugin']) + expect(b.view.queryByRole('separator')).toBeNull() + }) + + it.each(['sidebar', 'rightbar'] as const)('keeps only creation in %s without plugin navigation', (region) => { + const b = content({ region, usePluginNavigation: select => select(false) }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)).toEqual(['Create your plugin']) + expect(b.view.queryByRole('separator')).toBeNull() + fireEvent.click(b.view.getByRole('menuitem', { name: 'Create your plugin' })) + expect(b.createPlugin).toHaveBeenCalledExactlyOnceWith() + expect(b.onOpen).not.toHaveBeenCalled() + expect(b.onPinnedChange).not.toHaveBeenCalled() }) }) @@ -103,7 +107,7 @@ function Frame({ renderSlot }: PropsRenderSlots) { return <> {SEATS.map(seat =>
{renderSlot(seat, { - entries, + entries: seat === 'conversation.session.header.views.actions' ? [] : entries, onOpen: (id) => { setOpened(`${seat}:${id}`) }, onPinnedChange: (_id, next) => { setPinned(next) }, onDismiss: () => {}, @@ -148,8 +152,7 @@ describe('More menu registrations', () => { const sidebar = () => within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) expect(sidebar().queryByRole('menuitem', { name: 'Browse other plugins' })).toBeNull() expect(sidebar().getByRole('menuitem', { name: 'Create your plugin' })).toBeTruthy() - // The Creator row takes the separator the absent discovery row would carry. - expect(sidebar().getAllByRole('separator')).toHaveLength(1) + expect(sidebar().queryByRole('separator')).toBeNull() const injected = b.runtime.slots.entries('sidebar.panellist.actions')[0]!.inject!() as { browsePlugins: () => void } expect(() => { injected.browsePlugins() }).not.toThrow() @@ -170,15 +173,10 @@ describe('More menu registrations', () => { const b = await bench() for (const seat of SEATS) { expect(b.runtime.slots.entries(seat).map(entry => entry.options.id)).toEqual(['entry-customization']) - expect(within(b.view.getByRole('menu', { name: seat })).getByRole('menuitem', { name: 'Notes' })).toBeTruthy() + expect(within(b.view.getByRole('menu', { name: seat })).getAllByRole('menuitem').map(row => row.textContent)) + .toEqual(['Browse other plugins', 'Create your plugin']) } const sidebar = within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) - fireEvent.click(sidebar.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - expect(within(b.view.getByRole('menu', { name: 'sidebar.right.tab.guide.actions' })) - .getByRole('menuitem', { name: 'Pin to right sidebar: Notes' })).toBeTruthy() - fireEvent.click(within(b.view.getByRole('menu', { name: 'conversation.session.header.views.actions' })) - .getByRole('menuitem', { name: 'Notes' })) - expect(b.view.getByTestId('opened').textContent).toBe('conversation.session.header.views.actions:notes') fireEvent.click(sidebar.getByRole('menuitem', { name: 'Browse other plugins' })) expect(b.openList).toHaveBeenCalledExactlyOnceWith() await b.plugin.dispose() @@ -220,15 +218,6 @@ describe('More menu registrations', () => { expect(b.track).toHaveBeenCalledTimes(2) }) - it('reports neither entry opening nor pin changes', async () => { - const b = await bench() - const menu = within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) - fireEvent.click(menu.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - fireEvent.click(menu.getByRole('menuitem', { name: 'Notes' })) - expect(b.view.getByTestId('opened').textContent).toBe('sidebar.panellist.actions:notes') - expect(b.track).not.toHaveBeenCalled() - }) - it('keeps both footer actions working without the analytics service', async () => { const b = await bench('en', { analytics: false }) const menu = within(b.view.getByRole('menu', { name: 'sidebar.right.tab.guide.actions' })) diff --git a/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts b/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts deleted file mode 100644 index c30d3ee705..0000000000 --- a/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** Pin controls use Session-action ink without changing the navigation control's dimensions. */ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -const css = readFileSync(new URL('../src/client/EntryMenu.module.css', import.meta.url), 'utf8') - -describe('entry pin styling', () => { - it('uses tertiary ink for both pin states and primary ink on pointer hover', () => { - expect(css).toMatch(/\.pinButton\s*\{[^}]*color:\s*var\(--dsw-alias-label-tertiary\)/) - expect(css).toMatch(/\.pinButton:hover\s*\{[^}]*color:\s*var\(--dsw-alias-label-primary\)/) - expect(css).not.toMatch(/\[data-pinned=['"]true['"]\]\s+\.pinButton\s*\{[^}]*color:/) - }) - - it('centers the pin without creating a transformed tooltip container', () => { - const pinAnchor = css.match(/\.pinAnchor\s*\{([^}]*)\}/)?.[1] - expect(pinAnchor).toBeDefined() - expect(pinAnchor).toContain('align-items: center') - expect(pinAnchor).not.toContain('transform:') - }) -}) diff --git a/packages/client/ui-file-drag/locale/de.json b/packages/client/ui-file-drag/locale/de.json new file mode 100644 index 0000000000..5f610493c3 --- /dev/null +++ b/packages/client/ui-file-drag/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ziehen von Dateien", + "description": "Ziehen Sie Originaldateien in den Composer oder andere Anwendungen, mit lokalisierter Fehlerrückmeldung." + } +} diff --git a/packages/client/ui-file-drag/locale/es.json b/packages/client/ui-file-drag/locale/es.json new file mode 100644 index 0000000000..ff04cd6d4c --- /dev/null +++ b/packages/client/ui-file-drag/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Arrastrar archivos", + "description": "Arrastre los archivos originales al compositor u otras aplicaciones, con comentarios de falla localizados." + } +} diff --git a/packages/client/ui-file-drag/locale/fr.json b/packages/client/ui-file-drag/locale/fr.json new file mode 100644 index 0000000000..302a536b7d --- /dev/null +++ b/packages/client/ui-file-drag/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Glisser un fichier", + "description": "Faites glisser les fichiers originaux dans le composeur ou d'autres applications, avec un retour d'échec localisé." + } +} diff --git a/packages/client/ui-file-drag/locale/pt.json b/packages/client/ui-file-drag/locale/pt.json new file mode 100644 index 0000000000..9d59d7c683 --- /dev/null +++ b/packages/client/ui-file-drag/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Arrastando arquivo", + "description": "Arraste os arquivos originais para o compositor ou outros aplicativos, com feedback de falha localizado." + } +} diff --git a/packages/client/ui-file-drag/locale/ru.json b/packages/client/ui-file-drag/locale/ru.json new file mode 100644 index 0000000000..00cd72b430 --- /dev/null +++ b/packages/client/ui-file-drag/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Перетаскивание файлов", + "description": "Перетащите исходные файлы в композитор или другие приложения с локализованной обратной связью об ошибках." + } +} diff --git a/packages/client/ui-file-drag/package.json b/packages/client/ui-file-drag/package.json index 16e865db2a..2a066047f0 100644 --- a/packages/client/ui-file-drag/package.json +++ b/packages/client/ui-file-drag/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-file-drag", "description": "Application-owned file drag transfers and localized failure feedback", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-file-drag/src/client/index.ts b/packages/client/ui-file-drag/src/client/index.ts index efe3fb7eab..cfc9c1c85c 100644 --- a/packages/client/ui-file-drag/src/client/index.ts +++ b/packages/client/ui-file-drag/src/client/index.ts @@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import { FileDragFeedback, type FileDragFailure, type FileDragFeedbackInjected } from './feedback.tsx' import { FileTransfers } from './transfers.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Application-local file preparation and drop resolution. */ export interface FileDragService { @@ -33,7 +33,7 @@ export function apply(ctx: Context): void { const desktop = bridge?.protocolVersion === 1 ? bridge.fileDrag : undefined const failure = createSnapshotStore(null) const t = ctx.locale.bind('fileDrag') - ctx.effect(() => ctx.locale.register('fileDrag', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('fileDrag', { zh, en, ja, es, fr, de, ru, pt })) let sequence = 0 const reportFailure = (): void => { failure.set({ text: t('failed'), sequence: ++sequence }) } const transfers = new FileTransfers(desktop, reportFailure) diff --git a/packages/client/ui-file-drag/src/client/locales.ts b/packages/client/ui-file-drag/src/client/locales.ts index 9497b29dbc..d0da371eca 100644 --- a/packages/client/ui-file-drag/src/client/locales.ts +++ b/packages/client/ui-file-drag/src/client/locales.ts @@ -7,6 +7,21 @@ export const en = { failed: 'Drag failed. Try again.' } satisfies Record +/** Spanish dictionary, checked against the English keys. */ +export const es = { failed: 'Error al arrastrar. Intentar otra vez.' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { failed: 'Le glissement a échoué. Essayer à nouveau.' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { failed: 'Ziehen ist fehlgeschlagen. Versuchen Sie es erneut.' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { failed: 'Перетащить не удалось. Попробуйте еще раз.' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { failed: 'Falha ao arrastar. Tente novamente.' } satisfies Record + declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { /** File transfer failure feedback. */ diff --git a/packages/client/ui-goal/locale/de.json b/packages/client/ui-goal/locale/de.json new file mode 100644 index 0000000000..2a2420166b --- /dev/null +++ b/packages/client/ui-goal/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sitzungszielleiste", + "description": "Zeigt das Ziel und den Status der aktuellen Sitzung über dem Eingabefeld an." + } +} diff --git a/packages/client/ui-goal/locale/es.json b/packages/client/ui-goal/locale/es.json new file mode 100644 index 0000000000..e0135182ed --- /dev/null +++ b/packages/client/ui-goal/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barra de destino de sesión", + "description": "Muestra el objetivo y el estado de la sesión actual encima del cuadro de entrada." + } +} diff --git a/packages/client/ui-goal/locale/fr.json b/packages/client/ui-goal/locale/fr.json new file mode 100644 index 0000000000..2282fdbd25 --- /dev/null +++ b/packages/client/ui-goal/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barre de cible de session", + "description": "Affiche l'objectif et le statut de la session en cours au-dessus de la zone de saisie." + } +} diff --git a/packages/client/ui-goal/locale/pt.json b/packages/client/ui-goal/locale/pt.json new file mode 100644 index 0000000000..d7e3fa30c3 --- /dev/null +++ b/packages/client/ui-goal/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barra de destino da sessão", + "description": "Exibe a meta e o status da sessão atual acima da caixa de entrada." + } +} diff --git a/packages/client/ui-goal/locale/ru.json b/packages/client/ui-goal/locale/ru.json new file mode 100644 index 0000000000..2736841f94 --- /dev/null +++ b/packages/client/ui-goal/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "целевая панель сеанса", + "description": "Отображает цель и статус текущего сеанса над полем ввода." + } +} diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index 711af0de38..4e23521a01 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-goal/src/client/index.ts b/packages/client/ui-goal/src/client/index.ts index a38de98032..7c1eadb38a 100644 --- a/packages/client/ui-goal/src/client/index.ts +++ b/packages/client/ui-goal/src/client/index.ts @@ -35,7 +35,7 @@ import { GoalCommandReference } from './GoalCommandReference.tsx' import { goalCommandInputDefinition } from './goal-command-input.ts' import { goalCommandReferenceRule } from './user-text-rules.ts' export type {} from './user-text-rules.ts' -import { en, ja, zh, type GoalKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type GoalKey } from './locales.ts' export { GoalBar, GoalDock } from './GoalBar.tsx' export type { @@ -70,7 +70,7 @@ export const inject = ['slots', 'sessions', 'remote', 'remote.goals', 'locale', export function apply(ctx: ClientContext): void { ctx.uiConversation.events.register(goalCommandInputDefinition) ctx.uiConversation.userText.register(goalCommandReferenceRule) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-goal: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-goal: dictionaries') ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({ name: 'conversation.user.text.node', diff --git a/packages/client/ui-goal/src/client/locales.ts b/packages/client/ui-goal/src/client/locales.ts index b196c8c7a9..f52946329e 100644 --- a/packages/client/ui-goal/src/client/locales.ts +++ b/packages/client/ui-goal/src/client/locales.ts @@ -50,3 +50,83 @@ export const ja = { 'action.edit': '目標を編集', 'action.clear': '目標をクリア', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'phase.active': 'Meta en curso', + 'phase.active.disarmed': 'Meta inactiva', + 'phase.paused': 'Objetivo en pausa', + 'phase.blocked': 'Meta bloqueada', + 'objective.aria': 'Objetivo de meta', + 'commandInput.aria': 'Entrada de comando', + 'action.save': 'Guardar gol', + 'action.cancel': 'Cancelar edición', + 'action.pause': 'Pausar objetivo', + 'action.resume': 'Reanudar objetivo', + 'action.edit': 'Editar objetivo', + 'action.clear': 'Objetivo claro', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'phase.active': 'Objectif continu', + 'phase.active.disarmed': 'Objectif inactif', + 'phase.paused': 'Objectif suspendu', + 'phase.blocked': 'Objectif bloqué', + 'objective.aria': 'Objectif du but', + 'commandInput.aria': 'Entrée de commande', + 'action.save': "Sauvegarder l'objectif", + 'action.cancel': 'Annuler la modification', + 'action.pause': "Suspendre l'objectif", + 'action.resume': "Reprendre l'objectif", + 'action.edit': "Modifier l'objectif", + 'action.clear': 'Objectif clair', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'phase.active': 'Fortlaufendes Ziel', + 'phase.active.disarmed': 'Inaktives Ziel', + 'phase.paused': 'Pausiertes Ziel', + 'phase.blocked': 'Blockiertes Ziel', + 'objective.aria': 'Zielziel', + 'commandInput.aria': 'Befehlseingabe', + 'action.save': 'Ziel speichern', + 'action.cancel': 'Bearbeitung abbrechen', + 'action.pause': 'Ziel pausieren', + 'action.resume': 'Ziel fortsetzen', + 'action.edit': 'Ziel bearbeiten', + 'action.clear': 'Klares Ziel', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'phase.active': 'Текущая цель', + 'phase.active.disarmed': 'Неактивная цель', + 'phase.paused': 'Приостановленная цель', + 'phase.blocked': 'Заблокированная цель', + 'objective.aria': 'Цель Цель', + 'commandInput.aria': 'Ввод команды', + 'action.save': 'Сохранить цель', + 'action.cancel': 'Отменить редактирование', + 'action.pause': 'Приостановить цель', + 'action.resume': 'Возобновить цель', + 'action.edit': 'Изменить цель', + 'action.clear': 'Четкая цель', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'phase.active': 'Meta contínua', + 'phase.active.disarmed': 'Meta inativa', + 'phase.paused': 'Meta pausada', + 'phase.blocked': 'Meta bloqueada', + 'objective.aria': 'Objetivo da meta', + 'commandInput.aria': 'Entrada de comando', + 'action.save': 'Salvar meta', + 'action.cancel': 'Cancelar edição', + 'action.pause': 'Pausar meta', + 'action.resume': 'Retomar meta', + 'action.edit': 'Editar meta', + 'action.clear': 'Objetivo claro', +} satisfies Record diff --git a/packages/client/ui-input-trigger/locale/de.json b/packages/client/ui-input-trigger/locale/de.json new file mode 100644 index 0000000000..be4ceb8980 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Geben Sie Lenovo Services ein", + "description": "Erkennen Sie / und @ in der Eingabe, zeigen Sie die entsprechenden Kandidaten an und wählen Sie sie aus." + } +} diff --git a/packages/client/ui-input-trigger/locale/es.json b/packages/client/ui-input-trigger/locale/es.json new file mode 100644 index 0000000000..48be835ec5 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ingrese a los servicios de Lenovo", + "description": "Reconocer / y @ en la entrada, mostrar y seleccionar los candidatos correspondientes." + } +} diff --git a/packages/client/ui-input-trigger/locale/fr.json b/packages/client/ui-input-trigger/locale/fr.json new file mode 100644 index 0000000000..225c4f9e3f --- /dev/null +++ b/packages/client/ui-input-trigger/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrez dans les services Lenovo", + "description": "Reconnaître / et @ dans la saisie, afficher et sélectionner les candidats correspondants." + } +} diff --git a/packages/client/ui-input-trigger/locale/pt.json b/packages/client/ui-input-trigger/locale/pt.json new file mode 100644 index 0000000000..91b25e4363 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entre nos serviços Lenovo", + "description": "Reconheça / e @ na entrada, exiba e selecione os candidatos correspondentes." + } +} diff --git a/packages/client/ui-input-trigger/locale/ru.json b/packages/client/ui-input-trigger/locale/ru.json new file mode 100644 index 0000000000..9aeeefffd6 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Войдите в услуги Lenovo", + "description": "Распознайте / и @ во входных данных, отобразите и выберите соответствующих кандидатов." + } +} diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index 6772d6516d..ca1a483eea 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-input-trigger/src/client/index.ts b/packages/client/ui-input-trigger/src/client/index.ts index cad11a5452..de4186135f 100644 --- a/packages/client/ui-input-trigger/src/client/index.ts +++ b/packages/client/ui-input-trigger/src/client/index.ts @@ -13,7 +13,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { InputTriggerService } from './service.ts' import type { MenuViewInjected } from './slots.ts' import { MenuView } from './MenuView.tsx' -import { en, ja, zh, type MenuKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type MenuKey } from './locales.ts' export { InputTriggerService } from './service.ts' export { InputTriggerController } from './controller.ts' @@ -58,7 +58,7 @@ export const inject = ['sessions', 'locale'] */ export function apply(ctx: ClientContext): void { ctx.plugin(InputTriggerService) - ctx.effect(() => ctx.locale.register(MENU_NS, { zh, en, ja }), 'ui-input-trigger: menu dictionaries') + ctx.effect(() => ctx.locale.register(MENU_NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-input-trigger: menu dictionaries') ctx.inject(['slots', 'inputTriggers', 'sessions'], (scope: ClientContext) => { const inputTriggers = scope.inputTriggers const sessions = scope.sessions diff --git a/packages/client/ui-input-trigger/src/client/locales.ts b/packages/client/ui-input-trigger/src/client/locales.ts index 1b11342393..63f0780d1d 100644 --- a/packages/client/ui-input-trigger/src/client/locales.ts +++ b/packages/client/ui-input-trigger/src/client/locales.ts @@ -45,3 +45,68 @@ export const ja = { 'crumbs.aria': 'フォルダナビゲーション', 'suggestions.aria': '候補を表示', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'command': 'Comandos', + 'skill': 'Habilidades', + 'subagent': 'Subagentes', + 'loading': 'Cargando…', + 'drill.aria': 'Explorar carpeta', + 'drill.hint': 'Explorar carpeta', + 'drill.key': 'Pestaña', + 'crumbs.aria': 'Navegación de carpetas', + 'suggestions.aria': 'Sugerencias de activación', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'command': 'Commandes', + 'skill': 'Compétences', + 'subagent': 'Sous-agents', + 'loading': 'Chargement…', + 'drill.aria': 'Parcourir le dossier', + 'drill.hint': 'Parcourir le dossier', + 'drill.key': 'Onglet', + 'crumbs.aria': 'Navigation dans les dossiers', + 'suggestions.aria': 'Suggestions de déclenchement', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'command': 'Befehle', + 'skill': 'Fähigkeiten', + 'subagent': 'Subagenten', + 'loading': 'Laden…', + 'drill.aria': 'Ordner durchsuchen', + 'drill.hint': 'Ordner durchsuchen', + 'drill.key': 'Tab', + 'crumbs.aria': 'Ordnernavigation', + 'suggestions.aria': 'Trigger-Vorschläge', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'command': 'Команды', + 'skill': 'Навыки', + 'subagent': 'Субагенты', + 'loading': 'Загрузка…', + 'drill.aria': 'Просмотреть папку', + 'drill.hint': 'Просмотреть папку', + 'drill.key': 'Вкладка', + 'crumbs.aria': 'Навигация по папкам', + 'suggestions.aria': 'Триггерные предложения', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'command': 'Comandos', + 'skill': 'Habilidades', + 'subagent': 'Subagentes', + 'loading': 'Carregando…', + 'drill.aria': 'Navegar na pasta', + 'drill.hint': 'Navegar na pasta', + 'drill.key': 'Guia', + 'crumbs.aria': 'Navegação de pastas', + 'suggestions.aria': 'Sugestões de gatilho', +} satisfies Record diff --git a/packages/client/ui-jobs/locale/de.json b/packages/client/ui-jobs/locale/de.json new file mode 100644 index 0000000000..87cc3c7d1f --- /dev/null +++ b/packages/client/ui-jobs/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hintergrundaufgabenschnittstelle", + "description": "Zeigen Sie Hintergrundaufgabenlisten, Status und Echtzeitausgabe innerhalb einer Sitzung an." + } +} diff --git a/packages/client/ui-jobs/locale/es.json b/packages/client/ui-jobs/locale/es.json new file mode 100644 index 0000000000..9ce2933127 --- /dev/null +++ b/packages/client/ui-jobs/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de tareas en segundo plano", + "description": "Vea listas de tareas en segundo plano, estados y resultados en tiempo real dentro de una sesión." + } +} diff --git a/packages/client/ui-jobs/locale/fr.json b/packages/client/ui-jobs/locale/fr.json new file mode 100644 index 0000000000..e950919b81 --- /dev/null +++ b/packages/client/ui-jobs/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tâche en arrière-plan", + "description": "Affichez les listes de tâches en arrière-plan, l'état et la sortie en temps réel au sein d'une session." + } +} diff --git a/packages/client/ui-jobs/locale/pt.json b/packages/client/ui-jobs/locale/pt.json new file mode 100644 index 0000000000..2820658f19 --- /dev/null +++ b/packages/client/ui-jobs/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tarefa em segundo plano", + "description": "Visualize listas de tarefas em segundo plano, status e resultados em tempo real em uma sessão." + } +} diff --git a/packages/client/ui-jobs/locale/ru.json b/packages/client/ui-jobs/locale/ru.json new file mode 100644 index 0000000000..3d2a910429 --- /dev/null +++ b/packages/client/ui-jobs/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс фоновой задачи", + "description": "Просматривайте списки фоновых задач, статус и выходные данные в режиме реального времени в течение сеанса." + } +} diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index 95ea2ac2b1..31c56d7d94 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list with on-demand streaming record panels", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-jobs/src/client/index.ts b/packages/client/ui-jobs/src/client/index.ts index eb5f4d920c..5174aa1fb8 100644 --- a/packages/client/ui-jobs/src/client/index.ts +++ b/packages/client/ui-jobs/src/client/index.ts @@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-api-job-controller/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' -import { en, ja, NS, zh, type JobKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type JobKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -31,7 +31,7 @@ export const inject = ['jobs', 'slots', 'locale'] * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-jobs: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-jobs: dictionaries') ctx.slots.inject( 'conversation.session.header.actions', () => ctx.slots.register({ diff --git a/packages/client/ui-jobs/src/client/locales.ts b/packages/client/ui-jobs/src/client/locales.ts index c1aff15b1c..700009d9a5 100644 --- a/packages/client/ui-jobs/src/client/locales.ts +++ b/packages/client/ui-jobs/src/client/locales.ts @@ -137,3 +137,223 @@ export const ja = { 'terminal.expand': '残り {n} 行を表示', 'terminal.expandAria': '折りたたまれた出力 {n} 行を展開', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'count.live.one': 'Tareas en segundo plano en ejecución: {count}', + 'count.live.other': 'Tareas en segundo plano en ejecución: {count}', + 'count.idle.one': 'Tareas en segundo plano: {count}', + 'count.idle.other': 'Tareas en segundo plano: {count}', + 'list.aria': 'Trabajos en segundo plano', + 'section.live': 'corriendo', + 'section.settledCount': 'Terminado {count}', + 'section.clear': 'Borrar', + 'row.expandAria': 'Mostrar salida en vivo de {label}', + 'row.collapseAria': 'Ocultar salida en vivo de {label}', + 'kill.stop': 'Detener tarea {label}', + 'kill.confirm': 'Haga clic nuevamente para confirmar', + 'kill.confirmAction': 'Confirmar parada', + 'kill.failed': 'Detener falló', + 'status.running': 'corriendo', + 'status.stopping': 'parando', + 'status.completed': 'completado', + 'status.killed': 'cancelado', + 'status.failed': 'falló', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Corriendo para {duration}', + 'duration.title.done': 'Tomó {duration}', + 'output.gap': '… la producción anterior cayó…', + 'output.error': 'transmisión de salida en vivo interrumpida: {error}', + 'terminal.signal': 'señal {signal}', + 'terminal.exitCode': 'salir {code}', + 'terminal.noExitCode': 'sin código de salida', + 'terminal.commandLine': 'Línea de comando {n}', + 'terminal.running': 'corriendo', + 'terminal.failed': 'falló', + 'terminal.done': 'hecho', + 'terminal.copy': 'Copiar', + 'terminal.copied': 'Copiado', + 'terminal.noOutput': '(sin salida)', + 'terminal.collapse': 'Contraer', + 'terminal.collapseAria': 'Contraer salida', + 'terminal.expand': 'Mostrar {n} más líneas', + 'terminal.expandAria': 'Expandir líneas de salida colapsadas {n}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'count.live.one': 'Tâches en arrière-plan en cours : {count}', + 'count.live.other': 'Tâches en arrière-plan en cours : {count}', + 'count.idle.one': 'Tâches en arrière-plan : {count}', + 'count.idle.other': 'Tâches en arrière-plan : {count}', + 'list.aria': 'Travaux en arrière-plan', + 'section.live': 'Courir', + 'section.settledCount': 'Terminé {count}', + 'section.clear': 'Effacer', + 'row.expandAria': 'Afficher la sortie en direct de {label}', + 'row.collapseAria': 'Masquer la sortie en direct de {label}', + 'kill.stop': 'Arrêter la tâche {label}', + 'kill.confirm': 'Cliquez à nouveau pour confirmer', + 'kill.confirmAction': "Confirmer l'arrêt", + 'kill.failed': "L'arrêt a échoué", + 'status.running': 'courir', + 'status.stopping': 'arrêter', + 'status.completed': 'terminé', + 'status.killed': 'annulé', + 'status.failed': 'échoué', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Courir pour {duration}', + 'duration.title.done': 'A pris {duration}', + 'output.gap': '… la production antérieure a chuté…', + 'output.error': 'flux de sortie en direct interrompu : {error}', + 'terminal.signal': 'signal {signal}', + 'terminal.exitCode': 'quitter {code}', + 'terminal.noExitCode': 'pas de code de sortie', + 'terminal.commandLine': 'Ligne de commande {n}', + 'terminal.running': 'courir', + 'terminal.failed': 'échoué', + 'terminal.done': 'fait', + 'terminal.copy': 'Copier', + 'terminal.copied': 'Copié', + 'terminal.noOutput': '(pas de sortie)', + 'terminal.collapse': 'Réduire', + 'terminal.collapseAria': 'Réduire la sortie', + 'terminal.expand': 'Afficher {n} plus de lignes', + 'terminal.expandAria': 'Développer les lignes de sortie réduites {n}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'count.live.one': 'Laufende Hintergrundaufgaben: {count}', + 'count.live.other': 'Laufende Hintergrundaufgaben: {count}', + 'count.idle.one': 'Hintergrundaufgaben: {count}', + 'count.idle.other': 'Hintergrundaufgaben: {count}', + 'list.aria': 'Hintergrundjobs', + 'section.live': 'Laufen', + 'section.settledCount': 'Fertig {count}', + 'section.clear': 'Klar', + 'row.expandAria': 'Live-Ausgabe von {label} anzeigen', + 'row.collapseAria': 'Live-Ausgabe von {label} ausblenden', + 'kill.stop': 'Stoppen Sie die Aufgabe {label}', + 'kill.confirm': 'Klicken Sie zur Bestätigung erneut', + 'kill.confirmAction': 'Stopp bestätigen', + 'kill.failed': 'Stoppen ist fehlgeschlagen', + 'status.running': 'laufen', + 'status.stopping': 'anhalten', + 'status.completed': 'abgeschlossen', + 'status.killed': 'abgesagt', + 'status.failed': 'gescheitert', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Laufen für {duration}', + 'duration.title.done': 'Nahm {duration}', + 'output.gap': '… frühere Produktion gesunken …', + 'output.error': 'Live-Ausgabestream unterbrochen: {error}', + 'terminal.signal': 'Signal {signal}', + 'terminal.exitCode': 'Beenden Sie {code}', + 'terminal.noExitCode': 'kein Exit-Code', + 'terminal.commandLine': 'Befehlszeile {n}', + 'terminal.running': 'laufen', + 'terminal.failed': 'gescheitert', + 'terminal.done': 'erledigt', + 'terminal.copy': 'Kopieren', + 'terminal.copied': 'Kopiert', + 'terminal.noOutput': '(keine Ausgabe)', + 'terminal.collapse': 'Einklappen', + 'terminal.collapseAria': 'Ausgabe reduzieren', + 'terminal.expand': 'Zeige {n} weitere Zeilen', + 'terminal.expandAria': 'Erweitern Sie die minimierten Ausgabezeilen {n}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'count.live.one': 'Фоновые задачи выполняются: {count}', + 'count.live.other': 'Фоновые задачи выполняются: {count}', + 'count.idle.one': 'Фоновые задачи: {count}', + 'count.idle.other': 'Фоновые задачи: {count}', + 'list.aria': 'Фоновые задания', + 'section.live': 'Бег', + 'section.settledCount': 'Закончено {count}', + 'section.clear': 'Очистить', + 'row.expandAria': 'Показать живой вывод {label}', + 'row.collapseAria': 'Скрыть прямой вывод {label}', + 'kill.stop': 'Остановить задачу {label}', + 'kill.confirm': 'Нажмите еще раз, чтобы подтвердить', + 'kill.confirmAction': 'Подтвердить остановку', + 'kill.failed': 'Остановить не удалось', + 'status.running': 'бег', + 'status.stopping': 'остановка', + 'status.completed': 'завершено', + 'status.killed': 'отменен', + 'status.failed': 'не удалось', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Бегу за {duration}', + 'duration.title.done': 'Взял {duration}', + 'output.gap': '… ранее объемы производства упали …', + 'output.error': 'поток вывода в реальном времени прерван: {error}', + 'terminal.signal': 'сигнал {signal}', + 'terminal.exitCode': 'выход {code}', + 'terminal.noExitCode': 'нет кода выхода', + 'terminal.commandLine': 'Командная строка {n}', + 'terminal.running': 'бег', + 'terminal.failed': 'не удалось', + 'terminal.done': 'сделано', + 'terminal.copy': 'Копировать', + 'terminal.copied': 'Скопировано', + 'terminal.noOutput': '(нет вывода)', + 'terminal.collapse': 'Свернуть', + 'terminal.collapseAria': 'Свернуть вывод', + 'terminal.expand': 'Показать {n} больше строк', + 'terminal.expandAria': 'Развернуть свернутые выходные строки {n}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'count.live.one': 'Tarefas em segundo plano em execução: {count}', + 'count.live.other': 'Tarefas em segundo plano em execução: {count}', + 'count.idle.one': 'Tarefas em segundo plano: {count}', + 'count.idle.other': 'Tarefas em segundo plano: {count}', + 'list.aria': 'Trabalhos em segundo plano', + 'section.live': 'Correndo', + 'section.settledCount': 'Concluído {count}', + 'section.clear': 'Limpar', + 'row.expandAria': 'Mostrar saída ao vivo de {label}', + 'row.collapseAria': 'Ocultar saída ao vivo de {label}', + 'kill.stop': 'Parar tarefa {label}', + 'kill.confirm': 'Clique novamente para confirmar', + 'kill.confirmAction': 'Confirmar parada', + 'kill.failed': 'Falha na parada', + 'status.running': 'correndo', + 'status.stopping': 'parando', + 'status.completed': 'concluído', + 'status.killed': 'cancelado', + 'status.failed': 'falhou', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Correndo por {duration}', + 'duration.title.done': 'Levou {duration}', + 'output.gap': '… a produção anterior caiu…', + 'output.error': 'transmissão ao vivo interrompida: {error}', + 'terminal.signal': 'sinal {signal}', + 'terminal.exitCode': 'sair {code}', + 'terminal.noExitCode': 'sem código de saída', + 'terminal.commandLine': 'Linha de comando {n}', + 'terminal.running': 'correndo', + 'terminal.failed': 'falhou', + 'terminal.done': 'feito', + 'terminal.copy': 'Copiar', + 'terminal.copied': 'Copiado', + 'terminal.noOutput': '(sem saída)', + 'terminal.collapse': 'Recolher', + 'terminal.collapseAria': 'Recolher saída', + 'terminal.expand': 'Mostrar mais {n} linhas', + 'terminal.expandAria': 'Expanda {n} linhas de saída recolhidas', +} satisfies Record diff --git a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx index e6866c4182..4dcf892f87 100644 --- a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx +++ b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx @@ -5,7 +5,7 @@ import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import type { JobsSnapshot, JobView, ObservedJob } from '@deepseek-ai/dsh-api-job-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { JobListAction, type JobListActionProps } from '../src/client/JobListAction.tsx' -import { zh } from '../src/client/locales.ts' +import { zh, ru } from '../src/client/locales.ts' afterEach(() => { cleanup() @@ -590,3 +590,12 @@ describe('JobListAction human kill', () => { expect(document.querySelector('[data-kill-state]')).toBeNull() }) }) + +it.each([1, 2, 5, 21])('renders count-independent Russian labels for %i jobs', (count) => { + const jobs = Array.from({ length: count }, (_, i) => job({ id: `job-${i}` as JobView['id'] })) + const translate: JobListActionProps['t'] = makeTranslate(ru) + const view = render() + expect(screen.getByRole('button', { name: `Фоновые задачи выполняются: ${count}` })).toBeDefined() + view.rerender( ({ ...item, status: 'completed', finishedAt: 1_700_000_003_000 })))} t={translate} />) + expect(screen.getByRole('button', { name: `Фоновые задачи: ${count}` })).toBeDefined() +}) diff --git a/packages/client/ui-layout/locale/de.json b/packages/client/ui-layout/locale/de.json new file mode 100644 index 0000000000..9d6d23789f --- /dev/null +++ b/packages/client/ui-layout/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Layout anwenden", + "description": "Organisieren Sie Navigation, Konversationen und Seitenbereiche und unterstützen Sie das Ziehen, um das Layout anzupassen." + } +} diff --git a/packages/client/ui-layout/locale/es.json b/packages/client/ui-layout/locale/es.json new file mode 100644 index 0000000000..01d553183d --- /dev/null +++ b/packages/client/ui-layout/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aplicar diseño", + "description": "Organice la navegación, las conversaciones y los paneles laterales, y admita arrastrar para ajustar el diseño." + } +} diff --git a/packages/client/ui-layout/locale/fr.json b/packages/client/ui-layout/locale/fr.json new file mode 100644 index 0000000000..7526ace8cc --- /dev/null +++ b/packages/client/ui-layout/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Appliquer la mise en page", + "description": "Organisez la navigation, les conversations et les panneaux latéraux, et prenez en charge le glisser-déposer pour ajuster la mise en page." + } +} diff --git a/packages/client/ui-layout/locale/pt.json b/packages/client/ui-layout/locale/pt.json new file mode 100644 index 0000000000..10b0dcaae8 --- /dev/null +++ b/packages/client/ui-layout/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aplicar layout", + "description": "Organize a navegação, as conversas e os painéis laterais e suporte o recurso de arrastar para ajustar o layout." + } +} diff --git a/packages/client/ui-layout/locale/ru.json b/packages/client/ui-layout/locale/ru.json new file mode 100644 index 0000000000..ab90630f4d --- /dev/null +++ b/packages/client/ui-layout/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Применить макет", + "description": "Организуйте навигацию, диалоги и боковые панели, а также поддерживайте перетаскивание для настройки макета." + } +} diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index a233d3410a..f92629e6c7 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-layout/src/client/index.ts b/packages/client/ui-layout/src/client/index.ts index fb66376cfb..ee9da22d2b 100644 --- a/packages/client/ui-layout/src/client/index.ts +++ b/packages/client/ui-layout/src/client/index.ts @@ -18,7 +18,7 @@ import { AppFrame } from './AppFrame.tsx' import { createLayoutStore } from './stores.ts' import { LayoutController } from './service.ts' import type { ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client' -import { en, ja, zh } from './shortcut-locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './shortcut-locales.ts' import { ThemePresenter } from './theme-presenter.ts' // Contract exports only (export-convergence rule: cross-package consumers @@ -156,7 +156,7 @@ export const inject = ['slots', 'theme', 'locale', 'shortcuts'] * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register('shortcuts.layout', { zh, en, ja }), 'layout: command labels') + ctx.effect(() => ctx.locale.register('shortcuts.layout', { zh, en, ja, es, fr, de, ru, pt }), 'layout: command labels') const t = ctx.locale.bind('shortcuts.layout') ctx.effect(() => { diff --git a/packages/client/ui-layout/src/client/shortcut-locales.ts b/packages/client/ui-layout/src/client/shortcut-locales.ts index dcccfc8641..c87cdc5026 100644 --- a/packages/client/ui-layout/src/client/shortcut-locales.ts +++ b/packages/client/ui-layout/src/client/shortcut-locales.ts @@ -7,3 +7,18 @@ export const en: Record = { toggle: 'Toggle left sideba export const ja = { 'toggle': '左サイドバーを切り替え', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { toggle: 'Alternar barra lateral izquierda' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { toggle: 'Basculer la barre latérale gauche' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { toggle: 'Linke Seitenleiste umschalten' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { toggle: 'Переключить левую боковую панель' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { toggle: 'Alternar barra lateral esquerda' } satisfies Record diff --git a/packages/client/ui-message-feedback/locale/de.json b/packages/client/ui-message-feedback/locale/de.json new file mode 100644 index 0000000000..71b5fc4559 --- /dev/null +++ b/packages/client/ui-message-feedback/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Nachrichten-Feedback-Schnittstelle", + "description": "Stellen Sie „Gefällt mir“- und „Gefällt mir nicht“-Schaltflächen für Antworten und Popup-Fenster für Gesprächsfeedback bereit." + } +} diff --git a/packages/client/ui-message-feedback/locale/es.json b/packages/client/ui-message-feedback/locale/es.json new file mode 100644 index 0000000000..26672a96ab --- /dev/null +++ b/packages/client/ui-message-feedback/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de comentarios de mensajes", + "description": "Proporcione botones Me gusta y No me gusta para las ventanas emergentes de respuestas y comentarios de la conversación." + } +} diff --git a/packages/client/ui-message-feedback/locale/fr.json b/packages/client/ui-message-feedback/locale/fr.json new file mode 100644 index 0000000000..71009572ad --- /dev/null +++ b/packages/client/ui-message-feedback/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de retour de messages", + "description": "Fournissez des boutons J'aime et Je n'aime pas pour les réponses et les fenêtres contextuelles de commentaires sur la conversation." + } +} diff --git a/packages/client/ui-message-feedback/locale/pt.json b/packages/client/ui-message-feedback/locale/pt.json new file mode 100644 index 0000000000..dcb779d6b0 --- /dev/null +++ b/packages/client/ui-message-feedback/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de feedback de mensagens", + "description": "Forneça botões de gostar e não gostar para respostas e janelas pop-up de feedback de conversas." + } +} diff --git a/packages/client/ui-message-feedback/locale/ru.json b/packages/client/ui-message-feedback/locale/ru.json new file mode 100644 index 0000000000..388f33bf3a --- /dev/null +++ b/packages/client/ui-message-feedback/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс обратной связи по сообщениям", + "description": "Предоставьте кнопки «Мне нравится» и «Не нравится» для ответов и всплывающих окон обратной связи." + } +} diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index 03e4b05a6c..bcd578c59f 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-message-feedback/src/client/index.ts b/packages/client/ui-message-feedback/src/client/index.ts index 7d95fe469a..0d22230e13 100644 --- a/packages/client/ui-message-feedback/src/client/index.ts +++ b/packages/client/ui-message-feedback/src/client/index.ts @@ -26,7 +26,7 @@ import { FeedbackDialog } from './FeedbackDialog.tsx' import { MessageFeedbackActions } from './MessageFeedbackActions.tsx' import type { FeedbackDialogInjected, MessageFeedbackInjected } from './slots.ts' import { FeedbackSurface } from './surface.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { MessageFeedbackActionFailure, MessageFeedbackActionResult, MessageFeedbackStatus, @@ -66,7 +66,7 @@ export const inject = ['slots', 'remote', 'remote.messageFeedback', 'remote.sess * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-message-feedback: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-message-feedback: dictionaries') const surfaces = new Map() const surfaceFor = (sessionId: SessionId): FeedbackSurface => { diff --git a/packages/client/ui-message-feedback/src/client/locales.ts b/packages/client/ui-message-feedback/src/client/locales.ts index f55054c71d..d5a0fc66e3 100644 --- a/packages/client/ui-message-feedback/src/client/locales.ts +++ b/packages/client/ui-message-feedback/src/client/locales.ts @@ -81,3 +81,123 @@ export const ja = { 'error.generic': 'フィードバックを保存できませんでした', 'error.noteTooLarge': '説明が長すぎます。短くして再送信してください', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'action.like': 'buena respuesta', + 'action.likeActive': 'Eliminar calificación', + 'action.dislike': 'Mala respuesta', + 'action.dislikeActive': 'Eliminar calificación', + 'dialog.title': 'Enviar comentarios', + 'dialog.categories': 'Categoría de comentarios', + 'dialog.detail': 'Detalles de comentarios', + 'dialog.hint': 'Añade detalles para ayudarnos a mejorar. Su envío incluirá el registro de conversación actual.', + 'category.task-result': 'Resultado de la tarea', + 'category.instruction-following': 'Comprensión y seguimiento de instrucciones.', + 'category.product-interaction': 'Características del producto e interacción.', + 'category.service-stability': 'Estabilidad y velocidad', + 'category.resource-cost': 'Uso y costo de recursos', + 'category.security-privacy-permission': 'Seguridad, privacidad y permisos', + 'category.other': 'Otro', + 'toast.recorded': 'Gracias por tus comentarios', + 'error.conflict': 'Esta retroalimentación cambió en otros lugares; se muestra el último estado', + 'error.load': 'No se pudieron cargar los comentarios', + 'error.generic': 'No se pudieron guardar los comentarios', + 'error.noteTooLarge': 'La descripción es demasiado larga; acortarlo y enviarlo de nuevo', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'action.like': 'Bonne réponse', + 'action.likeActive': 'Supprimer la note', + 'action.dislike': 'Mauvaise réponse', + 'action.dislikeActive': 'Supprimer la note', + 'dialog.title': 'Soumettre des commentaires', + 'dialog.categories': 'Catégorie de commentaires', + 'dialog.detail': 'Détails des commentaires', + 'dialog.hint': 'Ajoutez des détails pour nous aider à nous améliorer. Votre soumission inclura le journal de conversation actuel.', + 'category.task-result': 'Résultat de la tâche', + 'category.instruction-following': 'Compréhension et suivi des instructions', + 'category.product-interaction': 'Caractéristiques du produit et interaction', + 'category.service-stability': 'Stabilité et vitesse', + 'category.resource-cost': 'Utilisation et coût des ressources', + 'category.security-privacy-permission': 'Sécurité, confidentialité et autorisations', + 'category.other': 'Autre', + 'toast.recorded': 'Merci pour vos commentaires', + 'error.conflict': 'Cette rétroaction a changé ailleurs ; le dernier état est affiché', + 'error.load': 'Impossible de charger les commentaires', + 'error.generic': "Impossible d'enregistrer les commentaires", + 'error.noteTooLarge': 'La description est trop longue ; raccourcissez-le et soumettez-le à nouveau', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'action.like': 'Gute Resonanz', + 'action.likeActive': 'Bewertung entfernen', + 'action.dislike': 'Schlechte Reaktion', + 'action.dislikeActive': 'Bewertung entfernen', + 'dialog.title': 'Feedback abgeben', + 'dialog.categories': 'Feedback-Kategorie', + 'dialog.detail': 'Feedback-Details', + 'dialog.hint': 'Fügen Sie Details hinzu, um uns bei der Verbesserung zu helfen. Ihre Einreichung enthält das aktuelle Gesprächsprotokoll.', + 'category.task-result': 'Aufgabenergebnis', + 'category.instruction-following': 'Anweisungen verstehen und befolgen', + 'category.product-interaction': 'Produkteigenschaften und Interaktion', + 'category.service-stability': 'Stabilität und Geschwindigkeit', + 'category.resource-cost': 'Ressourcennutzung und Kosten', + 'category.security-privacy-permission': 'Sicherheit, Datenschutz und Berechtigungen', + 'category.other': 'Andere', + 'toast.recorded': 'Vielen Dank für Ihr Feedback', + 'error.conflict': 'Dieses Feedback hat sich an anderer Stelle geändert; der aktuelle Stand wird angezeigt', + 'error.load': 'Feedback konnte nicht geladen werden', + 'error.generic': 'Das Feedback konnte nicht gespeichert werden', + 'error.noteTooLarge': 'Die Beschreibung ist zu lang; kürzen Sie es und reichen Sie es erneut ein', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'action.like': 'Хороший ответ', + 'action.likeActive': 'Удалить оценку', + 'action.dislike': 'Плохой ответ', + 'action.dislikeActive': 'Удалить оценку', + 'dialog.title': 'Отправить отзыв', + 'dialog.categories': 'Категория обратной связи', + 'dialog.detail': 'Подробности обратной связи', + 'dialog.hint': 'Добавьте детали, чтобы помочь нам стать лучше. Ваше сообщение будет включать текущий журнал разговоров.', + 'category.task-result': 'Результат задачи', + 'category.instruction-following': 'Понимание и следование инструкциям', + 'category.product-interaction': 'Особенности продукта и взаимодействие', + 'category.service-stability': 'Стабильность и скорость', + 'category.resource-cost': 'Использование ресурсов и стоимость', + 'category.security-privacy-permission': 'Безопасность, конфиденциальность и разрешения', + 'category.other': 'Другое', + 'toast.recorded': 'Спасибо за ваш отзыв', + 'error.conflict': 'Этот отзыв изменился в другом месте; отображается последнее состояние', + 'error.load': 'Не удалось загрузить отзыв.', + 'error.generic': 'Не удалось сохранить отзыв', + 'error.noteTooLarge': 'Описание слишком длинное; сократите его и отправьте снова', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'action.like': 'Boa resposta', + 'action.likeActive': 'Remover avaliação', + 'action.dislike': 'Resposta ruim', + 'action.dislikeActive': 'Remover avaliação', + 'dialog.title': 'Enviar comentários', + 'dialog.categories': 'Categoria de comentários', + 'dialog.detail': 'Detalhes do feedback', + 'dialog.hint': 'Adicione detalhes para nos ajudar a melhorar. Seu envio incluirá o registro da conversa atual.', + 'category.task-result': 'Resultado da tarefa', + 'category.instruction-following': 'Compreensão e seguimento das instruções', + 'category.product-interaction': 'Recursos e interação do produto', + 'category.service-stability': 'Estabilidade e velocidade', + 'category.resource-cost': 'Uso e custo de recursos', + 'category.security-privacy-permission': 'Segurança, privacidade e permissões', + 'category.other': 'Outro', + 'toast.recorded': 'Obrigado pelo seu feedback', + 'error.conflict': 'Este feedback mudou em outro lugar; o estado mais recente é mostrado', + 'error.load': 'Não foi possível carregar o feedback', + 'error.generic': 'Não foi possível salvar o feedback', + 'error.noteTooLarge': 'A descrição é muito longa; encurte-o e envie novamente', +} satisfies Record diff --git a/packages/client/ui-model-selection/README.i18n.yaml b/packages/client/ui-model-selection/README.i18n.yaml index 7665b701db..177bc09fb2 100644 --- a/packages/client/ui-model-selection/README.i18n.yaml +++ b/packages/client/ui-model-selection/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-model-selection/use-this-package: - en: ef50e68e7493d0a9 - zh: 3b1faea17b606173 + en: 9b1eca0aec81ac29 + zh: 9a1599c3abf76513 /deepseek-ai-dsh-client-ui-model-selection/use-this-package/model-and-effort: - en: aa78900b8856b7eb - zh: b140e3e1218a7c7b + en: 2e6d9965e01dc0ce + zh: 332cc82233eb02f3 /deepseek-ai-dsh-client-ui-model-selection/use-this-package/model-label-extensions: en: bc47871cf8b61451 zh: 17d21da46604f071 diff --git a/packages/client/ui-model-selection/README.md b/packages/client/ui-model-selection/README.md index bcc65faff5..b2c809123f 100644 --- a/packages/client/ui-model-selection/README.md +++ b/packages/client/ui-model-selection/README.md @@ -31,19 +31,19 @@ DeepSeek account and API-key routes appear as separate provider groups, each exp When no model is selected or the saved selection is absent from the catalog, the composer shows “Select model” in regular weight and hides the reasoning effort caption. Clicking it opens the model list directly; Escape and Shift+Tab close it. An empty catalog displays “No models available.” -Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus through root and effort rows, or move the model-list highlight without leaving search. Enter and Tab settle the highlighted model or focused row; Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling focuses model search when shown, otherwise the current model or effort row, and going back lands on the cell that opened the pane left. The composer shows the catalog name while the selected model is available. Removing a model or provider, including account sign-out, preserves the stored provider, model, and reasoning effort without displaying their IDs. Restoring the catalog entry restores its name and effort caption. +Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus through root and effort rows, or move the model-list highlight without leaving search. Enter and Tab settle the highlighted model or focused row; Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling focuses model search when shown on desktop devices; otherwise it focuses the current model or effort row, or the first enabled row when none is selected. Going back focuses the cell that opened the pane. Phones and tablets leave search unfocused until the user taps it or uses keyboard navigation. The composer shows the catalog name while the selected model is available. Removing a model or provider, including account sign-out, preserves the stored provider, model, and reasoning effort without displaying their IDs. Restoring the catalog entry restores its name and effort caption. Mouse selection uses native browser clicks, including their cancellation behavior; a press alone never selects. Opening the root menu focuses its trigger, and clicking the trigger again closes the menu and returns focus there. Root rows and the search-clear button show the same fill for keyboard focus and hover, without a native outline. Selecting a model or effort returns focus to the trigger without a focus ring; leaving the trigger or reopening the menu restores its normal focus indication. While a selection from either entry is pending, focus stays on the trigger, the trigger shows a spinner in place of its chevron, and each row whose value the selection carries shows one in place of its check; a rejected selection leaves the menu open. Tab returns to model search with the current model highlighted when search is shown, otherwise to the current row. ### Model and effort -The button's model menu shows search only when its full catalog contains more than four models; smaller catalogs focus the current model or first row and support keyboard navigation directly. The `/model` command always keeps its search field. Both search fields match model names case-insensitively, including nonconsecutive characters in order; leading and trailing spaces are ignored. Within each provider, prefix matches come first, followed by alignment score and then catalog order. Empty groups are hidden. Empty catalogs and queries without matches are announced through a status region. Search keeps focus while arrow keys cycle the highlighted result across provider groups; Enter or Tab selects it. Left and right arrows keep their native caret behavior. Opening highlights the current model or the first available row, and editing the query resets the highlight to the first result. Reopening the model pane clears the query. +The button's model menu shows search only when its full catalog contains more than four models; smaller catalogs support keyboard navigation directly and focus the current model or first enabled row on all devices. The `/model` command always keeps its search field. Both search fields match model names case-insensitively, including nonconsecutive characters in order; leading and trailing spaces are ignored. Within each provider, prefix matches come first, followed by alignment score and then catalog order. Empty groups are hidden. Empty catalogs and queries without matches are announced through a status region. Search keeps focus while arrow keys cycle the highlighted result across provider groups; Enter or Tab selects it. Left and right arrows keep their native caret behavior. Opening highlights the current model or the first available row, and editing the query resets the highlight to the first result. Reopening the model pane clears the query. Model and reasoning-effort names use weight 400 (regular) in the composer menu, including the selected item. The search field follows the command popup's compact treatment with transparent background and border in both palettes, with no leading icon and a caption-tone placeholder. The clear button appears for a nonempty query and restores the full list with focus in the search field. Both entries group models by provider, with DeepSeek Account first and DeepSeek second; third-party providers retain their catalog order. Both use the shared, asynchronously observed sticky headings from [ui-primitives](../ui-primitives/README.md#understand-the-implementation): transparent at rest, with the theme's 94%-opaque fill only while pinned, and `md` corners outside macOS Desktop. The composer menu shows model and effort names only. Navigation chevrons use `--dsw-alias-menu-icon`. The `/model` popup shows provider names as group headings and model names as rows, without repeating the provider on each row or showing catalog descriptions. Its search placeholder, no-match text, and empty-catalog text use the same localized labels as the composer model menu. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input. -The composer replaces the model and effort text with the Models icon when the expanded controls cannot share one line, and restores the text when space permits. The full selection remains available in the trigger's accessible name, tooltip, and menu. Model and effort text that does not fit its space, in the trigger, the root menu values, and the option rows, is clipped and fades out over its last 24px instead of ending in an ellipsis; browsers without scroll-driven animations keep the ellipsis. +The composer replaces the model and effort text with the Models icon when the expanded controls cannot share one line, and restores the text when space permits. The full selection remains available in the trigger's accessible name, tooltip, and menu. ### Model label extensions diff --git a/packages/client/ui-model-selection/README.zh.md b/packages/client/ui-model-selection/README.zh.md index b02f103add..e99a1e05ce 100644 --- a/packages/client/ui-model-selection/README.zh.md +++ b/packages/client/ui-model-selection/README.zh.md @@ -31,19 +31,19 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示 未选择模型或已保存的选择不在目录中时,composer 以常规字重显示“请选择模型”,并隐藏推理强度文案。点击后直接打开模型列表,Escape 和 Shift+Tab 关闭列表。目录为空时显示“暂无可用模型” -与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在根菜单和推理等级行间移动焦点,或在模型列表中移动高亮并让焦点保留在搜索框。Enter 和 Tab 选定高亮模型或聚焦行;Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻时若显示模型搜索框则聚焦搜索框,否则聚焦当前模型或推理强度行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称。模型或提供方被删除时(包括账号退登),保留已保存的提供方、模型和推理强度,但不展示其 ID。目录条目恢复后重新显示名称和推理强度。 +与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在根菜单和推理等级行间移动焦点,或在模型列表中移动高亮并让焦点保留在搜索框。Enter 和 Tab 选定高亮模型或聚焦行;Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻时,桌面设备在显示模型搜索框时聚焦搜索框;其他情况聚焦当前模型或推理强度行,没有选中项时聚焦首个可用行。返回时聚焦打开该面板的格子。手机和平板在用户主动点按搜索框或使用键盘导航前,不会聚焦搜索框。所选模型可用时,composer 显示目录中的模型名称。模型或提供方被删除时(包括账号退登),保留已保存的提供方、模型和推理强度,但不展示其 ID。目录条目恢复后重新显示名称和推理强度。 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开根菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。根菜单行和搜索清除按钮的键盘焦点与悬停使用相同底色,不显示原生焦点轮廓。选定模型或推理等级后,焦点返回触发按钮但不显示焦点环;焦点离开按钮或重新打开菜单后恢复正常焦点提示。等待任一入口发起的选择结果时,焦点停在触发按钮,触发按钮以加载图标代替下拉箭头,该选择包含的值所在行以加载图标代替勾选标记;选择被拒绝后菜单保持打开。显示模型搜索框时,Tab 返回搜索框并高亮当前模型,否则返回当前选中行。 ### 模型与推理强度 -按钮模型菜单仅在完整目录超过四个模型时显示搜索框;较小目录聚焦当前模型或首行,直接支持键盘导航。`/model` 命令始终保留搜索框。两个搜索框均按模型名称进行不区分大小写的匹配,支持按顺序输入不连续字符,并忽略首尾空格。每个提供方内的结果优先按前缀匹配、其次按匹配得分、最后按目录顺序排列。没有匹配项的分组会隐藏。空目录和无匹配结果均通过状态区域通知屏幕阅读器。方向键在各提供方分组的结果间循环移动高亮,焦点保留在搜索框;Enter 或 Tab 选定高亮项。左右方向键仍用于移动输入光标。打开时高亮当前模型或首个可用选项,修改搜索内容后高亮首个结果。重新打开模型面板会清空搜索内容。 +按钮模型菜单仅在完整目录超过四个模型时显示搜索框;较小目录直接支持键盘导航,并在所有设备上聚焦当前模型或首个可用行。`/model` 命令始终保留搜索框。两个搜索框均按模型名称进行不区分大小写的匹配,支持按顺序输入不连续字符,并忽略首尾空格。每个提供方内的结果优先按前缀匹配、其次按匹配得分、最后按目录顺序排列。没有匹配项的分组会隐藏。空目录和无匹配结果均通过状态区域通知屏幕阅读器。方向键在各提供方分组的结果间循环移动高亮,焦点保留在搜索框;Enter 或 Tab 选定高亮项。左右方向键仍用于移动输入光标。打开时高亮当前模型或首个可用选项,修改搜索内容后高亮首个结果。重新打开模型面板会清空搜索内容。 composer 菜单中的模型名称和推理等级均使用 400(regular)字重,包括选中项。搜索框沿用 command 弹窗的紧凑样式,在浅/深色主题下均保持背景和边框透明,不显示前置图标,占位文字使用 caption 色。搜索内容非空时显示清除按钮;点击后恢复完整列表,并将焦点留在搜索框。 两个入口均按提供方分组,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。两者采用 [ui-primitives](../ui-primitives/README.zh.md#understand-the-implementation) 异步观察的共享吸顶标题:原位透明,仅吸顶时使用主题的 94% 不透明填充,macOS 桌面端以外使用 `md` 圆角。composer 菜单只显示模型与推理强度名称。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗以提供方名称作为分组标题、模型名称作为选项行,不在每行重复提供方,也不显示目录说明。搜索占位文字、无匹配提示和空目录提示与 composer 模型菜单共用同一组本地化文案。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。 -展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。触发按钮、根菜单当前值和选项行中放不下的模型与推理强度文字会被裁切,并在末尾 24px 内渐隐,不显示省略号;不支持滚动驱动动画的浏览器仍显示省略号。 +展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。 ### 模型名称扩展 diff --git a/packages/client/ui-model-selection/locale/de.json b/packages/client/ui-model-selection/locale/de.json new file mode 100644 index 0000000000..c37cd4ec24 --- /dev/null +++ b/packages/client/ui-model-selection/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Modellauswahl", + "description": "Sehen Sie sich die verfügbaren Modelle an und wechseln Sie das von der aktuellen Sitzung verwendete Modell." + } +} diff --git a/packages/client/ui-model-selection/locale/es.json b/packages/client/ui-model-selection/locale/es.json new file mode 100644 index 0000000000..4ce95350a1 --- /dev/null +++ b/packages/client/ui-model-selection/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de modelo", + "description": "Vea los modelos disponibles y cambie el modelo utilizado por la sesión actual." + } +} diff --git a/packages/client/ui-model-selection/locale/fr.json b/packages/client/ui-model-selection/locale/fr.json new file mode 100644 index 0000000000..bc3548d52e --- /dev/null +++ b/packages/client/ui-model-selection/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de modèle", + "description": "Affichez les modèles disponibles et changez le modèle utilisé par la session en cours." + } +} diff --git a/packages/client/ui-model-selection/locale/pt.json b/packages/client/ui-model-selection/locale/pt.json new file mode 100644 index 0000000000..17caef7c92 --- /dev/null +++ b/packages/client/ui-model-selection/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de modelo", + "description": "Visualize os modelos disponíveis e alterne o modelo usado pela sessão atual." + } +} diff --git a/packages/client/ui-model-selection/locale/ru.json b/packages/client/ui-model-selection/locale/ru.json new file mode 100644 index 0000000000..572c9b5541 --- /dev/null +++ b/packages/client/ui-model-selection/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора модели", + "description": "Просмотрите доступные модели и переключите модель, используемую в текущем сеансе." + } +} diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 51277d84c6..594bef2c3a 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.module.css b/packages/client/ui-model-selection/src/client/ModelSelect.module.css index 02e802819d..76532308c9 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.module.css +++ b/packages/client/ui-model-selection/src/client/ModelSelect.module.css @@ -70,31 +70,6 @@ flex: 0 0 auto; } -/* Clipped selector text fades over its last 24px instead of ending in an - ellipsis. A self scroll timeline is active only while the box overflows, - so the mask never touches text that fits; browsers without scroll-driven - animations keep the ellipsis. A mask rather than a painted gradient, so - the fade matches the trigger's bare and hover fills and every menu row. */ -@supports (animation-timeline: scroll()) { - .triggerLabel, - .triggerEffort, - .modelName, - .cellValue { - text-overflow: clip; - /* Longhands only: the build folds a full set into the `animation` - shorthand, which cannot carry a timeline. */ - animation-name: clipped-fade; - animation-fill-mode: both; - animation-timeline: scroll(self inline); - } -} - -@keyframes clipped-fade { - from, to { - mask-image: linear-gradient(to right, black calc(100% - 24px), transparent); - } -} - /* The composer measures expanded row demand and owns these display variables. Outside a composer, the trigger retains its text. */ .triggerLabel, diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index c497bcbe77..59dbf85427 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -12,7 +12,9 @@ * search field. Provider headings paint their background only while pinned * by scrolling. Clearing a query restores the full list and search focus. * Selecting restores trigger focus without a ring until the trigger loses focus - * or the menu reopens. Model names match a case-insensitive ordered subsequence + * or the menu reopens. Mobile devices restore button focus when opening or + * drilling; model search takes focus only after explicit user input. + * Model names match a case-insensitive ordered subsequence * within each provider group, ranked by * prefix, alignment score, then catalog order. Returning to the root pane * hands focus back to the cell that opened it. Data and submission ride the @@ -35,7 +37,7 @@ import clsx from 'clsx' import type { ModelReasoningEffort, ModelSelection } from '@deepseek-ai/dsh-api-remotes/client' import { IconCheckOutlineRegular, IconChevronDownOutlineRegular, IconChevronRightOutlineRegular, IconCloseFillRegular, - IconDataOutlineRegular, IconWarningOutlineRegular, Input, rankByName, StateDot, Toast, + IconDataOutlineRegular, IconWarningOutlineRegular, Input, isMobileDevice, rankByName, StateDot, Toast, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { ModelLabelOwner, ModelSelectInjected } from './slots.ts' @@ -184,7 +186,7 @@ export function ModelSelect( paneFocus.current = null if (!open || intent === null) return if (intent === 'drill') { - if (pane === 'model' && showSearch) { + if (pane === 'model' && showSearch && !isMobileDevice()) { searchRef.current?.focus() return } diff --git a/packages/client/ui-model-selection/src/client/index.ts b/packages/client/ui-model-selection/src/client/index.ts index ae47f156ed..f1f15a1154 100644 --- a/packages/client/ui-model-selection/src/client/index.ts +++ b/packages/client/ui-model-selection/src/client/index.ts @@ -27,7 +27,7 @@ import type { ModelDirectoryState } from './directory.ts' import { ModelDirectoryResolver } from './service.ts' import type { ModelSelectInjected } from './slots.ts' import { ModelSelect } from './ModelSelect.tsx' -import { en, ja, zh, type ModelKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ModelKey } from './locales.ts' import { orderModelProviders } from './provider-order.ts' export { ModelDirectory } from './directory.ts' @@ -113,7 +113,7 @@ export const inject = ['commandUi', 'locale', 'sessions', 'slots', 'remote', 're * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-model-selection: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-model-selection: dictionaries') // Non-slot faces (the command description, the popup option builder) read // through the bound translate; the seat component reads the standard seat. diff --git a/packages/client/ui-model-selection/src/client/locales.ts b/packages/client/ui-model-selection/src/client/locales.ts index 4e931226ea..cb2749f9a9 100644 --- a/packages/client/ui-model-selection/src/client/locales.ts +++ b/packages/client/ui-model-selection/src/client/locales.ts @@ -91,3 +91,138 @@ export const ja = { 'empty.models': '利用可能なモデルがありません。', 'empty.efforts': 'このモデルには推論強度の設定がありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'provider.account': 'Cuenta DeepSeek', + 'command.label': 'modelo', + 'command.description': 'Selecciona el modelo para esta conversación', + 'option.loadError': 'No se pudo cargar el catálogo: {message}', + 'trigger.fallback': 'Seleccionar modelo', + 'trigger.loading': 'Cargando modelos…', + 'trigger.selectAria': 'Seleccionar modelo', + 'trigger.aria': 'Seleccionar modelo, actual {model}', + 'trigger.ariaEffort': 'Seleccionar modelo, {model} actual, esfuerzo de razonamiento {effort}', + 'menu.aria': 'Esfuerzo de modelo y razonamiento.', + 'menu.model': 'modelo', + 'menu.effort': 'Esfuerzo', + 'effort.providerDefault': 'Predeterminado', + 'status.loading': 'Lista de modelos refrescante...', + 'error.action': 'Error en la operación del modelo: {message}', + 'error.sessionInUse': 'Esta sesión ya está en uso, posiblemente por otra instancia de DSH en ejecución (como la web dsh o la aplicación de escritorio). Salga de otras instancias de DSH en ejecución y vuelva a intentarlo.', + 'action.reload': 'recargar', + 'warning.groupLoad': '{name} no pudo cargar: {message}', + 'search.placeholder': 'Buscar modelos…', + 'search.clear': 'Borrar búsqueda', + 'search.empty': 'No hay modelos coincidentes.', + 'empty.models': 'No hay modelos disponibles.', + 'empty.efforts': 'Este modelo no proporciona niveles de esfuerzo de razonamiento.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'provider.account': 'Compte DeepSeek', + 'command.label': 'Modèle', + 'command.description': 'Sélectionnez le modèle pour cette conversation', + 'option.loadError': 'Échec du chargement du catalogue : {message}', + 'trigger.fallback': 'Sélectionnez le modèle', + 'trigger.loading': 'Chargement des modèles…', + 'trigger.selectAria': 'Sélectionnez le modèle', + 'trigger.aria': 'Sélectionnez le modèle, {model} actuel', + 'trigger.ariaEffort': 'Sélectionnez le modèle, {model} actuel, effort de raisonnement {effort}', + 'menu.aria': 'Effort de modélisation et de raisonnement', + 'menu.model': 'Modèle', + 'menu.effort': 'Effort', + 'effort.providerDefault': 'Par défaut', + 'status.loading': 'Liste des modèles actualisée…', + 'error.action': "Échec de l'opération du modèle : {message}", + 'error.sessionInUse': "Cette session est déjà utilisée, éventuellement par une autre instance DSH en cours d'exécution (telle que le Web dsh ou l'application de bureau). Quittez les autres instances DSH en cours d'exécution et réessayez.", + 'action.reload': 'Recharger', + 'warning.groupLoad': 'Échec du chargement de {name} : {message}', + 'search.placeholder': 'Rechercher des modèles…', + 'search.clear': 'Effacer la recherche', + 'search.empty': 'Aucun modèle correspondant.', + 'empty.models': 'Aucun modèle disponible.', + 'empty.efforts': 'Ce modèle ne fournit aucun niveau d’effort de raisonnement.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'provider.account': 'DeepSeek-Konto', + 'command.label': 'Modell', + 'command.description': 'Wählen Sie das Modell für dieses Gespräch aus', + 'option.loadError': 'Katalog konnte nicht geladen werden: {message}', + 'trigger.fallback': 'Modell auswählen', + 'trigger.loading': 'Modelle werden geladen…', + 'trigger.selectAria': 'Modell auswählen', + 'trigger.aria': 'Modell auswählen, aktuell {model}', + 'trigger.ariaEffort': 'Modell auswählen, aktuell {model}, Argumentationsaufwand {effort}', + 'menu.aria': 'Modell- und Argumentationsaufwand', + 'menu.model': 'Modell', + 'menu.effort': 'Aufwand', + 'effort.providerDefault': 'Standard', + 'status.loading': 'Erfrischende Modellliste…', + 'error.action': 'Modellvorgang fehlgeschlagen: {message}', + 'error.sessionInUse': 'Diese Sitzung wird bereits verwendet, möglicherweise von einer anderen laufenden DSH-Instanz (z. B. dsh Web oder der Desktop-App). Beenden Sie andere laufende DSH-Instanzen und versuchen Sie es erneut.', + 'action.reload': 'Neu laden', + 'warning.groupLoad': '{name} konnte nicht geladen werden: {message}', + 'search.placeholder': 'Modelle suchen…', + 'search.clear': 'Suche löschen', + 'search.empty': 'Keine passenden Modelle.', + 'empty.models': 'Keine Modelle verfügbar.', + 'empty.efforts': 'Dieses Modell liefert keine Argumentationsaufwandsstufen.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'provider.account': 'DeepSeek Аккаунт', + 'command.label': 'Модель', + 'command.description': 'Выберите модель для этого разговора', + 'option.loadError': 'Не удалось загрузить каталог: {message}.', + 'trigger.fallback': 'Выберите модель', + 'trigger.loading': 'Загрузка моделей…', + 'trigger.selectAria': 'Выберите модель', + 'trigger.aria': 'Выберите модель, текущую {model}', + 'trigger.ariaEffort': 'Выберите модель, текущую версию {model}, аргументацию {effort}', + 'menu.aria': 'Модель и рассуждения', + 'menu.model': 'Модель', + 'menu.effort': 'Усилие', + 'effort.providerDefault': 'По умолчанию', + 'status.loading': 'Обновление списка моделей…', + 'error.action': 'Не удалось выполнить операцию модели: {message}.', + 'error.sessionInUse': 'Этот сеанс уже используется, возможно, другим работающим экземпляром DSH (например, веб-сайтом dsh или настольным приложением). Закройте другие работающие экземпляры DSH и повторите попытку.', + 'action.reload': 'Перезагрузить', + 'warning.groupLoad': '{name} не удалось загрузить: {message}', + 'search.placeholder': 'Поиск моделей…', + 'search.clear': 'Очистить поиск', + 'search.empty': 'Нет подходящих моделей.', + 'empty.models': 'Нет доступных моделей.', + 'empty.efforts': 'Эта модель не обеспечивает обоснования уровней усилий.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'provider.account': 'DeepSeek Conta', + 'command.label': 'Modelo', + 'command.description': 'Selecione o modelo para esta conversa', + 'option.loadError': 'Falha ao carregar o catálogo: {message}', + 'trigger.fallback': 'Selecione o modelo', + 'trigger.loading': 'Carregando modelos…', + 'trigger.selectAria': 'Selecione o modelo', + 'trigger.aria': 'Selecione o modelo, atual {model}', + 'trigger.ariaEffort': 'Selecione modelo, atual {model}, esforço de raciocínio {effort}', + 'menu.aria': 'Modelo e esforço de raciocínio', + 'menu.model': 'Modelo', + 'menu.effort': 'Esforço', + 'effort.providerDefault': 'Padrão', + 'status.loading': 'Atualizando lista de modelos…', + 'error.action': 'Falha na operação do modelo: {message}', + 'error.sessionInUse': 'Esta sessão já está em uso, possivelmente por outra instância DSH em execução (como dsh web ou aplicativo de desktop). Saia de outras instâncias DSH em execução e tente novamente.', + 'action.reload': 'Recarregar', + 'warning.groupLoad': '{name} falhou ao carregar: {message}', + 'search.placeholder': 'Pesquisar modelos…', + 'search.clear': 'Limpar pesquisa', + 'search.empty': 'Não há modelos correspondentes.', + 'empty.models': 'Nenhum modelo disponível.', + 'empty.efforts': 'Este modelo não fornece níveis de esforço de raciocínio.', +} satisfies Record diff --git a/packages/client/ui-model-selection/tests/model-select.client.spec.tsx b/packages/client/ui-model-selection/tests/model-select.client.spec.tsx index 54c606f636..bb4832a0ce 100644 --- a/packages/client/ui-model-selection/tests/model-select.client.spec.tsx +++ b/packages/client/ui-model-selection/tests/model-select.client.spec.tsx @@ -1,4 +1,5 @@ // @vitest-environment jsdom +import * as uiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { RemoteError, SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' @@ -457,6 +458,71 @@ describe('ModelSelect keyboard walk', () => { return select } + it.each([ + { device: 'iPhone', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1', platform: 'iPhone', maxTouchPoints: 5 }, + { device: 'desktop-mode iPad', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 5 }, + ].flatMap(device => [1, 5].map(modelCount => ({ ...device, modelCount }))))( + 'retains menu keyboard focus on $device with $modelCount models without automatically focusing search', + async ({ userAgent, platform, maxTouchPoints, modelCount }) => { + for (const [name, value] of Object.entries({ userAgent, platform, maxTouchPoints })) { + const descriptor = Object.getOwnPropertyDescriptor(navigator, name) + Object.defineProperty(navigator, name, { configurable: true, value }) + onTestFinished(() => { + if (descriptor === undefined) Reflect.deleteProperty(navigator, name) + else Object.defineProperty(navigator, name, descriptor) + }) + } + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const select = vi.fn() + render() + const trigger = screen.getByRole('button', { name: /选择模型/ }) + fireEvent.click(trigger) + expect(document.activeElement).toBe(trigger) + + const modelCell = screen.getByRole('menuitem', { name: /^模型/ }) + const effortCell = screen.getByRole('menuitem', { name: /推理等级/ }) + fireEvent.keyDown(trigger, { key: 'ArrowDown' }) + fireEvent.keyDown(modelCell, { key: 'ArrowDown' }) + expect(document.activeElement).toBe(effortCell) + fireEvent.keyDown(effortCell, { key: 'Tab' }) + const high = screen.getByRole('menuitemradio', { name: 'High' }) + const max = screen.getByRole('menuitemradio', { name: /Max/ }) + expect(document.activeElement).toBe(high) + fireEvent.keyDown(high, { key: 'ArrowDown' }) + expect(document.activeElement).toBe(max) + fireEvent.keyDown(max, { key: 'Escape' }) + + const restoredEffortCell = screen.getByRole('menuitem', { name: /推理等级/ }) + expect(document.activeElement).toBe(restoredEffortCell) + fireEvent.keyDown(restoredEffortCell, { key: 'ArrowUp' }) + const restoredModelCell = screen.getByRole('menuitem', { name: /^模型/ }) + expect(document.activeElement).toBe(restoredModelCell) + fireEvent.keyDown(restoredModelCell, { key: 'Tab' }) + const model = screen.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash' }) + expect(document.activeElement).toBe(model) + if (modelCount > 4) { + const search = screen.getByRole('searchbox') + expect(document.activeElement).not.toBe(search) + fireEvent.keyDown(model, { key: 'ArrowDown' }) + expect(document.activeElement).toBe(search) + } else { + expect(screen.queryByRole('searchbox')).toBeNull() + } + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) + expect(document.activeElement).toBe(screen.getByRole('menuitem', { name: /^模型/ })) + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) + expect(screen.queryByRole('menu')).toBeNull() + await waitFor(() => { expect(document.activeElement).toBe(trigger) }) + expect(select).not.toHaveBeenCalled() + }, + ) + it.each(['model', 'effort'])('prevents button mousedown defaults in the %s pane without selecting it', (pane) => { const select = mountOpen() const cell = screen.getByRole('menuitem', { name: pane === 'model' ? /^模型/ : /推理等级/ }) diff --git a/packages/client/ui-open-in-app/locale/de.json b/packages/client/ui-open-in-app/locale/de.json new file mode 100644 index 0000000000..f5b24225cb --- /dev/null +++ b/packages/client/ui-open-in-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zum Öffnen externer Anwendungen", + "description": "Bietet den Eintrag „Öffnen mit“ in der Sitzungs- und Dokumentvorschau." + } +} diff --git a/packages/client/ui-open-in-app/locale/es.json b/packages/client/ui-open-in-app/locale/es.json new file mode 100644 index 0000000000..1164cb3c6b --- /dev/null +++ b/packages/client/ui-open-in-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de apertura de aplicaciones externas", + "description": "Proporciona entrada \"Abrir con\" en la sesión y vista previa del documento." + } +} diff --git a/packages/client/ui-open-in-app/locale/fr.json b/packages/client/ui-open-in-app/locale/fr.json new file mode 100644 index 0000000000..5f104625e3 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'ouverture d'application externe", + "description": "Fournit une entrée « Ouvrir avec » dans la session et un aperçu du document." + } +} diff --git a/packages/client/ui-open-in-app/locale/pt.json b/packages/client/ui-open-in-app/locale/pt.json new file mode 100644 index 0000000000..105759e7e0 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de abertura de aplicativo externo", + "description": "Fornece entrada \"Abrir com\" na sessão e visualização do documento." + } +} diff --git a/packages/client/ui-open-in-app/locale/ru.json b/packages/client/ui-open-in-app/locale/ru.json new file mode 100644 index 0000000000..2655015ac7 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс открытия внешнего приложения", + "description": "Обеспечивает запись «Открыть с помощью» в сеансе и предварительном просмотре документа." + } +} diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index 63e93267b7..6088f56dd7 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/src/client/index.ts b/packages/client/ui-open-in-app/src/client/index.ts index db99a2a23e..8da9f15f9e 100644 --- a/packages/client/ui-open-in-app/src/client/index.ts +++ b/packages/client/ui-open-in-app/src/client/index.ts @@ -25,7 +25,7 @@ import { OpenInAppPathController } from './open-path.ts' import { OpenPathAction, type OpenPathInjected } from './OpenPathAction.tsx' import { FileRouteAction } from './FileRouteAction.tsx' import { OpenPathEmptyAction } from './OpenPathEmptyAction.tsx' -import { en, ja, NS, zh, type OpenInAppKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type OpenInAppKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -51,7 +51,7 @@ export function apply(ctx: ClientContext): void { const controller = new OpenInAppController() void controller.load() const paths = new OpenInAppPathController(ctx.remote) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'open-in-app: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'open-in-app: dictionaries') const t = ctx.locale.bind(NS) const target = () => { if (ctx.layout.panelInfo.getSnapshot().activePanelId !== null) return undefined diff --git a/packages/client/ui-open-in-app/src/client/locales.ts b/packages/client/ui-open-in-app/src/client/locales.ts index f43d4746bb..737b027d00 100644 --- a/packages/client/ui-open-in-app/src/client/locales.ts +++ b/packages/client/ui-open-in-app/src/client/locales.ts @@ -99,3 +99,103 @@ export const ja = { 'app.filemanager': 'ファイル', 'app.terminal': 'ターミナル', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'open.title': 'Abrir en {app}', + 'path.appDefault': '{app} (predeterminado)', + 'path.appsError': 'No se pudieron cargar aplicaciones', + 'shortcut.busy': 'Abrir espacio de trabajo', + 'shortcut.unavailable': 'El espacio de trabajo actual o la aplicación local no están disponibles', + 'open.tooltip': 'Abierto localmente', + 'path.open': 'Abrir', + 'path.more': 'Más formas de abrir', + 'path.reveal': 'Mostrar ubicación del archivo', + 'path.openError': 'No se pudo abrir. Intentar otra vez.', + 'path.revealError': 'No se pudo mostrar la ubicación del archivo. Intentar otra vez.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorador de archivos', + 'app.filemanager': 'Archivos', + 'app.terminal': 'terminales', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'open.title': 'Ouvrir dans {app}', + 'path.appDefault': '{app} (par défaut)', + 'path.appsError': 'Impossible de charger les applications', + 'shortcut.busy': "Ouverture de l'espace de travail", + 'shortcut.unavailable': 'Espace de travail actuel ou application locale indisponible', + 'open.tooltip': 'Ouvrir localement', + 'path.open': 'Ouvrir', + 'path.more': "Plus de façons d'ouvrir", + 'path.reveal': "Afficher l'emplacement du fichier", + 'path.openError': "Impossible d'ouvrir. Essayer à nouveau.", + 'path.revealError': "Impossible d'afficher l'emplacement du fichier. Essayer à nouveau.", + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorateur de fichiers', + 'app.filemanager': 'Fichiers', + 'app.terminal': 'Borne', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'open.title': 'In {app} öffnen', + 'path.appDefault': '{app} (Standard)', + 'path.appsError': 'Anwendungen konnten nicht geladen werden', + 'shortcut.busy': 'Arbeitsbereich öffnen', + 'shortcut.unavailable': 'Aktueller Arbeitsbereich oder lokale Anwendung nicht verfügbar', + 'open.tooltip': 'Vor Ort geöffnet', + 'path.open': 'Öffnen', + 'path.more': 'Mehr Möglichkeiten zum Öffnen', + 'path.reveal': 'Dateispeicherort anzeigen', + 'path.openError': 'Konnte nicht geöffnet werden. Versuchen Sie es erneut.', + 'path.revealError': 'Der Dateispeicherort konnte nicht angezeigt werden. Versuchen Sie es erneut.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Datei-Explorer', + 'app.filemanager': 'Dateien', + 'app.terminal': 'Terminal', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'open.title': 'Открыть в {app}', + 'path.appDefault': '{app} (по умолчанию)', + 'path.appsError': 'Не удалось загрузить приложения', + 'shortcut.busy': 'Открытие рабочей области', + 'shortcut.unavailable': 'Текущая рабочая область или локальное приложение недоступно.', + 'open.tooltip': 'Открыть локально', + 'path.open': 'Открыть', + 'path.more': 'Больше способов открыть', + 'path.reveal': 'Показать местоположение файла', + 'path.openError': 'Не удалось открыть. Попробуйте еще раз.', + 'path.revealError': 'Не удалось показать местоположение файла. Попробуйте еще раз.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Проводник файлов', + 'app.filemanager': 'Файлы', + 'app.terminal': 'Терминал', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'open.title': 'Abrir em {app}', + 'path.appDefault': '{app} (padrão)', + 'path.appsError': 'Não foi possível carregar aplicativos', + 'shortcut.busy': 'Abrindo espaço de trabalho', + 'shortcut.unavailable': 'Espaço de trabalho atual ou aplicativo local indisponível', + 'open.tooltip': 'Abrir localmente', + 'path.open': 'Abrir', + 'path.more': 'Mais maneiras de abrir', + 'path.reveal': 'Mostrar localização do arquivo', + 'path.openError': 'Não foi possível abrir. Tente novamente.', + 'path.revealError': 'Não foi possível mostrar o local do arquivo. Tente novamente.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorador de arquivos', + 'app.filemanager': 'Arquivos', + 'app.terminal': 'Terminais', +} satisfies Record diff --git a/packages/client/ui-permission-presets/locale/de.json b/packages/client/ui-permission-presets/locale/de.json new file mode 100644 index 0000000000..0f2614b5c2 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle für den Berechtigungsmodus", + "description": "Legt die Standardberechtigungen für neue Sitzungen fest und wechselt den Berechtigungsmodus für die aktuelle Sitzung." + } +} diff --git a/packages/client/ui-permission-presets/locale/es.json b/packages/client/ui-permission-presets/locale/es.json new file mode 100644 index 0000000000..aed991a5c6 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de modo de permiso", + "description": "Establece los permisos predeterminados para nuevas sesiones y cambia el modo de permisos para la sesión actual." + } +} diff --git a/packages/client/ui-permission-presets/locale/fr.json b/packages/client/ui-permission-presets/locale/fr.json new file mode 100644 index 0000000000..8119ff4849 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface du mode autorisation", + "description": "Définit les autorisations par défaut pour les nouvelles sessions et change le mode d'autorisations pour la session en cours." + } +} diff --git a/packages/client/ui-permission-presets/locale/pt.json b/packages/client/ui-permission-presets/locale/pt.json new file mode 100644 index 0000000000..fc65c0a5d0 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface do modo de permissão", + "description": "Define as permissões padrão para novas sessões e alterna o modo de permissões para a sessão atual." + } +} diff --git a/packages/client/ui-permission-presets/locale/ru.json b/packages/client/ui-permission-presets/locale/ru.json new file mode 100644 index 0000000000..74ad24f1d4 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс режима разрешений", + "description": "Устанавливает разрешения по умолчанию для новых сеансов и переключает режим разрешений для текущего сеанса." + } +} diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index 279acebe6c..8e5a256421 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/src/client/index.ts b/packages/client/ui-permission-presets/src/client/index.ts index 940b5baa3f..8f4d98c854 100644 --- a/packages/client/ui-permission-presets/src/client/index.ts +++ b/packages/client/ui-permission-presets/src/client/index.ts @@ -41,7 +41,7 @@ import type { PermissionSelectInjected } from './PermissionSelect.tsx' import { PermissionRow } from './PermissionRow.tsx' import type { PermissionRowInjected } from './PermissionRow.tsx' import { - accessEn, accessJa, accessZh, en, ja, PERMISSION_ACCESS_NS, zh, + accessEn, accessJa, accessEs, accessFr, accessDe, accessRu, accessPt, accessZh, en, ja, es, fr, de, ru, pt, PERMISSION_ACCESS_NS, zh, } from './locales.ts' import { AUTO_REVIEW_PRESET, displayPermissionPreset, FULL_ACCESS_PRESET, @@ -113,7 +113,16 @@ function optionsOf( export function apply(ctx: ClientContext): void { const command = ctx.get('commandUi') as CommandUiContract const sessions = ctx.sessions - const accessDictionaries = { zh: accessZh, en: accessEn, ja: accessJa } + const accessDictionaries = { + zh: accessZh, + en: accessEn, + ja: accessJa, + es: accessEs, + fr: accessFr, + de: accessDe, + ru: accessRu, + pt: accessPt, + } ctx.effect( () => ctx.locale.register(PERMISSION_ACCESS_NS, accessDictionaries), 'ui-permission: current-session dictionaries', @@ -142,7 +151,7 @@ export function apply(ctx: ClientContext): void { 'ui-permission: dismiss stale slash choices', ) - ctx.effect(() => ctx.locale.register('settings.permission', { zh, en, ja }), 'ui-permission: settings row dictionaries') + ctx.effect(() => ctx.locale.register('settings.permission', { zh, en, ja, es, fr, de, ru, pt }), 'ui-permission: settings row dictionaries') // The shared ConfigForm mirror updates after document commits and reconnects. const controller = new PermissionPresetSettingsController( diff --git a/packages/client/ui-permission-presets/src/client/locales.ts b/packages/client/ui-permission-presets/src/client/locales.ts index 792c1d5605..9fe5025f87 100644 --- a/packages/client/ui-permission-presets/src/client/locales.ts +++ b/packages/client/ui-permission-presets/src/client/locales.ts @@ -99,6 +99,86 @@ export const ja = { 'confirm.enable': 'フルアクセスを有効にする', } satisfies Record +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'title': 'Permiso', + 'description': 'Elija el modo de permiso predeterminado para nuevas sesiones', + 'loading': 'Cargando', + 'unavailable': 'No disponible', + 'preset.readOnly': 'Sólo lectura', + 'preset.workspaceWrite': 'Escritura en el espacio de trabajo', + 'preset.fullAccess': 'Acceso completo', + 'confirm.title': '¿Habilitar acceso completo?', + 'confirm.description': 'El acceso completo permite que las nuevas sesiones reduzcan los pasos de confirmación y realicen más acciones directamente, incluidas operaciones confidenciales, cambios de archivos o comandos externos. Úselo sólo cuando confíe en las tareas posteriores.', + 'confirm.acknowledge': 'Entiendo los riesgos y quiero continuar.', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Habilitar acceso completo', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'title': 'Autorisation', + 'description': "Choisissez le mode d'autorisation par défaut pour les nouvelles sessions", + 'loading': 'Chargement', + 'unavailable': 'Indisponible', + 'preset.readOnly': 'Lecture seule', + 'preset.workspaceWrite': "Écriture dans l'espace de travail", + 'preset.fullAccess': 'Accès complet', + 'confirm.title': "Activer l'accès complet ?", + 'confirm.description': "L'accès complet permet aux nouvelles sessions de réduire les étapes de confirmation et d'effectuer directement davantage d'actions, notamment des opérations sensibles, des modifications de fichiers ou des commandes externes. Utilisez-le uniquement lorsque vous faites confiance aux tâches suivantes.", + 'confirm.acknowledge': 'Je comprends les risques et je veux continuer', + 'confirm.cancel': 'Annuler', + 'confirm.enable': "Activer l'accès complet", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'title': 'Erlaubnis', + 'description': 'Wählen Sie den Standardberechtigungsmodus für neue Sitzungen', + 'loading': 'Laden', + 'unavailable': 'Nicht verfügbar', + 'preset.readOnly': 'Nur lesen', + 'preset.workspaceWrite': 'Arbeitsbereich schreiben', + 'preset.fullAccess': 'Voller Zugriff', + 'confirm.title': 'Vollzugriff aktivieren?', + 'confirm.description': 'Durch den vollständigen Zugriff können neue Sitzungen die Bestätigungsschritte reduzieren und mehr Aktionen direkt ausführen, einschließlich vertraulicher Vorgänge, Dateiänderungen oder externer Befehle. Verwenden Sie es nur, wenn Sie nachfolgenden Aufgaben vertrauen.', + 'confirm.acknowledge': 'Ich verstehe die Risiken und möchte weitermachen', + 'confirm.cancel': 'Abbrechen', + 'confirm.enable': 'Aktivieren Sie Vollzugriff', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'title': 'Разрешение', + 'description': 'Выберите режим разрешений по умолчанию для новых сеансов', + 'loading': 'Загрузка', + 'unavailable': 'Недоступно', + 'preset.readOnly': 'Только чтение', + 'preset.workspaceWrite': 'Запись в рабочей области', + 'preset.fullAccess': 'Полный доступ', + 'confirm.title': 'Включить полный доступ?', + 'confirm.description': 'Полный доступ позволяет новым сеансам сократить количество шагов подтверждения и напрямую выполнять больше действий, включая конфиденциальные операции, изменение файлов или внешние команды. Используйте его только в том случае, если вы доверяете последующим задачам.', + 'confirm.acknowledge': 'Я понимаю риски и хочу продолжить', + 'confirm.cancel': 'Отмена', + 'confirm.enable': 'Включить полный доступ', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'title': 'Permissão', + 'description': 'Escolha o modo de permissão padrão para novas sessões', + 'loading': 'Carregando', + 'unavailable': 'Indisponível', + 'preset.readOnly': 'Somente leitura', + 'preset.workspaceWrite': 'Gravação no espaço de trabalho', + 'preset.fullAccess': 'Acesso total', + 'confirm.title': 'Ativar acesso total?', + 'confirm.description': 'O acesso total permite que novas sessões reduzam as etapas de confirmação e executem mais ações diretamente, incluindo operações confidenciais, alterações de arquivos ou comandos externos. Use-o apenas quando confiar nas tarefas subsequentes.', + 'confirm.acknowledge': 'Entendo os riscos e quero continuar', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Ativar acesso total', +} satisfies Record + /** Japanese copy, checked against the English keys. */ export const accessJa = { 'mode': 'アクセスモード、現在: {name}', @@ -119,3 +199,108 @@ export const accessJa = { 'auto.confirm.acknowledge': 'これらのリスクを理解し、続行します', 'auto.confirm.enable': '自動認可レビューを有効にする', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const accessEs = { + 'mode': 'Modo de acceso, actual: {name}', + 'close': 'Cerrar', + 'preset.readOnly': 'Sólo lectura', + 'preset.workspaceWrite': 'Escritura en el espacio de trabajo', + 'preset.fullAccess': 'Acceso completo', + 'confirm.title': '¿Habilitar acceso completo?', + 'confirm.description': 'El acceso completo reduce los pasos de confirmación y permite al agente realizar más acciones directamente, incluidas operaciones confidenciales, cambios de archivos o comandos externos. Úselo solo cuando confíe en la tarea actual.', + 'confirm.acknowledge': 'Entiendo los riesgos y quiero continuar.', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Habilitar acceso completo', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Ejecute sin una zona de pruebas después de una revisión experimental del mismo modelo de cada llamada de herramienta nativa y llamada interna PTC.', + 'auto.confirm.title': '¿Habilitar Auto review (experimental)?', + 'auto.confirm.description': 'Auto review se ejecuta sin zona de pruebas. Antes de cada llamada a la herramienta nativa y llamada interna PTC, el mismo modelo que el agente actual revisa si se permite; usted aprueba o rechaza cada llamada que rechaza. Esta característica es experimental, puede permitir o denegar acciones falsamente y utiliza tokens adicionales.', + 'auto.confirm.acknowledge': 'Entiendo estos riesgos y quiero continuar', + 'auto.confirm.enable': 'Habilitar Auto review', +} satisfies typeof accessEn + +/** French dictionary, checked against the English keys. */ +export const accessFr = { + 'mode': "Mode d'accès, actuel : {name}", + 'close': 'Fermer', + 'preset.readOnly': 'Lecture seule', + 'preset.workspaceWrite': "Écriture dans l'espace de travail", + 'preset.fullAccess': 'Accès complet', + 'confirm.title': "Activer l'accès complet ?", + 'confirm.description': "L'accès complet réduit les étapes de confirmation et permet à l'agent d'effectuer directement davantage d'actions, notamment des opérations sensibles, des modifications de fichiers ou des commandes externes. Utilisez-le uniquement lorsque vous faites confiance à la tâche en cours.", + 'confirm.acknowledge': 'Je comprends les risques et je veux continuer', + 'confirm.cancel': 'Annuler', + 'confirm.enable': "Activer l'accès complet", + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': "Exécutez sans bac à sable après un examen expérimental sur le même modèle de chaque appel d'outil natif et appel interne PTC.", + 'auto.confirm.title': 'Activer Auto review (expérimental) ?', + 'auto.confirm.description': "Auto review fonctionne sans bac à sable. Avant chaque appel d'outil natif et appel interne PTC, le même modèle que l'agent actuel vérifie s'il doit l'autoriser ; vous approuvez ou rejetez chaque appel qu'il refuse. Cette fonctionnalité est expérimentale, peut autoriser ou refuser à tort des actions et utilise des jetons supplémentaires.", + 'auto.confirm.acknowledge': 'Je comprends ces risques et je veux continuer', + 'auto.confirm.enable': 'Activer Auto review', +} satisfies typeof accessEn + +/** German dictionary, checked against the English keys. */ +export const accessDe = { + 'mode': 'Zugriffsmodus, aktuell: {name}', + 'close': 'Schließen', + 'preset.readOnly': 'Nur lesen', + 'preset.workspaceWrite': 'Arbeitsbereich schreiben', + 'preset.fullAccess': 'Voller Zugriff', + 'confirm.title': 'Vollzugriff aktivieren?', + 'confirm.description': 'Durch den Vollzugriff werden Bestätigungsschritte reduziert und der Agent kann mehr Aktionen direkt ausführen, einschließlich vertraulicher Vorgänge, Dateiänderungen oder externer Befehle. Verwenden Sie es nur, wenn Sie der aktuellen Aufgabe vertrauen.', + 'confirm.acknowledge': 'Ich verstehe die Risiken und möchte weitermachen', + 'confirm.cancel': 'Abbrechen', + 'confirm.enable': 'Aktivieren Sie Vollzugriff', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Ausführung ohne Sandbox nach einer experimentellen, modellgleichen Überprüfung jedes nativen Toolaufrufs und PTC-Innenaufrufs.', + 'auto.confirm.title': 'Auto review aktivieren (experimentell)?', + 'auto.confirm.description': 'Auto review läuft ohne Sandbox. Vor jedem nativen Tool-Aufruf und PTC-Innenaufruf überprüft dasselbe Modell wie der aktuelle Agent, ob dies zulässig ist. Sie genehmigen oder lehnen jeden abgelehnten Aufruf ab. Diese Funktion ist experimentell, kann Aktionen fälschlicherweise zulassen oder verweigern und verwendet zusätzliche Token.', + 'auto.confirm.acknowledge': 'Ich verstehe diese Risiken und möchte weitermachen', + 'auto.confirm.enable': 'Aktivieren Sie Auto review', +} satisfies typeof accessEn + +/** Russian dictionary, checked against the English keys. */ +export const accessRu = { + 'mode': 'Режим доступа, текущий: {name}', + 'close': 'Закрыть', + 'preset.readOnly': 'Только чтение', + 'preset.workspaceWrite': 'Запись в рабочей области', + 'preset.fullAccess': 'Полный доступ', + 'confirm.title': 'Включить полный доступ?', + 'confirm.description': 'Полный доступ сокращает количество шагов подтверждения и позволяет агенту напрямую выполнять больше действий, включая конфиденциальные операции, изменения файлов или внешние команды. Используйте его только в том случае, если вы доверяете текущей задаче.', + 'confirm.acknowledge': 'Я понимаю риски и хочу продолжить', + 'confirm.cancel': 'Отмена', + 'confirm.enable': 'Включить полный доступ', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Запуск без песочницы после экспериментальной проверки одной и той же модели каждого вызова встроенного инструмента и внутреннего вызова PTC.', + 'auto.confirm.title': 'Включить Auto review (экспериментально)?', + 'auto.confirm.description': 'Auto review работает без песочницы. Перед каждым вызовом собственного инструмента и внутренним вызовом PTC та же модель, что и текущий агент, проверяет, разрешить ли это; вы одобряете или отклоняете каждый вызов, который он отклоняет. Эта функция является экспериментальной, может ошибочно разрешать или запрещать действия и использует дополнительные токены.', + 'auto.confirm.acknowledge': 'Я понимаю эти риски и хочу продолжить', + 'auto.confirm.enable': 'Включить Auto review', +} satisfies typeof accessEn + +/** Portuguese dictionary, checked against the English keys. */ +export const accessPt = { + 'mode': 'Modo de acesso, atual: {name}', + 'close': 'Fechar', + 'preset.readOnly': 'Somente leitura', + 'preset.workspaceWrite': 'Gravação no espaço de trabalho', + 'preset.fullAccess': 'Acesso total', + 'confirm.title': 'Ativar acesso total?', + 'confirm.description': 'O acesso total reduz as etapas de confirmação e permite que o agente execute mais ações diretamente, incluindo operações confidenciais, alterações de arquivos ou comandos externos. Use-o apenas quando confiar na tarefa atual.', + 'confirm.acknowledge': 'Entendo os riscos e quero continuar', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Ativar acesso total', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Execute sem um sandbox após uma revisão experimental do mesmo modelo de cada chamada de ferramenta nativa e PTC chamada interna.', + 'auto.confirm.title': 'Ativar Auto review (experimental)?', + 'auto.confirm.description': 'Auto review é executado sem sandbox. Antes de cada chamada de ferramenta nativa e chamada interna PTC, o mesmo modelo do agente atual analisa se deve permitir isso; você aprova ou rejeita cada chamada negada. Este recurso é experimental, pode permitir ou negar ações falsamente e usa tokens adicionais.', + 'auto.confirm.acknowledge': 'Entendo esses riscos e quero continuar', + 'auto.confirm.enable': 'Ativar Auto review', +} satisfies typeof accessEn diff --git a/packages/client/ui-plan/locale/de.json b/packages/client/ui-plan/locale/de.json new file mode 100644 index 0000000000..ce90df31f6 --- /dev/null +++ b/packages/client/ui-plan/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Planüberprüfung", + "description": "Zeigt Steuerelemente für den Planungsmodus, Planungskarten und eine Markdown-Vorschau an." + } +} diff --git a/packages/client/ui-plan/locale/es.json b/packages/client/ui-plan/locale/es.json new file mode 100644 index 0000000000..1bc8dde649 --- /dev/null +++ b/packages/client/ui-plan/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de revisión del plan", + "description": "Muestra controles del modo de planificación, tarjetas de planificación y vista previa de Markdown." + } +} diff --git a/packages/client/ui-plan/locale/fr.json b/packages/client/ui-plan/locale/fr.json new file mode 100644 index 0000000000..165c62a645 --- /dev/null +++ b/packages/client/ui-plan/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de révision des plans", + "description": "Affiche les commandes du mode planification, les cartes de planification et l'aperçu Markdown." + } +} diff --git a/packages/client/ui-plan/locale/pt.json b/packages/client/ui-plan/locale/pt.json new file mode 100644 index 0000000000..1ad0ee2f88 --- /dev/null +++ b/packages/client/ui-plan/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de revisão do plano", + "description": "Mostra controles do modo de planejamento, cartões de planejamento e visualização do Markdown." + } +} diff --git a/packages/client/ui-plan/locale/ru.json b/packages/client/ui-plan/locale/ru.json new file mode 100644 index 0000000000..08eba25394 --- /dev/null +++ b/packages/client/ui-plan/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс проверки плана", + "description": "Показывает элементы управления режимом планирования, карты планирования и предварительный просмотр Markdown." + } +} diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index a6a3dea5cf..7956c6b884 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plan", "description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plan/src/client/index.ts b/packages/client/ui-plan/src/client/index.ts index 1b226f8a37..34196a00da 100644 --- a/packages/client/ui-plan/src/client/index.ts +++ b/packages/client/ui-plan/src/client/index.ts @@ -25,7 +25,7 @@ import { planAddress, parsePlanAddress } from './plan.ts' import { isReviewPreviewAddress, reviewPreviewAddress } from './review-preview.ts' import { createPlanReviewStore } from './review-store.ts' import { PlanChip } from './PlanModeControl.tsx' -import { en, ja, zh, type PlanKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PlanKey } from './locales.ts' export type { PlanKey } from './locales.ts' @@ -56,7 +56,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'remote.session', ' * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plan: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plan: dictionaries') const previewId = '@deepseek-ai/dsh-client-ui-plan' const t = ctx.locale.bind(NS) diff --git a/packages/client/ui-plan/src/client/locales.ts b/packages/client/ui-plan/src/client/locales.ts index 331fdb5234..5940519fa2 100644 --- a/packages/client/ui-plan/src/client/locales.ts +++ b/packages/client/ui-plan/src/client/locales.ts @@ -65,3 +65,108 @@ export const ja = { 'chip.on.title': '計画モードはオンです — クリックでオフ (/plan off)', 'chip.exitFailed': '計画モードを終了できませんでした', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'chip.label': 'Planificar', + 'preview.title': 'Planificar', + 'preview.document': 'Plano · Markdown', + 'preview.action': 'Abrir', + 'preview.open': 'Plan abierto en la barra lateral', + 'preview.full': 'Ver plano completo', + 'preview.openNamed': 'Plan abierto: {title}', + 'preview.loading': 'Cargando plan…', + 'preview.failed': 'No se pudo cargar el plan', + 'preview.invalidAddress': 'Dirección del plan no válida', + 'preview.historyUnavailable': 'El historial de sesiones no está disponible', + 'preview.notFound': 'Este plan no fue encontrado.', + 'preview.unavailable': 'La vista previa del plan no está disponible', + 'preview.expired': 'Esta vista previa del plan temporal ha caducado. Vuelva a abrirlo desde la tarjeta de revisión pendiente.', + 'chip.on.aria': 'Modo plan activado, presione para apagar', + 'chip.on.title': 'Modo de planificación activado: haga clic para desactivar (/plan off)', + 'chip.exitFailed': 'No se pudo salir del modo de plan', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'chip.label': 'Planifier', + 'preview.title': 'Planifier', + 'preview.document': 'Plan · Markdown', + 'preview.action': 'Ouvrir', + 'preview.open': 'Plan ouvert dans la barre latérale', + 'preview.full': 'Voir le forfait complet', + 'preview.openNamed': 'Plan ouvert : {title}', + 'preview.loading': 'Plan de chargement…', + 'preview.failed': 'Impossible de charger le plan', + 'preview.invalidAddress': 'Adresse du forfait invalide', + 'preview.historyUnavailable': "L'historique des sessions n'est pas disponible", + 'preview.notFound': "Ce plan n'a pas été trouvé", + 'preview.unavailable': "L'aperçu du plan n'est pas disponible", + 'preview.expired': 'Cet aperçu du plan temporaire a expiré. Rouvrez-le à partir de la carte de révision en attente.', + 'chip.on.aria': 'Mode Plan activé, appuyez pour désactiver', + 'chip.on.title': 'Mode Plan activé — cliquez pour désactiver (/plan off)', + 'chip.exitFailed': 'Échec de la sortie du mode Plan', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'chip.label': 'Planen', + 'preview.title': 'Planen', + 'preview.document': 'Plan · Markdown', + 'preview.action': 'Öffnen', + 'preview.open': 'Plan in der Seitenleiste öffnen', + 'preview.full': 'Vollständigen Plan anzeigen', + 'preview.openNamed': 'Offener Grundriss: {title}', + 'preview.loading': 'Ladeplan…', + 'preview.failed': 'Plan konnte nicht geladen werden', + 'preview.invalidAddress': 'Ungültige Planadresse', + 'preview.historyUnavailable': 'Der Sitzungsverlauf ist nicht verfügbar', + 'preview.notFound': 'Dieser Plan wurde nicht gefunden', + 'preview.unavailable': 'Die Planvorschau ist nicht verfügbar', + 'preview.expired': 'Diese temporäre Planvorschau ist abgelaufen. Öffnen Sie es erneut über die ausstehende Überprüfungskarte.', + 'chip.on.aria': 'Planmodus ein, zum Ausschalten drücken', + 'chip.on.title': 'Planmodus ein – zum Ausschalten klicken (/plan off)', + 'chip.exitFailed': 'Der Planmodus konnte nicht beendet werden', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'chip.label': 'План', + 'preview.title': 'План', + 'preview.document': 'План · Markdown', + 'preview.action': 'Открыть', + 'preview.open': 'Открытая планировка в боковой панели', + 'preview.full': 'Посмотреть полный план', + 'preview.openNamed': 'Открытая планировка: {title}', + 'preview.loading': 'План загрузки…', + 'preview.failed': 'Не удалось загрузить план', + 'preview.invalidAddress': 'Неверный адрес плана', + 'preview.historyUnavailable': 'История сеансов недоступна', + 'preview.notFound': 'Этот план не найден', + 'preview.unavailable': 'Предварительный просмотр плана недоступен.', + 'preview.expired': 'Срок действия этой временной предварительной версии плана истек. Откройте его снова из карточки ожидающего рассмотрения.', + 'chip.on.aria': 'Режим планирования включен. Нажмите, чтобы выключить.', + 'chip.on.title': 'Режим планирования включен — нажмите, чтобы выключить (/plan off)', + 'chip.exitFailed': 'Не удалось выйти из режима планирования.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'chip.label': 'Plano', + 'preview.title': 'Plano', + 'preview.document': 'Plano · Markdown', + 'preview.action': 'Abrir', + 'preview.open': 'Abrir plano na barra lateral', + 'preview.full': 'Ver plano completo', + 'preview.openNamed': 'Plano aberto: {title}', + 'preview.loading': 'Carregando plano…', + 'preview.failed': 'Não foi possível carregar o plano', + 'preview.invalidAddress': 'Endereço do plano inválido', + 'preview.historyUnavailable': 'O histórico da sessão não está disponível', + 'preview.notFound': 'Este plano não foi encontrado', + 'preview.unavailable': 'A visualização do plano não está disponível', + 'preview.expired': 'Esta visualização temporária do plano expirou. Reabra-o no cartão de revisão pendente.', + 'chip.on.aria': 'Modo de planejamento ativado, pressione para desligar', + 'chip.on.title': 'Modo de planejamento ativado – clique para desligar (/plan off)', + 'chip.exitFailed': 'Falha ao sair do modo de plano', +} satisfies Record diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index 66bac65580..1fe97166b2 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: fd8e3ad7cda08270 - zh: cce22df78619f63d + en: 216dcea8d6903007 + zh: 160888842501694e /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: en: fcb2e69c8b028c0a zh: d505a075679974eb @@ -48,8 +48,8 @@ en: 21a0bbe8eb92229c zh: 096ff63db61992bb /deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation/configuration-slots: - en: b3b8bd3606fc9e97 - zh: 7cd90e182073c8eb + en: 2b34cac9647abe63 + zh: 1929fc24aee6793c /deepseek-ai-dsh-client-ui-plugin-manager/further-exploration: en: f9a42dfed0360d6a zh: 455a70bcb6c69624 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 99c1a4fb43..bc7802a592 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -31,7 +31,7 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built-in plugin settings** lists official configuration pages by their configured order: Subagent, web search, agent loop, then shell. **Experimental plugins** shows the primary optional bundles, with **More** beside its heading opening a separate page of the same title. That page lists the official bundles eligible for display, including those on the main list, with Codex and Claude Code subagents last. Its back button returns to the main list, where **Installed plugins** remains below Experimental plugins. Bundle details return to the list they were opened from. Catalog entries keep their position after installation or removal. **Installed plugins** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages retain the **Experimental** marker on both lists. Switching on a bundle that page holds back adds it to the main list in the position that page shows it, and switching it off leaves it on that page alone unless its manifest hides unselected bundles; other bundles keep their place on both. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section. -The heading moves its complete action group to another line when space is insufficient. Action labels stay on one line, and the information button follows the subtitle text. Hover styling applies only where the pointer can hover, so a tap leaves no card, row, or menu option in its hover state; cards and icon actions answer a touch-down with the pressed fill instead. The install, registry, and filter fields take a 16px face under a coarse pointer, which keeps iOS Safari from zooming a focused field. +As available header width decreases, Refresh and Add plugin first become icon-only controls with accessible names and tooltips; the tabs then shrink from their usual minimum width to fit their labels, and the action group wraps only when necessary. The information button follows the subtitle text. The main page header and list share one scroll container. Header controls move with the scroll position until they reach their compact positions; scrolling back follows the same coordinates. The header retains its resting layout height, so moving its controls does not change the list's scroll range. A barely scrolling list can stop before the header is fully compact. The More list's back-and-title row stays fixed, and detail pages scroll their head row with the content. The Installed/Discover tabs use the compact 34px control with a 2px inset around the selection indicator. The subtitle scrolls out of view; with tabs present, the title also scrolls out, and the tabs settle beside Refresh and Add plugin where space permits. On narrow pages, the tabs and icon actions share a row beneath the heading where they fit; otherwise the tabs stay above the actions. Their relative order stays the same throughout scrolling. The resting and compact layouts retain their 28px and 16px page-top insets respectively, with 16px below the compact controls. macOS adds the frame's titlebar clearance while resting; its compact bar keeps 16px above and below with the sidebar expanded, and with the sidebar collapsed it also clears the frame's top strip so the controls stay below the native window controls. Only on macOS, when the sidebar is collapsed, the compact controls keep 8px rather than 16px above them in addition to the unchanged frame clearance, at every width. The resting layout and compact bottom spacing stay unchanged. Windows relies on the frame's caption offset without adding it again. The divider spans the page at the bottom of the compact header; the list's initial top spacing scrolls with its content. Hover styling applies only where the pointer can hover, so a tap leaves no card, row, or menu option in its hover state; cards and icon actions answer a touch-down with the pressed fill instead. The install, registry, and filter fields take a 16px face under a coarse pointer, which keeps iOS Safari from zooming a focused field. Installed bundles and their plugin rows display their own title and description in the current UI language on cards and detail pages. Each field falls back from exported locale `meta` to the accessible `package.json` of a package-root plugin; subpath plugins never read one; the final title is the full package or module name, with no package description when both sources omit it. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format. Bundle cards, details, and component rows display the package root's manifest `icon` or exported `./icon`, or a subpath plugin's exported `/icon`; absent or undecodable images retain the default artwork. Installation previews still use registry or manifest information. @@ -142,7 +142,7 @@ While the Plugins page has access, other Client plugins can read `pluginNavigati Custom item pages use the Host entry id as their registration id; row pages use the bundle package and row id. The page owner supplies `form.state` and `form.mutate(operations, expectedRevision)` when the entry exposes editable Config fields. A custom page owns its draft and validation display, and may reuse `ConfigField` from ui-primitives. Bundle-wide pages can contain several entries and have no single form. -The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins read `pluginNavigation` optionally, through `ctx.get` or `ctx.inject`, and call its `openBundle(packageName)` to open bundle details without changing the current Session; listing it in a plugin's `inject` keeps that plugin pending on a page without access. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Cards and detail pages show the Experimental marker only for packages in `EXPERIMENTAL_BUNDLES` in [presentation.ts](src/client/presentation.ts); package-name prefixes do not select the marker. +The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins read `pluginNavigation` optionally, through `ctx.get` or `ctx.inject`, and call its `openBundle(packageName)` to open bundle details without changing the current Session; listing it in a plugin's `inject` keeps that plugin pending on a page without access. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Cards and detail pages show the Experimental marker only for packages in `EXPERIMENTAL_BUNDLES` in [`dsh-official-bundles`](../../util/official-bundles/README.md#plugin-manager-lists); package-name prefixes do not select the marker. `plugins.bundle.config` supplies bundle detail configuration, keyed by npm package name. Client plugins can subscribe to the read-only `ctx.pluginChanges.bundle(packageName)` source to pause their queries while a bundle or one of its rows is being changed. Notifications precede the management request and continue when the page closes; subscribers release their observation on disposal. `plugins.bundle.activation` offers bundle-owned guidance after explicit enablement from the list, with callbacks to dismiss it or open the bundle details. It does not appear merely because an enabled bundle was listed. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index 996dba9132..5be88a24b2 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" 在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置插件配置**按配置的顺序列出官方配置页:子智能体、网页搜索、Agent 循环、终端。**实验性插件**展示主要的可选组合包,标题旁的**更多**打开同名的独立页面。该页列出符合展示条件的官方组合包,也包含主列表已有的条目,Codex 和 Claude Code 子智能体排在最后。返回按钮回到主列表,**已安装插件**保持在实验性插件下方。组合包详情返回打开它的列表。目录条目在安装或移除后保持原位置。**已安装插件**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包在两个列表中均保留**实验性**标签。开启仅在该页列出的组合包会按该页中的位置把它加入主列表,关闭后它只留在该页,但 manifest 声明默认隐藏的组合包会从该页消失;其他组合包在两个列表中均保持原位。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。 -空间不足时,页头将整个操作组移到下一行。操作文案保持单行,信息按钮紧随副标题文字。悬停样式只在可悬停的指针下生效,点按不会让卡片、行或菜单项停留在悬停态;卡片与图标操作改以按下填充回馈触摸。安装、注册表与筛选输入框在粗指针下使用 16px 字号,iOS Safari 不会因聚焦而放大页面。 +页头可用宽度缩小时,“刷新”和“添加插件”先变为保留可访问名称与 tooltip 的图标按钮;标签再从常规最小宽度收紧为文案所需宽度,仍不足时操作组才换行。信息按钮紧随副标题文字。主页面的页头和列表共用一个滚动容器。页头控件随滚动位置移动,到达紧凑态位置后停住;反向滚动沿同样的坐标返回。页头保留静止态的布局高度,因此控件移动不会改变列表的滚动范围。只能滚动少量距离的列表可能在页头完全收拢前到达底部。“更多”列表的返回与标题行固定,详情页的页头随内容滚动。“已安装/发现”标签使用总高 34px 的紧凑控件,选中指示块与外框之间留 2px。副标题随滚动离开视口;有标签时标题也随之滚出,标签在空间足够时停靠到“刷新”“添加插件”旁边。窄页面中,标签与图标操作优先在标题下方共用一行;放不下时,标签始终在操作组上方。滚动过程中两组控件的相对顺序保持不变。静止态与紧凑态分别保留 28px 和 16px 的页面顶部留白,紧凑态控件下方保留 16px。macOS 静止态保留框架的标题栏安全区;折叠态在侧栏展开时上下各留 16px,侧栏收起时再避让框架顶部安全区,使控件位于原生窗口控件下方。仅在 macOS 侧栏收起时,紧凑态控件在原有框架安全区之外的顶部留白统一由 16px 减为 8px,不受窗口宽度影响;静止态布局与紧凑态底部留白均不变。Windows 使用框架已有的标题栏偏移,不重复叠加。分隔线在紧凑页头底部横跨页面,列表初始的顶部留白随内容滚动。悬停样式只在可悬停的指针下生效,点按不会让卡片、行或菜单项停留在悬停态;卡片与图标操作改以按下填充回馈触摸。安装、注册表与筛选输入框在粗指针下使用 16px 字号,iOS Safari 不会因聚焦而放大页面。 已安装的组合包及其插件行在卡片和详情页中,按当前界面语言显示各自的标题与描述。每个字段先读取导出的 locale `meta`,缺失时回退到包根插件可访问的 `package.json`,子路径插件从不读取;标题最终使用完整包名或模块名,两处都没有描述时不提供包描述。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。组合包卡片、详情和组件行显示包根的清单 `icon` 或导出的 `./icon`,或子路径插件导出的 `<子路径>/icon`;未声明或无法解码时保留默认插画。安装预览仍使用注册表或 manifest 信息。 @@ -142,7 +142,7 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()` 自定义条目页以 Host 条目 id 作为注册 id;行页面使用 bundle 包名和行 id。当条目提供可编辑 Config 字段时,页面宿主传入 `form.state` 和 `form.mutate(operations, expectedRevision)`。自定义页面负责草稿和校验提示,并可复用 ui-primitives 的 `ConfigField`。整个 bundle 的页面可以包含多个条目,因此没有单一表单。 -页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件经 `ctx.get` 或 `ctx.inject` 以可选方式读取 `pluginNavigation`,调用其 `openBundle(packageName)` 即可打开组合包详情,不改变当前 Session;把它写进插件的 `inject` 会让该插件在无权限的页面上一直等待。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 卡片和详情页仅为 [presentation.ts](src/client/presentation.ts) 中 `EXPERIMENTAL_BUNDLES` 名单内的包显示实验性标签,包名前缀不决定标签。 +页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件经 `ctx.get` 或 `ctx.inject` 以可选方式读取 `pluginNavigation`,调用其 `openBundle(packageName)` 即可打开组合包详情,不改变当前 Session;把它写进插件的 `inject` 会让该插件在无权限的页面上一直等待。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 卡片和详情页仅为 [`dsh-official-bundles`](../../util/official-bundles/README.zh.md#plugin-manager-lists) 中 `EXPERIMENTAL_BUNDLES` 名单内的包显示实验性标签,包名前缀不决定标签。 `plugins.bundle.config` 以 npm 包名为 key,提供 Bundle 详情配置。Client 插件可订阅只读的 `ctx.pluginChanges.bundle(packageName)`,在组合包或其组件行变更期间暂停查询。通知先于管理请求发出,页面关闭后仍会更新;订阅方在卸载时释放观察。`plugins.bundle.activation` 在用户从列表显式启用后提供 Bundle 自有引导,并传入关闭引导和打开详情的回调。仅列出已启用的 Bundle 不会触发引导。 diff --git a/packages/client/ui-plugin-manager/locale/de.json b/packages/client/ui-plugin-manager/locale/de.json new file mode 100644 index 0000000000..16a03e8049 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Verwaltungsschnittstelle", + "description": "Plug-in-Pakete anzeigen und installieren sowie Vorgänge aktivieren, deaktivieren und wiederholen." + } +} diff --git a/packages/client/ui-plugin-manager/locale/es.json b/packages/client/ui-plugin-manager/locale/es.json new file mode 100644 index 0000000000..3ca20e033d --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de gestión de complementos", + "description": "Vea e instale paquetes de complementos y habilite, deshabilite y reintente operaciones." + } +} diff --git a/packages/client/ui-plugin-manager/locale/fr.json b/packages/client/ui-plugin-manager/locale/fr.json new file mode 100644 index 0000000000..48bf4551cd --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de gestion des plug-ins", + "description": "Affichez et installez les packages de plug-ins, puis activez, désactivez et réessayez les opérations." + } +} diff --git a/packages/client/ui-plugin-manager/locale/pt.json b/packages/client/ui-plugin-manager/locale/pt.json new file mode 100644 index 0000000000..f1e246b3a2 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de gerenciamento de plug-ins", + "description": "Visualize e instale pacotes de plug-ins e ative, desative e repita as operações." + } +} diff --git a/packages/client/ui-plugin-manager/locale/ru.json b/packages/client/ui-plugin-manager/locale/ru.json new file mode 100644 index 0000000000..155d905c69 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс управления плагинами", + "description": "Просмотрите и установите пакеты подключаемых модулей, а также включите, отключите и повторите операции." + } +} diff --git a/packages/client/ui-plugin-manager/package.json b/packages/client/ui-plugin-manager/package.json index a9eceb424d..c8637e13ca 100644 --- a/packages/client/ui-plugin-manager/package.json +++ b/packages/client/ui-plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plugin-manager", "description": "Plugin management for the dsh web client: the sidebar Plugins panel installs, enables, disables, retries, and composes installed plugin packages", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, @@ -65,6 +65,7 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", "@deepseek-ai/dsh-client-ui-sidebar": "workspace:*", "@deepseek-ai/dsh-client-ui-slots": "workspace:*", + "@deepseek-ai/dsh-official-bundles": "workspace:*", "@deepseek-ai/dsh-package-manifest": "workspace:*", "@testing-library/react": "^16.3.3", "clsx": "^2.1.1", diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css index 9c51e514d6..6e779d10df 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -1,4 +1,5 @@ -/* Plugin manager page: a centred column with its header, notices, and the package cards. +/* Plugin manager page: the main header and content share one scroller; the header retains its flow height. + The More list has a separate fixed head, and detail views scroll with their content. Hover affordances sit under @media (hover: hover): a tap latches :hover, which would strand a card, row, or control in its pointer-only state. Text fields take @@ -6,27 +7,144 @@ for a focused field under 16px and leaves the page pannable afterwards. */ .page { + --plugin-page-inset: clamp(24px, 4vw, 48px); + /* Every head row's top inset; the collapsed page bar narrows it. */ + --plugin-page-top-inset: 28px; --bundle-progress-size: 14px; --bundle-toggle-gap: 8px; + + display: flex; + flex-direction: column; + align-items: center; + box-sizing: border-box; + height: 100%; + /* Inline inset only: the 28px top inset belongs to the head row + (`.pageHead`, `.detailTop`, `.moreHead`). macOS window dragging is owned by a row's own box, + so an inset above that box leaves a top strip no drag region covers. */ + padding: 0 var(--plugin-page-inset); + color: var(--dsw-alias-label-primary); +} + +.page > * { + flex: none; + width: 100%; + max-width: 960px; +} + +.pageBody > .pageBar { + container: plugin-page-bar / inline-size; + align-self: flex-start; + position: relative; + display: flex; + flex-direction: column; + width: calc(100% + 2 * var(--plugin-page-inset) + var(--plugin-page-scrollbar-width, 0px)); + max-width: none; + margin-inline: calc(-1 * var(--plugin-page-inset)); + box-sizing: border-box; + padding-inline: var(--plugin-page-inset); +} + +.pageBar > .pageHead { + align-self: center; + width: 100%; + max-width: 960px; +} + +/* Spans the page's full width, so its scrollbar sits at the page edge, and re-centres the column inside. */ +.page > .pageBody { + flex: 1; + min-height: 0; + width: calc(100% + 2 * var(--plugin-page-inset)); + max-width: none; + margin-inline: calc(-1 * var(--plugin-page-inset)); display: flex; flex-direction: column; align-items: center; gap: 32px; box-sizing: border-box; - height: 100%; - overflow: auto; - /* Inline and bottom insets only: the 28px top inset belongs to the head row - (`.pageHead`, `.detailTop`). macOS window dragging is owned by a row's own box, - so an inset above that box leaves a top strip no drag region covers. */ - padding: 0 clamp(24px, 4vw, 48px) 48px; - color: var(--dsw-alias-label-primary); + padding: 0 var(--plugin-page-inset) 48px; + overflow-x: hidden; + overflow-y: auto; + position: relative; + scroll-padding-top: var(--plugin-header-scroll-padding, 0px); } -.page > * { +/* The initial list inset scrolls away; the scroll viewport meets the fixed bar directly. */ +.page > :not(.pageBody) + .pageBody { + padding-top: 32px; +} + +.pageBody > * { + flex: none; width: 100%; max-width: 960px; } +/* Endpoint copies never enter the scroll layout or the accessibility tree. */ +.pageBody > .pageBar[data-measuring] { + position: absolute; + top: 0; + left: 0; + margin: 0; + visibility: hidden; + pointer-events: none; +} + +.pageBody > .pageBar[data-natural] { + position: sticky; + top: 0; + z-index: 2; + pointer-events: none; +} + +.pageBar[data-natural] .pageHead { + position: relative; + height: var(--plugin-header-rest-height); + clip-path: inset(0 0 max(0px, calc(var(--plugin-header-rest-height) - var(--plugin-header-paint-height))) 0); + pointer-events: auto; +} + +.pageBar[data-natural] .pageHeading { + display: contents; +} + +.pageBar[data-natural] [data-plugin-header-part] { + position: absolute; + left: var(--plugin-part-x); + top: var(--plugin-part-y); + width: var(--plugin-part-width); + margin: 0; + pointer-events: auto; +} + +.pageBar[data-natural] .pageTabs { + z-index: 2; + pointer-events: none; +} + +.pageBar[data-natural] .pageTabList { + pointer-events: auto; +} + +.pageBar[data-natural]::before { + content: ''; + position: absolute; + inset: 0 0 auto; + height: var(--plugin-header-paint-height, var(--plugin-header-rest-height)); + background: var(--dsw-alias-bg-base); + pointer-events: none; +} + +.pageBar[data-natural][data-compact]::after { + content: ''; + position: absolute; + inset-inline: 0; + top: 0; + height: var(--plugin-header-compact-height); + box-sizing: border-box; + border-bottom: 0.5px solid var(--dsw-alias-border-l3); +} + .pageHead { display: flex; flex-wrap: wrap; @@ -36,7 +154,7 @@ box-sizing: border-box; /* The page's former top inset: the row starts at the window's top edge, so the whole strip above the page content drags the window. */ - padding-top: 28px; + padding-top: var(--plugin-page-top-inset); } /* macOS hiddenInset titlebar: the frame's top clearance keeps the head below @@ -44,12 +162,13 @@ owns the window drag over its own box — clearance included — so this page's top strip drags without a frame band. */ :global([data-platform='darwin']) .pageHead { - padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px)); + padding-top: calc(var(--plugin-page-top-inset) + var(--dsh-frame-top-clearance, 0px)); } .pageHeading { min-width: 0; max-width: 100%; + order: 1; } .pageTitle { @@ -86,6 +205,7 @@ gap: 8px 16px; min-width: 0; white-space: nowrap; + order: 2; } .addMenu { @@ -1263,9 +1383,9 @@ label.registryOption { .detailTop, .moreHead { display: flex; - /* The same 28px top inset `.pageHead` carries, so the row starts at the window's + /* The same top inset `.pageHead` carries, so the row starts at the window's top edge and the strip above the detail content drags the window. */ - padding-top: 28px; + padding-top: var(--plugin-page-top-inset); } .detailTop { @@ -1274,7 +1394,7 @@ label.registryOption { :global([data-platform='darwin']) .detailTop, :global([data-platform='darwin']) .moreHead { - padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px)); + padding-top: calc(var(--plugin-page-top-inset) + var(--dsh-frame-top-clearance, 0px)); } .crumb { @@ -1565,11 +1685,64 @@ label.registryOption { display: none; } -/* Installed and contributed tabs under the page header, aligned with the title; the negative margin halves - the page's 32px row gap, so the tabs read as part of the header. */ +/* Installed and contributed tabs under the page header at rest; the scrolled bar + pulls them onto the actions' row. */ .pageTabs { display: flex; - margin-top: -16px; + order: 3; + flex: 1 1 100%; +} + +/* Keep spacing inside the compact bar, above its divider rather than outside the scroll viewport. */ +.pageBar[data-compact]:not([data-natural]) { + --plugin-page-top-inset: 16px; + + padding-bottom: 16px; +} + +/* With the sidebar open, native window controls sit in the sidebar rather than above the page. */ +:global([data-platform='darwin']) .pageBar[data-compact]:not([data-natural]) .pageHead { + padding-top: var(--plugin-page-top-inset); +} + +/* Collapsing the sidebar moves those controls above the page; the compact bar then clears them. */ +:global([data-platform='darwin']) :global([data-sidebar-collapsed]) .pageBar[data-compact]:not([data-natural]) .pageHead { + --plugin-page-top-inset: 8px; + + padding-top: calc(var(--plugin-page-top-inset) + var(--dsh-frame-top-clearance, 0px)); +} + +/* The collapsed bar's divider spans the page like the scrolled list and its scrollbar. */ +.pageBar[data-compact]:not([data-natural])::after { + content: ''; + position: absolute; + inset-inline: 0; + bottom: 0; + border-bottom: 0.5px solid var(--dsw-alias-border-l3); + pointer-events: none; +} + +.pageBar[data-compact]:not([data-natural]) .pageHead { + align-items: center; +} + +.pageBar[data-compact]:not([data-natural]) .pageIntro { + display: none; +} + +/* Visually hidden rather than removed, so the page keeps its only heading for assistive technology. */ +.pageBarWithTabs[data-compact]:not([data-natural]) .pageHeading { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.pageBarWithTabs[data-compact]:not([data-natural]) .pageTabs { + order: 1; + flex: 0 0 auto; } .pageTabList { @@ -1577,6 +1750,56 @@ label.registryOption { min-width: 240px; } +@container plugin-page-bar (width < 600px) { + .pageBarWithTabs .pageHead { + align-items: center; + } + + .pageBarWithTabs .pageHeading { + flex-basis: 100%; + } + + .pageBarWithTabs .pageTabs { + order: 2; + flex: 0 0 auto; + } + + .pageBarWithTabs .toolbar { + order: 3; + } + + .toolbarLabel { + display: none; + } + + .pageHead .addButton, + .pageHead .addPrimary { + width: 32px; + min-width: 32px; + padding-inline: 0; + } + + /* The icon actions keep their width; a wrapped update note stays within the head. */ + .toolbar { + flex-shrink: 0; + max-width: 100%; + } + + .updatesNote { + order: 3; + flex-basis: 100%; + min-width: 0; + white-space: normal; + } +} + +/* Keep the usual tab width until the icon actions and their gaps need its space. */ +@container plugin-page-bar (width < 372px) { + .pageTabList { + min-width: 0; + } +} + /* The Installed groups keep the page's row gap inside their tab panel. */ .installedPanel { display: flex; diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index 5adf279a1f..b1f8d5790e 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -21,7 +21,9 @@ import { type IconProps, type StateDotState, type TerminalBlockLabels, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' +import { EXTENSION_BUNDLES, HIDDEN_BUNDLES, MORE_BUNDLES } from '@deepseek-ai/dsh-official-bundles' import type { createNavigationStore } from './navigation-store.ts' +import type { PluginHeaderLayoutFace } from './header-scroll.ts' import { rowConfigKey, type OfficialItem } from './config-ledger.ts' import { INSTALL_GIT_EXAMPLE, INSTALL_PATH_EXAMPLE, type PluginManagerLocaleKey } from './locales.ts' import { @@ -45,7 +47,7 @@ export type PluginManagerPageProps = | 'plugins.add.actions' | 'plugins.tab' | 'plugins.detail.actions' | 'plugins.detail.badge' | 'plugins.detail.section' > - & InjectFace + & InjectFace & PropsStore> /** The page's slot renderer, narrowed to the configuration slots. */ @@ -65,10 +67,12 @@ function AddPluginMenu({ t, disabled, openInstall, renderSlot }: { const onDismiss = (): void => { setOpen(false) } return ( - + + + = 0) return alwaysHeld + const ranked = EXTENSION_BUNDLES.indexOf(name) + if (ranked >= 0) return ranked const heldBack = MORE_BUNDLES.indexOf(name) return heldBack < 0 ? EXTENSION_BUNDLES.length : EXTENSION_BUNDLES.length + 1 + heldBack } @@ -1551,6 +1521,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { useEffect(() => { if (tab !== selectedTab) setTab(tab) }, [tab, selectedTab, setTab]) const tabsId = useId() const [activation, setActivation] = useState(null) + const [headerRef] = useState(props.createHeaderRef) useEffect(() => { ensure() }, [ensure]) // Installation highlights visible cards without changing the user's current page. const { highlight, clearHighlight } = { highlight: state.highlight, clearHighlight: props.clearHighlight } @@ -1645,64 +1616,6 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { return (
- {showsMainList - ? ( -
-
-

{t('title')}

-
- {t('intro')} - - - -
-
- -
- ) - : null} - {showsMainList && tabs.length > 0 - ? ( -
- { setTab(next) }} - items={[ - { value: INSTALLED_TAB, label: t('installedTab'), id: `${tabsId}-${INSTALLED_TAB}`, panelId: `${tabsId}-${INSTALLED_TAB}-panel` }, - ...tabs.map(item => ({ value: item.id, label: item.label, id: `${tabsId}-${item.id}`, panelId: `${tabsId}-${item.id}-panel` })), - ]} - /> -
- ) - : null} - {!onInstalled - ? ( -
- {renderSlot('plugins.tab', { - install: props.installFrom, - openPackage: (name) => { setTab(INSTALLED_TAB); setView({ kind: 'package', name, returnTab: tab }) }, - }, { only: tab })} -
- ) - : null} {showsMoreList ? (
) : null} - {installedCards && !loaded ? ( -
- {state.status === 'loading' ? : null} - {state.status === 'unavailable' ? ( -

- {t('unavailable')} -

- ) : null} -
- ) : null} - {state.notice === null || noticeLine === null - ? null - : ( - } - holdMs={toastHoldMs(noticeLine)} - onDone={props.dismissNotice} - /> - )} - {!showsCards && state.status === 'error' && !refreshing - ? ( -
-

- {t(state.refreshStatus === 'failed' ? 'refreshError' : 'error')} -

- -
- ) - : null} - {onInstalled && loaded && openPkg !== undefined && openRow !== undefined - ? ( - { setView({ kind: 'package', name: openPkg.name, ...pkgOrigin }) }} - /> - ) - : null} - {onInstalled && loaded && openPkg !== undefined && openRow === undefined - ? ( - row.entryId !== undefined && state.busy.includes(rowKey(row.entryId))} - configured={ledger.bundles.has(openPkg.name)} - configure={configure(openPkg)} - renderSlot={renderSlot} - inMore={backToMore} - detail={( - - )} - onBack={() => { - if (returnTab !== undefined) setTab(returnTab) - setView({ kind: backToMore ? 'more' : 'list' }) - }} - onSetEnabled={(enabled) => { props.setEnabled(openPkg.name, enabled) }} - onUninstall={() => { props.uninstall(openPkg.name) }} - onUpdate={() => { props.update(openPkg.name) }} - onSetRowEnabled={setRowEnabled} - versions={state.versions[openPkg.name]} - onEnsureVersions={props.ensureVersions} - onCheckVersions={() => { props.checkVersions(openPkg.name) }} - onUpdateVersion={(version) => { props.openUpdate(openPkg.name, version) }} - /> - ) - : null} - {onInstalled && loaded && openItem !== undefined - ? { setView({ kind: 'list' }) }} /> - : null} - {loaded && installedCards - ? basicCards.length === 0 && official.length === 0 && mine.length === 0 && state.status !== 'error' - ?

{t('empty')}

- : ( -
- {showsMoreList ? ( -
-
    {more.map(packageCard)}
-
- ) : ( - <> - {renderGroup('basic', t('basicTitle'), basicCards)} - {renderGroup('extensions', t('extensionsTitle'), extensions.map(packageCard))} - {renderGroup('bundles', t('bundlesTitle'), mine.map(packageCard))} - - )} - {/* A failed package read trails the groups it left incomplete: right under the groups on a - first-load failure, and after the kept cards when a refresh fails over stale data. */} - {state.status === 'error' && !refreshing - ? ( -
-

- {t(state.refreshStatus === 'failed' ? 'refreshError' : 'error')} -

- +
+ {showsMainList + ? ( +
0 ? ` ${css.pageBarWithTabs}` : ''}`} + data-plugin-bar + ref={headerRef} + > +
+
+

{t('title')}

+
+ {t('intro')} + + +
- ) - : null} +
+ + {tabs.length > 0 + ? ( +
+ { setTab(next) }} + items={[ + { value: INSTALLED_TAB, label: t('installedTab'), id: `${tabsId}-${INSTALLED_TAB}`, panelId: `${tabsId}-${INSTALLED_TAB}-panel` }, + ...tabs.map(item => ({ value: item.id, label: item.label, id: `${tabsId}-${item.id}`, panelId: `${tabsId}-${item.id}-panel` })), + ]} + /> +
+ ) + : null} +
) - : null} - {/* Activation guidance belongs beside the Installed list that holds the enabled bundle's card. */} - {installedCards && activated !== undefined && !state.install.open - ? renderSlot('plugins.bundle.activation', { - packageName: activated.name, - onDismiss: () => { setActivation(null) }, - onOpenDetails: () => { - setActivation(null) - setView({ kind: 'package', name: activated.name, ...showsMoreList ? { from: 'more' } : {} }) - }, - }, { entryKey: activated.name }) : null} + : null} + + {!onInstalled + ? ( +
+ {renderSlot('plugins.tab', { + install: props.installFrom, + openPackage: (name) => { setTab(INSTALLED_TAB); setView({ kind: 'package', name, returnTab: tab }) }, + }, { only: tab })} +
+ ) + : null} + {installedCards && !loaded ? ( +
+ {state.status === 'loading' ? : null} + {state.status === 'unavailable' ? ( +

+ {t('unavailable')} +

+ ) : null} +
+ ) : null} + {state.notice === null || noticeLine === null + ? null + : ( + } + holdMs={toastHoldMs(noticeLine)} + onDone={props.dismissNotice} + /> + )} + {!showsCards && state.status === 'error' && !refreshing + ? ( +
+

+ {t(state.refreshStatus === 'failed' ? 'refreshError' : 'error')} +

+ +
+ ) + : null} + {onInstalled && loaded && openPkg !== undefined && openRow !== undefined + ? ( + { setView({ kind: 'package', name: openPkg.name, ...pkgOrigin }) }} + /> + ) + : null} + {onInstalled && loaded && openPkg !== undefined && openRow === undefined + ? ( + row.entryId !== undefined && state.busy.includes(rowKey(row.entryId))} + configured={ledger.bundles.has(openPkg.name)} + configure={configure(openPkg)} + renderSlot={renderSlot} + inMore={backToMore} + detail={( + + )} + onBack={() => { + if (returnTab !== undefined) setTab(returnTab) + setView({ kind: backToMore ? 'more' : 'list' }) + }} + onSetEnabled={(enabled) => { props.setEnabled(openPkg.name, enabled) }} + onUninstall={() => { props.uninstall(openPkg.name) }} + onUpdate={() => { props.update(openPkg.name) }} + onSetRowEnabled={setRowEnabled} + versions={state.versions[openPkg.name]} + onEnsureVersions={props.ensureVersions} + onCheckVersions={() => { props.checkVersions(openPkg.name) }} + onUpdateVersion={(version) => { props.openUpdate(openPkg.name, version) }} + /> + ) + : null} + {onInstalled && loaded && openItem !== undefined + ? { setView({ kind: 'list' }) }} /> + : null} + {loaded && installedCards + ? basicCards.length === 0 && official.length === 0 && mine.length === 0 && state.status !== 'error' + ?

{t('empty')}

+ : ( +
+ {showsMoreList ? ( +
+
    {more.map(packageCard)}
+
+ ) : ( + <> + {renderGroup('basic', t('basicTitle'), basicCards)} + {renderGroup('extensions', t('extensionsTitle'), extensions.map(packageCard))} + {renderGroup('bundles', t('bundlesTitle'), mine.map(packageCard))} + + )} + {/* A failed package read trails the groups it left incomplete: right under the groups on a + first-load failure, and after the kept cards when a refresh fails over stale data. */} + {state.status === 'error' && !refreshing + ? ( +
+

+ {t(state.refreshStatus === 'failed' ? 'refreshError' : 'error')} +

+ +
+ ) + : null} +
+ ) + : null} + {/* Activation guidance belongs beside the Installed list that holds the enabled bundle's card. */} + {installedCards && activated !== undefined && !state.install.open + ? renderSlot('plugins.bundle.activation', { + packageName: activated.name, + onDismiss: () => { setActivation(null) }, + onOpenDetails: () => { + setActivation(null) + setView({ kind: 'package', name: activated.name, ...showsMoreList ? { from: 'more' } : {} }) + }, + }, { entryKey: activated.name }) : null} +
{state.update === null ? null : ( (element: HTMLDivElement | null) => void +} + +interface Box { + x: number + y: number + width: number + height: number +} + +interface Part { + element: HTMLElement + from: Box + to: Box + distance: number + clipStart?: number + clipEnd?: number +} + +const PARTS = ['title', 'intro', 'toolbar', 'tabs'] as const + +/** + * Measure the existing resting and compact CSS layouts in an inert, out-of-flow copy. + * Their independent flex wrapping requires pixel measurements; interpolating only positioned + * contents leaves the real header's resting footprint and the scroll range unchanged. + * @returns A ref callback that disposes its element observers and scroll listener on detachment. + */ +export function createPluginHeaderRef(): (element: HTMLDivElement | null) => void { + let dispose: (() => void) | undefined + return (element) => { + dispose?.() + dispose = element === null ? undefined : attach(element) + } +} + +function attach(bar: HTMLDivElement): () => void { + const body = bar.parentElement as HTMLElement + const parts: Part[] = [] + let distance = 1 + let restHeight = 0 + let compactHeight = 0 + let disposed = false + + const scroll = (): void => { + const offset = Math.max(0, body.scrollTop) + bar.toggleAttribute('data-compact', offset >= distance) + bar.toggleAttribute('data-scrolled', offset > 0) + for (const part of parts) { + const progress = Math.min(1, offset / part.distance) + part.element.style.translate = `${(part.to.x - part.from.x) * progress}px ${(part.to.y - part.from.y) * progress}px` + part.element.style.width = `${part.from.width + (part.to.width - part.from.width) * progress}px` + if (part.clipStart !== undefined && part.clipEnd !== undefined) { + const clip = Math.max(0, Math.min(1, (offset - part.clipStart) / (part.clipEnd - part.clipStart))) + part.element.style.clipPath = `inset(${clip * 100}% 0 0)` + } else { + part.element.style.removeProperty('clip-path') + } + if (part.element.dataset.pluginHeaderPart === 'intro') { + part.element.inert = offset >= (part.clipEnd ?? part.distance) + } + } + bar.style.setProperty('--plugin-header-paint-height', `${Math.max(compactHeight, restHeight - offset)}px`) + } + + const measure = (): void => { + if (disposed) return + body.style.setProperty('--plugin-page-scrollbar-width', `${body.offsetWidth - body.clientWidth}px`) + const clone = bar.cloneNode(true) as HTMLDivElement + clone.removeAttribute('data-natural') + clone.removeAttribute('data-compact') + clone.removeAttribute('data-scrolled') + clone.setAttribute('data-measuring', '') + clone.setAttribute('aria-hidden', 'true') + clone.inert = true + clone.style.width = `${bar.getBoundingClientRect().width}px` + for (const element of clone.querySelectorAll('[id], [data-plugin-header-part]')) { + element.removeAttribute('id') + if (element.hasAttribute('data-plugin-header-part')) element.removeAttribute('style') + } + body.append(clone) + const snapshot = (): { height: number; boxes: Map } => { + const head = (clone.querySelector('header') as HTMLElement).getBoundingClientRect() + const boxes = new Map() + for (const name of PARTS) { + const element = clone.querySelector(`[data-plugin-header-part="${name}"]`) + if (element === null) continue + const box = element.getBoundingClientRect() + boxes.set(name, { x: box.x - head.x, y: box.y - head.y, width: box.width, height: box.height }) + } + return { height: clone.getBoundingClientRect().height, boxes } + } + let resting: ReturnType + let compact: ReturnType + try { + resting = snapshot() + clone.setAttribute('data-compact', '') + compact = snapshot() + } finally { + clone.remove() + } + restHeight = resting.height + compactHeight = compact.height + distance = Math.max(1, restHeight - compactHeight) + parts.length = 0 + const hasTabs = resting.boxes.has('tabs') + const compactTitle = compact.boxes.get('title') as Box + for (const name of PARTS) { + const from = resting.boxes.get(name) + if (from === undefined) continue + const element = bar.querySelector(`[data-plugin-header-part="${name}"]`) as HTMLElement + let to = compact.boxes.get(name) as Box + let clipStart: number | undefined + let clipEnd: number | undefined + if (name === 'intro' || (name === 'title' && hasTabs)) { + to = { ...from, y: -from.height } + if (name === 'intro' && !hasTabs) { + clipStart = Math.max(0, from.y - compactTitle.y - compactTitle.height) + clipEnd = clipStart + from.height + to.y = from.y - clipEnd + } + } + const travel = Math.max(1, Math.abs(to.y - from.y)) + distance = Math.max(distance, travel) + element.style.setProperty('--plugin-part-x', `${from.x}px`) + element.style.setProperty('--plugin-part-y', `${from.y}px`) + element.style.setProperty('--plugin-part-width', `${from.width}px`) + parts.push({ + element, from, to, distance: travel, + ...clipStart !== undefined && clipEnd !== undefined ? { clipStart, clipEnd } : {}, + }) + } + bar.style.setProperty('--plugin-header-rest-height', `${restHeight}px`) + bar.style.setProperty('--plugin-header-compact-height', `${compactHeight}px`) + bar.style.setProperty('--plugin-header-travel', `${distance}px`) + body.style.setProperty('--plugin-header-scroll-padding', `${compactHeight + 8}px`) + bar.setAttribute('data-natural', '') + scroll() + } + + measure() + const resize = new ResizeObserver(measure) + resize.observe(body) + const mutation = new MutationObserver(measure) + mutation.observe(bar.querySelector('header') as HTMLElement, { + childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ['hidden'], + }) + body.addEventListener('scroll', scroll, { passive: true }) + document.fonts.addEventListener('loadingdone', measure) + return () => { + disposed = true + resize.disconnect() + mutation.disconnect() + body.removeEventListener('scroll', scroll) + document.fonts.removeEventListener('loadingdone', measure) + body.style.removeProperty('--plugin-header-scroll-padding') + body.style.removeProperty('--plugin-page-scrollbar-width') + } +} diff --git a/packages/client/ui-plugin-manager/src/client/index.ts b/packages/client/ui-plugin-manager/src/client/index.ts index 2a6a3a48da..f5908227a4 100644 --- a/packages/client/ui-plugin-manager/src/client/index.ts +++ b/packages/client/ui-plugin-manager/src/client/index.ts @@ -24,12 +24,13 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client' // through the owning package's client-safe types subpath). import type {} from '@deepseek-ai/dsh-plugin-manager/types' import { PluginManagerPage } from './PluginManagerPage.tsx' +import { createPluginHeaderRef } from './header-scroll.ts' import { PluginRefreshToast, type PluginRefreshToastFace } from './PluginRefreshToast.tsx' import { PluginsPanelIcon } from './PluginsPanelIcon.tsx' import { configLedgerSource } from './config-ledger.ts' import { tabLedgerSource } from './tab-ledger.ts' import { PluginManagerController } from './manager-store.ts' -import { en, ja, zh, type PluginManagerLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PluginManagerLocaleKey } from './locales.ts' import { createNavigationStore } from './navigation-store.ts' import type {} from './slot-contract.ts' @@ -91,7 +92,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.pluginManager', 'rem * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plugin-manager: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plugin-manager: dictionaries') const t = ctx.locale.bind(NS) const controller = new PluginManagerController(ctx) ctx.effect(() => () => { controller.dispose() }, 'ui-plugin-manager: controller') @@ -133,7 +134,7 @@ export function apply(ctx: ClientContext): void { key: PANEL_ID, locale: NS, store, - inject: () => face, + inject: () => ({ ...face, createHeaderRef: createPluginHeaderRef }), children: { 'plugins.add.actions': { kind: 'list', scope: 'root' }, 'plugins.tab': { kind: 'list', scope: 'root' }, diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 912baa2c35..a871ab3692 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -880,3 +880,1458 @@ export const ja = { 'detailFailed': '詳細を読み込めませんでした。', 'detailFootnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + panel: 'Complementos', + title: 'Complementos', + intro: 'Instalar, habilitar y configurar complementos', + infoLabel: 'Acerca de los complementos', + infoDescription: 'Configure complementos oficiales e instale o administre otros complementos aquí. Vea la lista de complementos integrados y el estado del tiempo de ejecución en Configuración → Complementos integrados.', + loading: 'Leyendo complementos…', + error: 'No se pudieron leer todos los complementos, posiblemente debido a un problema de red', + unavailable: 'Esta implementación se ejecuta sin un perfil manejable, por lo que los complementos no se pueden instalar ni cambiar aquí.', + retry: 'Reintentar', + refresh: 'Actualizar', + refreshError: 'Error al actualizar. Por favor inténtalo de nuevo.', + empty: 'Aún no hay complementos instalados.', + addPlugin: 'Agregar complemento', + chooseAddMethod: 'Elija cómo agregar un complemento', + installExisting: 'Instalar un complemento de terceros', + installExistingDescription: 'Admite paquetes npm, repositorios Git y directorios locales', + restartNotice: 'Reinicie DSH para aplicar el cambio. {reason}', + restartReasonNoHmr: 'Este perfil aplica cambios solo al inicio.', + restartReasonPackageMoved: 'Un paquete instalado ahora se resuelve en otro directorio o el cambio reemplaza un paquete que proporciona la instalación de DSH. Desinstale un paquete antes de instalarlo desde otra ubicación.', + restartReasonHostModule: 'El cambio llega a los módulos DSH propiamente cargados.', + restartReasonUnmanagedPlugin: 'El cambio afecta a un complemento que se ejecuta fuera de la lista de complementos.', + restartReasonModuleComparison: 'DSH no pudo comparar los complementos cargados con sus archivos.', + overriddenNotice: '{name} se guardó, pero una configuración de mayor prioridad lo anula, por lo que no está vigente', + bundlesTitle: 'Complementos instalados', + basicTitle: 'Configuración del complemento incorporado', + extensionsTitle: 'Complementos experimentales', + moreTitle: 'Complementos experimentales', + moreAction: 'Más', + tabsLabel: 'Vistas de complementos', + installedTab: 'Instalado', + statusProblem: 'problema', + statusBeta: 'Experimental', + reasonLabel: 'Razón', + metadataError: 'Error de metadatos del paquete: {error}', + versionTag: 'v{version}', + partsLabel: 'Componentes', + sourceTitle: 'Fuente', + sourceSpec: 'fuente del código', + sourceBuiltIn: 'Construido en', + sourceVersion: 'Versión actual', + partsEmpty: 'Este paquete de complementos no contiene componentes.', + partsCountTotal: '{count} total', + partsCountRunning: '{count} corriendo', + partsCountOff: '{count} apagado', + partOff: 'Apagado', + partsCountFailed: '{count} falló', + partsFilter: 'Componentes del filtro', + partsFilterEmpty: 'Ningún componente coincide.', + partToggle: 'Habilitar componente {name}', + rowPhasePending: 'Esperando dependencias', + rowPhaseLoading: 'Cargando', + rowPhaseActive: 'corriendo', + rowPhaseFailed: 'problema', + rowPhaseUnloading: 'Descarga', + enableToggle: 'Habilitar {name}', + applyingChange: 'Aplicar cambios a {name}', + openDetail: 'Ver {name}', + backToList: 'Volver a complementos', + crumbRoot: 'Complementos', + backToPackage: 'Volver a {name}', + configureRow: 'Configurar {name}', + rowStateIdle: 'no corriendo', + uninstall: 'Desinstalar', + uninstallLabel: 'Desinstalar {name}', + installTitle: 'Agregar complemento', + installDescription: 'Ingrese el nombre del paquete del complemento, la dirección del repositorio GitHub o la ruta del directorio local.', + installSpecLabel: 'Nombre o dirección del paquete', + installSpecPlaceholder: 'por ejemplo dsh-plugin-ballena-mascota', + installGuideToggle: 'Guía de instalación y ejemplos.', + installGuideHide: 'Ocultar la guía', + installGuideIdTitle: 'Ingrese el nombre del paquete npm del complemento', + installGuideIdExample: 'dsh-plugin-ballena-mascota', + installGuideIdHint: 'El nombre del paquete del complemento es el nombre del paquete npm (como dsh-xxx o @author/plugin): la parte después de dsh plugin add o pnpm add en el comando de instalación README de un complemento comunitario.', + installGuideExampleLabel: 'Ejemplo: ', + installGitTemplateHint: 'Reemplace esto con la dirección real del repositorio de Git.', + installPathTemplateHint: 'Reemplace esto con la ruta real a su directorio de complementos local.', + installGuideFill: 'ejemplo de uso', + installGuideFillAria: 'Utilice el ejemplo {example}', + installGuideSafety: 'Instale sólo complementos en los que confíe: se ejecutan con sus permisos y pueden dañar DeepSeek Harness o filtrar sus datos.', + installUpgradeNotice: 'Los complementos oficiales bajo demanda ofrecen actualización cuando su versión difiere de DSH. Para actualizar otros complementos, desinstálelos y vuelva a instalarlos.', + registryToggle: 'Registro', + registryLegend: 'El registro npm desde el que se descarga el complemento', + registryDefault: 'Registro predeterminado', + registryOfficial: 'Registro oficial de npm', + registryNpmmirror: 'Espejo de China continental', + registryCustom: 'Dirección personalizada', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Ingrese una dirección de registro npm interna o privada que comience con http:// o https://.. Si requiere iniciar sesión, almacene las credenciales en ~/.npmrc en esta máquina.', + registryCustomInvalid: 'Introduzca una dirección que comience con http:// o https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Instalar', + installChecking: 'Comprobando…', + installConfirmedTitle: 'Instalar complemento', + installConfirmedRefused: 'Este complemento no se puede instalar', + installProblemInvalid: 'Este no es un nombre o dirección de paquete que se pueda instalar: {reason}', + installProblemInstalled: 'Este complemento ya está instalado. Para actualizarlo, desinstálelo e instálelo nuevamente.', + installProblemShipped: 'Este complemento se envía con DSH; actualizar DSH lo actualiza', + installBundledTitle: 'Este complemento se envía con DSH', + installBundledDescription: 'Habilítelo sin descargar otra copia', + installProblemNotFound: 'No se encontró tal complemento', + installProblemNotPackage: 'La ruta no existe o no es un paquete de complemento válido', + installProblemNotBundle: 'Este paquete no declara ningún paquete, por lo que no se puede instalar como complemento: {reason}', + installProblemNetwork: 'No se pudo acceder al registro del complemento; comprueba la red y vuelve a intentarlo', + installProblemNetworkAll: 'No se pudo acceder a ningún registro (probado: {registries}); verifique la configuración de red o proxy, o cambie el registro', + installProblemUnknown: 'No se pudo buscar el complemento: {reason}', + installingTitle: 'Instalando el complemento…', + installedTitle: 'Instalado', + statusNotInstalled: 'No instalado', + statusMissingFiles: 'Paquete no disponible', + statusUpdateAvailable: 'Actualización disponible', + sourceNotInstalled: 'No instalado', + sourceTargetSpec: 'Fuente de este DSH', + sourceTargetVersion: 'Versión para este DSH', + update: 'Actualizar', + updateLabel: 'Actualización {name}', + versionsTitle: 'Versiones', + versionsRecheck: 'comprobar de nuevo', + versionsChecking: 'Buscando actualizaciones', + versionsAvailable: 'Actualización a {version} disponible', + versionsPublished: 'Publicado {date}', + versionsLatest: 'actualizado', + versionsCheckedAt: 'Última comprobación en {time}', + versionsBlocked: 'Las versiones más nuevas necesitan una actualización DSH', + versionsBlockedDetail: '{version} necesita {peers}; este DSH es {runtime}', + versionsNoneInstallable: 'No hay una versión más nueva para instalar directamente', + versionsNoneInstallableDetail: 'Las versiones más recientes están obsoletas, son versiones preliminares o no declaran ningún paquete. Ampliar para verlos', + versionsFailedNetwork: 'No se pudo acceder al registro', + versionsFailedOther: 'No se pudo buscar actualizaciones', + versionsDetails: 'Detalles', + versionsNewerCount: '{count} más nuevo', + versionsCollapse: 'Contraer', + versionsInstalledTag: 'Instalado', + versionsPrereleaseTag: 'Presentación', + versionsDeprecatedTag: 'En desuso', + versionsNeedsDsh: 'Necesita {peers}', + versionsNotBundle: 'Esta versión no declara ningún paquete y no se puede instalar.', + versionsUpdateLabel: 'Actualización a {version}', + versionsRegistry: 'Registro', + versionsCheckedAtLabel: 'Comprobado en', + versionsFallbackTag: 'Respaldo', + versionsFallbackHint: 'No se pudo alcanzar {registry}; la lista proviene de un registro alternativo', + versionsPrimaryTag: 'primero', + versionsShowPrerelease: 'Mostrar presentaciones preliminares', + versionsOmitted: '{count} no se enumeran más versiones más recientes que la instalada', + versionsProblemNetwork: 'Inalcanzable', + versionsProblemNotFound: 'No encontrado', + versionsProblemUnknown: 'Fallido', + updatesAvailable: 'Actualizaciones disponibles: {count}', + updatesNone: 'Todos los complementos están actualizados.', + updatesFailed: 'Algunos complementos no se pudieron verificar', + updateTitle: 'Actualización {name}', + updateSource: 'Instalar desde', + updateDescription: 'La actualización reemplaza {from} por {to}; el complemento puede detenerse brevemente. Si la instalación falla, {from} se conserva o se vuelve a instalar.', + updateRunning: 'Actualizando', + updateStepChecking: 'Comprobar {version}', + updateStepRemoving: 'Detener y eliminar {version}', + updateStepInstalling: 'Instale {version} desde {registry}', + updateStepRestoring: 'Instale {version} nuevamente', + updateDone: 'Actualizado a {version}', + updateDoneDetail: 'La nueva versión se está ejecutando.', + updateRestart: '{version} instalado; entra en vigor después de un reinicio', + updateRefused: 'No se puede actualizar a {version}', + updateRemoveFailed: 'No se pudo eliminar {version}', + updateRestored: 'La actualización falló; restaurado {version}', + updateRestoredDetail: '{to} no pudo instalarse; {from} se instaló nuevamente.', + updateKept: 'No se pudo actualizar a {version}', + updateKeptDetail: '{from} no se eliminó y permanece en uso. {reason}', + updateBroken: 'La actualización falló; el complemento no está instalado', + updateBrokenDetail: '{to} no se pudo instalar y la instalación de {from} nuevamente también falló. Instale {name} nuevamente desde Agregar complemento.', + updateLost: 'Se desconoce el resultado de la actualización.', + updateLostDetail: 'Se interrumpió la conexión con DSH: {reason}. Actualice la lista de complementos para ver la versión instalada.', + updateOutput: 'Mostrar resultado de instalación', + updateFinish: 'hecho', + installFailedTitle: 'No se pudo instalar el complemento', + installGithubFailedTitle: 'No se puede acceder a GitHub', + installGithubTimeoutTitle: 'Se agotó el tiempo de espera de la conexión GitHub', + installGithubFailedDescription: 'Pruebe con otra fuente de instalación.', + installUseGithubMirror: 'Utilice el espejo de China continental', + installTryAnotherWay: 'Prueba de otra manera', + installPackageLabel: 'Nombre del paquete del complemento', + installEdit: 'Editar', + installEditAria: 'Volver a editar', + installCancelAndEdit: 'Cancelar la instalación y volver a editar', + installApplyingCancellationError: 'La cancelación no fue confirmada. Se está aplicando la instalación; espera su resultado. {reason}', + installReconcile: 'Verificar el estado de la instalación', + installUnknownTitle: 'El resultado de la instalación no está disponible', + installUnknownDescription: 'El Host no tiene ninguna instalación activa con esta identificación de solicitud. Consulte la lista de complementos antes de volver a intentarlo.', + installResultUnconfirmed: 'No se recibió el resultado de la instalación. Verificar el estado de la instalación. {reason}', + installAwaitingAcceptance: 'Esperando que el Host acepte la instalación. La cancelación se volverá a intentar automáticamente después de la confirmación.', + installBackgroundUnknown: 'El resultado de la instalación no está disponible. Consulte la lista de complementos.', + installCancel: 'Cancelar instalación', + installCloseCancels: 'Cancelar instalación y cerrar', + installViewTask: 'Ver instalación', + installUnconfirmedTitle: 'Estado de instalación no confirmado', + installBackgroundDone: 'Instalación finalizada. Ver detalles de instalación.', + installBackgroundFailed: 'La instalación falló. Ver detalles de instalación.', + installBackgroundUnconfirmed: 'El estado de la instalación no está confirmado. Vea la instalación para más detalles.', + installBackgroundApplying: 'La instalación se está aplicando y no se puede cancelar. Ver el progreso de la instalación.', + installStarting: 'Preparando la instalación…', + installCancelling: 'Deteniendo la instalación…', + installApplying: 'Aplicando configuración, espere...', + installCancelledShort: 'Cancelado', + installCancelled: 'Instalación cancelada; el complemento no está habilitado y los archivos descargados pueden permanecer', + installCancelUnconfirmed: 'No se ha confirmado que la instalación se haya detenido. Vuelva a intentar la cancelación o espere el resultado de la instalación. {reason}', + installEnableNow: 'Habilitar ahora', + installDetailsShow: 'Mostrar detalles de instalación', + installDetailsHide: 'Ocultar detalles de instalación', + installVersion: 'Versión {version}', + installSubjectPath: 'directorio local', + installSubjectGit: 'repositorio git', + installSubjectTarball: 'bola de tar', + installLocation: 'Se instala en {dir}', + installRetry: 'Reintentar', + installChangeRegistry: 'Cambiar registro', + installAttempt: '{previous} no pudo entregar el paquete; reintentando a través de {registry} (registro {index} de {total})', + installAttemptBadge: 'Intento {index} · {registry}', + installFailureNetwork: 'La conexión de red falló', + installFailureNetworkAll: 'No se pudo acceder a ningún registro (probado: {registries}). Verifique la configuración de red o proxy, o cambie el registro y vuelva a intentarlo.', + installFailureNetworkHost: 'No se pudo contactar con {host}. Una dirección GitHub o un enlace .tgz no se recupera a través del registro: esta máquina debe llegar directamente o mediante un proxy. Si el complemento también está publicado en npm, ingrese el nombre del paquete.', + installFailureNotFound: 'No se encontró tal complemento', + installFailureNoMatchingVersion: 'Ninguna versión coincide con la solicitud', + installFailureDiskFull: 'El disco está lleno; la instalación se detuvo', + installFailurePermission: 'Sin permiso de escritura; el complemento no se puede instalar', + installFailureBuildBlocked: 'Los scripts de instalación de una dependencia necesitan su permiso antes de que la instalación pueda continuar', + installFailureBuildBlockedManual: 'pnpm bloqueó los scripts de instalación; permítalos en enableBuilds en pnpm-workspace.yaml y vuelva a intentarlo', + installFailureIntegrity: 'El paquete descargado no pasó la verificación de integridad', + installFailureTimeout: 'La instalación expiró', + installFailurePnpmMissing: 'No se encontró pnpm, por lo que no se puede instalar nada.', + installFailureGeneric: 'Algo salió mal durante la instalación; los detalles dicen lo que', + terminalCommandLine: 'Línea de comando {n}', + terminalRunning: 'corriendo', + terminalFailed: 'Fallido', + terminalDone: 'hecho', + terminalCopy: 'Copiar', + terminalCopied: 'Copiado', + terminalNoOutput: 'Sin salida', + terminalCollapseAria: 'Contraer salida', + terminalCollapse: 'Contraer', + terminalExpandAria: 'Expanda las líneas de salida {n} restantes', + terminalExpand: '… {n} más líneas', + terminalExitCode: 'código de salida {code}', + terminalSignal: 'señal {signal}', + terminalNoExitCode: 'sin código de salida', + installDoneNothing: 'Instalación finalizada sin nuevas dependencias.', + installDoneRestart: 'Instalado; reinicie DSH para cargarlo. {reason}', + installDoneApproved: 'Instalar scripts permitidos para {names}', + installDoneOtherVersion: 'Según la política de seguridad de la cadena de suministro de pnpm (minimumReleaseAge), las versiones publicadas más recientemente que la antigüedad requerida no se instalan automáticamente, por lo que esta instalación utilizó {installed} en lugar de la última versión {version}. Una versión recién publicada no ha completado este período de espera y puede contener códigos maliciosos o defectos no detectados. Si necesita {version}, desinstale la versión actual y luego instale {exact} para solicitar esa versión.', + installApprovalTitle: 'Instalar scripts necesita permiso', + installApprovalDescription: 'Estos paquetes tienen scripts de instalación que pnpm no ejecutó.', + installApprovalConsequence: 'Una vez permitidos, los scripts se ejecutan aquí con sus permisos y el permiso se guarda en este perfil.', + installApprovalCaution: 'Permita solo paquetes en los que confíe.', + installApproveAndRetry: 'Permita estos scripts y vuelva a intentarlo', + installClose: 'hecho', + close: 'Cerrar', + cancel: 'Cancelar', + confirmUninstallTitle: '¿Desinstalar "{name}"?', + confirmUninstallDescription: 'Lo que proporciona desaparece una vez que se desinstala.', + confirmUninstall: 'Desinstalar', + failedEnable: 'No se pudo habilitar: {reason}', + failedDisable: 'No se pudo desactivar: {reason}', + failedUninstall: 'No se pudo desinstalar: {reason}', + failedRowEnable: 'No se pudo habilitar el componente: {reason}', + failedRowDisable: 'No se pudo desactivar el componente: {reason}', + reasonManagementRequired: 'La gestión de complementos lo necesita; no se puede apagar ni desinstalar.', + reasonUnaddressable: 'El parche de perfil no puede abordar este de forma única.', + reasonUnknownPlugin: 'No existe tal complemento.', + reasonInvalidSpec: 'Ingrese un nombre o dirección de paquete válido.', + reasonAlreadyInstalled: 'Este paquete ya está instalado; desinstálelo antes de instalar otra versión.', + reasonAmbiguousInstall: 'No se puede saber qué paquete se instaló a partir del cambio de dependencia.', + reasonNotBundle: 'Este paquete no declara ningún paquete, por lo que no se puede administrar como un complemento.', + reasonNotRemovable: 'Este paquete no es propiedad del perfil o la administración del complemento lo necesita.', + reasonStopProfile: 'Este perfil se ejecuta sin HMR; deténgalo y desinstale el paquete con el complemento dsh.', + reasonBundleInUse: 'Otra configuración todavía usa los componentes de este paquete; apáguelos primero.', + reasonStaleApproval: 'Las aprobaciones de guiones pendientes cambiaron; instálelo nuevamente para actualizarlos.', + reasonIncompatibleVersion: '{plugin} es incompatible con DSH {runtime} (requiere {requirements}); ejecutarlo puede causar fallas o pérdida de datos.', + reasonIncompatibleVersionUnnamed: 'Este complemento es incompatible con la versión DSH en ejecución; ejecutarlo puede causar fallas o pérdida de datos.', + reasonIncompatibleInstall: 'Instale una versión del complemento compatible con este DSH.', + reasonIncompatibleInstalled: 'Desinstálalo e instala una versión compatible con este DSH.', + reasonOperationError: 'El anfitrión informó un error.', + detailTitle: 'Detalles', + detailLoading: 'Detalles de lectura', + detailTooLarge: 'Los detalles exceden el límite de tamaño y no se cargaron.', + detailNetwork: 'No se pudo llegar a la fuente de los detalles.', + detailInvalid: 'El complemento declara un archivo de detalles no válido.', + detailFailed: 'No se pudieron leer los detalles.', + detailFootnotes: 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + panel: 'Plugins', + title: 'Plugins', + intro: 'Installer, activer et configurer les plugins', + infoLabel: 'À propos des plugins', + infoDescription: "Configurez les plugins officiels et installez ou gérez d'autres plugins ici. Affichez la liste des plugins intégrés et l'état d'exécution dans Paramètres → Plugins intégrés.", + loading: 'Plugins de lecture…', + error: "Impossible de lire tous les plugins, probablement à cause d'un problème de réseau", + unavailable: "Ce déploiement s'exécute sans profil gérable, les plugins ne peuvent donc pas être installés ou commutés ici.", + retry: 'Réessayer', + refresh: 'Actualiser', + refreshError: "L'actualisation a échoué. Veuillez réessayer.", + empty: "Aucun plugin n'est encore installé.", + addPlugin: 'Ajouter un plugin', + chooseAddMethod: 'Choisissez comment ajouter un plugin', + installExisting: 'Installer un plugin tiers', + installExistingDescription: 'Prend en charge les packages npm, les référentiels Git et les répertoires locaux', + restartNotice: 'Redémarrez DSH pour appliquer la modification. {reason}', + restartReasonNoHmr: 'Ce profil applique les modifications uniquement au démarrage.', + restartReasonPackageMoved: "Un package installé est désormais résolu dans un autre répertoire ou la modification remplace un package fourni par l'installation de DSH. Désinstallez un package avant de l’installer à partir d’un autre emplacement.", + restartReasonHostModule: 'Le changement atteint les modules DSH lui-même chargés.', + restartReasonUnmanagedPlugin: 'La modification atteint un plugin exécuté en dehors de la liste des plugins.', + restartReasonModuleComparison: "DSH n'a pas pu comparer les plugins chargés avec leurs fichiers.", + overriddenNotice: "{name} a été enregistré, mais une configuration de priorité plus élevée l'écrase, elle n'est donc pas effective", + bundlesTitle: 'Plugins installés', + basicTitle: 'Paramètres du plugin intégré', + extensionsTitle: 'Plugins expérimentaux', + moreTitle: 'Plugins expérimentaux', + moreAction: 'Plus', + tabsLabel: 'Vues des plugins', + installedTab: 'Installé', + statusProblem: 'Problème', + statusBeta: 'Expérimental', + reasonLabel: 'Raison', + metadataError: 'Erreur de métadonnées du package : {error}', + versionTag: 'v{version}', + partsLabel: 'Composants', + sourceTitle: 'Source', + sourceSpec: 'Source du code', + sourceBuiltIn: 'Intégré', + sourceVersion: 'Version actuelle', + partsEmpty: 'Ce pack de plugins ne contient aucun composant.', + partsCountTotal: 'Total {count}', + partsCountRunning: "{count} en cours d'exécution", + partsCountOff: '{count} désactivé', + partOff: 'Désactivé', + partsCountFailed: 'Échec du {count}', + partsFilter: 'Composants du filtre', + partsFilterEmpty: 'Aucun composant ne correspond.', + partToggle: 'Activer le composant {name}', + rowPhasePending: 'En attente de dépendances', + rowPhaseLoading: 'Chargement', + rowPhaseActive: 'Courir', + rowPhaseFailed: 'Problème', + rowPhaseUnloading: 'Déchargement', + enableToggle: 'Activer {name}', + applyingChange: 'Application des modifications à {name}', + openDetail: 'Voir {name}', + backToList: 'Retour aux plugins', + crumbRoot: 'Plugins', + backToPackage: 'Retour à {name}', + configureRow: 'Configurer {name}', + rowStateIdle: 'Ne fonctionne pas', + uninstall: 'Désinstaller', + uninstallLabel: 'Désinstaller {name}', + installTitle: 'Ajouter un plugin', + installDescription: "Entrez le nom du package du plugin, l'adresse du référentiel GitHub ou le chemin du répertoire local.", + installSpecLabel: 'Nom ou adresse du colis', + installSpecPlaceholder: 'par exemple dsh-plugin-whale-pet', + installGuideToggle: "Guide d'installation et exemples", + installGuideHide: 'Masquer le guide', + installGuideIdTitle: 'Entrez le nom du package npm du plugin', + installGuideIdExample: 'dsh-plugin-baleine-animal de compagnie', + installGuideIdHint: "Le nom du package du plugin est le nom du package npm (comme dsh-xxx ou @author/plugin) : la partie après dsh plugin add ou pnpm add dans la commande d'installation README d'un plugin communautaire.", + installGuideExampleLabel: 'Exemple : ', + installGitTemplateHint: "Remplacez-le par l'adresse réelle du référentiel Git.", + installPathTemplateHint: 'Remplacez-le par le chemin réel vers votre répertoire de plugin local.', + installGuideFill: "Utiliser l'exemple", + installGuideFillAria: "Utilisez l'exemple {example}", + installGuideSafety: "Installez uniquement les plugins en qui vous avez confiance : ils s'exécutent avec vos autorisations et peuvent endommager DeepSeek Harness ou divulguer vos données.", + installUpgradeNotice: "Les plugins officiels à la demande proposent la mise à jour lorsque leur version diffère de DSH. Pour mettre à jour d'autres plugins, désinstallez-les et réinstallez-les.", + registryToggle: 'Registre', + registryLegend: 'Le registre npm à partir duquel le plugin est téléchargé', + registryDefault: 'Registre par défaut', + registryOfficial: 'Registre officiel npm', + registryNpmmirror: 'Miroir de Chine continentale', + registryCustom: 'Adresse personnalisée', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: "Entrez une adresse de registre npm interne ou privée commençant par http:// ou https://.. Si cela nécessite une connexion, stockez les informations d'identification dans ~/.npmrc sur cette machine.", + registryCustomInvalid: 'Saisissez une adresse commençant par http:// ou https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Installer', + installChecking: 'Vérification…', + installConfirmedTitle: 'Installer le plug-in', + installConfirmedRefused: 'Ce plugin ne peut pas être installé', + installProblemInvalid: "Il ne s'agit pas d'un nom ou d'une adresse de package pouvant être installé : {reason}", + installProblemInstalled: 'Ce plugin est déjà installé. Pour le mettre à niveau, désinstallez-le et réinstallez-le', + installProblemShipped: 'Ce plugin est livré avec DSH ; la mise à niveau de DSH le met à jour', + installBundledTitle: 'Ce plugin est livré avec DSH', + installBundledDescription: 'Activez-le sans télécharger une autre copie', + installProblemNotFound: "Aucun plugin de ce type n'a été trouvé", + installProblemNotPackage: "Le chemin n'existe pas ou n'est pas un package de plugin valide", + installProblemNotBundle: 'Ce package ne déclare aucun bundle, il ne peut donc pas être installé en tant que plugin : {reason}', + installProblemNetwork: "Le registre du plugin n'a pas pu être atteint ; vérifie le réseau et réessaye", + installProblemNetworkAll: "Aucun registre n'a pu être atteint (essayé : {registries}) ; vérifiez les paramètres réseau ou proxy, ou modifiez le registre", + installProblemUnknown: "Le plugin n'a pas pu être recherché : {reason}", + installingTitle: 'Installation du plugin…', + installedTitle: 'Installé', + statusNotInstalled: 'Non installé', + statusMissingFiles: 'Forfait indisponible', + statusUpdateAvailable: 'Mise à jour disponible', + sourceNotInstalled: 'Non installé', + sourceTargetSpec: 'Source pour ce DSH', + sourceTargetVersion: 'Version pour ce DSH', + update: 'Mise à jour', + updateLabel: 'Mettre à jour {name}', + versionsTitle: 'Versions', + versionsRecheck: 'Vérifiez à nouveau', + versionsChecking: 'Vérification des mises à jour', + versionsAvailable: 'Mise à jour vers {version} disponible', + versionsPublished: 'Publié {date}', + versionsLatest: 'À jour', + versionsCheckedAt: 'Dernière vérification à {time}', + versionsBlocked: 'Les versions plus récentes nécessitent une mise à jour DSH', + versionsBlockedDetail: '{version} a besoin de {peers} ; ce DSH est {runtime}', + versionsNoneInstallable: 'Aucune version plus récente à installer directement', + versionsNoneInstallableDetail: 'Les versions les plus récentes sont obsolètes, sont des versions préliminaires ou ne déclarent aucun bundle. Agrandissez pour les voir', + versionsFailedNetwork: "Impossible d'accéder au registre", + versionsFailedOther: 'Impossible de vérifier les mises à jour', + versionsDetails: 'Détails', + versionsNewerCount: '{count} plus récent', + versionsCollapse: 'Réduire', + versionsInstalledTag: 'Installé', + versionsPrereleaseTag: 'Avant-première', + versionsDeprecatedTag: 'Obsolète', + versionsNeedsDsh: 'Besoins {peers}', + versionsNotBundle: 'Cette version ne déclare aucun bundle et ne peut pas être installée', + versionsUpdateLabel: 'Mise à jour vers {version}', + versionsRegistry: 'Registre', + versionsCheckedAtLabel: 'Vérifié à', + versionsFallbackTag: 'Repli', + versionsFallbackHint: "{registry} n'a pas pu être atteint ; la liste provient d'un registre de secours", + versionsPrimaryTag: "D'abord", + versionsShowPrerelease: 'Afficher les avant-premières', + versionsOmitted: '{count} et les versions plus récentes que celle installée ne sont pas répertoriées', + versionsProblemNetwork: 'Injoignable', + versionsProblemNotFound: 'Pas trouvé', + versionsProblemUnknown: 'Échec', + updatesAvailable: 'Mises à jour disponibles : {count}', + updatesNone: 'Tous les plugins sont à jour', + updatesFailed: "Certains plugins n'ont pas pu être vérifiés", + updateTitle: 'Mettre à jour {name}', + updateSource: 'Installer à partir de', + updateDescription: "La mise à jour remplace {from} par {to} ; le plugin peut s'arrêter brièvement. Si l'installation échoue, {from} est conservé ou réinstallé.", + updateRunning: 'Mise à jour', + updateStepChecking: 'Vérifiez {version}', + updateStepRemoving: 'Arrêtez et supprimez {version}', + updateStepInstalling: 'Installez {version} à partir de {registry}', + updateStepRestoring: 'Installez à nouveau {version}', + updateDone: 'Mis à jour vers {version}', + updateDoneDetail: "La nouvelle version est en cours d'exécution.", + updateRestart: '{version} installé ; cela prend effet après un redémarrage', + updateRefused: 'Impossible de mettre à jour vers {version}', + updateRemoveFailed: 'Impossible de supprimer {version}', + updateRestored: 'La mise à jour a échoué ; restauré {version}', + updateRestoredDetail: "Échec de l'installation de {to} ; {from} a été réinstallé.", + updateKept: 'Impossible de mettre à jour vers {version}', + updateKeptDetail: '{from} n’a pas été supprimé et reste utilisé. {reason}', + updateBroken: "La mise à jour a échoué ; le plugin n'est pas installé", + updateBrokenDetail: "L'installation de {to} a échoué et l'installation de {from} a également échoué. Installez à nouveau {name} à partir d'Ajouter un plugin.", + updateLost: 'Le résultat de la mise à jour est inconnu', + updateLostDetail: 'La connexion à DSH a été interrompue : {reason}. Actualisez la liste des plugins pour voir la version installée.', + updateOutput: "Afficher le résultat de l'installation", + updateFinish: 'Terminé', + installFailedTitle: "Le plugin n'a pas pu être installé", + installGithubFailedTitle: "Impossible d'accéder à GitHub", + installGithubTimeoutTitle: 'La connexion GitHub a expiré', + installGithubFailedDescription: "Essayez une autre source d'installation.", + installUseGithubMirror: 'Utiliser un miroir de Chine continentale', + installTryAnotherWay: 'Essayez une autre façon', + installPackageLabel: 'Nom du package du plugin', + installEdit: 'Modifier', + installEditAria: "Retour à l'édition", + installCancelAndEdit: "Annuler l'installation et revenir à l'édition", + installApplyingCancellationError: "L'annulation n'a pas été confirmée. L'installation est en cours d'application ; attendre son résultat. {reason}", + installReconcile: "Vérifier l'état de l'installation", + installUnknownTitle: "Résultat de l'installation indisponible", + installUnknownDescription: "L'hôte n'a aucune installation active avec cet identifiant de demande. Vérifiez la liste des plugins avant de réessayer.", + installResultUnconfirmed: "Le résultat de l'installation n'a pas été reçu. Vérifiez l'état de l'installation. {reason}", + installAwaitingAcceptance: "En attente que l'hôte accepte l'installation. L'annulation sera réessayée automatiquement après confirmation.", + installBackgroundUnknown: "Résultat de l'installation indisponible. Vérifiez la liste des plugins.", + installCancel: "Annuler l'installation", + installCloseCancels: "Annuler l'installation et fermer", + installViewTask: "Voir l'installation", + installUnconfirmedTitle: "État de l'installation non confirmé", + installBackgroundDone: "Installation terminée. Afficher les détails de l'installation.", + installBackgroundFailed: "L'installation a échoué. Afficher les détails de l'installation.", + installBackgroundUnconfirmed: "L'état de l'installation n'est pas confirmé. Consultez l'installation pour plus de détails.", + installBackgroundApplying: "L'installation est en cours d'application et ne peut pas être annulée. Afficher la progression de l'installation.", + installStarting: "Préparation de l'installation…", + installCancelling: "Arrêt de l'installation…", + installApplying: 'Application de la configuration, veuillez patienter…', + installCancelledShort: 'Annulé', + installCancelled: "Installation annulée ; le plugin n'est pas activé et les fichiers téléchargés peuvent rester", + installCancelUnconfirmed: "L'installation n'a pas été confirmée arrêtée. Réessayez l'annulation ou attendez le résultat de l'installation. {reason}", + installEnableNow: 'Activer maintenant', + installDetailsShow: "Afficher les détails de l'installation", + installDetailsHide: "Masquer les détails de l'installation", + installVersion: 'Version {version}', + installSubjectPath: 'Annuaire local', + installSubjectGit: 'Dépôt Git', + installSubjectTarball: 'Tarball', + installLocation: "S'installe dans {dir}", + installRetry: 'Réessayer', + installChangeRegistry: 'Changer de registre', + installAttempt: "{previous} n'a pas pu servir le package ; nouvelle tentative via {registry} (registre {index} de {total})", + installAttemptBadge: 'Tentative {index} · {registry}', + installFailureNetwork: 'La connexion réseau a échoué', + installFailureNetworkAll: "Aucun registre n'a pu être atteint (essayé : {registries}). Vérifiez les paramètres réseau ou proxy, ou modifiez le registre et réessayez.", + installFailureNetworkHost: "{host} n'a pas pu être atteint. Une adresse GitHub ou un lien .tgz ne sont pas récupérés via le registre : cette machine doit y accéder directement ou via un proxy. Si le plugin est également publié sur npm, saisissez plutôt le nom de son package.", + installFailureNotFound: "Aucun plugin de ce type n'a été trouvé", + installFailureNoMatchingVersion: 'Aucune version ne correspond à la demande', + installFailureDiskFull: "Le disque est plein ; l'installation s'est arrêtée", + installFailurePermission: "Aucune autorisation d'écriture ; le plugin ne peut pas être installé", + installFailureBuildBlocked: "Les scripts d'installation d'une dépendance ont besoin de votre autorisation avant que l'installation puisse continuer", + installFailureBuildBlockedManual: "pnpm a bloqué les scripts d'installation ; autorisez-les sous AllowBuilds dans pnpm-workspace.yaml et réessayez", + installFailureIntegrity: "Le package téléchargé a échoué à sa vérification d'intégrité", + installFailureTimeout: "L'installation a expiré", + installFailurePnpmMissing: "pnpm n'a pas été trouvé, donc rien ne peut être installé", + installFailureGeneric: "Quelque chose s'est mal passé lors de l'installation ; les détails disent quoi", + terminalCommandLine: 'Ligne de commande {n}', + terminalRunning: 'Courir', + terminalFailed: 'Échec', + terminalDone: 'Terminé', + terminalCopy: 'Copier', + terminalCopied: 'Copié', + terminalNoOutput: 'Aucune sortie', + terminalCollapseAria: 'Réduire la sortie', + terminalCollapse: 'Réduire', + terminalExpandAria: 'Développez les lignes de sortie {n} restantes', + terminalExpand: '… {n} plus de lignes', + terminalExitCode: 'code de sortie {code}', + terminalSignal: 'signal {signal}', + terminalNoExitCode: 'pas de code de sortie', + installDoneNothing: 'Installation terminée sans nouvelle dépendance.', + installDoneRestart: 'Installé ; redémarrez DSH pour le charger. {reason}', + installDoneApproved: "Scripts d'installation autorisés pour {names}", + installDoneOtherVersion: "Conformément à la politique de sécurité de la chaîne d'approvisionnement de pnpm (minimumReleaseAge), les versions publiées plus récemment que l'âge requis ne sont pas installées automatiquement. Cette installation a donc utilisé {installed} au lieu de la dernière version {version}. Une version nouvellement publiée n'a pas dépassé cette période d'attente et peut contenir du code malveillant ou des défauts non détectés. Si vous avez besoin de {version}, désinstallez la version actuelle, puis installez {exact} pour demander cette version.", + installApprovalTitle: "Les scripts d'installation nécessitent une autorisation", + installApprovalDescription: "Ces packages contiennent des scripts d'installation que pnpm n'a pas exécutés.", + installApprovalConsequence: "Une fois autorisés, les scripts s'exécutent ici avec vos autorisations et l'autorisation est enregistrée dans ce profil.", + installApprovalCaution: 'Autorisez uniquement les packages auxquels vous faites confiance.', + installApproveAndRetry: 'Autoriser ces scripts et réessayer', + installClose: 'Terminé', + close: 'Fermer', + cancel: 'Annuler', + confirmUninstallTitle: 'Désinstaller "{name}" ?', + confirmUninstallDescription: "Ce qu'il fournit disparaît une fois désinstallé.", + confirmUninstall: 'Désinstaller', + failedEnable: "Impossible d'activer : {reason}", + failedDisable: 'Impossible de désactiver : {reason}', + failedUninstall: 'Impossible de désinstaller : {reason}', + failedRowEnable: "Impossible d'activer le composant : {reason}", + failedRowDisable: 'Impossible de désactiver le composant : {reason}', + reasonManagementRequired: 'La gestion des plugins en a besoin ; il ne peut pas être éteint ou désinstallé.', + reasonUnaddressable: 'Le correctif de profil ne peut pas résoudre ce problème de manière unique.', + reasonUnknownPlugin: 'Aucun plugin de ce type.', + reasonInvalidSpec: 'Entrez un nom ou une adresse de package valide.', + reasonAlreadyInstalled: "Ce package est déjà installé ; désinstallez-le avant d'installer une autre version.", + reasonAmbiguousInstall: 'Le changement de dépendance ne permet pas de déterminer quel package a été installé.', + reasonNotBundle: 'Ce package ne déclare aucun bundle, il ne peut donc pas être géré comme un plugin.', + reasonNotRemovable: "Ce package n'appartient pas au profil, ou la gestion des plugins en a besoin.", + reasonStopProfile: "Ce profil s'exécute sans HMR ; arrêtez-le et désinstallez le package avec le plugin dsh.", + reasonBundleInUse: "Une autre configuration utilise toujours les composants de ce bundle ; éteignez-les d'abord.", + reasonStaleApproval: 'Les approbations de script en attente ont changé ; réinstallez-les pour les actualiser.', + reasonIncompatibleVersion: '{plugin} est incompatible avec DSH {runtime} (nécessite {requirements}) ; son exécution peut provoquer des plantages ou une perte de données.', + reasonIncompatibleVersionUnnamed: "Ce plugin est incompatible avec la version DSH en cours d'exécution ; son exécution peut provoquer des plantages ou une perte de données.", + reasonIncompatibleInstall: 'Installez une version du plugin compatible avec ce DSH.', + reasonIncompatibleInstalled: 'Désinstallez-le et installez une version compatible avec ce DSH.', + reasonOperationError: "L'hôte a signalé une erreur.", + detailTitle: 'Détails', + detailLoading: 'Détails de lecture', + detailTooLarge: "Les détails dépassent la limite de taille et n'ont pas été chargés.", + detailNetwork: "Impossible d'atteindre la source des détails.", + detailInvalid: 'Le plugin déclare un fichier de détails invalide.', + detailFailed: 'Impossible de lire les détails.', + detailFootnotes: 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + panel: 'Plugins', + title: 'Plugins', + intro: 'Plugins installieren, aktivieren und konfigurieren', + infoLabel: 'Über Plugins', + infoDescription: 'Konfigurieren Sie hier offizielle Plugins und installieren oder verwalten Sie andere Plugins. Sehen Sie sich die Liste der integrierten Plugins und den Laufzeitstatus unter Einstellungen → Integrierte Plugins an.', + loading: 'Plugins lesen…', + error: 'Es konnten nicht alle Plugins gelesen werden, möglicherweise aufgrund eines Netzwerkproblems', + unavailable: 'Diese Bereitstellung läuft ohne verwaltbares Profil, daher können hier keine Plugins installiert oder gewechselt werden.', + retry: 'Erneut versuchen', + refresh: 'Aktualisieren', + refreshError: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', + empty: 'Es sind noch keine Plugins installiert.', + addPlugin: 'Plugin hinzufügen', + chooseAddMethod: 'Wählen Sie aus, wie Sie ein Plugin hinzufügen möchten', + installExisting: 'Installieren Sie ein Drittanbieter-Plugin', + installExistingDescription: 'Unterstützt npm-Pakete, Git-Repositorys und lokale Verzeichnisse', + restartNotice: 'Starten Sie DSH neu, um die Änderung zu übernehmen. {reason}', + restartReasonNoHmr: 'Dieses Profil wendet Änderungen nur beim Start an.', + restartReasonPackageMoved: 'Ein installiertes Paket wird nun in ein anderes Verzeichnis aufgelöst, oder die Änderung ersetzt ein Paket, das die DSH-Installation bereitstellt. Deinstallieren Sie ein Paket, bevor Sie es von einem anderen Speicherort installieren.', + restartReasonHostModule: 'Die Änderung erreicht die geladenen Module DSH selbst.', + restartReasonUnmanagedPlugin: 'Die Änderung betrifft ein Plugin, das außerhalb der Plugin-Liste ausgeführt wird.', + restartReasonModuleComparison: 'DSH konnte die geladenen Plugins nicht mit ihren Dateien vergleichen.', + overriddenNotice: '{name} wurde gespeichert, aber eine Konfiguration mit höherer Priorität überschreibt es, sodass es nicht wirksam ist', + bundlesTitle: 'Installierte Plugins', + basicTitle: 'Integrierte Plugin-Einstellungen', + extensionsTitle: 'Experimentelle Plugins', + moreTitle: 'Experimentelle Plugins', + moreAction: 'Mehr', + tabsLabel: 'Plugin-Ansichten', + installedTab: 'Installiert', + statusProblem: 'Problem', + statusBeta: 'Experimentell', + reasonLabel: 'Grund', + metadataError: 'Paketmetadatenfehler: {error}', + versionTag: 'v{version}', + partsLabel: 'Komponenten', + sourceTitle: 'Quelle', + sourceSpec: 'Codequelle', + sourceBuiltIn: 'Eingebaut', + sourceVersion: 'Aktuelle Version', + partsEmpty: 'Dieses Plugin-Paket enthält keine Komponenten.', + partsCountTotal: '{count} insgesamt', + partsCountRunning: '{count} läuft', + partsCountOff: '{count} aus', + partOff: 'Aus', + partsCountFailed: '{count} ist fehlgeschlagen', + partsFilter: 'Komponenten filtern', + partsFilterEmpty: 'Keine Komponentenübereinstimmungen.', + partToggle: 'Aktivieren Sie die Komponente {name}', + rowPhasePending: 'Warten auf Abhängigkeiten', + rowPhaseLoading: 'Laden', + rowPhaseActive: 'Laufen', + rowPhaseFailed: 'Problem', + rowPhaseUnloading: 'Entladen', + enableToggle: 'Aktivieren Sie {name}', + applyingChange: 'Anwenden von Änderungen auf {name}', + openDetail: 'Siehe {name}', + backToList: 'Zurück zu Plugins', + crumbRoot: 'Plugins', + backToPackage: 'Zurück zu {name}', + configureRow: 'Konfigurieren Sie {name}', + rowStateIdle: 'Läuft nicht', + uninstall: 'Deinstallieren', + uninstallLabel: 'Deinstallieren Sie {name}', + installTitle: 'Plugin hinzufügen', + installDescription: 'Geben Sie den Paketnamen des Plugins, die GitHub-Repository-Adresse oder den lokalen Verzeichnispfad ein.', + installSpecLabel: 'Name oder Adresse des Pakets', + installSpecPlaceholder: 'zum Beispiel dsh-plugin-whale-pet', + installGuideToggle: 'Installationsanleitung und Beispiele', + installGuideHide: 'Verstecke den Leitfaden', + installGuideIdTitle: 'Geben Sie den NPM-Paketnamen des Plugins ein', + installGuideIdExample: 'dsh-plugin-whale-pet', + installGuideIdHint: 'Der Plugin-Paketname ist der npm-Paketname (wie dsh-xxx oder @author/plugin): der Teil nach dshplugin add oder pnpm add im README-Installationsbefehl eines Community-Plugins.', + installGuideExampleLabel: 'Beispiel: ', + installGitTemplateHint: 'Ersetzen Sie dies durch die tatsächliche Git-Repository-Adresse.', + installPathTemplateHint: 'Ersetzen Sie dies durch den tatsächlichen Pfad zu Ihrem lokalen Plugin-Verzeichnis.', + installGuideFill: 'Beispiel verwenden', + installGuideFillAria: 'Verwenden Sie das Beispiel {example}', + installGuideSafety: 'Installieren Sie nur Plugins, denen Sie vertrauen: Sie werden mit Ihren Berechtigungen ausgeführt und können DeepSeek Harness beschädigen oder Ihre Daten preisgeben.', + installUpgradeNotice: 'Offizielle On-Demand-Plugins bieten Updates an, wenn ihre Version von DSH abweicht. Um andere Plugins zu aktualisieren, deinstallieren Sie sie und installieren Sie sie erneut.', + registryToggle: 'Registrierung', + registryLegend: 'Die NPM-Registrierung, aus der das Plugin heruntergeladen wird', + registryDefault: 'Standardregistrierung', + registryOfficial: 'Offizielle npm-Registrierung', + registryNpmmirror: 'Spiegel vom chinesischen Festland', + registryCustom: 'Benutzerdefinierte Adresse', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Geben Sie eine interne oder private NPM-Registrierungsadresse ein, beginnend mit http:// oder https://.. Wenn eine Anmeldung erforderlich ist, speichern Sie die Anmeldeinformationen in ~/.npmrc auf diesem Computer.', + registryCustomInvalid: 'Geben Sie eine Adresse ein, die mit http:// oder https:// beginnt.', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Installieren', + installChecking: 'Überprüfen…', + installConfirmedTitle: 'Plugin installieren', + installConfirmedRefused: 'Dieses Plugin kann nicht installiert werden', + installProblemInvalid: 'Dies ist kein Paketname oder keine Adresse, die installiert werden kann: {reason}', + installProblemInstalled: 'Dieses Plugin ist bereits installiert. Um es zu aktualisieren, deinstallieren Sie es und installieren Sie es erneut', + installProblemShipped: 'Dieses Plugin wird mit DSH geliefert; Durch das Aktualisieren von DSH wird es aktualisiert', + installBundledTitle: 'Dieses Plugin wird mit DSH geliefert', + installBundledDescription: 'Aktivieren Sie es, ohne eine weitere Kopie herunterzuladen', + installProblemNotFound: 'Es wurde kein solches Plugin gefunden', + installProblemNotPackage: 'Der Pfad existiert nicht oder ist kein gültiges Plugin-Paket', + installProblemNotBundle: 'Dieses Paket deklariert kein Bundle und kann daher nicht als Plugin installiert werden: {reason}', + installProblemNetwork: 'Die Plugin-Registrierung konnte nicht erreicht werden; Überprüfen Sie das Netzwerk und versuchen Sie es erneut', + installProblemNetworkAll: 'Es konnte keine Registry erreicht werden (versucht: {registries}); Überprüfen Sie die Netzwerk- oder Proxy-Einstellungen oder ändern Sie die Registrierung', + installProblemUnknown: 'Das Plugin konnte nicht gefunden werden: {reason}', + installingTitle: 'Plugin installieren…', + installedTitle: 'Installiert', + statusNotInstalled: 'Nicht installiert', + statusMissingFiles: 'Paket nicht verfügbar', + statusUpdateAvailable: 'Update verfügbar', + sourceNotInstalled: 'Nicht installiert', + sourceTargetSpec: 'Quelle hierfür DSH', + sourceTargetVersion: 'Version für dieses DSH', + update: 'Aktualisieren', + updateLabel: 'Aktualisieren Sie {name}', + versionsTitle: 'Versionen', + versionsRecheck: 'Überprüfen Sie es noch einmal', + versionsChecking: 'Suche nach Updates', + versionsAvailable: 'Update auf {version} verfügbar', + versionsPublished: 'Veröffentlicht {date}', + versionsLatest: 'Auf dem neuesten Stand', + versionsCheckedAt: 'Zuletzt überprüft bei {time}', + versionsBlocked: 'Neuere Versionen benötigen ein DSH-Update', + versionsBlockedDetail: '{version} benötigt {peers}; dieser DSH ist {runtime}', + versionsNoneInstallable: 'Keine neuere Version zur direkten Installation', + versionsNoneInstallableDetail: 'Die neueren Versionen sind veraltet, Vorabversionen oder deklarieren kein Bundle. Erweitern Sie, um sie zu sehen', + versionsFailedNetwork: 'Die Registrierung konnte nicht erreicht werden', + versionsFailedOther: 'Es konnte nicht nach Updates gesucht werden', + versionsDetails: 'Details', + versionsNewerCount: '{count} neuer', + versionsCollapse: 'Einklappen', + versionsInstalledTag: 'Installiert', + versionsPrereleaseTag: 'Vorabversion', + versionsDeprecatedTag: 'Veraltet', + versionsNeedsDsh: 'Benötigt {peers}', + versionsNotBundle: 'Diese Version deklariert kein Bundle und kann nicht installiert werden', + versionsUpdateLabel: 'Update auf {version}', + versionsRegistry: 'Registrierung', + versionsCheckedAtLabel: 'Geprüft bei', + versionsFallbackTag: 'Rückfall', + versionsFallbackHint: '{registry} konnte nicht erreicht werden; Die Liste stammt aus einer Fallback-Registrierung', + versionsPrimaryTag: 'Erstens', + versionsShowPrerelease: 'Vorabversionen anzeigen', + versionsOmitted: '{count} Weitere Versionen, die neuer als die installierte sind, werden nicht aufgeführt', + versionsProblemNetwork: 'Unerreichbar', + versionsProblemNotFound: 'Nicht gefunden', + versionsProblemUnknown: 'Fehlgeschlagen', + updatesAvailable: 'Verfügbare Updates: {count}', + updatesNone: 'Alle Plugins sind auf dem neuesten Stand', + updatesFailed: 'Einige Plugins konnten nicht überprüft werden', + updateTitle: 'Aktualisieren Sie {name}', + updateSource: 'Installieren von', + updateDescription: 'Das Update ersetzt {from} durch {to}; Das Plugin stoppt möglicherweise kurzzeitig. Wenn die Installation fehlschlägt, bleibt {from} erhalten oder wird erneut installiert.', + updateRunning: 'Aktualisierung', + updateStepChecking: 'Überprüfen Sie {version}', + updateStepRemoving: 'Stoppen und entfernen Sie {version}', + updateStepInstalling: 'Installieren Sie {version} von {registry}', + updateStepRestoring: 'Installieren Sie {version} erneut', + updateDone: 'Aktualisiert auf {version}', + updateDoneDetail: 'Die neue Version läuft.', + updateRestart: '{version} installiert; es wird erst nach einem Neustart wirksam', + updateRefused: 'Aktualisierung auf {version} nicht möglich', + updateRemoveFailed: '{version} konnte nicht entfernt werden', + updateRestored: 'Aktualisierung fehlgeschlagen; wiederhergestellt {version}', + updateRestoredDetail: '{to} konnte nicht installiert werden; {from} wurde erneut installiert.', + updateKept: 'Konnte nicht auf {version} aktualisiert werden', + updateKeptDetail: '{from} wurde nicht entfernt und wird weiterhin verwendet. {reason}', + updateBroken: 'Aktualisierung fehlgeschlagen; Das Plugin ist nicht installiert', + updateBrokenDetail: 'Die Installation von {to} ist fehlgeschlagen, und auch die erneute Installation von {from} schlug fehl. Installieren Sie {name} erneut über Plugin hinzufügen.', + updateLost: 'Das Update-Ergebnis ist unbekannt', + updateLostDetail: 'Die Verbindung zu DSH wurde unterbrochen: {reason}. Aktualisieren Sie die Plugin-Liste, um die installierte Version anzuzeigen.', + updateOutput: 'Installationsausgabe anzeigen', + updateFinish: 'Fertig', + installFailedTitle: 'Das Plugin konnte nicht installiert werden', + installGithubFailedTitle: 'Auf GitHub kann nicht zugegriffen werden', + installGithubTimeoutTitle: 'Zeitüberschreitung bei der GitHub-Verbindung', + installGithubFailedDescription: 'Versuchen Sie es mit einer anderen Installationsquelle.', + installUseGithubMirror: 'Verwenden Sie einen Spiegel vom chinesischen Festland', + installTryAnotherWay: 'Versuchen Sie es anders', + installPackageLabel: 'Name des Plugin-Pakets', + installEdit: 'Bearbeiten', + installEditAria: 'Zurück zur Bearbeitung', + installCancelAndEdit: 'Brechen Sie die Installation ab und kehren Sie zur Bearbeitung zurück', + installApplyingCancellationError: 'Die Stornierung wurde nicht bestätigt. Installation wird angewendet; warte auf das Ergebnis. {reason}', + installReconcile: 'Überprüfen Sie den Installationsstatus', + installUnknownTitle: 'Installationsergebnis nicht verfügbar', + installUnknownDescription: 'Der Host verfügt über keine aktive Installation mit dieser Anforderungs-ID. Überprüfen Sie die Plugin-Liste, bevor Sie es erneut versuchen.', + installResultUnconfirmed: 'Das Installationsergebnis wurde nicht empfangen. Überprüfen Sie den Installationsstatus. {reason}', + installAwaitingAcceptance: 'Warten darauf, dass der Host die Installation akzeptiert. Die Stornierung wird nach der Bestätigung automatisch wiederholt.', + installBackgroundUnknown: 'Installationsergebnis nicht verfügbar. Überprüfen Sie die Plugin-Liste.', + installCancel: 'Installation abbrechen', + installCloseCancels: 'Installation abbrechen und schließen', + installViewTask: 'Installation ansehen', + installUnconfirmedTitle: 'Installationsstatus unbestätigt', + installBackgroundDone: 'Installation abgeschlossen. Installationsdetails anzeigen.', + installBackgroundFailed: 'Die Installation ist fehlgeschlagen. Installationsdetails anzeigen.', + installBackgroundUnconfirmed: 'Der Installationsstatus ist unbestätigt. Weitere Informationen finden Sie in der Installation.', + installBackgroundApplying: 'Die Installation wird ausgeführt und kann nicht abgebrochen werden. Installationsfortschritt anzeigen.', + installStarting: 'Installation vorbereiten…', + installCancelling: 'Installation wird gestoppt…', + installApplying: 'Konfiguration wird angewendet, bitte warten…', + installCancelledShort: 'Abgesagt', + installCancelled: 'Installation abgebrochen; Das Plugin ist nicht aktiviert und heruntergeladene Dateien bleiben möglicherweise bestehen', + installCancelUnconfirmed: 'Die Installation wurde nicht bestätigt und gestoppt. Versuchen Sie den Abbruch erneut oder warten Sie auf das Installationsergebnis. {reason}', + installEnableNow: 'Jetzt aktivieren', + installDetailsShow: 'Installationsdetails anzeigen', + installDetailsHide: 'Installationsdetails ausblenden', + installVersion: 'Version {version}', + installSubjectPath: 'Lokales Verzeichnis', + installSubjectGit: 'Git-Repository', + installSubjectTarball: 'Tarball', + installLocation: 'Wird in {dir} installiert', + installRetry: 'Erneut versuchen', + installChangeRegistry: 'Registrierung ändern', + installAttempt: '{previous} konnte das Paket nicht bereitstellen; erneuter Versuch über {registry} (Registrierung {index} von {total})', + installAttemptBadge: 'Versuchen Sie {index} · {registry}', + installFailureNetwork: 'Die Netzwerkverbindung ist fehlgeschlagen', + installFailureNetworkAll: 'Es konnte keine Registry erreicht werden (versucht: {registries}). Überprüfen Sie die Netzwerk- oder Proxy-Einstellungen oder ändern Sie die Registrierung und versuchen Sie es erneut.', + installFailureNetworkHost: '{host} konnte nicht erreicht werden. Eine GitHub-Adresse oder ein .tgz-Link wird nicht über die Registrierung abgerufen: Dieser Computer muss sie direkt oder über einen Proxy erreichen. Wenn das Plugin auch auf npm veröffentlicht wird, geben Sie stattdessen seinen Paketnamen ein.', + installFailureNotFound: 'Es wurde kein solches Plugin gefunden', + installFailureNoMatchingVersion: 'Keine Version entspricht der Anfrage', + installFailureDiskFull: 'Die Festplatte ist voll; Die Installation wurde gestoppt', + installFailurePermission: 'Keine Schreibberechtigung; Das Plugin kann nicht installiert werden', + installFailureBuildBlocked: 'Die Installationsskripte einer Abhängigkeit benötigen Ihre Erlaubnis, bevor die Installation fortgesetzt werden kann', + installFailureBuildBlockedManual: 'pnpm blockierte Installationsskripte; Erlauben Sie sie unter „allowBuilds“ in „pnpm-workspace.yaml“ und versuchen Sie es erneut', + installFailureIntegrity: 'Das heruntergeladene Paket hat die Integritätsprüfung nicht bestanden', + installFailureTimeout: 'Bei der Installation ist eine Zeitüberschreitung aufgetreten', + installFailurePnpmMissing: 'pnpm wurde nicht gefunden, daher kann nichts installiert werden', + installFailureGeneric: 'Bei der Installation ist ein Fehler aufgetreten. Die Details sagen was', + terminalCommandLine: 'Befehlszeile {n}', + terminalRunning: 'Laufen', + terminalFailed: 'Fehlgeschlagen', + terminalDone: 'Fertig', + terminalCopy: 'Kopieren', + terminalCopied: 'Kopiert', + terminalNoOutput: 'Keine Ausgabe', + terminalCollapseAria: 'Ausgabe reduzieren', + terminalCollapse: 'Einklappen', + terminalExpandAria: 'Erweitern Sie die verbleibenden {n}-Ausgabezeilen', + terminalExpand: '… {n} weitere Zeilen', + terminalExitCode: 'Exit-Code {code}', + terminalSignal: 'Signal {signal}', + terminalNoExitCode: 'kein Exit-Code', + installDoneNothing: 'Die Installation ist ohne neue Abhängigkeit abgeschlossen.', + installDoneRestart: 'Installiert; Starten Sie DSH neu, um es zu laden. {reason}', + installDoneApproved: 'Für {names} zulässige Installationsskripte', + installDoneOtherVersion: 'Gemäß der Supply-Chain-Sicherheitsrichtlinie von pnpm (minimumReleaseAge) werden Versionen, die jünger als das erforderliche Alter veröffentlicht wurden, nicht automatisch installiert. Daher wurde bei dieser Installation {installed} anstelle der neuesten Version {version} verwendet. Eine neu veröffentlichte Version hat diese Wartezeit nicht durchlaufen und kann unerkannten Schadcode oder Mängel enthalten. Wenn Sie {version} benötigen, deinstallieren Sie die aktuelle Version und installieren Sie dann {exact}, um diese Version anzufordern.', + installApprovalTitle: 'Installationsskripte benötigen eine Genehmigung', + installApprovalDescription: 'Diese Pakete verfügen über Installationsskripte, die pnpm nicht ausgeführt hat.', + installApprovalConsequence: 'Sobald sie zugelassen sind, werden die Skripte hier mit Ihren Berechtigungen ausgeführt und die Berechtigung wird in diesem Profil gespeichert.', + installApprovalCaution: 'Lassen Sie nur Pakete zu, denen Sie vertrauen.', + installApproveAndRetry: 'Lassen Sie diese Skripte zu und versuchen Sie es erneut', + installClose: 'Fertig', + close: 'Schließen', + cancel: 'Abbrechen', + confirmUninstallTitle: '„{name}“ deinstallieren?', + confirmUninstallDescription: 'Was es bietet, verschwindet, sobald es deinstalliert wird.', + confirmUninstall: 'Deinstallieren', + failedEnable: 'Konnte nicht aktiviert werden: {reason}', + failedDisable: 'Konnte nicht deaktiviert werden: {reason}', + failedUninstall: 'Konnte nicht deinstalliert werden: {reason}', + failedRowEnable: 'Die Komponente {reason} konnte nicht aktiviert werden', + failedRowDisable: 'Die Komponente {reason} konnte nicht deaktiviert werden', + reasonManagementRequired: 'Das Plugin-Management braucht es; Es kann nicht ausgeschaltet oder deinstalliert werden.', + reasonUnaddressable: 'Der Profil-Patch kann dieses Problem nicht eindeutig beheben.', + reasonUnknownPlugin: 'Kein solches Plugin.', + reasonInvalidSpec: 'Geben Sie einen gültigen Paketnamen oder eine gültige Paketadresse ein.', + reasonAlreadyInstalled: 'Dieses Paket ist bereits installiert; Deinstallieren Sie es, bevor Sie eine andere Version installieren.', + reasonAmbiguousInstall: 'Welches Paket installiert wurde, lässt sich anhand der Abhängigkeitsänderung nicht erkennen.', + reasonNotBundle: 'Dieses Paket deklariert kein Bundle und kann daher nicht als Plugin verwaltet werden.', + reasonNotRemovable: 'Dieses Paket gehört nicht dem Profil oder die Plugin-Verwaltung benötigt es.', + reasonStopProfile: 'Dieses Profil läuft ohne HMR; Stoppen Sie es und deinstallieren Sie das Paket mit dem dsh-Plugin.', + reasonBundleInUse: 'Andere Konfigurationen verwenden weiterhin die Komponenten dieses Bundles; Schalten Sie sie zuerst aus.', + reasonStaleApproval: 'Die ausstehenden Drehbuchgenehmigungen haben sich geändert; Installieren Sie es erneut, um sie zu aktualisieren.', + reasonIncompatibleVersion: '{plugin} ist nicht kompatibel mit DSH {runtime} (erfordert {requirements}); Die Ausführung kann zu Abstürzen oder Datenverlust führen.', + reasonIncompatibleVersionUnnamed: 'Dieses Plugin ist nicht mit der laufenden DSH-Version kompatibel. Die Ausführung kann zu Abstürzen oder Datenverlust führen.', + reasonIncompatibleInstall: 'Installieren Sie eine mit diesem DSH kompatible Plugin-Version.', + reasonIncompatibleInstalled: 'Deinstallieren Sie es und installieren Sie eine mit diesem DSH kompatible Version.', + reasonOperationError: 'Der Host hat einen Fehler gemeldet.', + detailTitle: 'Details', + detailLoading: 'Details lesen', + detailTooLarge: 'Die Details überschreiten die Größenbeschränkung und wurden nicht geladen.', + detailNetwork: 'Die Quelle der Details konnte nicht erreicht werden.', + detailInvalid: 'Das Plugin deklariert eine ungültige Detaildatei.', + detailFailed: 'Die Details konnten nicht gelesen werden.', + detailFootnotes: 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + panel: 'Плагины', + title: 'Плагины', + intro: 'Установите, включите и настройте плагины', + infoLabel: 'О плагинах', + infoDescription: 'Настройте официальные плагины, а также установите или управляйте другими плагинами здесь. Посмотреть список встроенных плагинов и статус выполнения можно в разделе «Настройки» → «Встроенные плагины».', + loading: 'Чтение плагинов…', + error: 'Не удалось прочитать все плагины, возможно, из-за проблем с сетью.', + unavailable: 'Это развертывание выполняется без управляемого профиля, поэтому здесь нельзя устанавливать или переключать плагины.', + retry: 'Повторить', + refresh: 'Обновить', + refreshError: 'Обновить не удалось. Пожалуйста, попробуйте еще раз.', + empty: 'Плагины пока не установлены.', + addPlugin: 'Добавить плагин', + chooseAddMethod: 'Выберите, как добавить плагин', + installExisting: 'Установите сторонний плагин', + installExistingDescription: 'Поддерживает пакеты npm, репозитории Git и локальные каталоги.', + restartNotice: 'Перезапустите DSH, чтобы применить изменения. {reason}', + restartReasonNoHmr: 'Этот профиль применяет изменения только при запуске.', + restartReasonPackageMoved: 'Установленный пакет теперь разрешается в другой каталог, или изменение заменяет пакет, предоставляемый установкой DSH. Удалите пакет перед установкой его из другого места.', + restartReasonHostModule: 'Изменение доходит до загруженных модулей DSH.', + restartReasonUnmanagedPlugin: 'Изменение достигает плагина, работающего за пределами списка плагинов.', + restartReasonModuleComparison: 'DSH не смог сравнить загруженные плагины с их файлами.', + overriddenNotice: '{name} был сохранен, но конфигурация с более высоким приоритетом переопределяет его, поэтому он не действует.', + bundlesTitle: 'Установленные плагины', + basicTitle: 'Встроенные настройки плагина', + extensionsTitle: 'Экспериментальные плагины', + moreTitle: 'Экспериментальные плагины', + moreAction: 'Ещё', + tabsLabel: 'Просмотры плагинов', + installedTab: 'Установлено', + statusProblem: 'Проблема', + statusBeta: 'Экспериментальный', + reasonLabel: 'Причина', + metadataError: 'Ошибка метаданных пакета: {error}.', + versionTag: 'v{version}', + partsLabel: 'Компоненты', + sourceTitle: 'Источник', + sourceSpec: 'Источник кода', + sourceBuiltIn: 'Встроенный', + sourceVersion: 'Текущая версия', + partsEmpty: 'Этот пакет плагинов не содержит компонентов.', + partsCountTotal: '{count} всего', + partsCountRunning: '{count} работает', + partsCountOff: '{count} выкл.', + partOff: 'Выкл.', + partsCountFailed: '{count} не удалось', + partsFilter: 'Компоненты фильтра', + partsFilterEmpty: 'Ни один компонент не соответствует.', + partToggle: 'Включить компонент {name}', + rowPhasePending: 'Ожидание зависимостей', + rowPhaseLoading: 'Загрузка', + rowPhaseActive: 'Бег', + rowPhaseFailed: 'Проблема', + rowPhaseUnloading: 'Разгрузка', + enableToggle: 'Включить {name}', + applyingChange: 'Применение изменений в {name}', + openDetail: 'Посмотреть {name}', + backToList: 'Вернуться к плагинам', + crumbRoot: 'Плагины', + backToPackage: 'Вернуться к {name}', + configureRow: 'Настроить {name}', + rowStateIdle: 'Не работает', + uninstall: 'Удалить', + uninstallLabel: 'Удалить {name}', + installTitle: 'Добавить плагин', + installDescription: 'Введите имя пакета плагина, адрес репозитория GitHub или путь к локальному каталогу.', + installSpecLabel: 'Название или адрес пакета', + installSpecPlaceholder: 'например dsh-plugin-whale-pet', + installGuideToggle: 'Руководство по установке и примеры', + installGuideHide: 'Скрыть руководство', + installGuideIdTitle: 'Введите имя пакета npm плагина.', + installGuideIdExample: 'dsh-плагин-кит-питомец', + installGuideIdHint: 'Имя пакета плагина — это имя пакета npm (например, dsh-xxx или @author/plugin): часть после добавления плагина dsh или добавления pnpm в команде установки README плагина сообщества.', + installGuideExampleLabel: 'Пример: ', + installGitTemplateHint: 'Замените это фактическим адресом репозитория Git.', + installPathTemplateHint: 'Замените это фактическим путем к вашему локальному каталогу плагина.', + installGuideFill: 'Используйте пример', + installGuideFillAria: 'Используйте пример {example}', + installGuideSafety: 'Устанавливайте только те плагины, которым вы доверяете: они запускаются с вашими разрешениями и могут повредить DeepSeek Harness или привести к утечке ваших данных.', + installUpgradeNotice: 'Официальные плагины по требованию предлагают обновление, если их версия отличается от DSH. Чтобы обновить другие плагины, удалите и переустановите их.', + registryToggle: 'Реестр', + registryLegend: 'Реестр npm, из которого загружается плагин.', + registryDefault: 'Реестр по умолчанию', + registryOfficial: 'Официальный реестр npm', + registryNpmmirror: 'Зеркало материкового Китая', + registryCustom: 'Пользовательский адрес', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Введите внутренний или частный адрес реестра npm, начинающийся с http:// или https://.. Если требуется вход в систему, сохраните учетные данные в ~/.npmrc на этом компьютере.', + registryCustomInvalid: 'Введите адрес, начинающийся с http:// или https://.', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Установить', + installChecking: 'Проверка…', + installConfirmedTitle: 'Установить плагин', + installConfirmedRefused: 'Этот плагин не может быть установлен', + installProblemInvalid: 'Это не имя или адрес пакета, который можно установить: {reason}.', + installProblemInstalled: 'Этот плагин уже установлен. Чтобы обновить его, удалите его и установите снова.', + installProblemShipped: 'Этот плагин поставляется с DSH; обновление DSH обновляет его', + installBundledTitle: 'Этот плагин поставляется с DSH.', + installBundledDescription: 'Включите его, не загружая другую копию.', + installProblemNotFound: 'Такой плагин не найден', + installProblemNotPackage: 'Путь не существует или не является допустимым пакетом плагина.', + installProblemNotBundle: 'Этот пакет не декларирует отсутствие пакета, поэтому его нельзя установить как плагин: {reason}', + installProblemNetwork: 'Не удалось получить доступ к реестру плагинов; проверьте сеть и попробуйте еще раз', + installProblemNetworkAll: 'Не удалось получить доступ к реестру (пробовал: {registries}); проверьте настройки сети или прокси или измените реестр', + installProblemUnknown: 'Не удалось найти плагин: {reason}.', + installingTitle: 'Установка плагина…', + installedTitle: 'Установлено', + statusNotInstalled: 'Не установлено', + statusMissingFiles: 'Пакет недоступен', + statusUpdateAvailable: 'Доступно обновление', + sourceNotInstalled: 'Не установлено', + sourceTargetSpec: 'Источник для этого DSH', + sourceTargetVersion: 'Версия для этого DSH', + update: 'Обновить', + updateLabel: 'Обновление {name}', + versionsTitle: 'Версии', + versionsRecheck: 'Проверьте еще раз', + versionsChecking: 'Проверка обновлений', + versionsAvailable: 'Доступно обновление до {version}.', + versionsPublished: 'Опубликовано {date}', + versionsLatest: 'Актуально', + versionsCheckedAt: 'Последняя проверка на {time}.', + versionsBlocked: 'Для более новых версий требуется обновление DSH.', + versionsBlockedDetail: '{version} нужен {peers}; этот DSH - это {runtime}', + versionsNoneInstallable: 'Нет более новой версии для установки напрямую', + versionsNoneInstallableDetail: 'Более новые версии устарели, являются предварительными выпусками или не содержат пакетов. Разверните, чтобы увидеть их', + versionsFailedNetwork: 'Не удалось войти в реестр', + versionsFailedOther: 'Не удалось проверить наличие обновлений', + versionsDetails: 'Подробности', + versionsNewerCount: '{count} новее', + versionsCollapse: 'Свернуть', + versionsInstalledTag: 'Установлено', + versionsPrereleaseTag: 'Пререлиз', + versionsDeprecatedTag: 'Устарело', + versionsNeedsDsh: 'Требуется {peers}', + versionsNotBundle: 'Эта версия не декларирует отсутствие пакета и не может быть установлена.', + versionsUpdateLabel: 'Обновление до {version}', + versionsRegistry: 'Реестр', + versionsCheckedAtLabel: 'Проверено в', + versionsFallbackTag: 'Резервный вариант', + versionsFallbackHint: '{registry} не удалось связаться; список взят из резервного реестра', + versionsPrimaryTag: 'Первый', + versionsShowPrerelease: 'Показать пререлизы', + versionsOmitted: '{count} больше версий новее установленной не указано', + versionsProblemNetwork: 'недостижимый', + versionsProblemNotFound: 'Не найден', + versionsProblemUnknown: 'Не удалось', + updatesAvailable: 'Доступные обновления: {count}', + updatesNone: 'Все плагины актуальны', + updatesFailed: 'Некоторые плагины не удалось проверить', + updateTitle: 'Обновление {name}', + updateSource: 'Установить из', + updateDescription: 'Обновление заменяет {from} на {to}; плагин может ненадолго остановиться. Если установка не удалась, {from} сохраняется или устанавливается снова.', + updateRunning: 'Обновление', + updateStepChecking: 'Проверьте {version}', + updateStepRemoving: 'Остановите и удалите {version}.', + updateStepInstalling: 'Установите {version} из {registry}.', + updateStepRestoring: 'Установите {version} еще раз.', + updateDone: 'Обновлено до {version}.', + updateDoneDetail: 'Новая версия работает.', + updateRestart: '{version} установлен; оно вступит в силу после перезагрузки', + updateRefused: 'Не могу обновиться до {version}.', + updateRemoveFailed: 'Не удалось удалить {version}.', + updateRestored: 'Обновление не удалось; восстановлен {version}', + updateRestoredDetail: '{to} не удалось установить; {from} был установлен снова.', + updateKept: 'Не удалось обновиться до {version}.', + updateKeptDetail: '{from} не был удален и продолжает использоваться. {reason}', + updateBroken: 'Обновление не удалось; плагин не установлен', + updateBrokenDetail: '{to} не удалось установить, и повторная установка {from} также не удалась. Установите {name} снова из меню «Добавить плагин».', + updateLost: 'Результат обновления неизвестен', + updateLostDetail: 'Соединение с DSH было прервано: {reason}. Обновите список плагинов, чтобы увидеть установленную версию.', + updateOutput: 'Показать выходные данные установки', + updateFinish: 'Готово', + installFailedTitle: 'Плагин не удалось установить', + installGithubFailedTitle: 'Не могу получить доступ к GitHub', + installGithubTimeoutTitle: 'Время ожидания соединения GitHub истекло', + installGithubFailedDescription: 'Попробуйте другой источник установки.', + installUseGithubMirror: 'Используйте зеркало материкового Китая.', + installTryAnotherWay: 'Попробуйте другой способ', + installPackageLabel: 'Название пакета плагина', + installEdit: 'Редактировать', + installEditAria: 'Вернуться к редактированию', + installCancelAndEdit: 'Отменить установку и вернуться к редактированию', + installApplyingCancellationError: 'Отмена не подтвердилась. Установка применяется; дождитесь его результата. {reason}', + installReconcile: 'Проверьте статус установки', + installUnknownTitle: 'Результат установки недоступен', + installUnknownDescription: 'На хосте нет активной установки с этим идентификатором запроса. Прежде чем повторить попытку, проверьте список плагинов.', + installResultUnconfirmed: 'Результат установки не получен. Проверьте статус установки. {reason}', + installAwaitingAcceptance: 'Ожидание, пока хост примет установку. Отмена будет повторена автоматически после подтверждения.', + installBackgroundUnknown: 'Результат установки недоступен. Проверьте список плагинов.', + installCancel: 'Отменить установку', + installCloseCancels: 'Отменить установку и закрыть', + installViewTask: 'Посмотреть установку', + installUnconfirmedTitle: 'Статус установки не подтвержден', + installBackgroundDone: 'Установка завершена. Посмотреть подробности установки.', + installBackgroundFailed: 'Установка не удалась. Посмотреть подробности установки.', + installBackgroundUnconfirmed: 'Статус установки не подтвержден. Подробности смотрите в установке.', + installBackgroundApplying: 'Установка применяется и не может быть отменена. Просмотр хода установки.', + installStarting: 'Подготовка к установке…', + installCancelling: 'Остановка установки…', + installApplying: 'Применяется конфигурация, пожалуйста, подождите…', + installCancelledShort: 'Отменено', + installCancelled: 'Установка отменена; плагин не включен и скачанные файлы могут остаться', + installCancelUnconfirmed: 'Установка не подтверждена, остановлена. Повторите попытку отмены или дождитесь результата установки. {reason}', + installEnableNow: 'Включить сейчас', + installDetailsShow: 'Показать детали установки', + installDetailsHide: 'Скрыть сведения об установке', + installVersion: 'Версия {version}', + installSubjectPath: 'Локальный каталог', + installSubjectGit: 'Git-репозиторий', + installSubjectTarball: 'Тарбол', + installLocation: 'Устанавливается в {dir}', + installRetry: 'Повторить', + installChangeRegistry: 'Изменить реестр', + installAttempt: '{previous} не смог обслужить пакет; повторная попытка через {registry} (реестр {index} из {total})', + installAttemptBadge: 'Попытка {index} · {registry}', + installFailureNetwork: 'Сетевое подключение не удалось', + installFailureNetworkAll: 'Не удалось достичь реестра (пробовал: {registries}). Проверьте настройки сети или прокси-сервера или измените реестр и повторите попытку.', + installFailureNetworkHost: 'Не удалось связаться с {host}. Адрес GitHub или ссылка .tgz не извлекаются через реестр: этот компьютер должен получить доступ к ним напрямую или через прокси. Если плагин также опубликован в npm, вместо этого введите имя его пакета.', + installFailureNotFound: 'Такой плагин не найден', + installFailureNoMatchingVersion: 'Ни одна версия не соответствует запросу', + installFailureDiskFull: 'Диск заполнен; установка остановлена', + installFailurePermission: 'Нет разрешения на запись; плагин не может быть установлен', + installFailureBuildBlocked: 'Для продолжения установки сценариям установки зависимости требуется ваше разрешение.', + installFailureBuildBlockedManual: 'pnpm блокировал сценарии установки; разрешите их в разделеallowBuilds в pnpm-workspace.yaml и повторите попытку.', + installFailureIntegrity: 'Загруженный пакет не прошел проверку целостности.', + installFailureTimeout: 'Время установки истекло', + installFailurePnpmMissing: 'pnpm не найден, поэтому ничего установить невозможно', + installFailureGeneric: 'Во время установки что-то пошло не так; детали говорят о чем', + terminalCommandLine: 'Командная строка {n}', + terminalRunning: 'Бег', + terminalFailed: 'Не удалось', + terminalDone: 'Готово', + terminalCopy: 'Копировать', + terminalCopied: 'Скопировано', + terminalNoOutput: 'Нет вывода', + terminalCollapseAria: 'Свернуть вывод', + terminalCollapse: 'Свернуть', + terminalExpandAria: 'Разверните оставшиеся выходные строки {n}.', + terminalExpand: '… {n} еще строки', + terminalExitCode: 'код выхода {code}', + terminalSignal: 'сигнал {signal}', + terminalNoExitCode: 'нет кода выхода', + installDoneNothing: 'Установка завершена без новых зависимостей.', + installDoneRestart: 'Установлен; перезапустите DSH, чтобы загрузить его. {reason}', + installDoneApproved: 'Скрипты установки разрешены для {names}', + installDoneOtherVersion: 'В соответствии с политикой безопасности цепочки поставок pnpm (minimumReleaseAge) версии, опубликованные позже требуемого возраста, не устанавливаются автоматически, поэтому в этой установке использовалась {installed} вместо последней версии {version}. Недавно опубликованная версия не завершила этот период ожидания и может содержать необнаруженный вредоносный код или дефекты. Если вам нужен {version}, удалите текущую версию, а затем установите {exact}, чтобы запросить эту версию.', + installApprovalTitle: 'Для установки скриптов требуется разрешение', + installApprovalDescription: 'Эти пакеты содержат сценарии установки, которые pnpm не запускал.', + installApprovalConsequence: 'После разрешения сценарии запускаются здесь с вашими разрешениями, а разрешение сохраняется в этом профиле.', + installApprovalCaution: 'Разрешите только те пакеты, которым вы доверяете.', + installApproveAndRetry: 'Разрешите эти сценарии и повторите попытку.', + installClose: 'Готово', + close: 'Закрыть', + cancel: 'Отмена', + confirmUninstallTitle: 'Удалить "{name}"?', + confirmUninstallDescription: 'То, что он предоставляет, исчезает после его удаления.', + confirmUninstall: 'Удалить', + failedEnable: 'Не удалось включить: {reason}', + failedDisable: 'Не удалось отключить: {reason}', + failedUninstall: 'Не удалось удалить: {reason}', + failedRowEnable: 'Не удалось включить компонент: {reason}.', + failedRowDisable: 'Не удалось отключить компонент: {reason}.', + reasonManagementRequired: 'Управление плагинами нуждается в этом; его нельзя отключить или удалить.', + reasonUnaddressable: 'Патч профиля не может решить эту проблему однозначно.', + reasonUnknownPlugin: 'Нет такого плагина.', + reasonInvalidSpec: 'Введите действительное имя или адрес пакета.', + reasonAlreadyInstalled: 'Этот пакет уже установлен; удалите его перед установкой другой версии.', + reasonAmbiguousInstall: 'Какой пакет был установлен, невозможно определить по изменению зависимостей.', + reasonNotBundle: 'В этом пакете не объявлен пакет, поэтому им нельзя управлять как плагином.', + reasonNotRemovable: 'Этот пакет не принадлежит профилю, или он нужен управлению плагинами.', + reasonStopProfile: 'Этот профиль работает без HMR; остановите его и удалите пакет с плагином dsh.', + reasonBundleInUse: 'Другая конфигурация по-прежнему использует компоненты этого пакета; сначала выключите их.', + reasonStaleApproval: 'Ожидаемые утверждения сценария изменились; установите еще раз, чтобы обновить их.', + reasonIncompatibleVersion: '{plugin} несовместим с DSH {runtime} (требуется {requirements}); его запуск может привести к сбоям или потере данных.', + reasonIncompatibleVersionUnnamed: 'Этот плагин несовместим с работающей версией DSH; его запуск может привести к сбоям или потере данных.', + reasonIncompatibleInstall: 'Установите версию плагина, совместимую с этим DSH.', + reasonIncompatibleInstalled: 'Удалите его и установите версию, совместимую с этим DSH.', + reasonOperationError: 'Хост сообщил об ошибке.', + detailTitle: 'Подробности', + detailLoading: 'Читаем подробности', + detailTooLarge: 'Детали превышают лимит размера и не были загружены.', + detailNetwork: 'До источника подробностей добраться не удалось.', + detailInvalid: 'Плагин объявляет неверный файл сведений.', + detailFailed: 'Подробности прочитать не удалось.', + detailFootnotes: 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + panel: 'Plug-ins', + title: 'Plug-ins', + intro: 'Instalar, habilitar e configurar plug-ins', + infoLabel: 'Sobre plug-ins', + infoDescription: 'Configure plugins oficiais e instale ou gerencie outros plugins aqui. Veja a lista de plug-ins integrados e o status do tempo de execução em Configurações → Plug-ins integrados.', + loading: 'Lendo plug-ins…', + error: 'Não foi possível ler todos os plugins, possivelmente devido a um problema de rede', + unavailable: 'Esta implantação é executada sem um perfil gerenciável, portanto, os plug-ins não podem ser instalados ou trocados aqui.', + retry: 'Tentar novamente', + refresh: 'Atualizar', + refreshError: 'Falha na atualização. Por favor, tente novamente.', + empty: 'Nenhum plug-in está instalado ainda.', + addPlugin: 'Adicionar plug-in', + chooseAddMethod: 'Escolha como adicionar um plugin', + installExisting: 'Instale um plugin de terceiros', + installExistingDescription: 'Suporta pacotes npm, repositórios Git e diretórios locais', + restartNotice: 'Reinicie DSH para aplicar a alteração. {reason}', + restartReasonNoHmr: 'Este perfil aplica alterações somente na inicialização.', + restartReasonPackageMoved: 'Um pacote instalado agora é resolvido para outro diretório ou a alteração substitui um pacote fornecido pela instalação DSH. Desinstale um pacote antes de instalá-lo de outro local.', + restartReasonHostModule: 'A mudança atinge os módulos DSH carregados.', + restartReasonUnmanagedPlugin: 'A mudança atinge um plugin rodando fora da lista de plugins.', + restartReasonModuleComparison: 'DSH não conseguiu comparar os plug-ins carregados com seus arquivos.', + overriddenNotice: '{name} foi salvo, mas uma configuração de prioridade mais alta o substitui, portanto não está em vigor', + bundlesTitle: 'Plug-ins instalados', + basicTitle: 'Configurações de plug-in integradas', + extensionsTitle: 'Plug-ins experimentais', + moreTitle: 'Plug-ins experimentais', + moreAction: 'Mais', + tabsLabel: 'Visualizações de plug-in', + installedTab: 'Instalado', + statusProblem: 'Problema', + statusBeta: 'Experimental', + reasonLabel: 'Razão', + metadataError: 'Erro de metadados do pacote: {error}', + versionTag: 'v{version}', + partsLabel: 'Componentes', + sourceTitle: 'Fonte', + sourceSpec: 'Fonte do código', + sourceBuiltIn: 'Construído em', + sourceVersion: 'Versão atual', + partsEmpty: 'Este pacote de plugins não contém componentes.', + partsCountTotal: '{count} total', + partsCountRunning: '{count} em execução', + partsCountOff: '{count} desativado', + partOff: 'Desligado', + partsCountFailed: '{count} falhou', + partsFilter: 'Filtrar componentes', + partsFilterEmpty: 'Nenhum componente corresponde.', + partToggle: 'Ativar componente {name}', + rowPhasePending: 'Esperando por dependências', + rowPhaseLoading: 'Carregando', + rowPhaseActive: 'Correndo', + rowPhaseFailed: 'Problema', + rowPhaseUnloading: 'Descarregando', + enableToggle: 'Ativar {name}', + applyingChange: 'Aplicando alterações em {name}', + openDetail: 'Ver {name}', + backToList: 'Voltar aos plug-ins', + crumbRoot: 'Plug-ins', + backToPackage: 'Voltar para {name}', + configureRow: 'Configurar {name}', + rowStateIdle: 'Não está em execução', + uninstall: 'Desinstalar', + uninstallLabel: 'Desinstalar {name}', + installTitle: 'Adicionar plug-in', + installDescription: 'Insira o nome do pacote do plug-in, o endereço do repositório GitHub ou o caminho do diretório local.', + installSpecLabel: 'Nome ou endereço do pacote', + installSpecPlaceholder: 'por exemplo dsh-plugin-baleia-animal de estimação', + installGuideToggle: 'Guia de instalação e exemplos', + installGuideHide: 'Ocultar o guia', + installGuideIdTitle: 'Digite o nome do pacote npm do plugin', + installGuideIdExample: 'dsh-plugin-baleia-animal de estimação', + installGuideIdHint: 'O nome do pacote do plug-in é o nome do pacote npm (como dsh-xxx ou @author/plugin): a parte após dsh plugin add ou pnpm add no comando de instalação README de um plug-in da comunidade.', + installGuideExampleLabel: 'Exemplo: ', + installGitTemplateHint: 'Substitua pelo endereço real do repositório Git.', + installPathTemplateHint: 'Substitua isso pelo caminho real para o diretório local do plugin.', + installGuideFill: 'Exemplo de uso', + installGuideFillAria: 'Use o exemplo {example}', + installGuideSafety: 'Instale apenas plug-ins em que você confia: eles são executados com suas permissões e podem danificar DeepSeek Harness ou vazar seus dados.', + installUpgradeNotice: 'Plug-ins oficiais sob demanda oferecem atualização quando sua versão é diferente de DSH. Para atualizar outros plug-ins, desinstale-os e reinstale-os.', + registryToggle: 'Registro', + registryLegend: 'O registro npm do qual o plugin é baixado', + registryDefault: 'Registro padrão', + registryOfficial: 'Registro oficial do npm', + registryNpmmirror: 'Espelho da China Continental', + registryCustom: 'Endereço personalizado', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Insira um endereço de registro npm interno ou privado começando com http:// ou https://. Se for necessário um login, armazene as credenciais em ~/.npmrc nesta máquina.', + registryCustomInvalid: 'Insira um endereço começando com http:// ou https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Instalar', + installChecking: 'Verificando…', + installConfirmedTitle: 'Instalar plug-in', + installConfirmedRefused: 'Este plugin não pode ser instalado', + installProblemInvalid: 'Este não é um nome ou endereço de pacote que pode ser instalado: {reason}', + installProblemInstalled: 'Este plugin já está instalado. Para atualizá-lo, desinstale-o e instale-o novamente', + installProblemShipped: 'Este plug-in é fornecido com DSH; atualizar DSH atualiza-o', + installBundledTitle: 'Este plug-in é fornecido com DSH', + installBundledDescription: 'Habilite-o sem baixar outra cópia', + installProblemNotFound: 'Nenhum plugin desse tipo foi encontrado', + installProblemNotPackage: 'O caminho não existe ou não é um pacote de plugin válido', + installProblemNotBundle: 'Este pacote não declara nenhum pacote, portanto não pode ser instalado como um plug-in: {reason}', + installProblemNetwork: 'O registro do plugin não pôde ser acessado; verifique a rede e tente novamente', + installProblemNetworkAll: 'Nenhum registro pôde ser acessado (tentativa: {registries}); verifique as configurações de rede ou proxy ou altere o registro', + installProblemUnknown: 'Não foi possível pesquisar o plug-in: {reason}', + installingTitle: 'Instalando o plug-in…', + installedTitle: 'Instalado', + statusNotInstalled: 'Não instalado', + statusMissingFiles: 'Pacote indisponível', + statusUpdateAvailable: 'Atualização disponível', + sourceNotInstalled: 'Não instalado', + sourceTargetSpec: 'Fonte para este DSH', + sourceTargetVersion: 'Versão para este DSH', + update: 'Atualizar', + updateLabel: 'Atualizar {name}', + versionsTitle: 'Versões', + versionsRecheck: 'Verifique novamente', + versionsChecking: 'Verificando atualizações', + versionsAvailable: 'Atualização para {version} disponível', + versionsPublished: 'Publicado {date}', + versionsLatest: 'Atualizado', + versionsCheckedAt: 'Última verificação em {time}', + versionsBlocked: 'Versões mais recentes precisam de uma atualização DSH', + versionsBlockedDetail: '{version} precisa de {peers}; este DSH é {runtime}', + versionsNoneInstallable: 'Nenhuma versão mais recente para instalar diretamente', + versionsNoneInstallableDetail: 'As versões mais recentes estão obsoletas, são pré-lançamentos ou não declaram nenhum pacote. Expanda para vê-los', + versionsFailedNetwork: 'Não foi possível acessar o registro', + versionsFailedOther: 'Não foi possível verificar atualizações', + versionsDetails: 'Detalhes', + versionsNewerCount: '{count} mais recente', + versionsCollapse: 'Recolher', + versionsInstalledTag: 'Instalado', + versionsPrereleaseTag: 'Pré-lançamento', + versionsDeprecatedTag: 'Obsoleto', + versionsNeedsDsh: 'Precisa de {peers}', + versionsNotBundle: 'Esta versão não declara nenhum pacote e não pode ser instalada', + versionsUpdateLabel: 'Atualizar para {version}', + versionsRegistry: 'Registro', + versionsCheckedAtLabel: 'Verificado em', + versionsFallbackTag: 'Alternativa', + versionsFallbackHint: '{registry} não pôde ser contatado; a lista vem de um registro substituto', + versionsPrimaryTag: 'Primeiro', + versionsShowPrerelease: 'Mostrar pré-lançamentos', + versionsOmitted: '{count} mais versões mais recentes que a instalada não estão listadas', + versionsProblemNetwork: 'Inacessível', + versionsProblemNotFound: 'Não encontrado', + versionsProblemUnknown: 'Falha', + updatesAvailable: 'Atualizações disponíveis: {count}', + updatesNone: 'Todos os plug-ins estão atualizados', + updatesFailed: 'Alguns plug-ins não puderam ser verificados', + updateTitle: 'Atualizar {name}', + updateSource: 'Instalar de', + updateDescription: 'A atualização substitui {from} por {to}; o plugin pode parar brevemente. Se a instalação falhar, {from} será mantido ou instalado novamente.', + updateRunning: 'Atualizando', + updateStepChecking: 'Verifique {version}', + updateStepRemoving: 'Pare e remova {version}', + updateStepInstalling: 'Instalar {version} de {registry}', + updateStepRestoring: 'Instale {version} novamente', + updateDone: 'Atualizado para {version}', + updateDoneDetail: 'A nova versão está em execução.', + updateRestart: '{version} instalado; entra em vigor após uma reinicialização', + updateRefused: 'Não é possível atualizar para {version}', + updateRemoveFailed: 'Não foi possível remover {version}', + updateRestored: 'Falha na atualização; restaurado {version}', + updateRestoredDetail: '{to} falhou ao instalar; {from} foi instalado novamente.', + updateKept: 'Não foi possível atualizar para {version}', + updateKeptDetail: '{from} não foi removido e permanece em uso. {reason}', + updateBroken: 'Falha na atualização; o plugin não está instalado', + updateBrokenDetail: '{to} falhou ao instalar e instalar {from} novamente também falhou. Instale {name} novamente em Adicionar plug-in.', + updateLost: 'O resultado da atualização é desconhecido', + updateLostDetail: 'A conexão com DSH foi interrompida: {reason}. Atualize a lista de plugins para ver a versão instalada.', + updateOutput: 'Mostrar resultado da instalação', + updateFinish: 'Concluído', + installFailedTitle: 'O plugin não pôde ser instalado', + installGithubFailedTitle: 'Não é possível acessar GitHub', + installGithubTimeoutTitle: 'GitHub conexão expirou', + installGithubFailedDescription: 'Tente outra fonte de instalação.', + installUseGithubMirror: 'Use o espelho da China continental', + installTryAnotherWay: 'Tente outra maneira', + installPackageLabel: 'Nome do pacote de plug-ins', + installEdit: 'Editar', + installEditAria: 'Voltar para edição', + installCancelAndEdit: 'Cancele a instalação e retorne à edição', + installApplyingCancellationError: 'O cancelamento não foi confirmado. A instalação está sendo aplicada; espere pelo seu resultado. {reason}', + installReconcile: 'Verifique o status da instalação', + installUnknownTitle: 'Resultado da instalação indisponível', + installUnknownDescription: 'O Host não possui instalação ativa com este ID de solicitação. Verifique a lista de plugins antes de tentar novamente.', + installResultUnconfirmed: 'O resultado da instalação não foi recebido. Verifique o status da instalação. {reason}', + installAwaitingAcceptance: 'Aguardando o Host aceitar a instalação. O cancelamento será tentado novamente automaticamente após a confirmação.', + installBackgroundUnknown: 'Resultado da instalação indisponível. Verifique a lista de plugins.', + installCancel: 'Cancelar instalação', + installCloseCancels: 'Cancelar instalação e fechar', + installViewTask: 'Ver instalação', + installUnconfirmedTitle: 'Status de instalação não confirmado', + installBackgroundDone: 'Instalação concluída. Veja detalhes da instalação.', + installBackgroundFailed: 'A instalação falhou. Veja detalhes da instalação.', + installBackgroundUnconfirmed: 'O status da instalação não foi confirmado. Veja a instalação para obter detalhes.', + installBackgroundApplying: 'A instalação está sendo aplicada e não pode ser cancelada. Veja o progresso da instalação.', + installStarting: 'Preparando a instalação…', + installCancelling: 'Parando a instalação…', + installApplying: 'Aplicando configuração, aguarde…', + installCancelledShort: 'Cancelado', + installCancelled: 'Instalação cancelada; o plugin não está habilitado e os arquivos baixados podem permanecer', + installCancelUnconfirmed: 'A instalação não foi confirmada interrompida. Tente novamente o cancelamento ou aguarde o resultado da instalação. {reason}', + installEnableNow: 'Ativar agora', + installDetailsShow: 'Mostrar detalhes da instalação', + installDetailsHide: 'Ocultar detalhes de instalação', + installVersion: 'Versão {version}', + installSubjectPath: 'Diretório local', + installSubjectGit: 'Repositório Git', + installSubjectTarball: 'Tarball', + installLocation: 'Instala em {dir}', + installRetry: 'Tentar novamente', + installChangeRegistry: 'Alterar registro', + installAttempt: '{previous} não pôde entregar o pacote; tentando novamente através de {registry} (registro {index} de {total})', + installAttemptBadge: 'Tentativa {index} · {registry}', + installFailureNetwork: 'A conexão de rede falhou', + installFailureNetworkAll: 'Nenhum registro pôde ser acessado (tentativa: {registries}). Verifique as configurações de rede ou proxy ou altere o registro e tente novamente.', + installFailureNetworkHost: '{host} não pôde ser alcançado. Um endereço GitHub ou um link .tgz não é obtido através do registro: esta máquina deve acessá-lo diretamente ou através de um proxy. Se o plugin também for publicado no npm, insira o nome do pacote.', + installFailureNotFound: 'Nenhum plugin desse tipo foi encontrado', + installFailureNoMatchingVersion: 'Nenhuma versão corresponde à solicitação', + installFailureDiskFull: 'O disco está cheio; a instalação parou', + installFailurePermission: 'Sem permissão de gravação; o plugin não pode ser instalado', + installFailureBuildBlocked: 'Os scripts de instalação de uma dependência precisam da sua permissão antes que a instalação possa continuar', + installFailureBuildBlockedManual: 'scripts de instalação bloqueados por pnpm; permita-os em permitBuilds em pnpm-workspace.yaml e tente novamente', + installFailureIntegrity: 'O pacote baixado falhou na verificação de integridade', + installFailureTimeout: 'A instalação expirou', + installFailurePnpmMissing: 'pnpm não foi encontrado, então nada pode ser instalado', + installFailureGeneric: 'Algo deu errado durante a instalação; os detalhes dizem o que', + terminalCommandLine: 'Linha de comando {n}', + terminalRunning: 'Correndo', + terminalFailed: 'Falha', + terminalDone: 'Concluído', + terminalCopy: 'Copiar', + terminalCopied: 'Copiado', + terminalNoOutput: 'Sem saída', + terminalCollapseAria: 'Recolher saída', + terminalCollapse: 'Recolher', + terminalExpandAria: 'Expanda as {n} linhas de saída restantes', + terminalExpand: '… mais {n} linhas', + terminalExitCode: 'código de saída {code}', + terminalSignal: 'sinal {signal}', + terminalNoExitCode: 'sem código de saída', + installDoneNothing: 'A instalação foi concluída sem nenhuma nova dependência.', + installDoneRestart: 'Instalado; reinicie DSH para carregá-lo. {reason}', + installDoneApproved: 'Instalar scripts permitidos para {names}', + installDoneOtherVersion: 'De acordo com a política de segurança da cadeia de fornecimento do pnpm (minimumReleaseAge), as versões publicadas mais recentemente do que a idade exigida não são instaladas automaticamente, portanto, esta instalação usou {installed} em vez da versão mais recente {version}. Uma versão recém-publicada não completou esse período de espera e pode conter códigos maliciosos ou defeitos não detectados. Se precisar de {version}, desinstale a versão atual e instale {exact} para solicitar essa versão.', + installApprovalTitle: 'Os scripts de instalação precisam de permissão', + installApprovalDescription: 'Esses pacotes possuem scripts de instalação que o pnpm não executou.', + installApprovalConsequence: 'Uma vez permitido, os scripts são executados aqui com suas permissões, e a permissão é salva neste perfil.', + installApprovalCaution: 'Permita apenas pacotes em que você confia.', + installApproveAndRetry: 'Permita esses scripts e tente novamente', + installClose: 'Concluído', + close: 'Fechar', + cancel: 'Cancelar', + confirmUninstallTitle: 'Desinstalar "{name}"?', + confirmUninstallDescription: 'O que ele fornece desaparece quando é desinstalado.', + confirmUninstall: 'Desinstalar', + failedEnable: 'Não foi possível ativar: {reason}', + failedDisable: 'Não foi possível desativar: {reason}', + failedUninstall: 'Não foi possível desinstalar: {reason}', + failedRowEnable: 'Não foi possível ativar o componente: {reason}', + failedRowDisable: 'Não foi possível desativar o componente: {reason}', + reasonManagementRequired: 'O gerenciamento de plug-ins precisa disso; ele não pode ser desligado ou desinstalado.', + reasonUnaddressable: 'O patch de perfil não pode resolver este problema exclusivamente.', + reasonUnknownPlugin: 'Não existe tal plugin.', + reasonInvalidSpec: 'Insira um nome ou endereço de pacote válido.', + reasonAlreadyInstalled: 'Este pacote já está instalado; desinstale-o antes de instalar outra versão.', + reasonAmbiguousInstall: 'Qual pacote foi instalado não pode ser informado a partir da mudança de dependência.', + reasonNotBundle: 'Este pacote não declara nenhum pacote, portanto não pode ser gerenciado como um plugin.', + reasonNotRemovable: 'Este pacote não pertence ao perfil ou o gerenciamento do plugin precisa dele.', + reasonStopProfile: 'Este perfil é executado sem HMR; pare e desinstale o pacote com o plugin dsh.', + reasonBundleInUse: 'Outras configurações ainda usam os componentes deste pacote configurável; desligue-os primeiro.', + reasonStaleApproval: 'As aprovações de script pendentes foram alteradas; instale novamente para atualizá-los.', + reasonIncompatibleVersion: '{plugin} é incompatível com DSH {runtime} (requer {requirements}); executá-lo pode causar travamentos ou perda de dados.', + reasonIncompatibleVersionUnnamed: 'Este plug-in é incompatível com a versão DSH em execução; executá-lo pode causar travamentos ou perda de dados.', + reasonIncompatibleInstall: 'Instale uma versão de plug-in compatível com este DSH.', + reasonIncompatibleInstalled: 'Desinstale-o e instale uma versão compatível com este DSH.', + reasonOperationError: 'O Host relatou um erro.', + detailTitle: 'Detalhes', + detailLoading: 'Lendo detalhes', + detailTooLarge: 'Os detalhes excedem o limite de tamanho e não foram carregados.', + detailNetwork: 'Não foi possível acessar a fonte dos detalhes.', + detailInvalid: 'O plugin declara um arquivo de detalhes inválido.', + detailFailed: 'Não foi possível ler os detalhes.', + detailFootnotes: 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-plugin-manager/src/client/presentation.ts b/packages/client/ui-plugin-manager/src/client/presentation.ts index c817e84f46..23a95913bf 100644 --- a/packages/client/ui-plugin-manager/src/client/presentation.ts +++ b/packages/client/ui-plugin-manager/src/client/presentation.ts @@ -1,5 +1,6 @@ /** Display labels and toast sentences for global plugin management. */ +import { EXPERIMENTAL_BUNDLES } from '@deepseek-ai/dsh-official-bundles' import type { IncompatiblePlugin, ManagementError, Registry, RestartReason } from '@deepseek-ai/dsh-api-remotes/client' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { PluginManagerLocaleKey } from './locales.ts' @@ -108,22 +109,6 @@ export function shortName(name: string): string { return unscoped.replace(/^dsh-(?:host-|client-)?/, '') } -/** Official bundles that display the Experimental badge. */ -const EXPERIMENTAL_BUNDLES: readonly string[] = [ - '@deepseek-ai/dsh-cua-bundle', - '@deepseek-ai/dsh-experimental-agent-team-profile', - '@deepseek-ai/dsh-experimental-voice-input-bundle', - '@deepseek-ai/dsh-experimental-auto-review', - '@deepseek-ai/dsh-experimental-inspector-profile', - '@deepseek-ai/dsh-experimental-tool-worktree', - '@deepseek-ai/dsh-experimental-cot-translation-bundle', - '@deepseek-ai/dsh-experimental-session-search', - '@deepseek-ai/dsh-experimental-session-titles-bundle', - '@deepseek-ai/dsh-experimental-badge-skill-bundle', - '@deepseek-ai/dsh-experimental-ralph-bundle', - '@deepseek-ai/dsh-experimental-plugin-market-bundle', -] - /** * Resolve installed package metadata without changing its technical identity. * @param pkg - package identity and local metadata. @@ -136,7 +121,7 @@ export function packageText( return { title: pkg.meta?.title === undefined ? pkg.name : resolveText(pkg.meta.title), description: pkg.meta?.description === undefined ? undefined : resolveText(pkg.meta.description) || undefined, - beta: EXPERIMENTAL_BUNDLES.includes(pkg.name), + beta: EXPERIMENTAL_BUNDLES.has(pkg.name), } } diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index a64c1ba05c..336aa2e34f 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -162,6 +162,7 @@ function renderTab( } const navigation = createNavigationStore().create() const props: PluginManagerPageProps = { + createHeaderRef: vi.fn(() => vi.fn()), useStore: bindSnapshotSelector(navigation), actions: navigation.actions, ...standard, t, @@ -240,10 +241,13 @@ describe('PluginManagerPage', () => { expect(screen.queryByRole('tablist')).toBeNull() setTabs([{ id: 'discover', label: 'Discover' }]) expect(screen.getByRole('tab', { name: en.installedTab }).getAttribute('aria-selected')).toBe('true') + // The tabs stay in the pinned page head with the title; the tab panel scrolls in the page body. + expect(screen.getByRole('tablist').closest('header')?.closest('[data-plugin-bar]')).not.toBeNull() expect(document.querySelector('[data-plugin-group="bundles"]')).not.toBeNull() fireEvent.click(screen.getByRole('tab', { name: 'Discover' })) expect(navigation.getSnapshot().tab).toBe('discover') expect(screen.getByRole('tabpanel').getAttribute('aria-labelledby')).toBe(screen.getByRole('tab', { name: 'Discover' }).id) + expect(screen.getByRole('tabpanel').closest('[data-plugin-body]')).not.toBeNull() expect(document.querySelector('[data-plugin-group="bundles"]')).toBeNull() expect(screen.queryByRole('button', { name: en.refresh })).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Discover install' })) @@ -277,6 +281,22 @@ describe('PluginManagerPage', () => { expect(screen.getByRole('tabpanel', { name: 'Discover' })).toBeTruthy() }) + it('keeps the header controls in the scrolling page without remounting them', () => { + const { setTabs, props, hidePage } = renderTab({ packages: [pkg()] }) + const bar = document.querySelector('[data-plugin-bar]') + const body = document.querySelector('[data-plugin-body]')! + const refresh = screen.getByRole('button', { name: en.refresh }) + expect(bar?.parentElement).toBe(body) + expect(props.createHeaderRef).toHaveBeenCalledTimes(1) + setTabs([{ id: 'discover', label: 'Discover' }]) + expect(screen.getByRole('tablist').closest('header')?.closest('[data-plugin-bar]')).toBe(bar) + act(() => { body.scrollTop = 160; fireEvent.scroll(body) }) + expect(screen.getByRole('button', { name: en.refresh })).toBe(refresh) + expect(props.createHeaderRef).toHaveBeenCalledTimes(1) + hidePage() + expect(document.querySelector('[data-plugin-bar]')).toBeNull() + }) + it('keeps the Installed tab panel through loading and the More list out of the tabs, and returns a More plugin opened from Discover to Discover', () => { const worktree = '@deepseek-ai/dsh-experimental-tool-worktree' const { navigation, set } = renderTab({ status: 'loading', packages: [] }, {}, { @@ -1217,8 +1237,10 @@ describe('PluginManagerPage', () => { expect(document.querySelector(`[data-plugin-package="${name}"]`)).toBeTruthy() }) - it('returns from package details to the secondary page and retains it across React remounts', () => { - const name = '@deepseek-ai/dsh-experimental-session-titles-bundle' + it.each([ + '@deepseek-ai/dsh-experimental-session-titles-bundle', + '@deepseek-ai/dsh-personalized-recommendations', + ])('returns %s details to the secondary page and retains it across React remounts', (name) => { const b = renderTab({ packages: [pkg({ name, official: true, enabled: false })] }) fireEvent.click(screen.getByRole('button', { name: en.moreAction })) expect(screen.getByRole('heading', { name: en.moreTitle, level: 1 })).toBeTruthy() @@ -1233,9 +1255,11 @@ describe('PluginManagerPage', () => { expect(document.querySelector(`[data-plugin-package="${name}"]`)).toBeNull() }) - it('pins a bundle the profile picks onto the main list, in its full-list place', () => { + it.each([ + '@deepseek-ai/dsh-experimental-tool-worktree', + '@deepseek-ai/dsh-personalized-recommendations', + ])('pins enabled %s onto the main list and removes it when disabled', (first) => { const team = '@deepseek-ai/dsh-experimental-agent-team-profile' - const first = '@deepseek-ai/dsh-experimental-tool-worktree' const second = '@deepseek-ai/dsh-experimental-cot-translation-bundle' const entry = (name: string, enabled: boolean): PackageView => pkg({ name, official: true, enabled }) const b = renderTab({ packages: [entry(second, true), entry(team, true), entry(first, false)] }) diff --git a/packages/client/ui-plugin-manager/tests/header-scroll.client.spec.ts b/packages/client/ui-plugin-manager/tests/header-scroll.client.spec.ts new file mode 100644 index 0000000000..432dfdcb44 --- /dev/null +++ b/packages/client/ui-plugin-manager/tests/header-scroll.client.spec.ts @@ -0,0 +1,390 @@ +// @vitest-environment jsdom +/** Deterministic layout endpoints exercise scroll geometry without jsdom's absent layout engine. */ +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { createPluginHeaderRef } from '../src/client/header-scroll.ts' + +type PartName = 'title' | 'intro' | 'toolbar' | 'tabs' +interface Box { x: number; y: number; width: number; height: number } +interface Layout { + width: number + resting: { height: number; parts: Partial> } + compact: { height: number; parts: Partial> } +} + +function box(x: number, y: number, width: number, height: number): Box { + return { x, y, width, height } +} + +function wide(tabs = true): Layout { + return { + width: 960, + resting: { + height: tabs ? 140 : 90, + parts: { + title: box(0, 28, 120, 28), intro: box(0, 60, 320, 20), toolbar: box(600, 28, 220, 32), + ...tabs ? { tabs: box(0, 100, 240, 30) } : {}, + }, + }, + compact: { + height: 64, + parts: { + title: box(0, 16, 120, 28), intro: box(0, 0, 0, 0), toolbar: box(600, 16, 220, 32), + ...tabs ? { tabs: box(0, 16, 180, 30) } : {}, + }, + }, + } +} + +function narrow(): Layout { + return { + width: 320, + resting: { + height: 210, + parts: { + title: box(0, 28, 120, 28), intro: box(0, 60, 280, 20), + toolbar: box(0, 90, 280, 32), tabs: box(0, 160, 280, 30), + }, + }, + compact: { + height: 70, + parts: { + title: box(0, 16, 120, 28), intro: box(0, 0, 0, 0), + toolbar: box(200, 16, 80, 32), tabs: box(0, 16, 160, 30), + }, + }, + } +} + +function harness() { + const fontsDescriptor = Object.getOwnPropertyDescriptor(document, 'fonts') + const fonts = new EventTarget() + const releases: (() => void)[] = [] + onTestFinished(() => { + for (const release of releases.reverse()) release() + if (fontsDescriptor === undefined) Reflect.deleteProperty(document, 'fonts') + else Object.defineProperty(document, 'fonts', fontsDescriptor) + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + Object.defineProperty(document, 'fonts', { configurable: true, value: fonts }) + + const resizes: ControlledResize[] = [] + const mutations: ControlledMutation[] = [] + class ControlledResize implements ResizeObserver { + observe = vi.fn<(target: Element, options?: ResizeObserverOptions) => void>() + unobserve = vi.fn<(target: Element) => void>() + disconnect = vi.fn<() => void>() + constructor(private readonly callback: ResizeObserverCallback) { resizes.push(this) } + deliver(): void { this.callback([], this) } + } + class ControlledMutation implements MutationObserver { + observe = vi.fn<(target: Node, options?: MutationObserverInit) => void>() + disconnect = vi.fn<() => void>() + takeRecords(): MutationRecord[] { return [] } + constructor(private readonly callback: MutationCallback) { mutations.push(this) } + deliver(): void { this.callback([], this) } + } + vi.stubGlobal('ResizeObserver', ControlledResize) + vi.stubGlobal('MutationObserver', ControlledMutation) + + function fixture(initial: Layout = wide(), ref = createPluginHeaderRef()) { + const body = document.createElement('div') + const bar = document.createElement('div') + const header = document.createElement('header') + const content = document.createElement('div') + const index = releases.length + const elements = new Map() + for (const name of Object.keys(initial.resting.parts) as PartName[]) { + const part = document.createElement('div') + part.dataset.pluginHeaderPart = name + part.id = `header-${index}-${name}` + if (name === 'toolbar' || name === 'intro') { + const button = document.createElement('button') + button.id = `header-${index}-${name}-button` + button.textContent = name + part.append(button) + } else part.textContent = name + header.append(part) + elements.set(name, part) + } + bar.append(header) + content.textContent = 'Plugin cards' + body.append(bar, content) + document.body.append(body) + releases.push(() => { ref(null); body.remove() }) + const state = { layout: initial } + const clones: HTMLDivElement[] = [] + const measurements: { + compact: boolean + inert: boolean + ariaHidden: string | null + ids: number + styles: number + natural: boolean + scrolled: boolean + connected: boolean + width: string + }[] = [] + vi.spyOn(bar, 'getBoundingClientRect').mockImplementation(() => new DOMRect(50, 100, state.layout.width, state.layout.resting.height)) + const cloneNode = bar.cloneNode.bind(bar) + vi.spyOn(bar, 'cloneNode').mockImplementation((deep) => { + const clone = cloneNode(deep) as HTMLDivElement + clones.push(clone) + const cloneHeader = clone.querySelector('header')! + // A different viewport origin ensures only header-relative coordinates drive the live controls. + vi.spyOn(cloneHeader, 'getBoundingClientRect').mockImplementation(() => { + measurements.push({ + compact: clone.hasAttribute('data-compact'), inert: clone.inert, + ariaHidden: clone.getAttribute('aria-hidden'), ids: clone.querySelectorAll('[id]').length, + styles: clone.querySelectorAll('[data-plugin-header-part][style]').length, + natural: clone.hasAttribute('data-natural'), scrolled: clone.hasAttribute('data-scrolled'), + connected: clone.isConnected, width: clone.style.width, + }) + return new DOMRect(180, 240, state.layout.width, 0) + }) + vi.spyOn(clone, 'getBoundingClientRect').mockImplementation(() => + new DOMRect(180, 240, state.layout.width, endpoint().height)) + function endpoint() { return clone.hasAttribute('data-compact') ? state.layout.compact : state.layout.resting } + for (const name of elements.keys()) { + const part = clone.querySelector(`[data-plugin-header-part="${name}"]`)! + vi.spyOn(part, 'getBoundingClientRect').mockImplementation(() => { + const bounds = endpoint().parts[name]! + return new DOMRect(180 + bounds.x, 240 + bounds.y, bounds.width, bounds.height) + }) + } + return clone + }) + function part(name: PartName): HTMLDivElement { + const element = elements.get(name) + if (element === undefined) throw new Error(`Fixture has no ${name}`) + return element + } + function scroll(top: number): void { + body.scrollTop = top + body.dispatchEvent(new Event('scroll')) + } + return { body, bar, header, content, state, clones, measurements, part, scroll, ref, attach: () => { ref(bar) } } + } + return { fonts, resizes, mutations, fixture } +} + +describe('plugin header scroll geometry', () => { + it.each([0, 12, 600])('clamps each trajectory and reverses without using a %ipx scroll range', (range) => { + const h = harness(), f = h.fixture() + Object.defineProperties(f.body, { clientHeight: { value: 500 }, scrollHeight: { value: 500 + range } }) + f.attach() + expect(f.bar.style.getPropertyValue('--plugin-header-travel')).toBe('84px') + expect(f.bar.style.getPropertyValue('--plugin-header-rest-height')).toBe('140px') + expect(f.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('72px') + f.scroll(6) + expect(f.part('toolbar').style.translate).toBe('0px -6px') + expect(f.part('tabs').style.translate).toBe('0px -6px') + expect(f.bar.hasAttribute('data-compact')).toBe(false) + expect(f.bar.hasAttribute('data-scrolled')).toBe(true) + f.scroll(20) + expect(f.part('toolbar').style.translate).toBe('0px -12px') + expect(f.part('tabs').style.translate).toBe('0px -20px') + expect(f.bar.style.getPropertyValue('--plugin-header-paint-height')).toBe('120px') + f.scroll(83) + expect(f.bar.hasAttribute('data-compact')).toBe(false) + f.scroll(84) + expect(f.bar.hasAttribute('data-compact')).toBe(true) + expect(f.part('title').style.translate).toBe('0px -56px') + expect(f.part('intro').style.translate).toBe('0px -80px') + expect(f.part('tabs').style.translate).toBe('0px -84px') + f.scroll(1_000) + expect(f.part('tabs').style.translate).toBe('0px -84px') + expect(f.bar.style.getPropertyValue('--plugin-header-paint-height')).toBe('64px') + f.scroll(6) + expect(f.part('toolbar').style.translate).toBe('0px -6px') + expect(f.part('intro').inert).toBe(false) + expect(f.bar.hasAttribute('data-compact')).toBe(false) + f.scroll(-10) + expect(f.part('tabs').style.translate).toBe('0px 0px') + expect(f.bar.hasAttribute('data-scrolled')).toBe(false) + expect(f.bar.style.getPropertyValue('--plugin-header-paint-height')).toBe('140px') + expect(f.bar.style.getPropertyValue('--plugin-header-rest-height')).toBe('140px') + expect(f.body.scrollHeight - f.body.clientHeight).toBe(range) + expect(f.content.style.cssText).toBe('') + }) + + it('accounts for a layout scrollbar without retaining its sizing property after detachment', () => { + const h = harness(), f = h.fixture() + Object.defineProperties(f.body, { offsetWidth: { value: 960 }, clientWidth: { value: 955 } }) + f.attach() + expect(f.body.style.getPropertyValue('--plugin-page-scrollbar-width')).toBe('5px') + f.ref(null) + expect(f.body.style.getPropertyValue('--plugin-page-scrollbar-width')).toBe('') + }) + + it('interpolates narrow wrapped controls to their measured horizontal and width endpoints', () => { + const h = harness(), f = h.fixture(narrow()) + f.attach() + expect(f.part('toolbar').style.getPropertyValue('--plugin-part-y')).toBe('90px') + expect(f.part('toolbar').style.getPropertyValue('--plugin-part-width')).toBe('280px') + f.scroll(37) + expect(f.part('toolbar').style.translate).toBe('100px -37px') + expect(f.part('toolbar').style.width).toBe('180px') + f.scroll(144) + expect(f.part('toolbar').style.translate).toBe('200px -74px') + expect(f.part('toolbar').style.width).toBe('80px') + expect(f.part('tabs').style.translate).toBe('0px -144px') + expect(f.part('tabs').style.width).toBe('160px') + expect(f.bar.style.getPropertyValue('--plugin-header-paint-height')).toBe('70px') + f.scroll(0) + expect(f.part('toolbar').style.translate).toBe('0px 0px') + expect(f.part('toolbar').style.width).toBe('280px') + expect(f.part('tabs').style.width).toBe('280px') + }) + + it('clips the no-tabs intro beneath the pinned title and restores interaction when scrolling back', () => { + const h = harness(), f = h.fixture(wide(false)) + f.attach() + f.scroll(16) + expect(f.part('title').style.translate).toBe('0px -12px') + expect(f.part('intro').style.clipPath).toBe('inset(0% 0 0)') + expect(f.part('intro').inert).toBe(false) + f.scroll(26) + expect(f.part('intro').style.translate).toBe('0px -26px') + expect(f.part('intro').style.clipPath).toBe('inset(50% 0 0)') + expect(f.part('intro').inert).toBe(false) + f.scroll(36) + expect(f.part('intro').style.clipPath).toBe('inset(100% 0 0)') + expect(f.part('intro').inert).toBe(true) + expect(f.bar.hasAttribute('data-compact')).toBe(true) + f.scroll(0) + expect(f.part('intro').style.clipPath).toBe('inset(0% 0 0)') + expect(f.part('intro').inert).toBe(false) + }) + + it('scrolls the tabs intro fully out without the no-tabs clipping treatment', () => { + const h = harness(), f = h.fixture() + f.part('intro').style.clipPath = 'inset(100% 0 0)' + f.attach() + f.scroll(79) + expect(f.part('intro').style.clipPath).toBe('') + expect(f.part('intro').inert).toBe(false) + f.scroll(80) + expect(f.part('intro').style.translate).toBe('0px -80px') + expect(f.part('intro').inert).toBe(true) + f.scroll(40) + expect(f.part('intro').inert).toBe(false) + expect(f.part('intro').style.translate).toBe('0px -40px') + }) + + it.each(['resize', 'mutation', 'font'] as const)('remeasures changed endpoints on %s without replacing focused controls', (event) => { + const h = harness(), f = h.fixture() + const button = f.part('toolbar').querySelector('button')! + const clicked = vi.fn() + button.addEventListener('click', clicked) + f.attach() + f.scroll(144) + button.focus() + f.state.layout = narrow() + if (event === 'resize') h.resizes[0]!.deliver() + else if (event === 'mutation') h.mutations[0]!.deliver() + else h.fonts.dispatchEvent(new Event('loadingdone')) + expect(f.part('toolbar').style.translate).toBe('200px -74px') + expect(f.part('toolbar').style.width).toBe('80px') + expect(f.bar.style.getPropertyValue('--plugin-header-rest-height')).toBe('210px') + expect(f.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('78px') + expect(document.activeElement).toBe(button) + expect(f.part('toolbar').querySelector('button')).toBe(button) + button.click() + expect(clicked).toHaveBeenCalledTimes(1) + expect(f.clones).toHaveLength(2) + expect(f.clones.every(clone => !clone.isConnected)).toBe(true) + expect(f.body.querySelector('[data-measuring]')).toBeNull() + expect(f.measurements.slice(-2)).toEqual([false, true].map(compact => ({ + compact, inert: true, ariaHidden: 'true', ids: 0, styles: 0, + natural: false, scrolled: false, connected: true, width: '320px', + }))) + const ids = [...f.body.querySelectorAll('[id]')].map(element => element.id) + expect(new Set(ids).size).toBe(ids.length) + expect(h.resizes[0]!.observe).toHaveBeenCalledWith(f.body) + expect(h.mutations[0]!.observe).toHaveBeenCalledWith(f.header, { + childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ['hidden'], + }) + }) + + it('removes the inert measurement copy when a compact layout read fails', () => { + const h = harness(), f = h.fixture() + const failure = new Error('Layout read interrupted') + const resting = f.state.layout.resting + Object.defineProperty(f.state, 'layout', { + get: () => ({ + width: 960, + resting, + get compact(): Layout['compact'] { throw failure }, + }), + }) + expect(() => { f.attach() }).toThrow(failure) + expect(f.clones).toHaveLength(1) + expect(f.clones[0]!.isConnected).toBe(false) + expect(f.body.querySelector('[data-measuring]')).toBeNull() + expect(h.resizes).toHaveLength(0) + expect(h.mutations).toHaveLength(0) + }) + + it('disconnects observers and listeners and ignores callbacks already queued before detachment', () => { + const h = harness(), f = h.fixture() + f.attach() + f.scroll(20) + const before = f.bar.outerHTML + f.ref(null) + expect(h.resizes[0]!.disconnect).toHaveBeenCalledTimes(1) + expect(h.mutations[0]!.disconnect).toHaveBeenCalledTimes(1) + expect(f.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('') + f.state.layout = narrow() + f.scroll(144) + h.resizes[0]!.deliver() + h.mutations[0]!.deliver() + h.fonts.dispatchEvent(new Event('loadingdone')) + expect(f.bar.outerHTML).toBe(before) + expect(f.clones).toHaveLength(1) + f.ref(null) + expect(h.resizes[0]!.disconnect).toHaveBeenCalledTimes(1) + }) + + it('rebinds one ref without allowing the old element or its callbacks to change the replacement', () => { + const h = harness(), ref = createPluginHeaderRef(), first = h.fixture(wide(), ref), second = h.fixture(narrow(), ref) + first.attach() + first.scroll(20) + const oldMarkup = first.bar.outerHTML + second.attach() + second.scroll(37) + const newMarkup = second.bar.outerHTML + first.scroll(144) + h.resizes[0]!.deliver() + h.mutations[0]!.deliver() + expect(first.bar.outerHTML).toBe(oldMarkup) + expect(second.bar.outerHTML).toBe(newMarkup) + expect(first.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('') + expect(second.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('78px') + expect(first.clones).toHaveLength(1) + expect(second.clones).toHaveLength(1) + }) + + it('keeps two mounted refs independent when scrolling, remeasuring, and disposing either instance', () => { + const h = harness(), first = h.fixture(), second = h.fixture(narrow()) + first.attach() + second.attach() + first.scroll(20) + expect(second.part('toolbar').style.translate).toBe('0px 0px') + second.scroll(37) + expect(first.part('toolbar').style.translate).toBe('0px -12px') + expect(second.part('toolbar').style.translate).toBe('100px -37px') + first.ref(null) + const oldMarkup = first.bar.outerHTML + second.state.layout = wide() + h.fonts.dispatchEvent(new Event('loadingdone')) + expect(first.bar.outerHTML).toBe(oldMarkup) + expect(first.clones).toHaveLength(1) + expect(second.clones).toHaveLength(2) + expect(second.part('toolbar').style.translate).toBe('0px -12px') + expect(second.body.style.getPropertyValue('--plugin-header-scroll-padding')).toBe('72px') + expect(h.resizes[1]!.disconnect).not.toHaveBeenCalled() + second.scroll(0) + expect(second.part('toolbar').style.translate).toBe('0px 0px') + }) +}) diff --git a/packages/client/ui-plugin-manager/tsconfig.client.json b/packages/client/ui-plugin-manager/tsconfig.client.json index 5f8c1f5e42..eefbe67b00 100644 --- a/packages/client/ui-plugin-manager/tsconfig.client.json +++ b/packages/client/ui-plugin-manager/tsconfig.client.json @@ -9,6 +9,9 @@ { "path": "../../util/package-manifest" }, + { + "path": "../../util/official-bundles" + }, { "path": "../../../vendor/cordis" }, diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml index 4ca8e13747..1fa39181d9 100644 --- a/packages/client/ui-plugin-market/README.i18n.yaml +++ b/packages/client/ui-plugin-market/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-plugin-market/README.md /: - en: af3fe4b70bddbc1b - zh: ac1c4326165266d9 + en: b77d6d729562a88f + zh: b1634cc1d027424c /deepseek-ai-dsh-client-ui-plugin-market: en: 62972eb0dff8c145 zh: 682cc03b492cd730 /deepseek-ai-dsh-client-ui-plugin-market/summary: - en: 55a3d1b12cd7be61 - zh: a961cf0212b3fda8 + en: 71328d505b984fe5 + zh: 9281e7dbaa01ef9a /deepseek-ai-dsh-client-ui-plugin-market/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-client-ui-plugin-market/use-this-package: - en: f873bf36195df821 - zh: e13c78ba3f3e29f1 + en: 868b100459e30aad + zh: 8a3a01199dc3c14c /deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation: - en: 4107a6967934f6b6 - zh: 46acec8bedb86329 + en: e1a480caaff8efea + zh: 0b725fa5bc929f22 /deepseek-ai-dsh-client-ui-plugin-market/model-experience: en: 08d25ab9cbdd7567 zh: 26e6e75c123951ee diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md index 7a2bf9ffa8..283992a8c6 100644 --- a/packages/client/ui-plugin-market/README.md +++ b/packages/client/ui-plugin-market/README.md @@ -1,5 +1,5 @@ --- -description: "Discover tab of the Web Plugins page: search configured plugin market indexes, read an entry, and install it." +description: "Discover tab of the Web Plugins page: search configured plugin indexes, read an entry, and install it." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin market indexes offer; users manage index sources, switch between markets, search, filter by category and label, sort, open an entry's page, and install an entry after confirming it. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. +Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin indexes offer; users manage index sources, switch between indexes, search, filter by category and label, sort, open an entry's page, and install an entry after confirming it. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. ## Table of Contents @@ -24,11 +24,11 @@ Discover is the second tab of the **Plugins** page, after **Installed**. It list ## Use this package -Open **Plugins** and select **Discover**. With no index source, the tab shows an empty state whose **Add index source** button opens the **Index sources** dialog. The dialog lists each source by market title, or by host before the first read, with its URL, the error of its last failed read, and a remove button, and keeps a notice that indexes come from third parties. A refused URL keeps the draft and says why: an unsupported scheme, credentials, or fragment; a URL already listed; or a deployment that cannot save sources. A newly added source becomes the selected market. +Open **Plugins** and select **Discover**. With no index source, the tab shows an empty state whose **Add index source** button opens the **Index sources** dialog. The dialog lists each source by index title, or by host before the first read, with its URL, the error of its last failed read, and a remove button, and keeps a notice that indexes come from third parties. A refused URL keeps the draft and says why: an unsupported scheme, credentials, or fragment; a URL already listed; or a deployment that cannot save sources. A newly added source becomes the selected index. -The toolbar holds the market menu, which switches markets and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the market declares labels; the **Sort** menu; and a refresh button. Category pills, with **All** first, follow when the market declares categories. +The toolbar holds the index menu, which switches indexes and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the index declares labels; the **Sort** menu; and a refresh button. Category pills, with **All** first, follow when the index declares categories. -Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and market. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact. +Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and index title. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact. **Install** chooses the default version and opens a confirmation dialog with the entry's icon, title, that version, the author, its source facts, an exemption notice when one applies, and a trust notice. Confirming opens the Plugins install dialog at its check screen with the entry's exact spec and registry; there is no spec form, and a refusal offers **Retry**. The registry an entry names applies to that install only. @@ -36,7 +36,7 @@ The Host [`pluginMarket.search`](../../boot/plugin-market/README.md#use-this-pac The description section shows the default version's long description in the active UI language: the version's locale `meta.detail` file, otherwise its package README, otherwise its repository README, in the [order `pluginMarket` documents](../../boot/plugin-market/README.md#plugin-detail-documents). While the document loads, the section shows three skeleton lines; an entry without a document shows no section; a failed read names its reason, such as a size limit, an unreachable source, or a refused repository, with **Retry**. Raw HTML is omitted, and a document taller than the section's height limit scrolls inside it. Relative image paths load from the Host beside the document, absolute HTTP(S) images load directly without a referrer, and relative links stay text. The section is the shared [`DetailDocument`](../ui-primitives/README.md#component-catalog) primitive. -Reading a market shows card skeletons. A failed read shows a short sentence with **Retry**, the Host's reason appears under the source in **Index sources**, and keeps the market's last listing; a failed read of the source list shows **Retry** in place of the tab. Discover re-reads sources on `plugin-market/changed` and connection reset, and recomputes installation status on `plugin-manager/changed`. +Reading an index shows card skeletons. A failed read shows a short sentence with **Retry**, the Host's reason appears under the source in **Index sources**, and keeps the index's last listing; a failed read of the source list shows **Retry** in place of the tab. Discover re-reads sources on `plugin-market/changed` and connection reset, and recomputes installation status on `plugin-manager/changed`. ----- @@ -45,7 +45,7 @@ Reading a market shows card skeletons. A failed read shows a short sentence with Detail reads are kept in a second snapshot store keyed by index URL, entry, version, and language; a section starts its read when it renders, an npm version's read first asks the Host's registry comparison and passes `npmFileSources: ['npmmirror']` when npmmirror answered fastest, only the latest read of a key settles it, **Retry** asks the Host to read again instead of returning its cached result, and a connection reset forgets every read, because a restarted Host no longer knows the document ids its images use, so each shown section reads its document again. Image URLs resolve `api/plugin-market/detail-file` against `document.baseURI`, so they keep a deployment prefix and use the Desktop `dsh-app://app/` origin, which [`MarkdownText`](../ui-primitives/README.md) accepts for that route. -The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. +The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another index starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. ----- diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md index 40b98ec314..276c1a5556 100644 --- a/packages/client/ui-plugin-market/README.zh.md +++ b/packages/client/ui-plugin-market/README.zh.md @@ -1,5 +1,5 @@ --- -description: "Web 插件页的「发现」标签页:搜索已配置的插件市场索引,查看条目并安装。" +description: "Web 插件页的「发现」标签页:搜索已配置的插件索引,查看条目并安装。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件市场索引提供的组合包(bundle);用户可以管理索引源、在插件市场之间切换、搜索、按分类和标签筛选、排序、打开条目页面,并在确认后安装条目。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 +「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件索引提供的组合包(bundle);用户可以管理索引源、在插件索引之间切换、搜索、按分类和标签筛选、排序、打开条目页面,并在确认后安装条目。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 ## 目录 @@ -24,11 +24,11 @@ kind: "package-reference" ## 使用本包 -打开**插件**并选择**发现**。没有索引源时,该标签页显示空状态,其中的**添加索引源**按钮打开**索引源**对话框。对话框按插件市场标题列出每个索引源(首次读取前按主机名),并显示其 URL、最近一次读取失败的错误和删除按钮,同时保留一条提示,说明索引由第三方提供。URL 被拒绝时保留草稿并说明原因:不支持的协议、包含凭据或片段、URL 已在列表中,或当前部署无法保存索引源。新添加的索引源成为所选插件市场。 +打开**插件**并选择**发现**。没有索引源时,该标签页显示空状态,其中的**添加索引源**按钮打开**索引源**对话框。对话框按插件索引标题列出每个索引源(首次读取前按主机名),并显示其 URL、最近一次读取失败的错误和删除按钮,同时保留一条提示,说明索引由第三方提供。URL 被拒绝时保留草稿并说明原因:不支持的协议、包含凭据或片段、URL 已在列表中,或当前部署无法保存索引源。新添加的索引源成为所选插件索引。 -工具栏包含:插件市场菜单,用于切换插件市场,最后一项为**管理索引源…**;搜索框;插件市场声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件市场声明了分类时,其后显示分类按钮,**全部**排在第一位。 +工具栏包含:插件索引菜单,用于切换插件索引,最后一项为**管理索引源…**;搜索框;插件索引声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件索引声明了分类时,其后显示分类按钮,**全部**排在第一位。 -每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 +每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件索引标题。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 **安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。 @@ -36,7 +36,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- 详细介绍部分以当前界面语言显示默认版本的详细介绍:按 [`pluginMarket` 说明的顺序](../../boot/plugin-market/README.zh.md#plugin-detail-documents),依次为该版本 locale 中 `meta.detail` 指定的文件、其包 README、其仓库 README。文档加载期间,该部分显示三行骨架屏;没有文档的条目不显示该部分;读取失败时说明原因,例如超过大小限制、来源无法访问或仓库拒绝访问,并提供**重试**。原始 HTML 会被省略;文档高度超过该部分的高度上限时,在该部分内滚动。相对路径的图片从 Host 上该文档旁的位置加载,绝对 HTTP(S) 地址的图片不带 referrer 直接加载,相对链接保持为文本。该部分使用共享的 [`DetailDocument`](../ui-primitives/README.zh.md#component-catalog) 原语。 -读取插件市场时显示卡片骨架屏。读取失败时显示一句简短说明和**重试**,Host 给出的原因显示在**索引源**中对应索引源的下方,并保留该插件市场上一次的列表;读取索引源列表失败时,在该标签页的位置显示**重试**。「发现」在收到 `plugin-market/changed` 和连接重置时重新读取索引源,在收到 `plugin-manager/changed` 时重新计算安装状态。 +读取插件索引时显示卡片骨架屏。读取失败时显示一句简短说明和**重试**,Host 给出的原因显示在**索引源**中对应索引源的下方,并保留该插件索引上一次的列表;读取索引源列表失败时,在该标签页的位置显示**重试**。「发现」在收到 `plugin-market/changed` 和连接重置时重新读取索引源,在收到 `plugin-manager/changed` 时重新计算安装状态。 ----- @@ -45,7 +45,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- 详细介绍的读取结果保存在第二个快照存储中,以索引 URL、条目、版本和语言为键;该部分在渲染时开始读取;npm 版本的读取先询问 Host 的注册表比较,npmmirror 响应最快时传入 `npmFileSources: ['npmmirror']`;只有某个键最新一次的读取才会让该部分得到结果;**重试**要求 Host 重新读取,而不是返回其缓存的结果;连接重置时丢弃所有读取结果,因为重启后的 Host 不再知道其图片使用的文档 id,所以每个已显示的部分会重新读取其文档。图片 URL 基于 `document.baseURI` 解析 `api/plugin-market/detail-file`,因此保留部署前缀,并在 Desktop 中使用 `dsh-app://app/` origin;[`MarkdownText`](../ui-primitives/README.zh.md) 对该路由接受此 origin。 -本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 +本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件索引时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 ----- diff --git a/packages/client/ui-plugin-market/locale/de.json b/packages/client/ui-plugin-market/locale/de.json new file mode 100644 index 0000000000..7575ee4faa --- /dev/null +++ b/packages/client/ui-plugin-market/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plugin-Erkennung", + "description": "Konfigurierte Plugin-Indizes durchsuchen und deren Pakete installieren." + } +} diff --git a/packages/client/ui-plugin-market/locale/en.json b/packages/client/ui-plugin-market/locale/en.json index 2c72ab4cec..222f7277ed 100644 --- a/packages/client/ui-plugin-market/locale/en.json +++ b/packages/client/ui-plugin-market/locale/en.json @@ -1,6 +1,6 @@ { "meta": { "title": "Plugin discovery", - "description": "Browse configured plugin market indexes and install their packages." + "description": "Browse configured plugin indexes and install their packages." } } diff --git a/packages/client/ui-plugin-market/locale/es.json b/packages/client/ui-plugin-market/locale/es.json new file mode 100644 index 0000000000..a9dd1b071d --- /dev/null +++ b/packages/client/ui-plugin-market/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Descubrimiento de complementos", + "description": "Explore los índices de complementos configurados e instale sus paquetes." + } +} diff --git a/packages/client/ui-plugin-market/locale/fr.json b/packages/client/ui-plugin-market/locale/fr.json new file mode 100644 index 0000000000..e21adf5411 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Découverte de plugins", + "description": "Parcourez les index de plugins configurés et installez leurs paquets." + } +} diff --git a/packages/client/ui-plugin-market/locale/ja.json b/packages/client/ui-plugin-market/locale/ja.json index 7411851543..32e9d3c3d7 100644 --- a/packages/client/ui-plugin-market/locale/ja.json +++ b/packages/client/ui-plugin-market/locale/ja.json @@ -1,6 +1,6 @@ { "meta": { "title": "プラグイン検索 UI", - "description": "設定済みのプラグインマーケットのインデックスを参照し、プラグインパッケージをインストールします。" + "description": "設定済みのプラグインインデックスを参照し、プラグインパッケージをインストールします。" } } diff --git a/packages/client/ui-plugin-market/locale/pt.json b/packages/client/ui-plugin-market/locale/pt.json new file mode 100644 index 0000000000..6192b57fe6 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Descoberta de plug-ins", + "description": "Navegue pelos índices de plug-ins configurados e instale seus pacotes." + } +} diff --git a/packages/client/ui-plugin-market/locale/ru.json b/packages/client/ui-plugin-market/locale/ru.json new file mode 100644 index 0000000000..c990647d59 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Открытие плагина", + "description": "Просматривайте настроенные индексы плагинов и устанавливайте их пакеты." + } +} diff --git a/packages/client/ui-plugin-market/locale/zh.json b/packages/client/ui-plugin-market/locale/zh.json index 4e9e89725f..f01c89d5df 100644 --- a/packages/client/ui-plugin-market/locale/zh.json +++ b/packages/client/ui-plugin-market/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { "title": "插件发现界面", - "description": "浏览已配置的插件市场索引,并安装其中的插件包。" + "description": "浏览已配置的插件索引,并安装其中的插件包。" } } diff --git a/packages/client/ui-plugin-market/package.json b/packages/client/ui-plugin-market/package.json index 30883a8c70..9606553275 100644 --- a/packages/client/ui-plugin-market/package.json +++ b/packages/client/ui-plugin-market/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plugin-market", "description": "Discover tab of the dsh web Plugins page: browse configured plugin market indexes and install their bundles", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plugin-market/src/client/index.ts b/packages/client/ui-plugin-market/src/client/index.ts index c1aab2c746..36aed35a01 100644 --- a/packages/client/ui-plugin-market/src/client/index.ts +++ b/packages/client/ui-plugin-market/src/client/index.ts @@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-plugin-manager/types' import type {} from '@deepseek-ai/dsh-plugin-market/types' import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client' import { DiscoverSection } from './DiscoverSection.tsx' -import { en, ja, zh, type PluginMarketLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PluginMarketLocaleKey } from './locales.ts' import { DiscoverController } from './market-store.ts' export type { DiscoverFace, DiscoverState } from './market-store.ts' @@ -36,7 +36,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.pluginMarket', 'remo * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plugin-market: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plugin-market: dictionaries') const controller = new DiscoverController(ctx, () => document.baseURI) ctx.effect(() => () => { controller.dispose() }, 'ui-plugin-market: controller') ctx.effect(() => { diff --git a/packages/client/ui-plugin-market/src/client/locales.ts b/packages/client/ui-plugin-market/src/client/locales.ts index 20aada6cae..52753bd63a 100644 --- a/packages/client/ui-plugin-market/src/client/locales.ts +++ b/packages/client/ui-plugin-market/src/client/locales.ts @@ -8,14 +8,14 @@ export const zh = { retry: '重试', close: '关闭', cancel: '取消', - emptyTitle: '还没有插件市场', - emptyDescription: '添加一个插件市场索引,即可在这里浏览和安装其中的插件', + emptyTitle: '还没有插件索引', + emptyDescription: '添加一个插件索引,即可在这里浏览和安装其中的插件', addFirstSource: '添加索引源', sourcesFailed: '无法读取索引源', manageSources: '管理索引源…', - marketMenu: '切换插件市场,当前为 {market}', + marketMenu: '切换插件索引,当前为 {market}', sourcesTitle: '索引源', - sourcesDescription: '插件市场以一个索引文件的 URL 提供,「发现」从这里列出的索引浏览插件', + sourcesDescription: '插件索引以一个索引文件的 URL 提供,「发现」从这里列出的索引浏览插件', sourcesLabel: '已添加的索引源', sourceUrl: '索引 URL', sourcePlaceholder: 'https://example.com/index.json', @@ -70,7 +70,7 @@ export const zh = { factSourceCode: '源代码', factCategory: '分类', factLabels: '标签', - factMarket: '插件市场', + factMarket: '插件索引', versionsTitle: '版本', versionsLabel: '{name} 的版本', versionDefault: '推荐', @@ -102,14 +102,14 @@ export const en = { retry: 'Retry', close: 'Close', cancel: 'Cancel', - emptyTitle: 'No plugin markets yet', - emptyDescription: 'Add a plugin market index to browse and install its plugins here.', + emptyTitle: 'No plugin indexes yet', + emptyDescription: 'Add a plugin index to browse and install its plugins here.', addFirstSource: 'Add index source', sourcesFailed: 'Could not read the index sources.', manageSources: 'Manage index sources…', - marketMenu: 'Switch plugin market, currently {market}', + marketMenu: 'Switch plugin index, currently {market}', sourcesTitle: 'Index sources', - sourcesDescription: 'A plugin market is published as the URL of one index file. Discover browses the indexes listed here.', + sourcesDescription: 'A plugin index is published as the URL of one index file. Discover browses the indexes listed here.', sourcesLabel: 'Added index sources', sourceUrl: 'Index URL', sourcePlaceholder: 'https://example.com/index.json', @@ -164,7 +164,7 @@ export const en = { factSourceCode: 'Source code', factCategory: 'Category', factLabels: 'Labels', - factMarket: 'Plugin market', + factMarket: 'Plugin index', versionsTitle: 'Versions', versionsLabel: 'Versions of {name}', versionDefault: 'Recommended', @@ -193,14 +193,14 @@ export const ja = { 'retry': '再試行', 'close': '閉じる', 'cancel': 'キャンセル', - 'emptyTitle': 'プラグインマーケットはまだありません', - 'emptyDescription': 'プラグインマーケットのインデックスを追加すると、ここでプラグインを閲覧してインストールできます。', + 'emptyTitle': 'プラグインインデックスはまだありません', + 'emptyDescription': 'プラグインインデックスを追加すると、ここでそのプラグインを閲覧してインストールできます。', 'addFirstSource': 'インデックスソースを追加', 'sourcesFailed': 'インデックスソースを読み取れませんでした。', 'manageSources': 'インデックスソースを管理…', - 'marketMenu': 'プラグインマーケットを切り替え、現在は{market}', + 'marketMenu': 'プラグインインデックスを切り替え、現在は{market}', 'sourcesTitle': 'インデックスソース', - 'sourcesDescription': 'プラグインマーケットは1つのインデックスファイルのURLとして公開されます。「見つける」ではここに一覧されたインデックスを閲覧します。', + 'sourcesDescription': 'プラグインインデックスは1つのインデックスファイルのURLとして公開されます。「見つける」ではここに一覧されたインデックスを閲覧します。', 'sourcesLabel': '追加済みのインデックスソース', 'sourceUrl': 'インデックスURL', 'sourcePlaceholder': 'https://example.com/index.json', @@ -255,7 +255,7 @@ export const ja = { 'factSourceCode': 'ソースコード', 'factCategory': 'カテゴリ', 'factLabels': 'ラベル', - 'factMarket': 'プラグインマーケット', + 'factMarket': 'プラグインインデックス', 'versionsTitle': 'バージョン', 'versionsLabel': '{name} のバージョン', 'versionDefault': '推奨', @@ -275,3 +275,458 @@ export const ja = { 'detailFailed': '詳細を読み取れませんでした。', 'footnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Descubrir', + refresh: 'Actualizar', + loading: 'leyendo el índice', + retry: 'Reintentar', + close: 'Cerrar', + cancel: 'Cancelar', + emptyTitle: 'Aún no hay índices de complementos', + emptyDescription: 'Agregue un índice de complementos para explorar e instalar sus complementos aquí.', + addFirstSource: 'Agregar fuente de índice', + sourcesFailed: 'No se pudieron leer las fuentes del índice.', + manageSources: 'Administrar fuentes de índice...', + marketMenu: 'Cambiar el índice de complementos; actual: {market}', + sourcesTitle: 'Fuentes del índice', + sourcesDescription: 'Un índice de complementos se publica mediante la URL de un archivo de índice. Descubrir explora los índices enumerados aquí.', + sourcesLabel: 'Fuentes de índice agregadas', + sourceUrl: 'URL del índice', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Añadir', + removeSource: 'Quitar', + removeSourceNamed: 'Quitar {url}', + sourcesTrust: 'Los índices provienen de terceros. Agregue solo fuentes en las que confíe: sus complementos se ejecutan con sus permisos una vez instalados.', + sourceInvalid: 'Utilice una dirección http o https sin nombre de usuario, contraseña o fragmento #.', + sourceDuplicate: 'Esta fuente de índice ya está agregada.', + sourceUnavailable: 'Las fuentes del índice no se pueden guardar aquí.', + sourceFailed: 'No se pudieron guardar las fuentes del índice.', + readFailed: 'No se pudo leer este índice.', + readInvalid: 'Este índice no tiene un formato válido.', + readChanged: 'Este índice cambió durante la carga.', + search: 'Complementos de búsqueda', + filter: 'Etiquetas', + filterCount: 'Etiquetas · {count}', + sortBy: 'Tipo: {sort}', + sortRelevance: 'mejor partido', + sortDefault: 'Orden predeterminado', + sortPublished: 'Fecha de publicación', + switchToAscending: 'Orden ascendente', + switchToDescending: 'Orden descendente', + categoriesLabel: 'Categorías', + allCategories: 'Todos', + emptyIndex: 'Este índice no incluye complementos.', + noMatches: 'Ningún complemento coincide.', + showMore: 'Mostrar más', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Ver {name}', + install: 'Instalar', + installTitle: 'Instalar {name}', + installVersionTitle: 'Instalar {name} {version}', + openInstalled: 'Ver {name} en Instalado', + statusInstalled: 'Instalado', + statusUpdate: 'Actualización disponible', + statusProvided: 'Proporcionado por DSH', + incompatible: 'Esta versión no es compatible con DSH {runtime} en ejecución; requiere DSH {range}.', + noCompatibleVersion: 'Ninguna versión es compatible con DSH {runtime}', + exempted: 'Esta versión no es compatible con DSH en ejecución, pero usted establece una exención de compatibilidad para ella.', + backToList: 'Descubrir', + factSource: 'Fuente', + factPackage: 'Paquete', + factRegistry: 'Registro', + factRepository: 'Repositorio', + factCommit: 'comprometerse', + factDirectory: 'Directorio', + factPublished: 'Publicado', + factAuthor: 'Autor', + factHomepage: 'Página de inicio', + factSourceCode: 'código fuente', + factCategory: 'Categoría', + factLabels: 'Etiquetas', + factMarket: 'Índice de complementos', + versionsTitle: 'Versiones', + versionsLabel: 'Versiones de {name}', + versionDefault: 'Recomendado', + versionInstalled: 'Instalado', + versionIncompatible: 'incompatible', + versionExempted: 'exento', + versionPrerelease: 'Presentación', + confirmTitle: '¿Instalar "{name}"?', + installTrust: 'Los complementos se ejecutan en esta máquina con sus permisos. Asegúrate de confiar en el origen de este.', + detailTitle: 'Detalles', + detailLoading: 'Detalles de lectura', + detailTooLarge: 'Los detalles o su repositorio exceden el límite de tamaño y no se cargaron.', + detailNetwork: 'No se pudo llegar a la fuente de los detalles.', + detailDenied: 'No se pudo acceder al repositorio de detalles. Verifique sus credenciales de Git.', + detailGitUnavailable: 'Los detalles están en un repositorio que debe leerse con Git y Git no se pudo ejecutar. Instale Git y vuelva a intentarlo.', + detailInvalid: 'El complemento declara un archivo de detalles no válido.', + detailFailed: 'No se pudieron leer los detalles.', + footnotes: 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Découvrez', + refresh: 'Actualiser', + loading: "Lire l'index", + retry: 'Réessayer', + close: 'Fermer', + cancel: 'Annuler', + emptyTitle: 'Aucun index de plugins pour le moment', + emptyDescription: 'Ajoutez un index de plugins pour parcourir et installer ses plugins ici.', + addFirstSource: "Ajouter une source d'index", + sourcesFailed: "Impossible de lire les sources de l'index.", + manageSources: "Gérer les sources d'index…", + marketMenu: 'Changer d’index de plugins, actuellement {market}', + sourcesTitle: "Sources d'index", + sourcesDescription: 'Un index de plugins est publié à l’URL d’un fichier d’index. Découvrir parcourt les index répertoriés ici.', + sourcesLabel: "Sources d'index ajoutées", + sourceUrl: "URL de l'index", + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Ajouter', + removeSource: 'Supprimer', + removeSourceNamed: 'Supprimer {url}', + sourcesTrust: "Les index proviennent de tiers. Ajoutez uniquement des sources auxquelles vous faites confiance : leurs plugins s'exécutent avec vos autorisations une fois installés.", + sourceInvalid: "Utilisez une adresse http ou https sans nom d'utilisateur, mot de passe ou fragment #.", + sourceDuplicate: "Cette source d'index est déjà ajoutée.", + sourceUnavailable: "Les sources d'index ne peuvent pas être enregistrées ici.", + sourceFailed: "Impossible d'enregistrer les sources de l'index.", + readFailed: 'Impossible de lire cet index.', + readInvalid: "Cet index n'est pas dans un format valide.", + readChanged: 'Cet index a changé lors du chargement.', + search: 'Plugins de recherche', + filter: 'Étiquettes', + filterCount: 'Étiquettes · {count}', + sortBy: 'Trier : {sort}', + sortRelevance: 'Meilleure correspondance', + sortDefault: 'Ordre par défaut', + sortPublished: 'Date de publication', + switchToAscending: 'Trier par ordre croissant', + switchToDescending: 'Trier par ordre décroissant', + categoriesLabel: 'Catégories', + allCategories: 'Tout', + emptyIndex: 'Cet index ne répertorie aucun plugin.', + noMatches: 'Aucun plugin ne correspond.', + showMore: 'Afficher plus', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Voir {name}', + install: 'Installer', + installTitle: 'Installer {name}', + installVersionTitle: 'Installer {name} {version}', + openInstalled: 'Afficher {name} dans Installé', + statusInstalled: 'Installé', + statusUpdate: 'Mise à jour disponible', + statusProvided: 'Fourni par DSH', + incompatible: "Cette version n'est pas compatible avec le DSH {runtime} en cours d'exécution ; cela nécessite DSH {range}.", + noCompatibleVersion: "Aucune version n'est compatible avec DSH {runtime}", + exempted: "Cette version n'est pas compatible avec le DSH en cours d'exécution, mais vous avez défini une exemption de compatibilité pour celui-ci.", + backToList: 'Découvrez', + factSource: 'Source', + factPackage: 'Forfait', + factRegistry: 'Registre', + factRepository: 'Référentiel', + factCommit: "S'engager", + factDirectory: 'Annuaire', + factPublished: 'Publié', + factAuthor: 'Auteur', + factHomepage: "Page d'accueil", + factSourceCode: 'Code source', + factCategory: 'Catégorie', + factLabels: 'Étiquettes', + factMarket: 'Index de plugins', + versionsTitle: 'Versions', + versionsLabel: 'Versions de {name}', + versionDefault: 'Recommandé', + versionInstalled: 'Installé', + versionIncompatible: 'Incompatible', + versionExempted: 'Exonéré', + versionPrerelease: 'Avant-première', + confirmTitle: 'Installer « {name} » ?', + installTrust: "Les plugins s'exécutent sur cette machine avec vos autorisations. Assurez-vous de faire confiance à l'origine de celui-ci.", + detailTitle: 'Détails', + detailLoading: 'Détails de lecture', + detailTooLarge: "Les détails ou leur référentiel dépassent la limite de taille et n'ont pas été chargés.", + detailNetwork: "Impossible d'atteindre la source des détails.", + detailDenied: "Impossible d'accéder au référentiel des détails. Vérifiez vos informations d'identification Git.", + detailGitUnavailable: "Les détails se trouvent dans un référentiel qui doit être lu avec Git, et Git n'a pas pu s'exécuter. Installez Git, puis réessayez.", + detailInvalid: 'Le plugin déclare un fichier de détails invalide.', + detailFailed: 'Impossible de lire les détails.', + footnotes: 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Entdecken', + refresh: 'Aktualisieren', + loading: 'Den Index lesen', + retry: 'Erneut versuchen', + close: 'Schließen', + cancel: 'Abbrechen', + emptyTitle: 'Noch keine Plugin-Indizes', + emptyDescription: 'Einen Plugin-Index hinzufügen, um dessen Plugins hier zu durchsuchen und zu installieren.', + addFirstSource: 'Indexquelle hinzufügen', + sourcesFailed: 'Die Indexquellen konnten nicht gelesen werden.', + manageSources: 'Indexquellen verwalten…', + marketMenu: 'Plugin-Index wechseln, aktuell {market}', + sourcesTitle: 'Indexquellen', + sourcesDescription: 'Ein Plugin-Index wird über die URL einer Indexdatei veröffentlicht. Entdecken durchsucht die hier aufgeführten Indizes.', + sourcesLabel: 'Indexquellen hinzugefügt', + sourceUrl: 'Index-URL', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Hinzufügen', + removeSource: 'Entfernen', + removeSourceNamed: 'Entfernen Sie {url}', + sourcesTrust: 'Indizes stammen von Dritten. Fügen Sie nur Quellen hinzu, denen Sie vertrauen: Ihre Plugins werden nach der Installation mit Ihren Berechtigungen ausgeführt.', + sourceInvalid: 'Verwenden Sie eine http- oder https-Adresse ohne Benutzernamen, Passwort oder #-Fragment.', + sourceDuplicate: 'Diese Indexquelle wurde bereits hinzugefügt.', + sourceUnavailable: 'Indexquellen können hier nicht gespeichert werden.', + sourceFailed: 'Die Indexquellen konnten nicht gespeichert werden.', + readFailed: 'Dieser Index konnte nicht gelesen werden.', + readInvalid: 'Dieser Index hat kein gültiges Format.', + readChanged: 'Dieser Index hat sich beim Laden geändert.', + search: 'Such-Plugins', + filter: 'Etiketten', + filterCount: 'Etiketten · {count}', + sortBy: 'Sortierung: {sort}', + sortRelevance: 'Beste Übereinstimmung', + sortDefault: 'Standardreihenfolge', + sortPublished: 'Veröffentlichungsdatum', + switchToAscending: 'Aufsteigend sortieren', + switchToDescending: 'Absteigend sortieren', + categoriesLabel: 'Kategorien', + allCategories: 'Alle', + emptyIndex: 'Dieser Index listet keine Plugins auf.', + noMatches: 'Es stimmen keine Plugins überein.', + showMore: 'Mehr anzeigen', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Siehe {name}', + install: 'Installieren', + installTitle: 'Installieren Sie {name}', + installVersionTitle: 'Installieren Sie {name} {version}', + openInstalled: '{name} unter „Installiert“ anzeigen', + statusInstalled: 'Installiert', + statusUpdate: 'Update verfügbar', + statusProvided: 'Bereitgestellt von DSH', + incompatible: 'Diese Version ist nicht mit dem laufenden DSH {runtime} kompatibel; es erfordert DSH {range}.', + noCompatibleVersion: 'Keine Version ist mit DSH {runtime} kompatibel', + exempted: 'Diese Version ist nicht mit der laufenden Version DSH kompatibel, Sie haben jedoch eine Kompatibilitätsausnahme dafür festgelegt.', + backToList: 'Entdecken', + factSource: 'Quelle', + factPackage: 'Paket', + factRegistry: 'Registrierung', + factRepository: 'Repository', + factCommit: 'Begehen', + factDirectory: 'Verzeichnis', + factPublished: 'Veröffentlicht', + factAuthor: 'Autor', + factHomepage: 'Homepage', + factSourceCode: 'Quellcode', + factCategory: 'Kategorie', + factLabels: 'Etiketten', + factMarket: 'Plugin-Index', + versionsTitle: 'Versionen', + versionsLabel: 'Versionen von {name}', + versionDefault: 'Empfohlen', + versionInstalled: 'Installiert', + versionIncompatible: 'Inkompatibel', + versionExempted: 'Ausgenommen', + versionPrerelease: 'Vorabversion', + confirmTitle: '„{name}“ installieren?', + installTrust: 'Plugins werden mit Ihren Berechtigungen auf diesem Computer ausgeführt. Stellen Sie sicher, dass Sie darauf vertrauen, woher dieser kommt.', + detailTitle: 'Details', + detailLoading: 'Details lesen', + detailTooLarge: 'Die Details oder ihr Repository überschreiten die Größenbeschränkung und wurden nicht geladen.', + detailNetwork: 'Die Quelle der Details konnte nicht erreicht werden.', + detailDenied: 'Auf das Repository mit den Details konnte nicht zugegriffen werden. Überprüfen Sie Ihre Git-Anmeldeinformationen.', + detailGitUnavailable: 'Die Details befinden sich in einem Repository, das mit Git gelesen werden muss, und Git konnte nicht ausgeführt werden. Installieren Sie Git und versuchen Sie es erneut.', + detailInvalid: 'Das Plugin deklariert eine ungültige Detaildatei.', + detailFailed: 'Die Details konnten nicht gelesen werden.', + footnotes: 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Откройте для себя', + refresh: 'Обновить', + loading: 'Чтение индекса', + retry: 'Повторить', + close: 'Закрыть', + cancel: 'Отмена', + emptyTitle: 'Индексы плагинов ещё не добавлены', + emptyDescription: 'Добавьте индекс плагинов, чтобы просматривать и устанавливать его плагины здесь.', + addFirstSource: 'Добавить источник индекса', + sourcesFailed: 'Не удалось прочитать исходники индекса.', + manageSources: 'Управление источниками индексов…', + marketMenu: 'Переключить индекс плагинов, текущий: {market}', + sourcesTitle: 'Источники индекса', + sourcesDescription: 'Индекс плагинов публикуется по URL файла индекса. В разделе «Обнаружение» можно просматривать перечисленные здесь индексы.', + sourcesLabel: 'Добавлены источники индекса', + sourceUrl: 'URL-адрес индекса', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Добавить', + removeSource: 'Удалить', + removeSourceNamed: 'Удалить {url}', + sourcesTrust: 'Индексы поступают от третьих лиц. Добавляйте только те источники, которым вы доверяете: их плагины запускаются с вашими разрешениями после установки.', + sourceInvalid: 'Используйте адрес http или https без имени пользователя, пароля или фрагмента #.', + sourceDuplicate: 'Этот источник индекса уже добавлен.', + sourceUnavailable: 'Источники индекса здесь сохранить нельзя.', + sourceFailed: 'Не удалось сохранить источники индекса.', + readFailed: 'Не удалось прочитать этот индекс.', + readInvalid: 'Этот индекс имеет недопустимый формат.', + readChanged: 'Этот индекс менялся при загрузке.', + search: 'Поиск плагинов', + filter: 'Этикетки', + filterCount: 'Этикетки · {count}', + sortBy: 'Сортировать: {sort}', + sortRelevance: 'Лучший матч', + sortDefault: 'Порядок по умолчанию', + sortPublished: 'Дата публикации', + switchToAscending: 'Сортировать по возрастанию', + switchToDescending: 'Сортировать по убыванию', + categoriesLabel: 'Категории', + allCategories: 'Все', + emptyIndex: 'В этом индексе нет плагинов.', + noMatches: 'Никакие плагины не подходят.', + showMore: 'Показать больше', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Посмотреть {name}', + install: 'Установить', + installTitle: 'Установить {name}', + installVersionTitle: 'Установить {name} {version}', + openInstalled: 'Посмотреть {name} в списке «Установлено»', + statusInstalled: 'Установлено', + statusUpdate: 'Доступно обновление', + statusProvided: 'Предоставлено DSH', + incompatible: 'Эта версия несовместима с работающим DSH {runtime}; для этого требуется DSH {range}.', + noCompatibleVersion: 'Ни одна версия не совместима с DSH {runtime}.', + exempted: 'Эта версия несовместима с работающим DSH, но вы установили для нее исключение совместимости.', + backToList: 'Откройте для себя', + factSource: 'Источник', + factPackage: 'Пакет', + factRegistry: 'Реестр', + factRepository: 'Репозиторий', + factCommit: 'Зафиксировать', + factDirectory: 'Каталог', + factPublished: 'Опубликовано', + factAuthor: 'Автор', + factHomepage: 'Домашняя страница', + factSourceCode: 'Исходный код', + factCategory: 'Категория', + factLabels: 'Этикетки', + factMarket: 'Индекс плагинов', + versionsTitle: 'Версии', + versionsLabel: 'Версии {name}', + versionDefault: 'Рекомендуется', + versionInstalled: 'Установлено', + versionIncompatible: 'Несовместимый', + versionExempted: 'освобожден', + versionPrerelease: 'Пререлиз', + confirmTitle: 'Установить "{name}"?', + installTrust: 'Плагины запускаются на этом компьютере с вашего разрешения. Убедитесь, что вы доверяете источнику этого сообщения.', + detailTitle: 'Подробности', + detailLoading: 'Читаем подробности', + detailTooLarge: 'Детали или их репозиторий превышают предельный размер и не были загружены.', + detailNetwork: 'До источника подробностей добраться не удалось.', + detailDenied: 'Не удалось получить доступ к хранилищу реквизитов. Проверьте свои учетные данные Git.', + detailGitUnavailable: 'Подробности находятся в репозитории, который необходимо прочитать с помощью Git, и Git не удалось запустить. Установите Git и повторите попытку.', + detailInvalid: 'Плагин объявляет неверный файл сведений.', + detailFailed: 'Подробности прочитать не удалось.', + footnotes: 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Descubra', + refresh: 'Atualizar', + loading: 'Lendo o índice', + retry: 'Tentar novamente', + close: 'Fechar', + cancel: 'Cancelar', + emptyTitle: 'Ainda não há índices de plug-ins', + emptyDescription: 'Adicione um índice de plug-ins para navegar e instalar seus plug-ins aqui.', + addFirstSource: 'Adicionar fonte de índice', + sourcesFailed: 'Não foi possível ler as fontes do índice.', + manageSources: 'Gerenciar fontes de índice…', + marketMenu: 'Mudar de índice de plug-ins, atual: {market}', + sourcesTitle: 'Fontes de índice', + sourcesDescription: 'Um índice de plug-ins é publicado pela URL de um arquivo de índice. Descobrir navega pelos índices listados aqui.', + sourcesLabel: 'Fontes de índice adicionadas', + sourceUrl: 'URL do índice', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Adicionar', + removeSource: 'Remover', + removeSourceNamed: 'Remover {url}', + sourcesTrust: 'Os índices vêm de terceiros. Adicione apenas fontes em que você confia: seus plug-ins são executados com suas permissões, uma vez instalados.', + sourceInvalid: 'Use um endereço http ou https sem nome de usuário, senha ou fragmento #.', + sourceDuplicate: 'Esta fonte de índice já foi adicionada.', + sourceUnavailable: 'As fontes do índice não podem ser salvas aqui.', + sourceFailed: 'Não foi possível salvar as fontes do índice.', + readFailed: 'Não foi possível ler este índice.', + readInvalid: 'Este índice não está em um formato válido.', + readChanged: 'Este índice mudou durante o carregamento.', + search: 'Plug-ins de pesquisa', + filter: 'Etiquetas', + filterCount: 'Marcadores · {count}', + sortBy: 'Classificar: {sort}', + sortRelevance: 'Melhor correspondência', + sortDefault: 'Ordem padrão', + sortPublished: 'Data de publicação', + switchToAscending: 'Classificar em ordem crescente', + switchToDescending: 'Classificar em ordem decrescente', + categoriesLabel: 'Categorias', + allCategories: 'Todos', + emptyIndex: 'Este índice não lista plug-ins.', + noMatches: 'Nenhum plug-in corresponde.', + showMore: 'Mostrar mais', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Ver {name}', + install: 'Instalar', + installTitle: 'Instalar {name}', + installVersionTitle: 'Instalar {name} {version}', + openInstalled: 'Ver {name} em Instalado', + statusInstalled: 'Instalado', + statusUpdate: 'Atualização disponível', + statusProvided: 'Fornecido por DSH', + incompatible: 'Esta versão não é compatível com o DSH {runtime} em execução; requer DSH {range}.', + noCompatibleVersion: 'Nenhuma versão é compatível com DSH {runtime}', + exempted: 'Esta versão não é compatível com o DSH em execução, mas você definiu uma isenção de compatibilidade para ela.', + backToList: 'Descubra', + factSource: 'Fonte', + factPackage: 'Pacote', + factRegistry: 'Registro', + factRepository: 'Repositório', + factCommit: 'Confirmar', + factDirectory: 'Diretório', + factPublished: 'Publicado', + factAuthor: 'Autor', + factHomepage: 'Página inicial', + factSourceCode: 'Código fonte', + factCategory: 'Categoria', + factLabels: 'Etiquetas', + factMarket: 'Índice de plug-ins', + versionsTitle: 'Versões', + versionsLabel: 'Versões de {name}', + versionDefault: 'Recomendado', + versionInstalled: 'Instalado', + versionIncompatible: 'Incompatível', + versionExempted: 'Isento', + versionPrerelease: 'Pré-lançamento', + confirmTitle: 'Instalar "{name}"?', + installTrust: 'Os plug-ins são executados nesta máquina com suas permissões. Certifique-se de confiar de onde este vem.', + detailTitle: 'Detalhes', + detailLoading: 'Lendo detalhes', + detailTooLarge: 'Os detalhes ou seu repositório excedem o limite de tamanho e não foram carregados.', + detailNetwork: 'Não foi possível acessar a fonte dos detalhes.', + detailDenied: 'Não foi possível acessar o repositório dos detalhes. Verifique suas credenciais do Git.', + detailGitUnavailable: 'Os detalhes estão em um repositório que deve ser lido com o Git, e o Git não pôde ser executado. Instale o Git e tente novamente.', + detailInvalid: 'O plugin declara um arquivo de detalhes inválido.', + detailFailed: 'Não foi possível ler os detalhes.', + footnotes: 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx index 88ee3e6cd3..965275e9f8 100644 --- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx +++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx @@ -50,7 +50,7 @@ const LISTING: MarketSearchResult = { fetchedAt: 1, total: 6, nextOffset: null, - title: { en: 'Alpha market', zh: '甲市场' }, + title: { en: 'Alpha index', zh: '甲索引' }, metadata: { categories: { tools: { title: 'Tools' }, themes: { title: 'Themes' } }, labels: { official: { title: 'Official' }, beta: { title: 'Beta' } }, @@ -231,7 +231,7 @@ describe('DiscoverSection', () => { expect(screen.getByText(en.emptyIndex)).toBeTruthy() }) - it('sorts by publish date, a market sort key, either direction, and back to the index order', () => { + it('sorts by publish date, an index sort key, either direction, and back to the index order', () => { const { actions, answer } = renderTab() openMenu(/^Sort: Default order/) pick(en.sortPublished) @@ -256,7 +256,7 @@ describe('DiscoverSection', () => { answer(INITIAL_QUERY) openMenu(/^Sort: Default order/) expect(screen.queryByRole('menuitem', { name: en.switchToAscending })).toBeNull() - for (const name of [/^Sort:/, en.filter, /^Switch plugin market/]) { + for (const name of [/^Sort:/, en.filter, /^Switch plugin index/]) { fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' }) expect(screen.queryByRole('menu')).toBeNull() openMenu(name) @@ -264,17 +264,17 @@ describe('DiscoverSection', () => { } fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' }) expect(screen.queryByRole('menu')).toBeNull() - openMenu(/^Switch plugin market/) - openMenu(/^Switch plugin market/) + openMenu(/^Switch plugin index/) + openMenu(/^Switch plugin index/) expect(screen.queryByRole('menu')).toBeNull() }) - it('switches markets and opens source management from the market menu', () => { + it('switches indexes and opens source management from the index menu', () => { const { actions } = renderTab() - openMenu('Switch plugin market, currently Alpha market') + openMenu('Switch plugin index, currently Alpha index') pick('b.example') expect(actions.select).toHaveBeenCalledWith(B) - openMenu(/^Switch plugin market/) + openMenu(/^Switch plugin index/) pick(en.manageSources) expect(actions.toggleSources).toHaveBeenCalledOnce() }) @@ -287,7 +287,7 @@ describe('DiscoverSection', () => { expect(detail.getByRole('status', { name: en.detailLoading })).toBeTruthy() expect(detail.getByRole('heading', { name: 'Alpha' })).toBeTruthy() expect(detail.getByText('dsh-a')).toBeTruthy() - for (const value of ['Jane', 'dsh-a@1.0.0', 'https://r.example/', 'Tools', 'Official', 'Beta', 'Alpha market']) expect(detail.getByText(value)).toBeTruthy() + for (const value of ['Jane', 'dsh-a@1.0.0', 'https://r.example/', 'Tools', 'Official', 'Beta', 'Alpha index']) expect(detail.getByText(value)).toBeTruthy() const homepage = detail.getByRole('link', { name: 'https://example.com/alpha#readme' }) expect([homepage.getAttribute('href'), homepage.getAttribute('target'), homepage.getAttribute('rel')]) .toEqual(['https://example.com/alpha#readme', '_blank', 'noopener noreferrer']) @@ -420,14 +420,14 @@ describe('DiscoverSection', () => { expect(screen.getByRole('status', { name: en.loading })).toBeTruthy() expect(screen.getByRole('button', { name: en.refresh }).hasAttribute('disabled')).toBe(true) set({ listing: { url: B, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, url: B, entries: [], total: 0 }, answered: INITIAL_QUERY, failure: null } }) - expect(screen.getByRole('button', { name: 'Switch plugin market, currently b.example' })).toBeTruthy() + expect(screen.getByRole('button', { name: 'Switch plugin index, currently b.example' })).toBeTruthy() }) it('manages sources in a dialog with the trust notice and each refusal', () => { const { actions, set } = renderTab({ editing: true, draft: 'https://c.example/index.json', indexes: [{ url: A, title: LISTING.title }, { url: B, error: 'HTTP 404' }] }) const dialog = within(screen.getByRole('dialog', { name: en.sourcesTitle })) expect(dialog.getByText(en.sourcesTrust)).toBeTruthy() - expect(dialog.getByText('Alpha market')).toBeTruthy() + expect(dialog.getByText('Alpha index')).toBeTruthy() expect(dialog.getByText(B)).toBeTruthy() expect(dialog.getByText('HTTP 404')).toBeTruthy() fireEvent.click(dialog.getByRole('button', { name: `Remove ${B}` })) @@ -453,7 +453,7 @@ describe('DiscoverSection', () => { it('renders the Chinese copy and dates', () => { renderTab({}, zh) - expect(screen.getByRole('button', { name: '切换插件市场,当前为 甲市场' })).toBeTruthy() + expect(screen.getByRole('button', { name: '切换插件索引,当前为 甲索引' })).toBeTruthy() expect(card('dsh-a').getByText('甲')).toBeTruthy() expect(card('dsh-a').getByText(/^1\.0\.0 · Jane · 2026年9月/)).toBeTruthy() expect(card('dsh-b').getByText(zh.statusInstalled)).toBeTruthy() diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 732fbedef0..2e00fb223d 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -15,17 +15,17 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-primitives/use-this-package: - en: d2f5701274be5110 - zh: d60e2617136a6f26 + en: 10be7f75a1576575 + zh: abf2905d89b0da48 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: 8e9f354ef0f81d22 - zh: 8ee59b6bb7a7198e + en: 3c79351ff2210ccb + zh: 4cbe07ded89bbaa1 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: - en: fff68b60f4abdc7e - zh: 5cf5952ec78f7f91 + en: b3744373b4b2c1fb + zh: 2dbde8ee0bfa5f28 /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: 02f310acbae29955 - zh: 1e41393d5d714b2d + en: 31ed1a6239427512 + zh: cb489d77de109d3f /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: a943603b74080ef5 zh: 43f7a0188aff271b @@ -33,8 +33,8 @@ en: 846ac0f563a53a64 zh: bb89d7647bdcc1ab /deepseek-ai-dsh-client-ui-primitives/understand-the-implementation/source-map: - en: 44eb32370cbeafe8 - zh: 728009c66375c6bb + en: fd510d34404eb090 + zh: d05b0890db929ae6 /deepseek-ai-dsh-client-ui-primitives/understand-the-implementation/input-modality: en: cb61e1a37d6c27e5 zh: 1f3fa79dd5ab2e45 @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work: - en: 31117d1844eb3c19 - zh: 741eea7d78e00d3f + en: 9c5bc17e35af9d0b + zh: 4f1a142c7d56cf72 /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index a6492dc5a8..380ce75142 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -27,6 +27,8 @@ Use `dsh-client-ui-primitives` to build web-client controls and render agent out `Toast` uses the theme’s toast background and label colors in both light and dark modes. `Menu` data rows and `MenuItemButton` component rows accept owner-provided effective shortcuts and align their keys at the trailing edge as muted text without a background, including submenus. Menus hide these hints when the primary pointer is coarse (`pointer: coarse`) and [keyboard navigation](#input-modality) is inactive. Keyboard navigation reveals them until the next pointer press; keyboard bindings remain active. `ShortcutKeys` defaults to unboxed keys for menu and inline hints. Its `tooltip` variant uses lighter keycaps over the dark bubble and groups plus-separated combinations into one filled block. `Tooltip.shortcutKeys` vertically centers keycaps beside optional localized action text, or shows only keys when the label is empty. The optional `className` supplies owner interaction styles. `Modal` uses the frame’s shared overlay inset, with a minimum 24px viewport margin. Its mask leaves the Windows caption unpainted while the full-viewport layer blocks background clicks. `Modal` and the settings shell share top-layer Escape and Tab handling through `useModalLayer`, restoring the previous focus on close. `closeTopModal(document)` requests the foreground modal’s current close action; newer menus or unregistered dialogs block dismissal of the modal behind them. Components use `isBehindModal` to avoid moving focus behind a nested dialog. Menus consume their local Escape before modal dismissal. `observeComposition` supplies the same composition-end and legacy IME guard to local modal and recording handlers; callers dispose its document listeners with their interaction lifetime. +`isMobileDevice()` identifies iOS and Android phones and tablets for automatic text-input focus and keyboard dismissal. It recognizes `Android`, `iPhone`, `iPad`, or `iPod` in the UA, and iPads reporting `MacIntel` with more than one touch point. Other devices, including Windows touchscreen computers, return false regardless of window width. The function caches its first result for all callers in the page; component remounts reuse that result, and reloading the page reads the browser environment again. Identified mobile devices require explicit text-input focus. Browser modes or custom UAs that conceal the mobile platform can prevent identification; unrecognized UAs return false. A desktop UA alone cannot distinguish Android tablets from Linux computers. + Automatic modal entry and modal/menu return focus, including after Escape and application close shortcuts, use `focusWithoutRing(element, options?)` to suppress the outline until Tab or directional navigation resumes normal focus styling. Dialog containers keep their outline-free styling. Existing borders, shadows, and error states remain intact. Mark a dialog's initial control with `data-modal-autofocus` so the modal captures its invoking control before moving focus. Controls mounted with the dialog must not use React `autoFocus`, which runs before that capture. Tab and Shift+Tab from the dialog container enter its first and last focusable controls. This package is a Web-shell build input. Its static ESM retains third-party imports and styles for Vite; independent consumers supply its development dependencies ([dependency rules](../AGENTS.md#dependency-declaration)). @@ -47,10 +49,10 @@ Check this table before writing a control in a feature package. A plugin cannot | `Input` | Single-line text entry for search boxes and inline forms. Its ref targets the native input for focus and is cleared on unmount. | | `InlineEditor` | Autofocused multi-line inline editor for short drafts such as queued messages and goals. It grows with its text and its width up to six lines, then scrolls; Enter saves, Shift+Enter breaks the line, Enter during IME composition does nothing, and Escape cancels. The caller supplies the localized `label` and rebinds the scrollbar on an elevated surface. | | `ZoomControls`, `DOCUMENT_ZOOM` | Floating document zoom controls with localized labels, fixed presets, and visibility/activity callbacks. An omitted `fit` hides the fitting row. The toolbar carries `data-document-zoom-controls`, plus `data-document-zoom-visible` while visible. An owner `className` overrides `bottom` and `max-width` regardless of stylesheet order; `inset-inline-start` and `transform` center the toolbar and move it on enter and exit, `opacity` fades it, and all three stay with the primitive. Renderers share the published scale limits. | -| `Menu`, `MenuItemButton` | Dropdown of `items` data rows, separators, and group labels, with nested submenus; `children` adds component rows, each a `MenuItemButton` (`separatorBefore` starts a new group), in the same list. Every row shares the styling, the keyboard walk, and the focus return; closing stays the owner's state change for both kinds. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. | +| `Menu`, `MenuItemButton` | Dropdown of `items` data rows, separators, and group labels, with nested submenus; `children` adds component rows, each a `MenuItemButton` (`separatorBefore` starts a new group), in the same list. `MenuItemButton.current` exposes the current page or view through `aria-current` without changing selection or appearance. Every row shares the styling, the keyboard walk, and the focus return; closing stays the owner's state change for both kinds. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. | | `MenuGroup`, `observeStickyMenuGroups` | Localized, accessible groups for custom menus and listboxes, with shared sticky-heading styling and native intersection/size observation; the caller owns the observer's lifetime. | | `Pill` | Selectable capsule button for view switchers and filters; takes `active` and `onClick`. | -| `SegmentedTabs` | Controlled equal-width tabs with a sliding indicator and Left/Right, Home, and End navigation. The caller supplies labels, tab/panel ids, and panel content. | +| `SegmentedTabs` | Controlled equal-width tabs with a sliding indicator and Left/Right, Home, and End navigation. The caller supplies labels, tab/panel ids, and panel content. Default `size="md"` is 42px tall with a 4px inset; `size="sm"` is 34px tall with a 2px inset. | | `Tag` | Read-only capsule badge; `tone` selects one of eight palettes. | | `PathLabel` | Single-line file path with subdued directories, a primary filename, and the full path on hover. Fitting paths align left; clipped paths preserve their suffix with a left-edge fade that updates on path and size changes. | | `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader whose animations pin to document time zero on mount and on every animation restart, such as a list reorder, so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. | @@ -94,9 +96,9 @@ Writing your own component in your own package is fine when the need is genuinel ### Controls and icons -The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `HoverCard` and nested action tooltips display independently; use `Tooltip.portal` to layer the small bubble above the card. A visible nested `Tooltip` suppresses only its enclosing tooltip, not the hover card. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. +The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product icon renders an `` box with one `` of its `dsw-icon--` symbol, for example `dsw-icon-search-outline-regular`, and names that id in `data-icon`. `ICON_SYMBOLS` holds every symbol's default SVG document; ui-theme renders the document icon sprite from it, and an icon whose symbol is missing renders nothing. Artwork reached through `` inherits `currentColor` and CSS variables from the use site. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers; while an anchor is inside that top strip, its overlay, including a `HoverCard`, is never pushed lower than the anchor's bottom edge plus the overlay's gap. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance except below an anchor inside the top strip, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `HoverCard` and nested action tooltips display independently; use `Tooltip.portal` to layer the small bubble above the card. A visible nested `Tooltip` suppresses only its enclosing tooltip, not the hover card. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. -`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip. +`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip. Opt-in `onlyWhenClipped` shows the bubble only when the anchor or an HTML element inside it overflows horizontally when the bubble is about to show, so a label that repeats ellipsis-cut text appears only while it adds information. @@ -108,7 +110,7 @@ The pure Markdown helper `formatQuoteText` normalizes CRLF and CR line endings, The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `openFile` navigation callbacks. Nested providers replace the enclosing capabilities, and callback changes reach already-rendered links without rebuilding Markdown. Its `openFile` makes local Markdown links clickable after settlement. Absolute and workspace-relative paths support percent escapes and `#L24` / `#L24-L30` fragments; ranges open at their first line. Literal `?` and `#` in filenames must be percent-encoded. The tooltip uses the decoded path and supplies the accessible name when the label is empty. The callback receives the decoded path and optional line, while the renderer preserves the label and displays a file icon. Without a callback, local links remain text. URL schemes, queries, unsupported fragments, and malformed destinations never reach the file opener. -`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. Strikethrough requires `~~`; a single `~`, as in `10~20`, stays literal. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. `omitHtml` renders raw HTML as nothing instead of literal text, for documents written for HTML renderers. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. Rewritten images accept HTTP(S), data, blob, and the Desktop `dsh-app://app/api/file`, `dsh-app://app/api/plugin-market/detail-file`, and `dsh-app://app/api/plugin-manager/detail-file` routes; authored Desktop URLs remain inert. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty; `fileImages` additionally supplies a localized failure prefix. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `UserTextRun` renders uninterpreted text and `ReferenceChip` renders business-resolved labels. An explicit `onActivate` makes a chip a keyboard-accessible button; repeated pointer clicks and an existing text selection do not activate it, while keyboard activation remains available. Labels inherit their host’s wrapping policy. Recognition and JSON transforms belong to [ui-conversation](../ui-conversation/README.md#sent-user-text). +`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. Strikethrough requires `~~`; a single `~`, as in `10~20`, stays literal. A surrounding `MarkdownDelegateProvider` receives sanitized HTTP(S) URLs from ordinary clicks; modified clicks and links outside a provider retain native external-anchor behavior. `omitHtml` renders raw HTML as nothing instead of literal text, for documents written for HTML renderers. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. Rewritten images accept HTTP(S), data, blob, and the Desktop `dsh-app://app/api/file`, `dsh-app://app/api/plugin-market/detail-file`, and `dsh-app://app/api/plugin-manager/detail-file` routes; authored Desktop URLs remain inert. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty; `fileImages` additionally supplies a localized failure prefix. Changing the image source permits a fresh load. Each mounted image appends a `load` query parameter to a resolved Host `api/file` URL, because browsers reuse a same-URL image within one document despite `no-store`; a newly rendered image of an overwritten file therefore loads its current bytes, and every remount requests the file again; the Host file route redirects that request to a cacheable URL for the current file version. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `UserTextRun` renders uninterpreted text and `ReferenceChip` renders business-resolved labels. An explicit `onActivate` makes a chip a keyboard-accessible button; repeated pointer clicks and an existing text selection do not activate it, while keyboard activation remains available. Labels inherit their host’s wrapping policy. Recognition and JSON transforms belong to [ui-conversation](../ui-conversation/README.md#sent-user-text). `MarkdownText` defaults to `variant="body"`. Use `variant="compact"` for secondary content: its 13px text and 20px line height follow the content-size setting, all heading levels use the same size with weight 600, and paragraphs and lists use tighter spacing. Text, links, and code keep the tertiary color; dotted underlines distinguish links. Code headers scroll with their blocks. Tables and math stay enabled at the surrounding text size and scroll horizontally within the available width. Both variants share the parser and streaming cache. `textInteractionSource` marks its existing root as a source for text selection interactions. @@ -157,7 +159,7 @@ The package enforces one separation: presentational React atoms with zero Cordis | [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI escape parsing (`anser`) and terminal card rendering | | [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | Read and diff cards | | [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | Search and web-retrieval cards | -| [`src/icons/`](src/icons/) | Size-neutral `Regular` and `Medium` product glyph components | +| [`src/icons/`](src/icons/) | Default product glyph artwork, its `ICON_SYMBOLS` sprite documents, and the size-neutral `Regular` and `Medium` components that reference those symbols | | [`src/code-highlighting.ts`](src/code-highlighting.ts) | Shared filename grammar selection and lazy line highlighting | | [`src/input-modality.ts`](src/input-modality.ts) | Document-wide input modality published on `` | | [`src/plugin-artwork.tsx`](src/plugin-artwork.tsx) | Fixed-palette plugin artwork with per-instance SVG def ids | @@ -218,8 +220,10 @@ These limits define how the atoms behave at the edges; they are current package - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. - **Overlong lines stay uncolored** — a source line of 1,000 or more UTF-16 units renders as one plain run in every highlighted surface, because TextMate scanning cost grows superlinearly with line length and a single generated line can otherwise block the page for seconds. The grammar state skips that line, so a comment or template literal that opens or closes on it mis-colors the following lines. Highlighting still runs synchronously during render, so a file with thousands of ordinary lines costs proportionally more. +- **The Go struct-field rule is patched locally** — when the Go grammar loads, its repeated one-line struct-field rule is replaced with the rule [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) merged, because the vendored rule backtracks exponentially on lines such as a brace-bearing comment after `struct {`. If a dependency upgrade removes that rule, loading the grammar throws and Go code renders as plain text; delete the patch once `@shikijs/langs` ships the upstream rule. - **The fish logo is a redrawn approximation** — its font glyph has no exportable vector geometry in the local design data, so a hand-authored recreation stands in until an exact export path exists. - **`Pill` and `Input` have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms. +- **Product icons need the ui-theme sprite** — each icon only references its symbol, so without ui-theme, or inside another document such as an iframe, the icon renders nothing. - **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, error, and idle. - **User-facing copy is required at the render site** — the atoms are zero-Cordis and cannot reach `ctx.locale`; each feature must supply complete localized labels through the primitive's typed props ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). - **Display math keeps fallback shapes** — a `$$x$$ trailing` on the opening line resolves through upstream inline math and leaves the surrounding prose in place, while the same trailing text on a later line declines the block and upstream takes the prose up to the next lone `$$` or EOF; a block left open ends at the next lone `$$` or at EOF; a later fence behind a `%` comment that holds text closes nothing, so a block nothing else closes reaches the same fallback; a multi-line block whose opening line carries a solitary `$` stays in its paragraph unless a blank line separates them; and a container continuation line that leaves the item indentation hands the block to upstream, which drops the opening line. `$$` fence meta is no longer honored ([decision](../../../.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md)). diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index d539aff861..9d2ffe3698 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -27,6 +27,8 @@ kind: "package-library" `Toast` 在浅色和深色模式下均使用主题的系统提示背景与文字颜色。`Menu` 数据条目与 `MenuItemButton` 组件条目接收功能 owner 提供的有效快捷键,并在末端以无背景的浅灰色文字对齐显示,子菜单也采用同一呈现。主指针为粗指针(`pointer: coarse`)且未使用[键盘导航](#input-modality)时,菜单隐藏这些提示。键盘导航会显示提示,直到下一次指针按下;快捷键绑定始终生效。`ShortcutKeys` 默认以无方块的按键文字显示菜单和行内提示。`tooltip` 变体在深色气泡上使用稍浅的键帽,以加号连接的组合则共用一个填充色块。`Tooltip.shortcutKeys` 将键帽与可选的本地化操作文本垂直居中排列,标签为空时只显示按键。可选的 `className` 供调用方设置交互状态样式。`Modal` 使用框架共享的浮层起点,视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。`Modal` 与设置外壳通过 `useModalLayer` 共用顶层 Esc 和 Tab 处理,关闭时恢复先前焦点。`closeTopModal(document)` 请求前台弹窗当前的关闭操作;上层菜单或未注册的对话框会阻止关闭其后方弹窗。组件通过 `isBehindModal` 避免将焦点移到嵌套弹窗后方。菜单先消费自己的 Esc,再由模态层处理关闭。 `observeComposition` 为局部弹层和录键处理提供相同的 composition-end 与旧版 IME 保护;调用方随交互生命周期释放其 document 监听。 +`isMobileDevice()` 识别 iOS 和 Android 手机及平板,供自动聚焦和收起键盘使用。它识别 UA 中的 `Android`、`iPhone`、`iPad` 或 `iPod`,以及报告 `MacIntel` 且触摸点数大于一的 iPad。其他设备(包括 Windows 触屏电脑)返回 false,不受窗口宽度影响。函数缓存首次判断结果,供页面内的所有调用方共享;组件重新挂载会复用该结果,重新加载页面时会再次读取浏览器环境。识别出的移动设备需要用户主动聚焦文本输入框。隐藏移动平台信息的浏览器模式或自定义 UA 可能导致识别失败;未识别的 UA 返回 false。仅凭桌面 UA 无法区分 Android 平板与 Linux 电脑。 + 弹窗自动进入及弹窗、菜单回焦,包括通过 Esc 和应用关闭快捷键触发的回焦,均使用 `focusWithoutRing(element, options?)`,在 Tab 或方向键导航恢复正常焦点样式前抑制外轮廓线。弹窗容器仍不绘制焦点外框。原有边框、阴影和错误状态保持不变。用 `data-modal-autofocus` 标记弹窗的初始控件,让模态层先保存触发控件,再移动焦点。随弹窗挂载的控件不得使用 React `autoFocus`,因为它会在保存触发控件前执行。弹窗容器获得焦点时,Tab 和 Shift+Tab 分别进入第一个和最后一个可聚焦控件。 本包是 Web 壳的构建输入。静态 ESM 为 Vite 保留第三方导入和样式;独立消费方自行提供开发依赖([依赖规则](../AGENTS.md#dependency-declaration))。 @@ -47,10 +49,10 @@ kind: "package-library" | `Input` | 单行文本输入,用于搜索框与行内表单。ref 指向原生输入框,供焦点控制使用,并在卸载时清空。 | | `InlineEditor` | 自动聚焦的多行行内编辑器,用于排队消息、goal 等短草稿。它随文本和宽度增高,最多六行,之后滚动;Enter 保存,Shift+Enter 换行,输入法组合中的 Enter 不生效,Escape 取消。调用方提供本地化 `label`,并在 elevated 表面上重绑滚动条。 | | `ZoomControls`, `DOCUMENT_ZOOM` | 悬浮文档缩放控件,接收本地化标签、固定预设,以及可见性和交互状态回调。省略 `fit` 时不显示适配项。工具栏带 `data-document-zoom-controls`,可见时另带 `data-document-zoom-visible`。owner 的 `className` 覆盖 `bottom` 与 `max-width`,不受样式表加载顺序影响;`inset-inline-start` 和 `transform` 负责居中与进出场位移,`opacity` 负责淡入淡出,三者都由原语保留。渲染器共用导出的缩放范围。 | -| `Menu`, `MenuItemButton` | 由 `items` 数据行、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单;`children` 在同一列表中加入组件行,每行一个 `MenuItemButton`(`separatorBefore` 开启新分组)。所有行共享样式、键盘走位与焦点归还;两类行的关闭都是 owner 状态的改变。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 | +| `Menu`, `MenuItemButton` | 由 `items` 数据行、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单;`children` 在同一列表中加入组件行,每行一个 `MenuItemButton`(`separatorBefore` 开启新分组)。`MenuItemButton.current` 通过 `aria-current` 标识当前页面或视图,不改变选中状态或外观。所有行共享样式、键盘走位与焦点归还;两类行的关闭都是 owner 状态的改变。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 | | `MenuGroup`、`observeStickyMenuGroups` | 为自定义菜单与列表框提供本地化、可访问的分组,共用吸顶标题样式与原生交叉/尺寸观察;调用方负责观察器的生命周期。 | | `Pill` | 可选中的胶囊按钮,用于视图切换与筛选器;接受 `active` 与 `onClick`。 | -| `SegmentedTabs` | 受控的等宽分段标签,支持滑动指示条及左/右方向键、Home、End 导航。调用方提供文案、标签与面板 id,以及面板内容。 | +| `SegmentedTabs` | 受控的等宽分段标签,支持滑动指示条及左/右方向键、Home、End 导航。调用方提供文案、标签与面板 id,以及面板内容。默认 `size="md"` 总高 42px、内边距 4px;`size="sm"` 总高 34px、内边距 2px。 | | `Tag` | 只读胶囊徽章;`tone` 选择八种配色之一。 | | `PathLabel` | 单行文件路径:目录使用弱化颜色,文件名使用主色,悬停可查看完整路径。空间足够时靠左显示;溢出时保留尾部并在左侧渐隐,路径或尺寸变化时更新。 | | `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画在挂载时以及每次动画重启(如列表重排)时固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 | @@ -94,9 +96,9 @@ kind: "package-library" ### 控件与图标 -上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`HoverCard` 与嵌套的操作提示独立显示;使用 `Tooltip.portal` 将小气泡置于卡片上层。可见的嵌套 `Tooltip` 只隐藏外层 tooltip,不隐藏悬停卡片。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 +上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品图标渲染一个 `` 框,其中包含一个引用对应 `dsw-icon--` symbol 的 ``,例如 `dsw-icon-search-outline-regular`,并在 `data-icon` 中写出该 id。`ICON_SYMBOLS` 保存每个 symbol 的默认 SVG 文档;ui-theme 据此渲染文档的图标 sprite,缺少对应 symbol 的图标不渲染任何内容。通过 `` 引用的图形会从使用处继承 `currentColor` 与 CSS 变量。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距;锚点位于该顶部区域内时,其浮层(包括 `HoverCard`)不会被推到低于锚点底边加浮层间距的位置。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区(位于顶部区域内的锚点下方时除外),高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`HoverCard` 与嵌套的操作提示独立显示;使用 `Tooltip.portal` 将小气泡置于卡片上层。可见的嵌套 `Tooltip` 只隐藏外层 tooltip,不隐藏悬停卡片。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 -`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。 +`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。可选的 `onlyWhenClipped` 只在气泡即将显示时锚点或其内部某个 HTML 元素横向溢出才显示气泡,因此重复被省略号截断文本的标签只在能补充信息时出现。 @@ -108,7 +110,7 @@ kind: "package-library" 最近的 `MarkdownDelegateProvider` 提供可选的 `openExternalLink` 和 `openFile` 导航回调。嵌套 Provider 替换外层能力,回调变化无需重新构建 Markdown 即可到达已渲染链接。其 `openFile` 使本地 Markdown 链接在落定后可点击。绝对路径和工作区相对路径支持百分号转义以及 `#L24` / `#L24-L30` 片段;范围定位到起始行。文件名中的字面 `?` 和 `#` 必须百分号编码。悬停提示使用解码后的路径,并在标签为空时提供可访问名称。回调接收解码后的路径和可选行号,渲染器保留标签并显示文件图标。不传回调时,本地链接仍为文本。URL 协议、查询串、不支持的片段及格式错误的目标不会传给文件打开器。 -`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。删除线需要 `~~`;单个 `~`(如 `10~20`)保持字面文本。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。`omitHtml` 不渲染原始 HTML,而不是将其显示为原样文本,用于为 HTML 渲染器编写的文档。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。重写后的图片支持 HTTP(S)、data、blob 及桌面端 `dsh-app://app/api/file`、`dsh-app://app/api/plugin-market/detail-file` 与 `dsh-app://app/api/plugin-manager/detail-file` 路由;正文直接书写的桌面端 URL 仍保持惰性。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径;`fileImages` 还提供本地化的失败提示前缀。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`UserTextRun` 渲染不做解析的文本,`ReferenceChip` 渲染业务已解析的标签。显式传入 `onActivate` 后,胶囊成为可由键盘操作的按钮;重复指针点击和已有文本选区不触发激活,键盘激活仍然可用。标签继承所属容器的换行规则。识别和 JSON 变换由 [ui-conversation](../ui-conversation/README.zh.md#sent-user-text) 负责。 +`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。删除线需要 `~~`;单个 `~`(如 `10~20`)保持字面文本。外层 `MarkdownDelegateProvider` 会接收普通点击产生的已净化 HTTP(S) URL;带修饰键的点击和 Provider 外的链接保留原生外部 anchor 行为。`omitHtml` 不渲染原始 HTML,而不是将其显示为原样文本,用于为 HTML 渲染器编写的文档。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。重写后的图片支持 HTTP(S)、data、blob 及桌面端 `dsh-app://app/api/file`、`dsh-app://app/api/plugin-market/detail-file` 与 `dsh-app://app/api/plugin-manager/detail-file` 路由;正文直接书写的桌面端 URL 仍保持惰性。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径;`fileImages` 还提供本地化的失败提示前缀。图片源变化后可重新加载。每个挂载的图片会为解析得到的 Host `api/file` 地址追加 `load` 查询参数,因为浏览器在同一文档内会无视 `no-store` 复用相同 URL 的图片;因此覆盖后的文件在新渲染的图片中会加载当前内容,每次重新挂载都会重新请求该文件;Host 文件路由会把该请求重定向到当前文件版本的可缓存地址。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`UserTextRun` 渲染不做解析的文本,`ReferenceChip` 渲染业务已解析的标签。显式传入 `onActivate` 后,胶囊成为可由键盘操作的按钮;重复指针点击和已有文本选区不触发激活,键盘激活仍然可用。标签继承所属容器的换行规则。识别和 JSON 变换由 [ui-conversation](../ui-conversation/README.zh.md#sent-user-text) 负责。 `MarkdownText` 默认为 `variant="body"`。次级内容使用 `variant="compact"`:其 13px 字号与 20px 行高跟随内容字号设置,各级标题保持同一字号并使用 600 字重,段落与列表采用更紧凑的间距。正文、链接和代码均保持 tertiary 颜色,以点状下划线区分链接。代码标题栏随代码块滚动。表格和公式仍然启用,使用周围文字的字号,并在可用宽度内横向滚动。两个变体共享解析器与流式缓存。 `textInteractionSource` 将已有根元素标记为文本选区交互的来源。 @@ -157,7 +159,7 @@ kind: "package-library" | [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI 转义解析(`anser`)与终端卡片渲染 | | [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | 读取与差异卡片 | | [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | 搜索与网页检索卡片 | -| [`src/icons/`](src/icons/) | 与尺寸无关的 `Regular` 和 `Medium` 产品图标组件 | +| [`src/icons/`](src/icons/) | 默认产品图标图形、其 `ICON_SYMBOLS` sprite 文档,以及引用这些 symbol 的与尺寸无关的 `Regular` 和 `Medium` 组件 | | [`src/code-highlighting.ts`](src/code-highlighting.ts) | 共享的文件名 grammar 选择与惰性逐行高亮 | | [`src/input-modality.ts`](src/input-modality.ts) | 全文档输入模态,发布到 `` | | [`src/plugin-artwork.tsx`](src/plugin-artwork.tsx) | 固定配色插件插画,SVG def id 按实例生成 | @@ -218,8 +220,10 @@ kind: "package-library" - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 - **超长行不着色**:所有高亮界面中,长度达到 1,000 个 UTF-16 单元的源码行渲染为一段纯文本,因为 TextMate 扫描成本随行长超线性增长,单个生成行否则会阻塞页面数秒。语法状态会跳过该行,因此在该行开始或结束的注释、模板字符串会使后续行着色错误。高亮仍在渲染期间同步执行,因此数千个普通行的文件仍按比例增加开销。 +- **Go 结构体字段规则在本地打了补丁**:Go 语法加载时,重复匹配单行结构体字段的规则会替换为 [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) 合入的规则,因为内置规则在 `struct {` 之后含花括号注释等行上会指数级回溯。若依赖升级后该规则消失,加载语法会抛错,Go 代码以纯文本显示;`@shikijs/langs` 发布上游规则后删除此补丁。 - **鱼形标志是重新绘制的近似版本**:它来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 - **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。 +- **产品图标依赖 ui-theme 的 sprite**:每个图标只引用自己的 symbol,因此没有 ui-theme 时,或在 iframe 等另一个文档中,图标不渲染任何内容。 - **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing、error 和 idle。 - **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过原子组件的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。 - **显示数学保留若干回退形状**:开围栏行上的 `$$x$$ trailing` 走上游行内数学,前后正文保持不变;同样形态出现在后续行时块被放弃,随后由上游把正文吞到下一个独占行 `$$` 或文件结尾;未闭合的块在下一个独占行 `$$` 或文件结尾结束;后续行的围栏若位于**含文本**的 `%` 注释之后则不闭合,若无其他围栏闭合,块走同一条回退;开围栏行含孤立 `$` 的多行块会留在段落内,除非与前文空一行;容器内续行若离开条目缩进,块交由上游 flow 处理并丢弃开围栏行。`$$` 围栏 meta 不再生效(见[决策](../../../.agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.zh.md))。 diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 73f1c57c1a..70e96dd578 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/src/HoverCard.tsx b/packages/client/ui-primitives/src/HoverCard.tsx index 157cf35f06..f11c6bad30 100644 --- a/packages/client/ui-primitives/src/HoverCard.tsx +++ b/packages/client/ui-primitives/src/HoverCard.tsx @@ -33,7 +33,8 @@ interface CardPosition { * readable and selectable, but it carries no dismissal affordance of its own. * @param props.openDelayMs - hover dwell before the card shows (default 500). * @param props.variant - compact card beside the anchor, or a preview above/below it - * with 24px side insets, a 420px height cap, frame-top clearance, and 100ms opacity transitions. + * with 24px side insets, a 420px height cap, the frame-top clearance (capped below an anchor inside the top strip), + * and 100ms opacity transitions. * @param props.widthAnchorRef - optional element whose width and horizontal position size the preview. * @param props.inline - keep the anchor in prose; show a contained preview on hover or keyboard focus. * Its hover region connects the anchor and preview across the visual gap. @@ -178,7 +179,7 @@ export function HoverCard({ if (variant === 'preview') { const bounds = widthAnchorRef?.current?.getBoundingClientRect() ?? r const width = Math.max(0, Math.min(bounds.width - PREVIEW_INSET * 2, window.innerWidth - VIEWPORT_MARGIN * 2)) - const topMargin = overlayTopMargin(VIEWPORT_MARGIN) + const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP) const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP) const above = Math.max(0, r.top - ANCHOR_GAP - topMargin) const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN) @@ -195,7 +196,7 @@ export function HoverCard({ const height = Math.max(h, cardRef.current?.scrollHeight ?? 0) const maxWidth = Math.max(0, Math.min(INLINE_PREVIEW_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2)) const width = fitContent ? Math.min(cardRef.current?.offsetWidth ?? maxWidth, maxWidth) : maxWidth - const topMargin = overlayTopMargin(VIEWPORT_MARGIN) + const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP) const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP) const above = Math.max(0, r.top - ANCHOR_GAP - topMargin) const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN) diff --git a/packages/client/ui-primitives/src/Menu.tsx b/packages/client/ui-primitives/src/Menu.tsx index 0f1225fc42..68b3a5766e 100644 --- a/packages/client/ui-primitives/src/Menu.tsx +++ b/packages/client/ui-primitives/src/Menu.tsx @@ -51,6 +51,8 @@ export interface MenuItemButtonProps { shortcut?: MenuItem['shortcut'] /** Leading icon (figma .Menu_cell gap 8). */ icon?: ReactNode + /** Whether this row identifies the currently displayed page or view. */ + current?: boolean /** Whether the row cannot be activated. */ disabled?: boolean /** Destructive row: error-colored text/icon and danger hover fill. */ @@ -75,6 +77,7 @@ export interface MenuItemButtonProps { * @param props.children - visible row label. * @param props.shortcut - effective key labels and accessible combination. * @param props.icon - optional leading icon. + * @param props.current - whether the row identifies the currently displayed page or view. * @param props.disabled - whether the row cannot be activated. * @param props.danger - whether to use the destructive row colors. * @param props.separatorBefore - whether this row starts a new group (hairline above it). @@ -82,7 +85,7 @@ export interface MenuItemButtonProps { * @returns one menu-item row. */ export function MenuItemButton({ - children, shortcut, icon, disabled = false, danger = false, separatorBefore = false, onSelect, + children, shortcut, icon, current, disabled = false, danger = false, separatorBefore = false, onSelect, }: MenuItemButtonProps) { return (
@@ -93,6 +96,7 @@ export function MenuItemButton({ className={clsx(css.item, danger && css.danger)} disabled={disabled} aria-keyshortcuts={shortcut?.aria} + aria-current={current === true ? 'page' : undefined} onClick={onSelect} > {icon !== undefined && {icon}} @@ -255,6 +259,7 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI } if (r === null) return const MARGIN = 12 + const GAP = 4 const vw = window.innerWidth const vh = window.innerHeight const listEl = listRef.current @@ -268,18 +273,18 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI let x: number let y: number if (side === 'right') { - x = r.right + 4 + x = r.right + GAP y = r.top } else if (align === 'start') { x = r.left - y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4 + y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP } else { x = r.right - lw - y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4 + y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP } if (lw > 0) x = Math.min(Math.max(x, MARGIN), vw - lw - MARGIN) - if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN)), vh - lh - MARGIN) + if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN, r, GAP)), vh - lh - MARGIN) setFixedPos(current => current?.left === x && current.top === y ? current : { left: x, top: y }) } diff --git a/packages/client/ui-primitives/src/SegmentedTabs.module.css b/packages/client/ui-primitives/src/SegmentedTabs.module.css index cb12951a11..a4edb7ada9 100644 --- a/packages/client/ui-primitives/src/SegmentedTabs.module.css +++ b/packages/client/ui-primitives/src/SegmentedTabs.module.css @@ -1,14 +1,16 @@ .tabs { + --segmented-tabs-inset: 4px; + position: relative; display: grid; - padding: 4px; + padding: var(--segmented-tabs-inset); border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-module-platform); } .indicator { position: absolute; - inset: 4px auto 4px 4px; + inset: var(--segmented-tabs-inset) auto var(--segmented-tabs-inset) var(--segmented-tabs-inset); box-sizing: border-box; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-md); @@ -46,6 +48,20 @@ font-weight: 600; } +.tabs[data-size='sm'] { + --segmented-tabs-inset: 2px; + + border-radius: calc(var(--dsw-radius-md) + var(--segmented-tabs-inset)); +} + +.tabs[data-size='sm'] .tab { + height: 30px; +} + +.tabs[data-size='sm'] .tab[aria-selected='true'] { + font-weight: 500; +} + .tab:focus-visible { outline: none; text-decoration: underline; diff --git a/packages/client/ui-primitives/src/SegmentedTabs.tsx b/packages/client/ui-primitives/src/SegmentedTabs.tsx index 2ae51be8d5..33ac09f823 100644 --- a/packages/client/ui-primitives/src/SegmentedTabs.tsx +++ b/packages/client/ui-primitives/src/SegmentedTabs.tsx @@ -19,14 +19,16 @@ export interface SegmentedTab { * Keyboard selection also moves focus; only the selected tab is a tab stop. * @param props.label - localized accessible name for the tab list. * @param props.className - layout placement; panels remain caller-owned. + * @param props.size - `sm` draws 30px tabs with a 2px inset (34px tall); the default `md` draws 34px tabs with a 4px inset (42px tall). * @returns the tab list, without its panels. */ -export function SegmentedTabs({ items, value, onChange, label, className }: { +export function SegmentedTabs({ items, value, onChange, label, className, size = 'md' }: { items: readonly [SegmentedTab, ...SegmentedTab[]] value: Value onChange: (value: Value) => void label: string className?: string | undefined + size?: 'sm' | 'md' | undefined }): ReactNode { const selectedIndex = items.findIndex(item => item.value === value) @@ -52,6 +54,7 @@ export function SegmentedTabs({ items, value, onChange, la return (
({ items, value, onChange, la return } diff --git a/packages/client/ui-primitives/src/overlay-top-margin.ts b/packages/client/ui-primitives/src/overlay-top-margin.ts index 02ed679280..bb12ff729c 100644 --- a/packages/client/ui-primitives/src/overlay-top-margin.ts +++ b/packages/client/ui-primitives/src/overlay-top-margin.ts @@ -3,11 +3,16 @@ /** * Resolve the top margin an overlay keeps from the viewport edge. * @param min - the overlay's own viewport margin in px, used as the floor. - * @returns the larger of `min` and the frame clearance plus 20px; fullscreen keeps only the 20px gap. + * @param anchor - the overlay's anchor rect in viewport coordinates. While the anchor's top edge is inside the + * viewport, the margin never exceeds `anchor.bottom + gap`, so an overlay opened from a control inside the top strip + * stays next to it. + * @param gap - the distance the overlay keeps below `anchor`. + * @returns the frame clearance plus 20px (20px in fullscreen), capped by a visible `anchor`, but never below `min`. */ -export function overlayTopMargin(min: number): number { +export function overlayTopMargin(min: number, anchor?: { readonly top: number; readonly bottom: number }, gap = 0): number { const root = document.documentElement const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue('--dsh-frame-top-clearance')) if (Number.isNaN(clearance)) return min - return Math.max(min, (root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20) + const cap = anchor !== undefined && anchor.top >= 0 ? anchor.bottom + gap : Number.POSITIVE_INFINITY + return Math.max(min, Math.min((root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20, cap)) } diff --git a/packages/client/ui-primitives/src/useAnchoredPosition.ts b/packages/client/ui-primitives/src/useAnchoredPosition.ts index f87e4d0697..11256ae00a 100644 --- a/packages/client/ui-primitives/src/useAnchoredPosition.ts +++ b/packages/client/ui-primitives/src/useAnchoredPosition.ts @@ -56,7 +56,7 @@ export function useAnchoredPosition(options: AnchoredPositionOptions): CSSProper let left = align === 'end' ? rect.right - width : rect.left let top = side === 'top' ? rect.top - gap - height : rect.bottom + gap if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin) - if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin) + if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin, rect, gap)), window.innerHeight - height - margin) /* v8 ignore stop */ setPosition({ left, top }) } diff --git a/packages/client/ui-primitives/tests/SegmentedTabs.client.spec.tsx b/packages/client/ui-primitives/tests/SegmentedTabs.client.spec.tsx index 009b86d56b..8a145d2ec7 100644 --- a/packages/client/ui-primitives/tests/SegmentedTabs.client.spec.tsx +++ b/packages/client/ui-primitives/tests/SegmentedTabs.client.spec.tsx @@ -31,11 +31,11 @@ describe('SegmentedTabs', () => { expect(screen.getByRole('tablist', { name: 'Help' })).toBeTruthy() }) - it('moves focus and selection with arrows, Home, and End, and leaves Tab alone', () => { + it.each(['sm', 'md'] as const)('%s moves focus and selection with arrows, Home, and End, and leaves Tab alone', (size) => { const onKeyDown = vi.fn() function Harness() { const [value, setValue] = useState('details') - return
+ return
} render() const details = screen.getByRole('tab', { name: 'Details' }) diff --git a/packages/client/ui-primitives/tests/atoms.client.spec.tsx b/packages/client/ui-primitives/tests/atoms.client.spec.tsx index cfba130058..8b198e4365 100644 --- a/packages/client/ui-primitives/tests/atoms.client.spec.tsx +++ b/packages/client/ui-primitives/tests/atoms.client.spec.tsx @@ -1,13 +1,71 @@ // @vitest-environment jsdom import { createRef, useState } from 'react' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { Button, ConnectionIndicator, Input, Menu, MenuItemButton, Modal, Pill } from '@deepseek-ai/dsh-client-ui-primitives' import { POINTER_GRACE_MS } from '../src/pointer-grace.ts' +import { focusWithoutRing } from '../src/focus.ts' import buttonCss from '../src/Button.module.css' afterEach(cleanup) +describe('isMobileDevice', () => { + beforeEach(() => { vi.resetModules() }) + + it.each([ + { name: 'iPhone', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X)', platform: 'iPhone', maxTouchPoints: 5, mobile: true }, + { name: 'iPad', userAgent: 'Mozilla/5.0 (iPad; CPU OS 18_6 like Mac OS X)', platform: 'iPad', maxTouchPoints: 5, mobile: true }, + { name: 'iPod', userAgent: 'Mozilla/5.0 (iPod; U; CPU like Mac OS X; en)', platform: 'iPod', maxTouchPoints: 5, mobile: true }, + { name: 'Android phone', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) Mobile', platform: 'Linux armv8l', maxTouchPoints: 0, mobile: true }, + { name: 'Android tablet', userAgent: 'Mozilla/5.0 (Linux; Android 10; K)', platform: 'Linux armv8l', maxTouchPoints: 5, mobile: true }, + { name: 'desktop-mode iPad', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', platform: 'MacIntel', maxTouchPoints: 5, mobile: true }, + { name: 'Mac', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', platform: 'MacIntel', maxTouchPoints: 0, mobile: false }, + { name: 'Windows touchscreen', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', platform: 'Win32', maxTouchPoints: 5, mobile: false }, + { name: 'native networking UA', userAgent: 'DeepSeek%20Chat/0 CFNetwork/3860.400.51 Darwin/25.3.0', platform: '', maxTouchPoints: 0, mobile: false }, + { name: 'unknown UA', userAgent: 'Unrecognized/1.0', platform: '', maxTouchPoints: 0, mobile: false }, + ])('classifies $name', async ({ userAgent, platform, maxTouchPoints, mobile }) => { + for (const [name, value] of Object.entries({ userAgent, platform, maxTouchPoints })) { + const descriptor = Object.getOwnPropertyDescriptor(navigator, name) + Object.defineProperty(navigator, name, { configurable: true, value }) + onTestFinished(() => { + if (descriptor === undefined) Reflect.deleteProperty(navigator, name) + else Object.defineProperty(navigator, name, descriptor) + }) + } + const { isMobileDevice } = await import('../src/focus.ts') + expect(isMobileDevice()).toBe(mobile) + }) + + it.each([true, false])('shares a cached %s result between callers until the module reloads', async (mobile) => { + const mobileUA = 'Mozilla/5.0 (iPhone)' + const desktopUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' + const ua = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(mobile ? mobileUA : desktopUA) + onTestFinished(() => { ua.mockRestore() }) + const first = await import('../src/focus.ts') + expect(first.isMobileDevice()).toBe(mobile) + + ua.mockReturnValue(mobile ? desktopUA : mobileUA) + const other = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + expect(other.isMobileDevice()).toBe(mobile) + expect(ua).toHaveBeenCalledTimes(1) + + vi.resetModules() + const reloaded = await import('../src/focus.ts') + expect(reloaded.isMobileDevice()).toBe(!mobile) + expect(ua).toHaveBeenCalledTimes(2) + }) +}) + +it('retains automatic focus styling while typing and restores it for keyboard navigation', () => { + render() + const button = screen.getByRole('button') + focusWithoutRing(button) + fireEvent.keyDown(button, { key: 'a' }) + expect(button.hasAttribute('data-dsh-automatic-focus')).toBe(true) + fireEvent.keyDown(button, { key: 'ArrowDown' }) + expect(button.hasAttribute('data-dsh-automatic-focus')).toBe(false) +}) + describe('Button', () => { it('exposes its native button for focus and releases the ref on unmount', () => { const ref = createRef() @@ -99,6 +157,21 @@ describe('Input', () => { }) }) +describe('MenuItemButton', () => { + it('exposes the current page without preventing activation and clears it when selection moves', () => { + const onSelect = vi.fn() + const { rerender } = render(Notes) + const row = screen.getByRole('menuitem', { name: 'Notes' }) + expect(row.getAttribute('aria-current')).toBe('page') + fireEvent.click(row) + expect(onSelect).toHaveBeenCalledOnce() + rerender(Notes) + expect(row.hasAttribute('aria-current')).toBe(false) + rerender(Notes) + expect(row.hasAttribute('aria-current')).toBe(false) + }) +}) + describe('Menu', () => { const items = [ { id: 'a', label: 'Alpha' }, diff --git a/packages/client/ui-primitives/tests/go-highlighting.client.spec.ts b/packages/client/ui-primitives/tests/go-highlighting.client.spec.ts new file mode 100644 index 0000000000..bd18f005ad --- /dev/null +++ b/packages/client/ui-primitives/tests/go-highlighting.client.spec.ts @@ -0,0 +1,72 @@ +/** Go regex matching runs in a terminable source-mode worker, not on the test runner's event loop. */ +import { Worker } from 'node:worker_threads' +import { expect, it } from 'vitest' +import { patchGoGrammar } from '../src/markdown/highlight.ts' +import { GO_SOURCE } from './go-source.ts' + +it('highlights struct comments with braces without blocking settled or streaming code', async () => { + // The worker and highlight.ts use erasable syntax and only package-name imports, so Node loads them directly. + const worker = new Worker(new URL('./go-highlighting.worker.ts', import.meta.url)) + let timer: ReturnType | undefined + try { + const result = await new Promise((resolve, reject) => { + const fail = (message: string): void => { reject(new Error(message)) } + timer = setTimeout(() => { fail('Go grammar did not load within 120 seconds') }, 120_000) + worker.on('message', (reply: unknown) => { + if (reply !== 'loaded') { + resolve(reply) + return + } + clearTimeout(timer) + // Bound runaway synchronous matching once imports and grammar loading are done. + timer = setTimeout(() => { fail('Go highlighting did not complete within 10 seconds') }, 10_000) + }) + worker.once('error', reject) + worker.once('exit', (code) => { fail(`Go highlighting worker exited before its reply: ${code}`) }) + }) + expect(result).toMatchObject({ text: GO_SOURCE, streamingMatchesSettled: true }) + expect(result).toHaveProperty('html', expect.stringContaining('var(--shiki-')) + const keyword = (...texts: string[]): string[] => texts.map(text => `${text}:keyword`) + expect(result).toHaveProperty('colored', [ + ['package:keyword', 'example:function'], + [], + ['type:keyword', 'Record:function', 'struct:keyword'], + ['// docs: {id} explains the ordinary field names for an example record ending here:comment'], + ['int:keyword'], + [], + [], + ['type:keyword', 'Inline:function', ...keyword('struct', 'string', 'int')], + ['type:keyword', 'Nested:function', ...keyword('struct', 'map', 'string', 'struct', 'struct', 'int')], + ['type:keyword', 'OneLineFields:function', ...keyword('struct', 'int', 'chan<-', 'struct', 'string'), + 'sync:function', 'Mutex:function'], + ['type:keyword', 'SplitOneLine:function', ...keyword('struct', 'int'), 'B:function', 'string:keyword'], + [], + [...keyword('var', '=', 'struct', 'string'), 'Port:function', 'int:keyword', 'Tags:function', 'string:keyword'], + [], + ['type:keyword', 'Many:function', ...keyword('struct', ...Array(24).fill('int'), 'struct', 'int')], + ['type:keyword', 'Open:function', 'struct:keyword', ...Array(47).fill('field:function')], + ]) + } finally { + clearTimeout(timer) + await worker.terminate() + } +}, 150_000) + +it('patches exactly the vendored struct-field rule and rejects a grammar without it', async () => { + const { default: grammars } = await import('@shikijs/langs/go') + const other = { name: 'go-embedded', scopeName: 'source.go.embedded', patterns: [], repository: {} } + const before = grammars[0]?.repository.struct_variables_types_fields?.patterns + const patched = patchGoGrammar([...grammars, other]) + const after = patched[0]?.repository.struct_variables_types_fields?.patterns + const changed = after?.flatMap((rule, index) => rule === before?.[index] ? [] : [[before?.[index]?.match, rule.match]]) + expect(changed).toEqual([[ + expect.stringContaining('[^"/`\\s]+;?'), + '(?:(?<=\\{)([^/\\`"]*[^\\s/\\`"])\\s*(?=\\}))', + ]]) + expect(patched[1]).toBe(other) + for (const repository of [{}, { struct_variables_types_fields: {} }]) { + expect(() => patchGoGrammar([{ name: 'go', scopeName: 'source.go', patterns: [], repository }])) + .toThrow('expected exactly one Go struct-field rule to patch for worlpaker/go-syntax#25, found 0') + } + expect(() => patchGoGrammar([other])).toThrow('found 0') +}) diff --git a/packages/client/ui-primitives/tests/go-highlighting.worker.ts b/packages/client/ui-primitives/tests/go-highlighting.worker.ts new file mode 100644 index 0000000000..a1b65fe67d --- /dev/null +++ b/packages/client/ui-primitives/tests/go-highlighting.worker.ts @@ -0,0 +1,31 @@ +/** Source-mode highlighter fixture; the parent owns termination during synchronous regex matching. */ +import { parentPort } from 'node:worker_threads' +import { + highlightLines, highlightToHtml, StreamingHighlightSession, subscribeGrammarLoaded, +} from '../src/markdown/highlight.ts' +import { GO_SOURCE } from './go-source.ts' + +const ready = Promise.withResolvers() +const dispose = subscribeGrammarLoaded(() => { + if (highlightLines('package example', 'go') !== undefined) ready.resolve(undefined) +}) +try { + if (highlightLines('package example', 'go') === undefined) await ready.promise +} finally { + dispose() +} +// The parent times matching separately from imports and grammar loading. +parentPort?.postMessage('loaded') +const lines = highlightLines(GO_SOURCE, 'go') +const stream = new StreamingHighlightSession() +const split = GO_SOURCE.indexOf('\t//') +if (split <= 0) throw new Error('GO_SOURCE no longer contains the brace-bearing struct comment') +stream.update(GO_SOURCE.slice(0, split), 'go') +const streamed = stream.update(GO_SOURCE, 'go') +parentPort?.postMessage({ + text: lines?.map(line => line.map(span => span.text).join('')).join('\n'), + colored: lines?.map(line => line.flatMap(span => + span.style.color === 'var(--shiki-foreground)' ? [] : [`${span.text}:${String(span.style.color).replace('var(--shiki-token-', '').replace(')', '')}`])), + streamingMatchesSettled: JSON.stringify(streamed) === JSON.stringify(new StreamingHighlightSession().update(GO_SOURCE, 'go')), + html: highlightToHtml(GO_SOURCE, 'go'), +}) diff --git a/packages/client/ui-primitives/tests/go-source.ts b/packages/client/ui-primitives/tests/go-source.ts new file mode 100644 index 0000000000..62ac1b74ee --- /dev/null +++ b/packages/client/ui-primitives/tests/go-source.ts @@ -0,0 +1,21 @@ +/** Synthetic Go struct comments and declarations for syntax-highlighting regressions. */ +export const GO_SOURCE = [ + 'package example', + '', + 'type Record struct {', + '\t// docs: {id} explains the ordinary field names for an example record ending here', + '\tValue int', + '}', + '', + 'type Inline struct { Name string; Count int }', + 'type Nested struct { Flags map[string]struct{}; Child struct { Value int } }', + // Shapes from worlpaker/go-syntax#25's test/semantic_tokens.go and test/stress.go. + 'type OneLineFields struct{ A, B int; C chan<- struct{}; D []string; sync.Mutex }', + 'type SplitOneLine struct { A int; B string', + '}', + 'var stressFoo14 = struct{ Host string; Port int; Tags []string', + '}{}', + `type Many struct { ${Array.from({ length: 24 }, (_, index) => `F${index} int;`).join(' ')} Child struct { X int } }`, + // Last: its unclosed brace keeps the struct body open to the end of the source. + `type Open struct { ${'field '.repeat(48)}`, +].join('\n') diff --git a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx index 0f8a67c08b..ddc6718159 100644 --- a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx +++ b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx @@ -243,7 +243,7 @@ describe('HoverCard', () => { expect(vi.getTimerCount()).toBe(0) }) - it('keeps previews below the desktop frame clearance', () => { + it('keeps previews clear of the frame top strip except below an anchor inside it', () => { const prior = document.documentElement.style.getPropertyValue('--dsh-frame-top-clearance') const priority = document.documentElement.style.getPropertyPriority('--dsh-frame-top-clearance') document.documentElement.style.setProperty('--dsh-frame-top-clearance', '100px') @@ -258,6 +258,10 @@ describe('HoverCard', () => { fireEvent(window, new Event('resize')) expect(card.style.top).toBe('120px') expect(card.style.maxHeight).toBe('322px') + // An anchor inside the top strip keeps its preview directly below it. + wrapper.getBoundingClientRect = () => DOMRect.fromRect({ x: 100, y: 10, width: 600, height: 32 }) + fireEvent(window, new Event('resize')) + expect(card.style.top).toBe('50px') } finally { if (prior === '') document.documentElement.style.removeProperty('--dsh-frame-top-clearance') else document.documentElement.style.setProperty('--dsh-frame-top-clearance', prior, priority) diff --git a/packages/client/ui-primitives/tests/icons.client.spec.tsx b/packages/client/ui-primitives/tests/icons.client.spec.tsx index 97bc3cc02c..a5250f1126 100644 --- a/packages/client/ui-primitives/tests/icons.client.spec.tsx +++ b/packages/client/ui-primitives/tests/icons.client.spec.tsx @@ -4,8 +4,12 @@ import { afterEach, describe, expect, it } from 'vitest' import * as primitives from '@deepseek-ai/dsh-client-ui-primitives' import { IconAlarmClockOutlineRegular, IconApiOutlineRegular, IconArchiveOutlineRegular, IconFolderCloseRegular, - IconGoalOutlineRegular, IconSendOutlineRegular, + IconAppsAddOutlineMedium, IconGoalOutlineRegular, IconSendOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' +import { ICON_ARTWORK, type IconGlyph, type IconWeight } from '../src/icons/artwork.ts' +import { + BrowseOutlineArtwork, ChatLinesOutlineArtwork, CodeBracketsArtwork, FolderCloseArtwork, GlobeOutlineArtwork, +} from '../src/icons/shared-artwork.tsx' afterEach(cleanup) @@ -15,6 +19,12 @@ const icons = Object.fromEntries( Object.entries(primitives).filter(([name]) => name.startsWith('Icon')), ) as Record React.JSX.Element> const iconNames = Object.keys(icons) +const glyphs = Object.keys(ICON_ARTWORK) as IconGlyph[] + +function artwork(glyph: IconGlyph, weight: IconWeight): Document { + const stroke = weight === 'regular' ? primitives.ICON_REGULAR_STROKE : primitives.ICON_MEDIUM_STROKE + return new DOMParser().parseFromString(ICON_ARTWORK[glyph].svg(stroke), 'image/svg+xml') +} describe('product icon set', () => { it('exports regular and medium variants for all 96 public glyphs', () => { @@ -33,33 +43,75 @@ describe('product icon set', () => { it('draws the circled close as one currentColor knockout path in both weights', () => { // A filled disc with the cross cut out of it (even-odd), so the cross shows // the surface behind the glyph on any background instead of a second color. - for (const Icon of [primitives.IconCloseCircleFillRegular, primitives.IconCloseCircleFillMedium]) { - const { container, unmount } = render() - const paths = container.querySelectorAll('path') + for (const weight of ['regular', 'medium'] as const) { + const paths = artwork('close-circle-fill', weight).querySelectorAll('path') expect(paths).toHaveLength(1) expect(paths[0]!.getAttribute('fill')).toBe('currentColor') expect(paths[0]!.getAttribute('fill-rule')).toBe('evenodd') expect(paths[0]!.getAttribute('stroke')).toBeNull() - expect(container.querySelector('svg')!.getAttribute('width')).toBe('16') - unmount() } + expect(render().container.querySelector('svg')!.getAttribute('width')).toBe('16') }) it('exports the shield contour and regular stroke for composite glyphs', () => { - const { container } = render() - expect(container.querySelector('path')?.getAttribute('d')).toBe(primitives.SHIELD_OUTLINE_PATH) - expect(container.querySelector('svg')?.getAttribute('stroke-width')).toBe(String(primitives.ICON_REGULAR_STROKE)) + const shield = artwork('shield-outline', 'regular') + expect(shield.querySelector('path')?.getAttribute('d')).toBe(primitives.SHIELD_OUTLINE_PATH) + expect(shield.documentElement.getAttribute('stroke-width')).toBe(String(primitives.ICON_REGULAR_STROKE)) }) - it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => { + it.each(iconNames)('%s references its sprite symbol', (name) => { const Icon = icons[name]! const { container } = render() - const svg = container.querySelector('svg') - expect(svg).not.toBeNull() - expect(svg?.getAttribute('aria-hidden')).toBe('true') - const markup = container.innerHTML - expect(markup).not.toMatch(/#[0-9a-fA-F]{3,8}"/) - expect(markup).toContain('currentColor') + const svg = container.querySelector('svg')! + expect(svg.getAttribute('aria-hidden')).toBe('true') + const id = svg.getAttribute('data-icon')! + expect([...svg.children].map(child => [child.localName, child.getAttribute('href')])).toEqual([['use', `#${id}`]]) + expect(Object.hasOwn(primitives.ICON_SYMBOLS, id)).toBe(true) + }) + + it.each(glyphs)('%s artwork is a standalone currentColor SVG without a hardcoded palette', (glyph) => { + for (const weight of ['regular', 'medium'] as const) { + const markup = ICON_ARTWORK[glyph].svg(weight === 'regular' ? primitives.ICON_REGULAR_STROKE : primitives.ICON_MEDIUM_STROKE) + expect(markup).toMatch(/^ { + expect(Object.keys(primitives.ICON_SYMBOLS)).toHaveLength(2 * glyphs.length) + expect(primitives.ICON_SYMBOLS['dsw-icon-search-outline-medium']).toBe(ICON_ARTWORK['search-outline'].svg(primitives.ICON_MEDIUM_STROKE)) + }) + + it('keeps element ids unique across the default symbols', () => { + const ids = Object.values(primitives.ICON_SYMBOLS).flatMap(markup => [...markup.matchAll(/\sid="([^"]+)"/g)].map(([, id]) => id)) + expect(new Set(ids).size).toBe(ids.length) + }) + + it.each(iconNames)('%s references the glyph and weight its name spells', (name) => { + const weight = name.endsWith('Regular') ? 'regular' : 'medium' + const glyph = name.slice('Icon'.length, -weight.length).replace(/(?).container.querySelector('svg')!.getAttribute('data-icon')).toBe(`dsw-icon-${glyph}-${weight}`) + }) + + it.each([ + ['queue-outline', ChatLinesOutlineArtwork], + ['globe-outline', GlobeOutlineArtwork], + ['inspect-outline', CodeBracketsArtwork], + ['browse-outline', BrowseOutlineArtwork], + ['folder-close', FolderCloseArtwork], + ] as const)('keeps the shared %s artwork identical to its product glyph', (glyph, Artwork) => { + for (const weight of ['regular', 'medium'] as const) { + const stroke = weight === 'regular' ? primitives.ICON_REGULAR_STROKE : primitives.ICON_MEDIUM_STROKE + const inline = render().container.querySelector('svg')! + const product = artwork(glyph, weight).documentElement + expect(inline.getAttribute('viewBox')).toBe(product.getAttribute('viewBox')) + expect(inline.children.length).toBe(product.children.length) + expect([...inline.children].every((child, index) => child.isEqualNode(product.children[index]!))).toBe(true) + cleanup() + } }) it('size and className props land on the root svg', () => { @@ -70,11 +122,14 @@ describe('product icon set', () => { expect(svg.classList.contains('x')).toBe(true) }) + it('keeps a non-square glyph at its drawn ratio', () => { + const svg = render().container.querySelector('svg')! + expect([svg.getAttribute('width'), svg.getAttribute('height')]).toEqual(['16', '20']) + }) + it('uses 1px regular and 1.3px medium strokes', () => { - const regular = render() - expect(regular.container.querySelector('svg')?.getAttribute('stroke-width')).toBe('1') - const medium = render() - expect(medium.container.querySelector('svg')?.getAttribute('stroke-width')).toBe('1.3') + expect(artwork('search-outline', 'regular').documentElement.getAttribute('stroke-width')).toBe('1') + expect(artwork('search-outline', 'medium').documentElement.getAttribute('stroke-width')).toBe('1.3') }) it('exports regular and medium permission glyphs', () => { @@ -123,15 +178,15 @@ describe('product icon set', () => { }) it('draws the apps-add tiles closer together in the compact variant', () => { - const tiles = (compact: boolean) => { - const { container, unmount } = render() - const result = [...container.querySelectorAll('rect')].map(rect => `${rect.getAttribute('x')},${rect.getAttribute('y')}`) - const plus = container.querySelector('path')?.getAttribute('d') - unmount() - return { result, plus } + const tiles = (glyph: IconGlyph) => { + const doc = artwork(glyph, 'regular') + const result = [...doc.querySelectorAll('rect')].map(rect => `${rect.getAttribute('x')},${rect.getAttribute('y')}`) + return { result, plus: doc.querySelector('path')?.getAttribute('d') } } - expect(tiles(false)).toEqual({ result: ['2.25,2.25', '2.25,9.75', '9.75,9.75'], plus: 'M11.75 1.75v5M9.25 4.25h5' }) - expect(tiles(true)).toEqual({ result: ['2.75,2.75', '2.75,9.25', '9.25,9.25'], plus: 'M11.25 2.25v5M8.75 4.75h5' }) + expect(tiles('apps-add-outline')).toEqual({ result: ['2.25,2.25', '2.25,9.75', '9.75,9.75'], plus: 'M11.75 1.75v5M9.25 4.25h5' }) + expect(tiles('apps-add-compact-outline')).toEqual({ result: ['2.75,2.75', '2.75,9.25', '9.25,9.25'], plus: 'M11.25 2.25v5M8.75 4.75h5' }) + const glyphOf = (compact: boolean) => render().container.querySelector('svg')!.getAttribute('data-icon') + expect([glyphOf(false), glyphOf(true)]).toEqual(['dsw-icon-apps-add-outline-medium', 'dsw-icon-apps-add-compact-outline-medium']) }) }) diff --git a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx index c6d36335ba..59e7df2782 100644 --- a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx @@ -31,18 +31,19 @@ it('loads Desktop inline and hover previews through the file route after streami const view = render(content(true)) expect(view.container.querySelector('img')).toBeNull() view.rerender(content(false)) - expect(screen.getByRole('img').getAttribute('src')).toBe(src) + const inline = screen.getByRole('img').getAttribute('src') + expect(inline?.replace(/&load=\d+$/u, '')).toBe(src) const trigger = screen.getByRole('button', { name: 'View full image: 图片' }) trigger.focus() fireEvent.click(trigger) - expect(screen.getByRole('dialog').querySelector('img')?.getAttribute('src')).toBe(src) + expect(screen.getByRole('dialog').querySelector('img')?.getAttribute('src')).toBe(inline) fireEvent.keyDown(window, { key: 'Escape' }) expect(screen.queryByRole('dialog')).toBeNull() expect(document.activeElement).toBe(trigger) const link = screen.getByRole('button', { name: '打开图片' }) fireEvent.pointerEnter(link.parentElement!) act(() => { vi.advanceTimersByTime(500) }) - expect(screen.getByAltText(path).getAttribute('src')).toBe(src) + expect(screen.getByAltText(path).getAttribute('src')?.replace(/&load=\d+$/u, '')).toBe(src) fireEvent.error(screen.getByAltText(path)) expect(screen.getByText(labels.failed)).toBeTruthy() fireEvent.click(link) @@ -144,6 +145,13 @@ it('keeps an unavailable image link navigable without showing a hover preview', expect(openFile).toHaveBeenCalledWith('relative.png', undefined) }) +it('emits an unparsable file image URL unchanged', () => { + render( 'relative.png' }}> + + ) + expect(screen.getByRole('img').getAttribute('src')).toBe('relative.png') +}) + it('labels empty-alt image activation and retains a localized failure description', () => { mount('![](image.png)') expect(screen.getByRole('button', { name: 'View full image' })).toBeTruthy() diff --git a/packages/client/ui-primitives/tests/markdown-path-images.client.spec.tsx b/packages/client/ui-primitives/tests/markdown-path-images.client.spec.tsx index 11aa179572..a7b2238e96 100644 --- a/packages/client/ui-primitives/tests/markdown-path-images.client.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown-path-images.client.spec.tsx @@ -8,6 +8,10 @@ afterEach(cleanup) const LOCAL_IMAGE = '![diagram](/tmp/graph.png)' +/** Drop the per-mount load tag to compare against the resolver's URL. */ +const resolverSrc = (image: Element | null | undefined): string | undefined => + image?.getAttribute('src')?.replace(/[?&]load=\d+$/u, '') + const mapping = (value: string): string | undefined => value === '/tmp/graph.png' ? 'https://cdn.example.com/graph.png' : undefined @@ -43,7 +47,7 @@ describe('MarkdownText local-path images', () => { ) fireEvent.error(screen.getByRole('img')) rerender() - expect(container.querySelector('img')?.getAttribute('src')) + expect(resolverSrc(container.querySelector('img'))) .toBe('https://example.com/tmp/replacement.png') }) @@ -79,7 +83,7 @@ describe('MarkdownText local-path images', () => { const view = render( src }} />) expect(view.container.querySelector('img')).toBeNull() view.rerender( src }} />) - expect(view.getByRole('img').getAttribute('src')).toBe(src) + expect(resolverSrc(view.getByRole('img'))).toBe(src) const detail = 'dsh-app://app/api/plugin-market/detail-file?document=d&path=a.png' const bundle = 'dsh-app://app/api/plugin-manager/detail-file?bundle=b&document=README.md&path=a.png' for (const route of [detail, bundle]) { @@ -96,6 +100,19 @@ describe('MarkdownText local-path images', () => { expect(view.container.querySelector('img')).toBeNull() }) + it('gives each newly mounted image of one path its own source and keeps it across rerenders', () => { + const pathImages: MarkdownPathImages = { resolve: () => 'https://example.com/api/file?path=%2Ftmp%2Fgraph.png' } + const first = render() + const src = first.container.querySelector('img')?.getAttribute('src') + expect(src).toMatch(/^https:\/\/example\.com\/api\/file\?path=%2Ftmp%2Fgraph\.png&load=\d+$/u) + first.rerender() + expect(first.container.querySelector('img')?.getAttribute('src')).toBe(src) + const second = render() + expect(second.container.querySelector('img')?.getAttribute('src')).not.toBe(src) + const bare = render( 'https://example.com/api/file' }} />) + expect(bare.container.querySelector('img')?.getAttribute('src')).toMatch(/^https:\/\/example\.com\/api\/file\?load=\d+$/u) + }) + it('leaves remote images untouched even when the vocabulary maps them', () => { const remote = '![remote](https://example.com/a.png)' const pathImages: MarkdownPathImages = { resolve: () => 'https://cdn.example.com/b.png' } @@ -107,7 +124,7 @@ describe('MarkdownText local-path images', () => { const reference = ['![diagram][fig]', '', '[fig]: /tmp/graph.png'].join('\n') const pathImages: MarkdownPathImages = { resolve: mapping } const { container } = render() - expect(container.querySelector('img')?.getAttribute('src')) + expect(resolverSrc(container.querySelector('img'))) .toBe('https://cdn.example.com/graph.png') }) @@ -120,7 +137,7 @@ describe('MarkdownText local-path images', () => { // fallback until the settled pass self-heals it. expect(container.querySelector('img')).toBeNull() rerender() - expect(container.querySelector('img')?.getAttribute('src')) + expect(resolverSrc(container.querySelector('img'))) .toBe('https://cdn.example.com/graph.png') }) }) diff --git a/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx b/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx new file mode 100644 index 0000000000..6e1700e7f9 --- /dev/null +++ b/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx @@ -0,0 +1,55 @@ +// @vitest-environment jsdom +/** Portal menus keep the desktop frame's top clearance without leaving a gap below an anchor inside it. */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, render } from '@testing-library/react' +import { Menu } from '../src/Menu.tsx' + +const root = document.documentElement +let prior = '' + +beforeEach(() => { + prior = root.style.getPropertyValue('--dsh-frame-top-clearance') + root.style.setProperty('--dsh-frame-top-clearance', '48px') +}) + +afterEach(() => { + cleanup() + vi.restoreAllMocks() + if (prior === '') root.style.removeProperty('--dsh-frame-top-clearance') + else root.style.setProperty('--dsh-frame-top-clearance', prior) +}) + +/** + * Open an 80px-tall portal menu from an anchor with the stated rect. + * @param anchor - the anchor rect in viewport coordinates. + * @param side - which anchor edge the menu opens from. + * @returns the render result. + */ +function openMenu(anchor: DOMRect, side: 'top' | 'bottom') { + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor) + vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(120) + vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(80) + return render(Open} items={[{ id: 'one', label: 'One' }]} onClose={vi.fn()} />) +} + +describe('Menu top clearance', () => { + it('opens directly below an anchor that sits inside the top strip', () => { + const view = openMenu(new DOMRect(300, 14, 18, 22), 'bottom') + expect(view.getByRole('menu').style.top).toBe('40px') + }) + + it('keeps a menu that does not fit above its anchor below the top strip', () => { + const view = openMenu(new DOMRect(300, 100, 18, 22), 'top') + expect(view.getByRole('menu').style.top).toBe('68px') + }) + + it('places a menu that does not fit above an anchor inside the top strip directly below it', () => { + const view = openMenu(new DOMRect(300, 14, 18, 22), 'top') + expect(view.getByRole('menu').style.top).toBe('40px') + }) + + it('keeps the top strip clear when the anchor has scrolled above the viewport', () => { + const view = openMenu(new DOMRect(300, -30, 18, 22), 'bottom') + expect(view.getByRole('menu').style.top).toBe('68px') + }) +}) diff --git a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx index e4b7891e30..b2e445e080 100644 --- a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx +++ b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx @@ -765,6 +765,30 @@ describe('Tooltip', () => { expect(screen.getByRole('tooltip')).toBeTruthy() }) + it('shows a clipped-only bubble when the anchor or a descendant overflows at show time', () => { + render( + + + , + ) + const anchor = screen.getByRole('button') + const label = screen.getByText('Full…') + const overflowing = new Set() + vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockImplementation(function (this: Element) { return overflowing.has(this) ? 120 : 80 }) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + fireEvent.pointerEnter(anchor) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.pointerLeave(anchor) + overflowing.add(label) + fireEvent.pointerEnter(anchor) + expect(screen.getByRole('tooltip').textContent).toBe('Full label') + fireEvent.pointerLeave(anchor) + overflowing.clear() + overflowing.add(anchor) + fireEvent.pointerEnter(anchor) + expect(screen.getByRole('tooltip').textContent).toBe('Full label') + }) + it('mouse leave hides the bubble immediately, even while the anchor stays focused', () => { render( diff --git a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx index a3083af168..5dfb8d1e7e 100644 --- a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx +++ b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx @@ -51,10 +51,12 @@ function stubResizeObserver(): Recorded[] { * @param props - whether the panel is open. * @returns the anchor and, while open, the panel carrying the position. */ -function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) { +function Host({ open, align, side }: { open: boolean; align?: 'start' | 'end'; side?: 'top' | 'bottom' }) { const anchorRef = useRef(null) const panelRef = useRef(null) - const position = useAnchoredPosition({ open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align } }) + const position = useAnchoredPosition({ + open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align }, ...side === undefined ? {} : { side }, + }) return ( <> @@ -64,7 +66,11 @@ function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) { } describe('useAnchoredPosition', () => { - it.each([{ fullscreen: false, top: 60 }, { fullscreen: true, top: 20 }])('keeps an anchored panel at $top with fullscreen=$fullscreen', ({ fullscreen, top }) => { + it.each([ + { name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: false, top: 60 }, + { name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: true, top: 20 }, + { name: 'a panel below an anchor inside the top strip', side: 'bottom', anchor: new DOMRect(100, 0, 200, 10), fullscreen: false, top: 14 }, + ] as const)('keeps $name at $top with fullscreen=$fullscreen', ({ side, anchor, fullscreen, top }) => { const root = document.documentElement const previous = root.getAttribute('style') const priorFullscreen = root.getAttribute('data-fullscreen') @@ -77,10 +83,10 @@ describe('useAnchoredPosition', () => { }) root.style.setProperty('--dsh-frame-top-clearance', '40px') root.toggleAttribute('data-fullscreen', fullscreen) - vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 0, 200, 10)) + vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor) vi.spyOn(HTMLDivElement.prototype, 'offsetHeight', 'get').mockReturnValue(400) vi.stubGlobal('innerHeight', 600) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('panel').style.top).toBe(`${top}px`) }) diff --git a/packages/client/ui-reference/locale/de.json b/packages/client/ui-reference/locale/de.json new file mode 100644 index 0000000000..caffa08eac --- /dev/null +++ b/packages/client/ui-reference/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Datei- und Sitzungsreferenzschnittstelle", + "description": "Verweisen Sie auf Dateien oder andere Sitzungen über @ im Eingabefeld." + } +} diff --git a/packages/client/ui-reference/locale/es.json b/packages/client/ui-reference/locale/es.json new file mode 100644 index 0000000000..c82ce82b70 --- /dev/null +++ b/packages/client/ui-reference/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de referencia de archivos y sesiones", + "description": "Archivos de referencia u otras sesiones a través de @ en el cuadro de entrada." + } +} diff --git a/packages/client/ui-reference/locale/fr.json b/packages/client/ui-reference/locale/fr.json new file mode 100644 index 0000000000..392efe7eb4 --- /dev/null +++ b/packages/client/ui-reference/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de référence de fichiers et de sessions", + "description": "Référencez des fichiers ou d'autres sessions via @ dans la zone de saisie." + } +} diff --git a/packages/client/ui-reference/locale/pt.json b/packages/client/ui-reference/locale/pt.json new file mode 100644 index 0000000000..25bd646b1e --- /dev/null +++ b/packages/client/ui-reference/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de referência de arquivo e sessão", + "description": "Faça referência a arquivos ou outras sessões via @ na caixa de entrada." + } +} diff --git a/packages/client/ui-reference/locale/ru.json b/packages/client/ui-reference/locale/ru.json new file mode 100644 index 0000000000..80c1d5d22f --- /dev/null +++ b/packages/client/ui-reference/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Справочный интерфейс файлов и сеансов", + "description": "Ссылайтесь на файлы или другие сеансы через @ в поле ввода." + } +} diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index ebd3bb8842..e9308929ed 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-reference", "description": "Unified Web @file and @session reference source", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-reference/src/client/index.ts b/packages/client/ui-reference/src/client/index.ts index 4cfd6e8677..31e06bb0b8 100644 --- a/packages/client/ui-reference/src/client/index.ts +++ b/packages/client/ui-reference/src/client/index.ts @@ -33,7 +33,7 @@ import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar' import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types' import { abbreviateHomePath, fileAddressFor } from '@deepseek-ai/dsh-util-workspace-path' -import { en, ja, NS, zh, type ReferenceKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ReferenceKey } from './locales.ts' declare module '@deepseek-ai/dsh-api-session-controller/client' { interface SessionReferenceSourceMap { @@ -53,7 +53,7 @@ export const inject = [ * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-reference: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-reference: dictionaries') const t = ctx.locale.bind(NS) const sessions = ctx.get('sessions') as ISessions ctx.uiConversation.userText.register(fileReferenceRule) diff --git a/packages/client/ui-reference/src/client/locales.ts b/packages/client/ui-reference/src/client/locales.ts index af8e37175a..a742c82eca 100644 --- a/packages/client/ui-reference/src/client/locales.ts +++ b/packages/client/ui-reference/src/client/locales.ts @@ -77,3 +77,98 @@ export const ja = { 'time.months': '{n}か月', 'time.years': '{n}年', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'section.files': 'Archivos y carpetas', + 'section.subagents': 'Subagentes', + 'section.sessions': 'Sesiones', + 'candidate.noCwd': '(sin cwd)', + 'crumb.root': 'Espacio de trabajo', + 'fileOpen.title': 'No se pudo abrir el archivo', + 'fileOpen.failed': 'No se pudo abrir "{path}". Inténtalo de nuevo o cierra este mensaje.', + 'fileOpen.close': 'Cerrar', + 'fileOpen.retry': 'Reintentar', + 'time.now': 'ahora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'section.files': 'Fichiers et dossiers', + 'section.subagents': 'Sous-agents', + 'section.sessions': 'Séances', + 'candidate.noCwd': '(pas de cwd)', + 'crumb.root': 'Espace de travail', + 'fileOpen.title': "Impossible d'ouvrir le fichier", + 'fileOpen.failed': "Impossible d'ouvrir \"{path}\". Réessayez ou fermez ce message.", + 'fileOpen.close': 'Fermer', + 'fileOpen.retry': 'Réessayer', + 'time.now': 'maintenant', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'section.files': 'Dateien und Ordner', + 'section.subagents': 'Subagenten', + 'section.sessions': 'Sitzungen', + 'candidate.noCwd': '(kein CWD)', + 'crumb.root': 'Arbeitsbereich', + 'fileOpen.title': 'Datei konnte nicht geöffnet werden', + 'fileOpen.failed': '„{path}“ konnte nicht geöffnet werden. Versuchen Sie es erneut oder schließen Sie diese Nachricht.', + 'fileOpen.close': 'Schließen', + 'fileOpen.retry': 'Erneut versuchen', + 'time.now': 'jetzt', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'section.files': 'Файлы и папки', + 'section.subagents': 'Субагенты', + 'section.sessions': 'Сессии', + 'candidate.noCwd': '(без КДВ)', + 'crumb.root': 'Рабочая область', + 'fileOpen.title': 'Не удалось открыть файл', + 'fileOpen.failed': 'Не удалось открыть «{path}». Попробуйте еще раз или закройте это сообщение.', + 'fileOpen.close': 'Закрыть', + 'fileOpen.retry': 'Повторить', + 'time.now': 'сейчас', + 'time.minutes': '{n}мин', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'section.files': 'Arquivos e pastas', + 'section.subagents': 'Subagentes', + 'section.sessions': 'Sessões', + 'candidate.noCwd': '(sem cwd)', + 'crumb.root': 'Espaço de trabalho', + 'fileOpen.title': 'Não foi possível abrir o arquivo', + 'fileOpen.failed': 'Não foi possível abrir “{path}”. Tente novamente ou feche esta mensagem.', + 'fileOpen.close': 'Fechar', + 'fileOpen.retry': 'Tentar novamente', + 'time.now': 'agora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n} mês', + 'time.years': '{n}s', +} satisfies Record diff --git a/packages/client/ui-renderer/locale/de.json b/packages/client/ui-renderer/locale/de.json new file mode 100644 index 0000000000..d913a630cf --- /dev/null +++ b/packages/client/ui-renderer/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstellen-Rendering", + "description": "Integrieren Sie die von jedem Plug-In bereitgestellten Schnittstellenkomponenten in die Anwendung." + } +} diff --git a/packages/client/ui-renderer/locale/es.json b/packages/client/ui-renderer/locale/es.json new file mode 100644 index 0000000000..74b778c2c5 --- /dev/null +++ b/packages/client/ui-renderer/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Representación de interfaz", + "description": "Ensamble los componentes de la interfaz proporcionados por cada complemento en la aplicación." + } +} diff --git a/packages/client/ui-renderer/locale/fr.json b/packages/client/ui-renderer/locale/fr.json new file mode 100644 index 0000000000..40968cbcdf --- /dev/null +++ b/packages/client/ui-renderer/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Rendu d'interface", + "description": "Assemblez les composants d’interface fournis par chaque plug-in dans l’application." + } +} diff --git a/packages/client/ui-renderer/locale/pt.json b/packages/client/ui-renderer/locale/pt.json new file mode 100644 index 0000000000..b5d9f75753 --- /dev/null +++ b/packages/client/ui-renderer/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Renderização de interface", + "description": "Monte os componentes de interface fornecidos por cada plug-in no aplicativo." + } +} diff --git a/packages/client/ui-renderer/locale/ru.json b/packages/client/ui-renderer/locale/ru.json new file mode 100644 index 0000000000..09d2f66a5b --- /dev/null +++ b/packages/client/ui-renderer/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Рендеринг интерфейса", + "description": "Соберите в приложение компоненты интерфейса, предоставляемые каждым плагином." + } +} diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index c7e8e86283..5617eeafb0 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-renderer", "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index f21ddb5997..6af57d26c8 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -10,19 +10,19 @@ zh: eb3e5d5a802da592 /deepseek-ai-dsh-client-ui-schedule/summary: en: 8dfc371c14bb4415 - zh: 9b3545c1a7468a7a + zh: 5e2fa588983524ed /deepseek-ai-dsh-client-ui-schedule/table-of-contents: en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: 4c0dca55e4e9866d - zh: 9ee6b565c7a81353 + en: fccffd577fcf93f2 + zh: 4719236f7d89e0a8 /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 47cd16c142e82385 zh: 181849a3c7a167fd /deepseek-ai-dsh-client-ui-schedule/understand-the-implementation: en: 959145f9a08c3913 - zh: caae642a82d2acd3 + zh: 7c0afb742790c474 /deepseek-ai-dsh-client-ui-schedule/further-exploration: en: 06e265120b05dbf5 zh: 08ab1ef979b8ea40 diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index da8d202d0d..ee91c4aa00 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -25,13 +25,13 @@ Manage active and inactive tasks from the Automation tasks page and the right Si ## Use this package -The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. +The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Once the catalog holds an inactive task, the search row's More task actions menu offers Clear all inactive tasks, which removes every inactive task after a confirmation that states how many inactive tasks the catalog currently holds. The page's task list supports text search and one Enabled, Inactive, and All status filter row, starting on Enabled; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. -The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps. +The page header keeps the title at its natural width and moves the actions to another line when the title and the actions no longer fit together. Each action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps. Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view. -Rules shows the rule in a Run time card whose rows edit a local draft: Repeat is a menu of Weekly, Monday to Friday, Every day, Every N hours, Every N minutes, Every N seconds, Once, and Custom (cron), a weekly rule adds a Weekday row, a fixed-rate choice edits only the interval quantity in the unit it names, a one-shot rule edits separate Date and Time rows, and Custom (cron) edits one Cron expression row holding a five-field expression, interpreted in the selected time zone. The card parses that expression in the browser and, under the row, shows a localized sentence describing it; an expression the Host would reject shows the localized validation message in place of that sentence and blocks Save. A stored weekly rule whose weekday set is exactly Monday through Friday selects the Monday to Friday choice. The Date row is a read-only trigger showing the staged date as `YYYY/MM/DD` and opening a month calendar with localized weekday headings, month navigation, and a marked today; the draft and the submitted text keep the ISO form; the Time row is a read-only trigger showing one 24-hour clock and opening three scrolling columns of hours, minutes, and seconds with the staged value marked and scrolled into view. A calendar or clock pick stages the draft under the same staged save as every other row, arrow keys walk the open column or grid, Enter stages the marked value, Escape closes the panel and returns focus to its row, and a disabled row cannot open one. Clock rows show whole seconds, an untouched row keeps a stored millisecond value for its submit, and editing a time submits whole-second precision. Time zone is a searchable, height-bounded menu. An unlabeled top block keeps the five most recently selected exact zones in browser-local storage; first use seeds the actual system zone and UTC rather than inferring a location from UI language. The system zone is marked, and the remaining inventory is ordered by current UTC offset and canonical IANA id. Each visible label puts `UTC±HH:MM` before the localized zone name; canonical ids stay internal but remain searchable. Zones that ICU renders with the same offset and localized name share one visible row; every grouped id remains searchable, and an already stored id remains selected instead of being rewritten. The inventory comes from `Intl.supportedValuesOf('timeZone')`, and names come from `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`: both are browser ICU/CLDR data, not a scraped or product-maintained translation table. There is therefore no crawler or generated zone-name resource to refresh. A third-party locale supplies its ordinary `time.locale` value and automatically receives the corresponding ICU names; only the selector's surrounding UI copy needs translation. When the runtime cannot enumerate zones, a short identifier fallback is offered, and a stored zone outside the offered set is kept. Daily, Weekly, and Cron keep their stored rule and IANA zone, and each frequency line names the zone only when it differs from the host's system zone. A one-shot After/At seeds the date and time from the record's stored zone, or this device's zone when the record stores none, so the seeded clock names the stored instant; changing that zone keeps the entered clock values and changes the instant. A kind the card seeds on selection uses the committed occurrence's clock and the choice's zone, which is the record's stored zone or this device's zone, a seeded weekly rule starts from that occurrence's weekday in that zone, and a seeded cron rule states that occurrence's clock as a daily expression. Every accepts at least 60 whole seconds, independent of time zones. One Save submits the complete expected record with the staged change and preserves the id, original Session, latest receipt, and saved history; [Schedule](../../schedule/schedule/README.md#use-this-package) defines future-target selection, reanchoring, and unchanged-rule no-ops. +Rules shows the rule in a Run time card whose rows edit a local draft: Repeat is a menu of Weekly, Monday to Friday, Every day, Every N hours, Every N minutes, Every N seconds, Once, and Custom (cron), a weekly rule adds a Weekday row, a fixed-rate choice edits only the interval quantity in the unit it names, a one-shot rule edits separate Date and Time rows, and Custom (cron) edits one Cron expression row holding a five-field expression, interpreted in the selected time zone. The card parses that expression in the browser and, under the row, shows a localized sentence describing it; an expression the Host would reject shows the localized validation message in place of that sentence and blocks Save. A stored weekly rule whose weekday set is exactly Monday through Friday selects the Monday to Friday choice. The Date row is a read-only trigger showing the staged date as `YYYY/MM/DD` and opening a month calendar with localized weekday headings, month navigation, and a marked today; the draft and the submitted text keep the ISO form; the Time row is a read-only trigger showing one 24-hour clock and opening three scrolling columns of hours, minutes, and seconds with the staged value marked and scrolled into view. A calendar or clock pick stages the draft under the same staged save as every other row, arrow keys walk the open column or grid, Enter stages the marked value, Escape closes the panel and returns focus to its row, and a disabled row cannot open one. Clock rows show whole seconds, an untouched row keeps a stored millisecond value for its submit, and editing a time submits whole-second precision. Time zone is a searchable, height-bounded menu. Its search field takes focus automatically on desktop; phones and tablets require explicit focus. An unlabeled top block keeps the five most recently selected exact zones in browser-local storage; first use seeds the actual system zone and UTC rather than inferring a location from UI language. The system zone is marked, and the remaining inventory is ordered by current UTC offset and canonical IANA id. Each visible label puts `UTC±HH:MM` before the localized zone name; canonical ids stay internal but remain searchable. Zones that ICU renders with the same offset and localized name share one visible row; every grouped id remains searchable, and an already stored id remains selected instead of being rewritten. The inventory comes from `Intl.supportedValuesOf('timeZone')`, and names come from `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`: both are browser ICU/CLDR data, not a scraped or product-maintained translation table. There is therefore no crawler or generated zone-name resource to refresh. A third-party locale supplies its ordinary `time.locale` value and automatically receives the corresponding ICU names; only the selector's surrounding UI copy needs translation. When the runtime cannot enumerate zones, a short identifier fallback is offered, and a stored zone outside the offered set is kept. Daily, Weekly, and Cron keep their stored rule and IANA zone, and each frequency line names the zone only when it differs from the host's system zone. A one-shot After/At seeds the date and time from the record's stored zone, or this device's zone when the record stores none, so the seeded clock names the stored instant; changing that zone keeps the entered clock values and changes the instant. A kind the card seeds on selection uses the committed occurrence's clock and the choice's zone, which is the record's stored zone or this device's zone, a seeded weekly rule starts from that occurrence's weekday in that zone, and a seeded cron rule states that occurrence's clock as a daily expression. Every accepts at least 60 whole seconds, independent of time zones. One Save submits the complete expected record with the staged change and preserves the id, original Session, latest receipt, and saved history; [Schedule](../../schedule/schedule/README.md#use-this-package) defines future-target selection, reanchoring, and unchanged-rule no-ops. The rows are disabled while a save is in flight, while the catalog is loading, during a deletion, and for an inactive task. A save bar appears only while the local draft differs from the stored task: it shows Unsaved changes with Cancel and Save changes, and Save changes becomes Saving… while the save is in flight. Cancel restores the stored values. A catalog refresh becomes the shown values only while the draft has no unsaved edit, so an unsaved draft survives an authoritative refresh. A failed save keeps the draft and shows a localized message; after a conflict, retry against the refreshed record. If the task disappears from the catalog, Delete is disabled until the task returns. A successful update refreshes the authoritative catalog. A rejected or unconfirmed update is distinct from a catalog-read failure: Retry reloads the catalog without presenting the update as failed. If the update itself cannot be confirmed, check the task before retrying. Late responses after switching tasks or closing details do not replace the current view, and no failure rolls back a Host write already begun. @@ -41,7 +41,7 @@ After the last saved page loads, a nonempty history with confirmed pruning shows A successful empty history page is distinct from loading, query failure, a missing task, or an invalid cursor. Failed requests retain previously loaded records with a warning and Retry delivery records; an invalid cursor offers Refresh delivery records to reload the newest page. Changing the latest `messageId` refreshes the newest page without switching tabs. Responses superseded by a refresh or received after changing tasks, leaving Records, or closing details do not replace the current view. -The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, and the header popover — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. Confirming a deletion closes the detail or the tab it was confirmed from, and each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed. +The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, the header popover, and the Tasks page's bulk cleanup — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. The same notice reports a bulk cleanup: `Cleared N inactive tasks.` with the count the Host answered, `No inactive tasks to clear.` when the Host reports that it removed none, and `Could not clear tasks.` after a failure; a normal failure leaves the remaining inactive rows for a retry, while a backend `unknown-state` failure may have removed some rows and quarantines the Host's schedule domain until it is reopened. The bulk cleanup closes only its confirmation, whereas confirming a single deletion closes the detail or the tab it was confirmed from. Each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed. An open Session renders an icon-only clock button among its header's right-aligned utilities, immediately left of the overflow menu, only once it has something to open: the first read mounts nothing, a read that returns no active reminder leaves the header without the button, a refresh keeps the last known reminders visible, and a failed read keeps the button with its retry. That rule prevents the clock from appearing and disappearing around the first answer. Its accessible name states the reminder count when the read is ready and the generic reminder label otherwise. With exactly one active reminder, the button opens that task's detail in the right Sidebar directly. With any other count it opens a popover that lists overdue reminders first, then future reminders by target time, with stored title, frequency, browser-local target time, relative time, and a Delete button; the title is a button that opens that task's detail in the right Sidebar. The task tab names the shown task's stored title on its chip and carries the same rule, delivery records, name, instruction, and run-time editing, confirmed deletion, and original-Session link as the Automation tasks page; it never activates a Session. Daily frequency text retains the saved local time and zone, and every next-target instant is shown as a device-zone clock with its relative duration in parentheses; the rule's own zone appears only in that frequency text. Escape closes the popover and returns focus to the button; an outside pointer press dismisses it. Deleting the final reminder removes the button, and an already open popover stays expandable until then. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 057e42a2ac..c89225104e 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和未运行定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。 +在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和已结束定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。 ## 目录 @@ -25,15 +25,15 @@ kind: "package-reference" ## 使用本包 -随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 +随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。目录中存在已结束任务后,搜索行右侧的「更多任务操作」菜单提供「清除全部已结束任务」,经确认后移除全部已结束任务,确认提示说明目录当前包含多少个已结束任务。列表支持文本搜索和一行“已开启/已结束/全部”状态筛选,默认停在“已开启”;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或已结束任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 -页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。 +页头按标题的自然宽度排布;标题和操作无法并排容纳时,将操作移到下一行。每个操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。 -从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。 +从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,已结束任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。 -“规则”在“运行时间”卡片中以行编辑本地草稿:“重复”是含每周、周一至周五、每天、每 N 小时、每 N 分钟、每 N 秒、仅一次和自定义(cron)的菜单,每周规则增加“星期”行,固定速率选项只修改它所属单位的间隔数量,一次性规则用独立的“日期”和“时间”两行编辑目标,自定义(cron)用一行“Cron 表达式”编辑按所选时区解释的 5 字段表达式。卡片在浏览器中解析该表达式,并在该行下方显示描述它的本地化语句;Host 会拒绝的表达式改为显示本地化校验信息,并在解析通过前阻止保存。已存储的每周规则若星期集合恰好为周一至周五,则选中“周一至周五”选项。“日期”行是只读触发器,以 `YYYY/MM/DD` 显示暂存的日期并展开带本地化星期表头、月份切换和今天标记的月历;草稿与提交文本保持 ISO 形态;“时间”行是只读触发器,显示统一的 24 小时时钟,并展开时、分、秒三列可滚动列表,当前值高亮并在打开时滚动到可见。在月历或时钟上点选即按与其他行相同的分阶段保存写入草稿;方向键在打开的列或网格内移动,Enter 选定当前值,Escape 关闭面板并把焦点还给所在行,被禁用的行无法打开面板。时钟行显示到秒,未改动的行在提交时保留已存储的毫秒值,编辑时间时按整秒精度提交。“时区”使用高度受限的可搜索菜单。顶部无标题区域保留浏览器本地最近选过的 5 个准确时区;首次使用按真实系统时区和 UTC 初始化,而不根据界面语言猜测所在地。系统时区带有标记,其余清单按当前 UTC 偏移、再按规范 IANA 标识排序。可见文案把 `UTC±HH:MM` 放在本地化时区名称之前;IANA 标识仅在内部使用,但可用于搜索。ICU 显示成相同偏移和本地化名称的多个时区只占一行;所有被合并的标识仍可用于搜索,任务已保存的标识也会继续作为选中值,不会被改写。时区清单来自 `Intl.supportedValuesOf('timeZone')`,名称来自 `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`:两者都是浏览器内置的 ICU/CLDR 数据,不是抓取或产品自维护的翻译表。因此没有需要刷新的爬虫或生成时区资源;第三方语言包只需提供常规的 `time.locale` 值,即可自动获得对应的 ICU 名称,仅需翻译选择器周边的 UI 文案。运行时无法枚举时改用简短的标识候选列表,已存储但不在其中的时区会保留。每天、每周和 cron 保留各自已存储的规则与时区,其频率行仅在时区与宿主系统时区不同时标出时区。一次性 After/At 以记录已存储的时区初始化日期和时间;记录未保存时区时取本设备时区,初始钟表值在该时区下仍表示同一瞬时;更改时区会保留输入的钟表值并改变时点。因选择种类而重新初始化的种类使用已提交发生时点的钟表值和该选项的时区,该时区取记录已存储的时区或本设备时区:每周的星期取自该发生时点在该时区下的星期,cron 则把该发生时点的钟表值写成每天执行的表达式。固定间隔至少为 60 整秒,不受时区影响。一次保存以分阶段的变更提交完整预期记录,并保留 id、原会话、最近一次回执及已保存历史;未来目标选择、重新确定起算时间和未变更规则的无写入语义见 [Schedule](../../schedule/schedule/README.zh.md#use-this-package)。 +“规则”在“运行时间”卡片中以行编辑本地草稿:“重复”是含每周、周一至周五、每天、每 N 小时、每 N 分钟、每 N 秒、仅一次和自定义(cron)的菜单,每周规则增加“星期”行,固定速率选项只修改它所属单位的间隔数量,一次性规则用独立的“日期”和“时间”两行编辑目标,自定义(cron)用一行“Cron 表达式”编辑按所选时区解释的 5 字段表达式。卡片在浏览器中解析该表达式,并在该行下方显示描述它的本地化语句;Host 会拒绝的表达式改为显示本地化校验信息,并在解析通过前阻止保存。已存储的每周规则若星期集合恰好为周一至周五,则选中“周一至周五”选项。“日期”行是只读触发器,以 `YYYY/MM/DD` 显示暂存的日期并展开带本地化星期表头、月份切换和今天标记的月历;草稿与提交文本保持 ISO 形态;“时间”行是只读触发器,显示统一的 24 小时时钟,并展开时、分、秒三列可滚动列表,当前值高亮并在打开时滚动到可见。在月历或时钟上点选即按与其他行相同的分阶段保存写入草稿;方向键在打开的列或网格内移动,Enter 选定当前值,Escape 关闭面板并把焦点还给所在行,被禁用的行无法打开面板。时钟行显示到秒,未改动的行在提交时保留已存储的毫秒值,编辑时间时按整秒精度提交。“时区”使用高度受限的可搜索菜单。桌面端打开时自动聚焦搜索框;手机和平板需要手动聚焦。顶部无标题区域保留浏览器本地最近选过的 5 个准确时区;首次使用按真实系统时区和 UTC 初始化,而不根据界面语言猜测所在地。系统时区带有标记,其余清单按当前 UTC 偏移、再按规范 IANA 标识排序。可见文案把 `UTC±HH:MM` 放在本地化时区名称之前;IANA 标识仅在内部使用,但可用于搜索。ICU 显示成相同偏移和本地化名称的多个时区只占一行;所有被合并的标识仍可用于搜索,任务已保存的标识也会继续作为选中值,不会被改写。时区清单来自 `Intl.supportedValuesOf('timeZone')`,名称来自 `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`:两者都是浏览器内置的 ICU/CLDR 数据,不是抓取或产品自维护的翻译表。因此没有需要刷新的爬虫或生成时区资源;第三方语言包只需提供常规的 `time.locale` 值,即可自动获得对应的 ICU 名称,仅需翻译选择器周边的 UI 文案。运行时无法枚举时改用简短的标识候选列表,已存储但不在其中的时区会保留。每天、每周和 cron 保留各自已存储的规则与时区,其频率行仅在时区与宿主系统时区不同时标出时区。一次性 After/At 以记录已存储的时区初始化日期和时间;记录未保存时区时取本设备时区,初始钟表值在该时区下仍表示同一瞬时;更改时区会保留输入的钟表值并改变时点。因选择种类而重新初始化的种类使用已提交发生时点的钟表值和该选项的时区,该时区取记录已存储的时区或本设备时区:每周的星期取自该发生时点在该时区下的星期,cron 则把该发生时点的钟表值写成每天执行的表达式。固定间隔至少为 60 整秒,不受时区影响。一次保存以分阶段的变更提交完整预期记录,并保留 id、原会话、最近一次回执及已保存历史;未来目标选择、重新确定起算时间和未变更规则的无写入语义见 [Schedule](../../schedule/schedule/README.zh.md#use-this-package)。 -行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 +行在保存进行中、目录加载中、删除期间以及任务已结束时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。 @@ -41,7 +41,7 @@ kind: "package-reference" 历史查询成功的空页与加载中、查询失败、任务不存在或游标无效相互区分。请求失败时保留此前加载的记录,并显示警告和“重试加载任务运行记录”;游标无效时提供“刷新任务运行记录”,重新加载最新一页。最新 `messageId` 变化时刷新最新一页,不切换页签。被刷新取代的响应,以及切换任务、离开任务运行记录或关闭详情后才收到的响应,不会替换当前视图。 -自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以未运行状态保存。显式删除前,通过全部或未运行筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;未运行状态和该回执均不表示模型执行完成。 +自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。批量清理也由该提示报告:显示 Host 答复的行数「已清理 N 个已结束任务」;Host 答复未移除任何行时显示「没有可清理的已结束任务」;失败时显示「清除任务失败」;普通失败会保留尚未移除的已结束行以便重试,后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,直到重新打开。批量清理只关闭自己的确认对话框。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以已结束状态保存。显式删除前,通过全部或已结束筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;已结束状态和该回执均不表示模型执行完成。 已打开的会话只有在确实有可打开的内容时,才在页头右侧功能位中、紧邻更多操作菜单左侧渲染一个仅图标时钟按钮:首次读取前不渲染任何内容,读取结果为没有活动提醒时页头不显示该按钮,刷新期间保留上一次已知的提醒,读取失败则保留按钮及其重试。该规则避免时钟在首次结果前后出现又消失。其无障碍名称在读取就绪时给出提醒数量,否则显示通用提醒标签。当该会话只有一个活动提醒时,按钮直接在右侧栏打开该任务的详情。其余数量都会打开弹层:先列出逾期提醒,再按目标时间列出未来提醒,显示已存储的标题、频率、浏览器本地目标时间、相对时间及删除按钮;标题本身是一个按钮,点击后在右侧栏打开该任务的详情。任务页签在标签上显示当前任务已存储的标题,并提供与自动化任务页面相同的规则、任务运行记录、名称、指令和运行时间修改、确认删除和原会话入口;它不会激活会话。每日频率文本保留保存的本地时间和时区;每个下一目标时点都显示为设备时区的钟点和括号内的相对时长;规则自身的时区只显示在该频率文本中。Escape 关闭弹层并将焦点返回按钮;点击外部将其关闭。删除最后一条提醒会移除按钮;在此之前,已经打开的弹层保持可展开状态。 @@ -49,7 +49,7 @@ kind: "package-reference" 调用 `schedule_create` 时,创建的任务会以卡片形式渲染在 transcript 中,包含时钟图标、已存储的标题、频率,以及打开该任务右侧栏页签的“打开”按钮。卡片先按该调用持久化的结果 JSON 收窄出任务,并从该结果读取 `title`;当一次在卡片出现之后成功结束的目录读取解析到该任务时,卡片改为跟随目录显示当前名称与频率,若该读取中不含该记录,则以“已删除”代替频率显示。在已存储字段存在之前录制的结果回退为指令首行。仍在运行的调用、创建失败,或结果中没有完整任务时,卡片保留该行,并在结果为单个非空文本块时显示原始结果文本,不提供“打开”操作。卡片不增加 Host 字段,也不改变 Session 日志格式,因此早于该卡片录制的 Session 也会渲染它。 -侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与未运行 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。 +侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与已结束 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。 ----- @@ -65,7 +65,7 @@ kind: "package-reference"
实现内部细节 — 点击展开 -浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与未运行任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。 +浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与已结束任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。 刷新恢复出的任务标签没有导航参数,因为 Sidebar 只持久化标签的布局记录,不持久化打开方传入的内容。因此该页面类型自行保存从 Session 加布局记录 id 到该记录最后显示任务的 Session 和 id 的关联:每个 Session 一个 `dsh.schedule.task-tab.v1.` localStorage 键,按 tab id 一条记录,且从不保存任务内容。正文和 chip 只对没有导航参数的记录读取该条目,在标签出现之后成功的一次读取无法解析时将其删除,且不代替关联声称任务缺失。这样的一次读取无法为该标签给出任务标识时,正文陈述该状态,而不呈现永不结束的加载。 diff --git a/packages/client/ui-schedule/locale/de.json b/packages/client/ui-schedule/locale/de.json new file mode 100644 index 0000000000..d3a3f03e6b --- /dev/null +++ b/packages/client/ui-schedule/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aufgabenschnittstelle", + "description": "Die Seite „Automatisierungsaufgaben“ in der Seitenleiste sowie Aufgabenkarten und Erinnerungsmarkierungen in Sitzungen." + } +} diff --git a/packages/client/ui-schedule/locale/es.json b/packages/client/ui-schedule/locale/es.json new file mode 100644 index 0000000000..360c54c06c --- /dev/null +++ b/packages/client/ui-schedule/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de tareas", + "description": "La página de tareas de automatización en la barra lateral, además de tarjetas de tareas y marcas de recordatorio en las sesiones." + } +} diff --git a/packages/client/ui-schedule/locale/fr.json b/packages/client/ui-schedule/locale/fr.json new file mode 100644 index 0000000000..98209dca10 --- /dev/null +++ b/packages/client/ui-schedule/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface des tâches", + "description": "La page Tâches d'automatisation dans la barre latérale, ainsi que les fiches de tâches et les marques de rappel dans les sessions." + } +} diff --git a/packages/client/ui-schedule/locale/pt.json b/packages/client/ui-schedule/locale/pt.json new file mode 100644 index 0000000000..05e228e1cb --- /dev/null +++ b/packages/client/ui-schedule/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tarefas", + "description": "A página de tarefas de automação na barra lateral, além de cartões de tarefas e lembretes nas sessões." + } +} diff --git a/packages/client/ui-schedule/locale/ru.json b/packages/client/ui-schedule/locale/ru.json new file mode 100644 index 0000000000..6abb8e9b65 --- /dev/null +++ b/packages/client/ui-schedule/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс задачи", + "description": "Страница задач автоматизации на боковой панели, а также карточки задач и отметки напоминаний в сеансах." + } +} diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json index 5d149a8d39..ce7a2fd5e2 100644 --- a/packages/client/ui-schedule/package.json +++ b/packages/client/ui-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-schedule", "description": "Host task management page and Session reminder catalog", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-schedule/src/client/DeleteToast.tsx b/packages/client/ui-schedule/src/client/DeleteToast.tsx index af3da62aff..751b44c7a0 100644 --- a/packages/client/ui-schedule/src/client/DeleteToast.tsx +++ b/packages/client/ui-schedule/src/client/DeleteToast.tsx @@ -1,23 +1,26 @@ /** * Transient outcome notice for schedule deletions. * - * Every deletion — the Tasks page detail, the session task tab, and the - * Session-header popover — reports its settled outcome into one store, and one - * `shell.overlay` entry renders it as the app-wide banner. The surfaces keep no - * in-place deletion notice of their own, so an outcome outlives the panel or - * tab that asked for it. + * Every deletion — the Tasks page detail, the session task tab, the + * Session-header popover, and the Tasks page's removal of every ended task — + * reports its settled outcome into one store, and one `shell.overlay` entry + * renders it as the app-wide banner. The surfaces keep no in-place deletion + * notice of their own, so an outcome outlives the panel or tab that asked for + * it. */ import type { ReactNode } from 'react' import { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import { assertNever } from '@deepseek-ai/dsh-util-values' import type { CatalogDeleteOutcome } from './catalog-source.ts' +import type { ClearEndedOutcome } from './clear-ended.ts' -/** One reported deletion outcome; `seq` keys the banner so a re-show restarts it. */ -export interface DeleteToastState { - readonly kind: 'deleted' | 'deleteFailed' - readonly seq: number -} +/** One reported outcome; `seq` keys the banner so a re-show restarts it. */ +export type DeleteToastState = + | { readonly kind: 'deleted' | 'deleteFailed'; readonly seq: number } + | { readonly kind: 'cleared'; readonly count: number; readonly seq: number } + | { readonly kind: 'clearFailed'; readonly seq: number } /** Store the deletion wiring reports into and the overlay entry reads. */ export interface DeleteToastSource { @@ -29,6 +32,11 @@ export interface DeleteToastSource { * @param outcome - resolution of `CatalogInjected.onDelete`. */ readonly report: (outcome: CatalogDeleteOutcome) => void + /** + * Show the notice of one settled bulk removal. + * @param outcome - resolution of the management page's bulk removal callback. + */ + readonly reportClear: (outcome: ClearEndedOutcome) => void /** Clear the shown notice. */ readonly dismiss: () => void } @@ -59,6 +67,11 @@ export function createDeleteToastSource(): DeleteToastSource { if (outcome === 'pending') return publish({ kind: outcome === 'deleted' ? 'deleted' : 'deleteFailed', seq: ++seq }) }, + reportClear: (outcome) => { + publish(outcome.cleared + ? { kind: 'cleared', count: outcome.count, seq: ++seq } + : { kind: 'clearFailed', seq: ++seq }) + }, dismiss: () => { publish(null) }, } } @@ -66,23 +79,40 @@ export function createDeleteToastSource(): DeleteToastSource { /** Props of the `shell.overlay` entry: the notice store and localized copy. */ export type ScheduleDeleteToastProps = PropsRuntime<'shell.overlay'> & PropsLocale<'schedule.manager'> - & InjectFace> + & InjectFace> /** - * Render the current deletion notice: a success banner for a confirmed - * deletion, a warning for one that could not be confirmed, or nothing. + * Render the current deletion notice: a success banner for a confirmed deletion + * or settled bulk removal, a warning for one that could not be confirmed, or + * nothing. * @param props - the notice hook, its dismissal, and the locale seat. * @returns the banner on display, or null. */ export function ScheduleDeleteToast({ useToast, dismiss, t }: ScheduleDeleteToastProps): ReactNode { const toast = useToast(current => current) if (toast === null) return null - return toast.kind === 'deleted' - ? - : } - onDone={dismiss} - /> + const key = `schedule-delete-${String(toast.seq)}` + switch (toast.kind) { + case 'deleted': + return + case 'deleteFailed': + return } onDone={dismiss} /> + case 'cleared': + // A removal that found nothing states that instead of a zero count, and + // without the success tone: the ended rows can leave between the + // confirmation and the request. + return toast.count === 0 + ? + : + case 'clearFailed': + return } onDone={dismiss} /> + /* v8 ignore next -- the store publishes only the union's members, so the default holds no reachable statement. */ + default: + return assertNever(toast) + } } diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css index ac11fb33e0..7c4acf5a3d 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css @@ -87,8 +87,9 @@ white-space: nowrap; } -/* The Plugins toolbar's Add-plugin step: a 32px capsule with the 13px label. */ -.newButton { +/* The Plugins toolbar's steps: New and the bulk cleanup share one 32px + capsule with a 13px label. */ +.headerAction { height: 32px; font-size: 13px; line-height: 20px; @@ -148,11 +149,36 @@ neutral hairline, the 10px radius, and the mock's `--muted` leading icon. The mock brightens the border on hover and takes the brand stroke on focus rather than drawing a ring around the control. */ -.searchField { +.searchTools { + --task-search-height: 36px; display: flex; align-items: center; - height: 36px; - margin: 0 0 16px; + gap: 8px; + margin-bottom: 16px; +} + +.catalogActions { + flex: none; +} + +.catalogMore { + width: var(--task-search-height); + height: var(--task-search-height); + padding: 0; +} + +.catalogMore[aria-expanded='true'] { + background: var(--dsw-alias-interactive-bg-hover); +} + +.searchField { + box-sizing: border-box; + display: flex; + flex: 1; + min-width: 0; + align-items: center; + height: var(--task-search-height); + margin: 0; padding: 0 10px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 12px; @@ -1465,6 +1491,9 @@ .deleteButton { color: var(--dsw-alias-state-error-primary); + /* The outlined confirmation button keeps its border in the danger colour so + the destructive action reads as a bordered control, not bare text. */ + --dsh-button-outline-color: var(--dsw-alias-state-error-primary); } .page :focus-visible { diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx index 7d18063ae8..3c330c9f26 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx @@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { - Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconPlusOutlineRegular, IconSearchOutlineRegular, Input, + Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconEllipsisOutlineRegular, IconPlusOutlineRegular, + IconSearchOutlineRegular, IconTrashOutlineRegular, Input, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' @@ -25,6 +26,16 @@ export interface TaskManagerInjected extends TaskDetailInjected { * schedule it. The page deliberately has no creation form of its own. */ readonly onNewTask: () => void + /** + * Remove every ended Host task, after this page has confirmed the action. + * + * The call requests no task ids, so the Host's ended set is the one removed; + * the page's rows are only a snapshot that a delivery can have moved. It + * resolves once the settled outcome has been reported through the app-wide + * notice, which is the only place the result is stated. + * @returns resolution after the outcome is reported. + */ + readonly onClearEnded: () => Promise } /** Root-scoped task catalog props derived from the framework and injected actions. */ @@ -40,12 +51,15 @@ type StatusFilter = 'all' | ScheduleCatalogEntry['status'] * @returns the searchable task list beside the selected task's detail. */ export function TaskManagerPage(props: TaskManagerPageProps) { - const { useCatalog, onNewTask, onRetry, t } = props + const { useCatalog, onNewTask, onClearEnded, onRetry, t } = props const catalog = useCatalog(snapshot => snapshot) const { records, status } = catalog const [search, setSearch] = useState('') - const [statusFilter, setStatusFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState('active') const [selectedId, setSelectedId] = useState(null) + const [clearConfirm, setClearConfirm] = useState(false) + const [clearMenu, setClearMenu] = useState(false) + const [clearing, setClearing] = useState(false) // The rows state how long remains, so they read the shared ticking clock // rather than a value sampled at mount: a catalog refresh can move the target // this text describes. @@ -54,7 +68,20 @@ export function TaskManagerPage(props: TaskManagerPageProps) { const { record: catalogRecord, task: selected, id: detailId, confirmId, setConfirmId, setTab } = detail const rowRef = useRef(null) const headingRef = useRef(null) + const clearTriggerRef = useRef(null) + // Set when the confirmation opens: its settled removal can hide the trigger + // before the Modal returns focus, and so can another client's cleanup. + const clearFocusPending = useRef(false) + // Set when the removal settles: a readback superseded by `schedule/changed` + // can publish its rows after the dialog closed, removing the trigger then. + const clearSettled = useRef(false) const confirming = records.find(record => record.id === confirmId) + // The bulk action's target and count come from the complete catalog, not from + // the filtered rows: it removes every ended task, whatever the view shows. + const endedCount = useMemo( + () => records.filter(record => record.status === 'inactive').length, + [records], + ) const rows = useMemo(() => { const query = search.trim().toLowerCase() return records.filter(record => ( @@ -79,13 +106,15 @@ export function TaskManagerPage(props: TaskManagerPageProps) { useEffect(() => { // The detail focuses its own panel; with nothing selected, focus returns // to the row that opened it, or to the page heading once that row is gone. - if (selectedId !== null) return + // An open confirmation owns the focus until it closes, even when a refresh + // drops the selected task behind it. + if (selectedId !== null || clearConfirm) return if (rowRef.current !== null) { const target = rowRef.current.isConnected ? rowRef.current : headingRef.current target?.focus() rowRef.current = null } - }, [selectedId]) + }, [selectedId, clearConfirm]) useEffect(() => { if (status !== 'ready') return @@ -97,16 +126,55 @@ export function TaskManagerPage(props: TaskManagerPageProps) { setSelectedId(null) } + // The Modal restores focus to the element it captured when it opened; the + // trigger can be removed or rebuilt while the confirmation is open, so the + // page points focus at the current control, or the heading when it is gone. + // A settled removal whose readback lands later unmounts the trigger after the + // dialog closed, which drops focus to the document body; that handoff is + // caught by the same effect watching the ended count. + useEffect(() => { + if (clearConfirm) return + if (clearFocusPending.current) { + clearFocusPending.current = false + const trigger = clearTriggerRef.current + if (trigger?.isConnected === true) { + trigger.focus() + return + } + clearSettled.current = false + headingRef.current?.focus() + return + } + if (!clearSettled.current || endedCount > 0 || document.activeElement !== document.body) return + clearSettled.current = false + headingRef.current?.focus() + }, [clearConfirm, endedCount]) + + const confirmClearEnded = (): void => { + // The confirmation button stays focusable while the removal runs, so a + // second press has to be ignored here instead of by `disabled`. + if (clearing) return + setClearing(true) + void onClearEnded().finally(() => { + clearSettled.current = true + setClearing(false) + setClearConfirm(false) + }) + } + return (
{ - // A dropdown that closed on this Escape has already consumed the key: the - // Menu primitive closes from the document capture phase and calls - // preventDefault there, before React dispatches this handler. - if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || selectedId === null) return + // An Escape that closed a dropdown is already consumed by the Menu's + // document capture listener. A confirmation's own Escape reaches the + // Modal's document bubble listener, which runs after this React handler, + // so the `clearConfirm` guard is what keeps this page from also closing + // the detail while the dialog closes. + if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || clearConfirm + || selectedId === null) return event.preventDefault() event.stopPropagation() closeDetails() @@ -118,12 +186,12 @@ export function TaskManagerPage(props: TaskManagerPageProps) {

{t('title')}

- +
- {(['all', 'active', 'inactive'] as const).map(value => ( + {(['active', 'inactive', 'all'] as const).map(value => (
-
- } - aria-label={t('search.label')} - placeholder={t('search.placeholder')} - value={search} - onChange={(event) => { setSearch(event.target.value) }} - /> - {search !== '' && } +
+
+ } + aria-label={t('search.label')} + placeholder={t('search.placeholder')} + value={search} + onChange={(event) => { setSearch(event.target.value) }} + /> + {search !== '' && } +
+ {statusFilter !== 'active' && endedCount > 0 && { setClearMenu(false) }} + align="end" + portal + items={[{ + id: 'clear-inactive', label: t(clearing ? 'clear.pending' : 'clear.action'), + icon: , danger: true, + disabled: clearing || status !== 'ready', + }]} + onSelect={() => { + setClearMenu(false) + clearFocusPending.current = true + setClearConfirm(true) + }} + anchor={ + + } + />}
{selected === undefined && 0} onRetry={onRetry} t={t} />} @@ -209,6 +309,31 @@ export function TaskManagerPage(props: TaskManagerPageProps) { onDeleted={closeDetails} onClose={closeDetails} />} + { if (!clearing) setClearConfirm(false) }} + footer={
+ + +
} + /> +
) } diff --git a/packages/client/ui-schedule/src/client/TaskMenu.tsx b/packages/client/ui-schedule/src/client/TaskMenu.tsx index 458712d6b3..90683b9904 100644 --- a/packages/client/ui-schedule/src/client/TaskMenu.tsx +++ b/packages/client/ui-schedule/src/client/TaskMenu.tsx @@ -11,7 +11,7 @@ import type { CSSProperties, ReactNode } from 'react' import { createPortal } from 'react-dom' import clsx from 'clsx' import { - IconCheckOutlineRegular, useAnchoredPosition, useDismissOnOutsidePointer, + IconCheckOutlineRegular, isMobileDevice, useAnchoredPosition, useDismissOnOutsidePointer, } from '@deepseek-ai/dsh-client-ui-primitives' import css from './TaskMenu.module.css' @@ -114,8 +114,9 @@ function isLabel(entry: TaskMenuEntry): entry is TaskMenuLabel { * @param props.portal - render the list into document.body, fixed-positioned * from the anchor rect (follows scroll and resize while open). * @param props.header - owner content pinned above the scrolling rows; a control - * it marks `data-menu-field` takes the keyboard once the list is placed, which - * is the frame a portaled list becomes focusable in. + * it marks `data-menu-field` takes the keyboard on desktop once the list is placed, + * which is the frame a portaled list becomes focusable in. Phones and tablets + * require explicit field focus. * @param props.className - extra class on the anchor wrapper span. * @param props.listClassName - extra class on the dropdown card itself. * @returns the anchor wrapper with the conditional list. @@ -175,7 +176,7 @@ export function TaskMenu({ open, anchor, items = [], selectedId, onSelect, onClo handedOver.current = false return } - if (handedOver.current) return + if (handedOver.current || isMobileDevice()) return const list = listRef.current /* v8 ignore next -- the list is rendered in the same commit that sets `open`, before this effect runs. */ if (list === null) return diff --git a/packages/client/ui-schedule/src/client/clear-ended.ts b/packages/client/ui-schedule/src/client/clear-ended.ts new file mode 100644 index 0000000000..007d2b18d0 --- /dev/null +++ b/packages/client/ui-schedule/src/client/clear-ended.ts @@ -0,0 +1,45 @@ +/** Host-wide removal of every ended task, as the management page's one action reports it. */ +import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' +import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client' + +/** How one bulk removal settled; the wiring reports it as an app-wide notice. */ +export type ClearEndedOutcome = + | { readonly cleared: true; readonly count: number } + | { readonly cleared: false } + +/** Remote removal and the authoritative readback that follows it. */ +export interface ClearEndedDependencies { + /** Ask the Host to remove every ended task; it answers how many rows it removed. */ + readonly removeInactive: () => Promise> + /** + * Read the catalog again, naming the newest request ordinal so a read sent + * before the removal is superseded rather than reused. + * @returns resolution after the read result is published. + */ + readonly refresh: () => Promise +} + +/** + * Create the management page's bulk removal callback. + * + * The call names no task ids: the Host owns which rows are ended, and a + * delivery can have moved the catalog between the confirmation and the + * request. The readback follows the acknowledged removal and supersedes a read + * issued before it, so the list follows the removal rather than a stale read. + * @param deps - Remote removal and its catalog readback. + * @returns a callback that resolves once the settled outcome is available. + */ +export function createClearEnded(deps: ClearEndedDependencies): () => Promise { + return async () => { + let result: RemoteResult + try { + result = await deps.removeInactive() + } catch (_error: unknown) { + // A rejected request does not establish which rows changed. + return { cleared: false } + } + if (!result.ok) return { cleared: false } + await deps.refresh() + return { cleared: true, count: result.value.deleted } + } +} diff --git a/packages/client/ui-schedule/src/client/frequency-locales.ts b/packages/client/ui-schedule/src/client/frequency-locales.ts index d09bd39d30..0c2ad0e963 100644 --- a/packages/client/ui-schedule/src/client/frequency-locales.ts +++ b/packages/client/ui-schedule/src/client/frequency-locales.ts @@ -183,3 +183,293 @@ export const frequencyJa = { 'relative.future': '{value}{unit}後', 'relative.overdue': '{value}{unit}超過', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const frequencyEs = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Diariamente en {time} ({timeZone})', + 'frequency.dailyLocal': 'Diariamente en {time}', + 'frequency.weekly': 'Semanalmente en {weekdays} en {time} ({timeZone})', + 'frequency.weeklyLocal': 'Semanalmente en {weekdays} en {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' en {months}', + 'cron.day.every': 'Todos los días{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Día {days} de cada mes{months}', + 'cron.day.both': 'Día {days} de cada mes o {weekdays}{months}', + 'cron.day.bothStarred': 'Día {days} de cada mes y {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'cada minuto', + 'cron.time.everyMinutes': 'Cada minutos {step}', + 'cron.time.joinedEveryMinute': 'cada minuto', + 'cron.time.joinedEveryMinutes': 'cada {step} minutos', + 'cron.time.everyHour': 'cada hora', + 'cron.time.joinedEveryHour': 'cada hora', + 'cron.time.everyNHours': 'Cada horas {count}', + 'cron.time.joinedEveryNHours': 'cada horas {count}', + 'cron.time.hourlyAt': 'Cada hora al minuto {minutes}', + 'cron.time.joinedHourlyAt': 'cada hora al minuto {minutes}', + 'cron.time.hoursEveryMinute': 'cada minuto durante las horas {hours}', + 'cron.time.hoursEveryMinutes': 'cada {step} minutos durante las horas {hours}', + 'cron.time.at': 'en {times}', + 'cron.time.hoursAt': 'al minuto {minutes} de horas {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'lun', + 'frequency.weekday.2': 'mar', + 'frequency.weekday.3': 'mié', + 'frequency.weekday.4': 'jueves', + 'frequency.weekday.5': 'viernes', + 'frequency.weekday.6': 'sábado', + 'frequency.weekday.7': 'sol', + 'unit.day.one': 'dia', + 'unit.day.other': 'dias', + 'unit.hour.one': 'hora', + 'unit.hour.other': 'horas', + 'unit.minute.one': 'minuto', + 'unit.minute.other': 'minutos', + 'unit.second.one': 'segundo', + 'unit.second.other': 'segundos', + 'relative.now': 'Vencimiento ahora', + 'relative.future': 'en {value} {unit}', + 'relative.overdue': '{value} {unit} vencido', +} satisfies typeof frequencyEn + +/** French dictionary, checked against the English keys. */ +export const frequencyFr = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Tous les jours à {time} ({timeZone})', + 'frequency.dailyLocal': 'Tous les jours à {time}', + 'frequency.weekly': 'Hebdomadaire sur {weekdays} à {time} ({timeZone})', + 'frequency.weeklyLocal': 'Hebdomadaire sur {weekdays} à {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' dans {months}', + 'cron.day.every': 'Chaque jour{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Jour {days} de chaque mois{months}', + 'cron.day.both': 'Jour {days} de chaque mois ou {weekdays}{months}', + 'cron.day.bothStarred': 'Jour {days} de chaque mois et {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Chaque minute', + 'cron.time.everyMinutes': 'Toutes les minutes {step}', + 'cron.time.joinedEveryMinute': 'chaque minute', + 'cron.time.joinedEveryMinutes': 'toutes les minutes {step}', + 'cron.time.everyHour': 'Toutes les heures', + 'cron.time.joinedEveryHour': 'toutes les heures', + 'cron.time.everyNHours': 'Toutes les heures {count}', + 'cron.time.joinedEveryNHours': 'toutes les heures {count}', + 'cron.time.hourlyAt': 'Toutes les heures à la minute {minutes}', + 'cron.time.joinedHourlyAt': 'toutes les heures à la minute {minutes}', + 'cron.time.hoursEveryMinute': 'toutes les minutes pendant les heures {hours}', + 'cron.time.hoursEveryMinutes': 'toutes les minutes {step} pendant les heures {hours}', + 'cron.time.at': 'chez {times}', + 'cron.time.hoursAt': 'à la minute {minutes} des heures {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'lundi', + 'frequency.weekday.2': 'mar.', + 'frequency.weekday.3': 'mer', + 'frequency.weekday.4': 'jeu.', + 'frequency.weekday.5': 'vendredi', + 'frequency.weekday.6': 'Samedi', + 'frequency.weekday.7': 'Soleil', + 'unit.day.one': 'jour', + 'unit.day.other': 'jours', + 'unit.hour.one': 'heure', + 'unit.hour.other': 'heures', + 'unit.minute.one': 'minute', + 'unit.minute.other': 'minutes', + 'unit.second.one': 'seconde', + 'unit.second.other': 'secondes', + 'relative.now': 'À rendre maintenant', + 'relative.future': 'dans {value} {unit}', + 'relative.overdue': '{value} {unit} en retard', +} satisfies typeof frequencyEn + +/** German dictionary, checked against the English keys. */ +export const frequencyDe = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Täglich um {time} ({timeZone})', + 'frequency.dailyLocal': 'Täglich um {time}', + 'frequency.weekly': 'Wöchentlich auf {weekdays} um {time} ({timeZone})', + 'frequency.weeklyLocal': 'Wöchentlich auf {weekdays} und {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' in {months}', + 'cron.day.every': 'Jeden Tag{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Tag {days} jedes Monats{months}', + 'cron.day.both': 'Tag {days} eines jeden Monats oder {weekdays}{months}', + 'cron.day.bothStarred': 'Tag {days} jedes Monats und {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Jede Minute', + 'cron.time.everyMinutes': 'Alle {step} Minuten', + 'cron.time.joinedEveryMinute': 'jede Minute', + 'cron.time.joinedEveryMinutes': 'alle {step} Minuten', + 'cron.time.everyHour': 'Jede Stunde', + 'cron.time.joinedEveryHour': 'jede Stunde', + 'cron.time.everyNHours': 'Alle {count} Stunden', + 'cron.time.joinedEveryNHours': 'alle {count} Stunden', + 'cron.time.hourlyAt': 'Jede Stunde im Minutentakt {minutes}', + 'cron.time.joinedHourlyAt': 'jede Stunde im Minutentakt {minutes}', + 'cron.time.hoursEveryMinute': 'jede Minute während der Stunden {hours}', + 'cron.time.hoursEveryMinutes': 'alle {step} Minuten während der Stunden {hours}', + 'cron.time.at': 'bei {times}', + 'cron.time.hoursAt': 'zur Minute {minutes} der Stunden {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'Mo', + 'frequency.weekday.2': 'Di', + 'frequency.weekday.3': 'Mi', + 'frequency.weekday.4': 'Do', + 'frequency.weekday.5': 'Fr', + 'frequency.weekday.6': 'Sa', + 'frequency.weekday.7': 'Sonne', + 'unit.day.one': 'Tag', + 'unit.day.other': 'Tage', + 'unit.hour.one': 'Stunde', + 'unit.hour.other': 'Stunden', + 'unit.minute.one': 'Minute', + 'unit.minute.other': 'Minuten', + 'unit.second.one': 'zweitens', + 'unit.second.other': 'Sekunden', + 'relative.now': 'Jetzt fällig', + 'relative.future': 'in {value} {unit}', + 'relative.overdue': '{value} {unit} überfällig', +} satisfies typeof frequencyEn + +/** Russian dictionary, checked against the English keys. */ +export const frequencyRu = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Ежедневно по адресу {time} ({timeZone})', + 'frequency.dailyLocal': 'Ежедневно в {time}', + 'frequency.weekly': 'Еженедельно на {weekdays} по адресу {time} ({timeZone})', + 'frequency.weeklyLocal': 'Еженедельно на {weekdays} по адресу {time}', + 'frequency.cron': 'Крон {expression} ({timeZone})', + 'frequency.cronLocal': 'Крон {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from} – {to}', + 'cron.months': ' в {months}', + 'cron.day.every': 'Каждый день{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'День {days} каждого месяца{months}', + 'cron.day.both': 'День {days} каждого месяца или {weekdays}{months}.', + 'cron.day.bothStarred': 'День {days} каждого месяца и {weekdays}{months}.', + 'cron.hours.range': '{from} – {to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Каждую минуту', + 'cron.time.everyMinutes': 'Каждые {step} минут', + 'cron.time.joinedEveryMinute': 'каждую минуту', + 'cron.time.joinedEveryMinutes': 'каждые {step} минут', + 'cron.time.everyHour': 'Каждый час', + 'cron.time.joinedEveryHour': 'каждый час', + 'cron.time.everyNHours': 'Каждые {count} часов', + 'cron.time.joinedEveryNHours': 'каждые {count} часов', + 'cron.time.hourlyAt': 'Каждый час в минуту {minutes}', + 'cron.time.joinedHourlyAt': 'каждый час в минуту {minutes}', + 'cron.time.hoursEveryMinute': 'каждую минуту в течение часов {hours}', + 'cron.time.hoursEveryMinutes': 'каждые {step} минуты в течение часов {hours}', + 'cron.time.at': 'в {times}', + 'cron.time.hoursAt': 'в минуту {minutes} часов {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'Пн.', + 'frequency.weekday.2': 'Вт', + 'frequency.weekday.3': 'ср.', + 'frequency.weekday.4': 'Чт', + 'frequency.weekday.5': 'Пт', + 'frequency.weekday.6': 'Суббота', + 'frequency.weekday.7': 'Солнце', + 'unit.day.one': 'день', + 'unit.day.other': 'дни', + 'unit.hour.one': 'час', + 'unit.hour.other': 'часы', + 'unit.minute.one': 'минута', + 'unit.minute.other': 'минуты', + 'unit.second.one': 'второй', + 'unit.second.other': 'секунды', + 'relative.now': 'Срок оплаты сейчас', + 'relative.future': 'в {value} {unit}', + 'relative.overdue': '{value} {unit} просрочен', +} satisfies typeof frequencyEn + +/** Portuguese dictionary, checked against the English keys. */ +export const frequencyPt = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Diariamente às {time} ({timeZone})', + 'frequency.dailyLocal': 'Diariamente às {time}', + 'frequency.weekly': 'Semanalmente em {weekdays} às {time} ({timeZone})', + 'frequency.weeklyLocal': 'Semanalmente em {weekdays} às {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' em {months}', + 'cron.day.every': 'Todos os dias{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Dia {days} de cada mês{months}', + 'cron.day.both': 'Dia {days} de cada mês ou {weekdays}{months}', + 'cron.day.bothStarred': 'Dia {days} de cada mês e {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Cada minuto', + 'cron.time.everyMinutes': 'A cada {step} minutos', + 'cron.time.joinedEveryMinute': 'cada minuto', + 'cron.time.joinedEveryMinutes': 'a cada {step} minutos', + 'cron.time.everyHour': 'A cada hora', + 'cron.time.joinedEveryHour': 'a cada hora', + 'cron.time.everyNHours': 'A cada {count} horas', + 'cron.time.joinedEveryNHours': 'a cada {count} horas', + 'cron.time.hourlyAt': 'A cada hora, minuto {minutes}', + 'cron.time.joinedHourlyAt': 'a cada hora por minuto {minutes}', + 'cron.time.hoursEveryMinute': 'a cada minuto durante horas {hours}', + 'cron.time.hoursEveryMinutes': 'a cada {step} minutos durante horas {hours}', + 'cron.time.at': 'em {times}', + 'cron.time.hoursAt': 'no minuto {minutes} das horas {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'segunda-feira', + 'frequency.weekday.2': 'Terça', + 'frequency.weekday.3': 'Quarta', + 'frequency.weekday.4': 'Qui', + 'frequency.weekday.5': 'Sexta-feira', + 'frequency.weekday.6': 'Sábado', + 'frequency.weekday.7': 'Sol', + 'unit.day.one': 'dia', + 'unit.day.other': 'dias', + 'unit.hour.one': 'hora', + 'unit.hour.other': 'horas', + 'unit.minute.one': 'minuto', + 'unit.minute.other': 'minutos', + 'unit.second.one': 'segundo', + 'unit.second.other': 'segundos', + 'relative.now': 'Vencimento agora', + 'relative.future': 'em {value} {unit}', + 'relative.overdue': '{value} {unit} atrasado', +} satisfies typeof frequencyEn diff --git a/packages/client/ui-schedule/src/client/index.ts b/packages/client/ui-schedule/src/client/index.ts index 8c6dcb3c5f..7dcbc32ee3 100644 --- a/packages/client/ui-schedule/src/client/index.ts +++ b/packages/client/ui-schedule/src/client/index.ts @@ -44,6 +44,7 @@ import type {} from '@deepseek-ai/dsh-client-connection/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { ScheduleCatalogEntry, ScheduleDeliveryHistoryRequest, ScheduleId, ScheduleUpdateRequest } from '@deepseek-ai/dsh-schedule/client' import { createCatalogSource, type CatalogDeleteOutcome, type CatalogInjected } from './catalog-source.ts' +import { createClearEnded } from './clear-ended.ts' import { createDeleteToastSource, ScheduleDeleteToast } from './DeleteToast.tsx' import { SCHEDULE_TASK_ID, SCHEDULE_TASK_KIND, scheduleTaskDefinition } from './definition.ts' import { ScheduleCatalogAction } from './ScheduleCatalogAction.tsx' @@ -59,8 +60,8 @@ import { TaskManagerPage, type TaskManagerInjected } from './TaskManagerPage.tsx import type { TaskDetailInjected } from './TaskDetail.tsx' import { TaskManagerIcon } from './TaskManagerIcon.tsx' import { sessionLinkState } from './session-link.ts' -import { en, ja, NS, zh, type ScheduleCatalogKey } from './locales.ts' -import { en as managerEn, ja as managerJa, zh as managerZh, type TaskManagerKey } from './task-manager-locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ScheduleCatalogKey } from './locales.ts' +import { en as managerEn, ja as managerJa, es as managerEs, fr as managerFr, de as managerDe, ru as managerRu, pt as managerPt, zh as managerZh, type TaskManagerKey } from './task-manager-locales.ts' const MANAGER_NS = 'schedule.manager' const PANEL_ID = 'schedules' as MainPanelId @@ -87,8 +88,17 @@ export const inject = [ * @param ctx - browser services used by these contributions. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-schedule: dictionaries') - const managerDictionaries = { zh: managerZh, en: managerEn, ja: managerJa } + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-schedule: dictionaries') + const managerDictionaries = { + zh: managerZh, + en: managerEn, + ja: managerJa, + es: managerEs, + fr: managerFr, + de: managerDe, + ru: managerRu, + pt: managerPt, + } ctx.effect(() => ctx.locale.register(MANAGER_NS, managerDictionaries), 'ui-schedule: manager dictionaries') const t = ctx.locale.bind(MANAGER_NS) ctx.effect(() => ctx.sidebarRightTabs.register(scheduleTaskDefinition(t)), 'ui-schedule: task tab type') @@ -145,6 +155,17 @@ export function apply(ctx: ClientContext): void { ctx.uiWorkspace.openSession(id) } } + /** + * Remove every ended Host task and read the catalog back. + * + * The readback names every request seen so far, so it supersedes a read sent + * before the removal instead of joining it. + * @returns the settled bulk-removal outcome. + */ + const clearEnded = createClearEnded({ + removeInactive: () => ctx.remote.schedule.deleteInactive(), + refresh: () => manager.onRetry(manager.hooks.catalog.getSnapshot().readRequest), + }) const detail: TaskDetailInjected & ScheduleTaskCatalogInjected = { hooks: manager.hooks, onDelete: reportedDelete(manager.onDelete), @@ -180,6 +201,10 @@ export function apply(ctx: ClientContext): void { ...detail, // The page has no creation form: a new reminder starts in a Session. onNewTask: () => { ctx.uiWorkspace.startSession() }, + onClearEnded: async () => { + const outcome = await clearEnded() + deleteToast.reportClear(outcome) + }, }), }, TaskManagerPage)) yield ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ diff --git a/packages/client/ui-schedule/src/client/locales.ts b/packages/client/ui-schedule/src/client/locales.ts index ff04c31ce4..1ded1fa0c6 100644 --- a/packages/client/ui-schedule/src/client/locales.ts +++ b/packages/client/ui-schedule/src/client/locales.ts @@ -2,7 +2,7 @@ * `schedule.catalog` namespace dictionaries: the Session header catalog and the * Sidebar row mark with its hover-card task section. */ -import { frequencyEn, frequencyJa, frequencyZh } from './frequency-locales.ts' +import { frequencyEn, frequencyJa, frequencyEs, frequencyFr, frequencyDe, frequencyRu, frequencyPt, frequencyZh } from './frequency-locales.ts' /** Dictionary namespace owned by this plugin. */ export const NS = 'schedule.catalog' @@ -72,3 +72,108 @@ export const ja = { 'mark.aria': 'スケジュールされたタスク {count}件', 'hover.more': '他 {count}件', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'trigger.label': 'Recordatorios', + 'list.loading': 'Cargando recordatorios…', + 'list.error': 'No se pudieron cargar los recordatorios.', + 'list.retry': 'Reintentar', + 'delete.action': 'Eliminar', + 'delete.pending': 'Eliminando…', + 'delete.label': 'Eliminar recordatorio: {title}', + 'list.open': 'Abrir detalles del recordatorio: {title}', + 'trigger.one': 'Recordatorio de {count}', + 'trigger.other': 'Recordatorios de {count}', + 'list.aria': 'Recordatorios activos', + 'list.nextRun': 'Próxima ejecución', + 'frequency.once': 'una vez', + 'frequency.every': 'Cada {value} {unit}', + ...frequencyEs, + 'mark.aria': 'Tareas programadas {count}', + 'hover.more': '{count} más', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'trigger.label': 'Rappels', + 'list.loading': 'Chargement des rappels…', + 'list.error': 'Impossible de charger les rappels.', + 'list.retry': 'Réessayer', + 'delete.action': 'Supprimer', + 'delete.pending': 'Suppression…', + 'delete.label': 'Supprimer le rappel : {title}', + 'list.open': 'Détails du rappel ouvert : {title}', + 'trigger.one': 'Rappel {count}', + 'trigger.other': 'Rappels {count}', + 'list.aria': 'Rappels actifs', + 'list.nextRun': 'Prochaine exécution', + 'frequency.once': 'Une fois', + 'frequency.every': 'Chaque {value} {unit}', + ...frequencyFr, + 'mark.aria': 'Tâches planifiées {count}', + 'hover.more': '{count} plus', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'trigger.label': 'Erinnerungen', + 'list.loading': 'Erinnerungen werden geladen…', + 'list.error': 'Erinnerungen konnten nicht geladen werden.', + 'list.retry': 'Erneut versuchen', + 'delete.action': 'Löschen', + 'delete.pending': 'Löschen…', + 'delete.label': 'Erinnerung löschen: {title}', + 'list.open': 'Offene Erinnerungsdetails: {title}', + 'trigger.one': '{count} Erinnerung', + 'trigger.other': '{count} Erinnerungen', + 'list.aria': 'Aktive Erinnerungen', + 'list.nextRun': 'Nächster Lauf', + 'frequency.once': 'Einmal', + 'frequency.every': 'Jeder {value} {unit}', + ...frequencyDe, + 'mark.aria': '{count} geplante Aufgaben', + 'hover.more': '{count} mehr', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'trigger.label': 'Напоминания', + 'list.loading': 'Загрузка напоминаний…', + 'list.error': 'Не удалось загрузить напоминания.', + 'list.retry': 'Повторить', + 'delete.action': 'Удалить', + 'delete.pending': 'Удаление…', + 'delete.label': 'Удалить напоминание: {title}', + 'list.open': 'Открыть детали напоминания: {title}.', + 'trigger.one': '{count} напоминание', + 'trigger.other': '{count} напоминания', + 'list.aria': 'Активные напоминания', + 'list.nextRun': 'Следующий запуск', + 'frequency.once': 'Однажды', + 'frequency.every': 'Каждый {value} {unit}', + ...frequencyRu, + 'mark.aria': '{count} запланированные задачи', + 'hover.more': '{count} подробнее', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'trigger.label': 'Lembretes', + 'list.loading': 'Carregando lembretes…', + 'list.error': 'Não foi possível carregar os lembretes.', + 'list.retry': 'Tentar novamente', + 'delete.action': 'Excluir', + 'delete.pending': 'Excluindo…', + 'delete.label': 'Excluir lembrete: {title}', + 'list.open': 'Detalhes do lembrete aberto: {title}', + 'trigger.one': '{count} lembrete', + 'trigger.other': '{count} lembretes', + 'list.aria': 'Lembretes ativos', + 'list.nextRun': 'Próxima execução', + 'frequency.once': 'Uma vez', + 'frequency.every': 'A cada {value} {unit}', + ...frequencyPt, + 'mark.aria': '{count} tarefas agendadas', + 'hover.more': '{count} mais', +} satisfies Record diff --git a/packages/client/ui-schedule/src/client/task-manager-locales.ts b/packages/client/ui-schedule/src/client/task-manager-locales.ts index 7917d8dbb5..fffde14568 100644 --- a/packages/client/ui-schedule/src/client/task-manager-locales.ts +++ b/packages/client/ui-schedule/src/client/task-manager-locales.ts @@ -1,5 +1,5 @@ /** Copy for the retained-reminder task manager's `schedule.manager` namespace. */ -import { frequencyEn, frequencyJa, frequencyZh } from './frequency-locales.ts' +import { frequencyEn, frequencyJa, frequencyEs, frequencyFr, frequencyDe, frequencyRu, frequencyPt, frequencyZh } from './frequency-locales.ts' /** English task-manager dictionary and key domain. */ export const en = { @@ -134,8 +134,20 @@ export const en = { 'delete.cancel': 'Cancel', 'delete.close': 'Close deletion confirmation', 'delete.pending': 'Deleting…', + 'list.more': 'More task actions', + 'clear.action': 'Clear all inactive tasks', + 'clear.title': 'Clear inactive tasks ({count})?', + 'clear.description': 'These tasks and their saved delivery records will be permanently deleted. ' + + 'Enabled tasks, original sessions, and queued messages are not affected.', + 'clear.confirm': 'Clear tasks', + 'clear.pending': 'Clearing…', + 'clear.close': 'Close clear confirmation', 'toast.deleted': 'Task deleted.', 'toast.deleteFailed': 'Could not delete the task.', + 'toast.cleared.one': 'Cleared {count} inactive task.', + 'toast.cleared.other': 'Cleared {count} inactive tasks.', + 'toast.clearedNone': 'No inactive tasks to clear.', + 'toast.clearFailed': 'Could not clear tasks.', 'card.open': 'Open', 'card.openLabel': 'Open task details: {title}', 'card.deleted': 'Deleted', @@ -276,8 +288,19 @@ export const zh: Record = { 'delete.cancel': '取消', 'delete.close': '关闭删除确认', 'delete.pending': '正在删除…', + 'list.more': '更多任务操作', + 'clear.action': '清除全部已结束任务', + 'clear.title': '清除 {count} 个已结束任务?', + 'clear.description': '这些任务及其运行记录将被永久删除,已开启任务、原会话和已排队消息等不受影响。', + 'clear.confirm': '确认清除', + 'clear.pending': '正在清理…', + 'clear.close': '关闭清理确认', 'toast.deleted': '任务已删除', 'toast.deleteFailed': '无法删除任务', + 'toast.cleared.one': '已清除 {count} 个已结束任务', + 'toast.cleared.other': '已清除 {count} 个已结束任务', + 'toast.clearedNone': '没有可清理的已结束任务', + 'toast.clearFailed': '清除任务失败', 'card.open': '打开', 'card.openLabel': '打开任务详情:{title}', 'card.deleted': '已删除', @@ -415,10 +438,781 @@ export const ja = { 'delete.cancel': 'キャンセル', 'delete.close': '削除確認を閉じる', 'delete.pending': '削除中…', + 'list.more': 'その他のタスク操作', + 'clear.action': '終了済みタスクをすべてクリア', + 'clear.title': '終了済みタスク {count} 件をクリアしますか?', + 'clear.description': 'これらのタスクと実行履歴は完全に削除されます。実行中のタスク、元のセッション、キューに入ったメッセージなどは影響を受けません。', + 'clear.confirm': 'クリアする', + 'clear.pending': 'クリア中…', + 'clear.close': 'クリア確認を閉じる', 'toast.deleted': 'タスクを削除しました。', 'toast.deleteFailed': 'タスクを削除できませんでした。', + 'toast.cleared.one': '終了したタスクを {count} 件クリアしました。', + 'toast.cleared.other': '終了したタスクを {count} 件クリアしました。', + 'toast.clearedNone': 'クリアする終了済みタスクはありません。', + 'toast.clearFailed': 'タスクをクリアできませんでした。', 'card.open': '開く', 'card.openLabel': 'タスクの詳細を開く: {title}', 'card.deleted': '削除済み', 'tool.invoked': '{name} を実行しました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'panel': 'Tareas de automatización', + 'title': 'Tareas de automatización', + 'new.action': 'Nuevo', + 'list.label': 'Catálogo de tareas', + 'list.nextPrefix': 'Próxima hora prevista: ', + 'list.loading': 'Cargando tareas…', + 'list.error': 'No se pudieron cargar las tareas.', + 'list.retry': 'Reintentar', + 'list.empty': 'Aún no hay tareas. Las tareas creadas en tus sesiones aparecen aquí.', + 'list.emptyInactive': 'Sin tareas de automatización inactivas', + 'list.noMatches': 'No hay tareas coincidentes', + 'search.label': 'Tareas de búsqueda', + 'search.placeholder': 'Tareas de automatización de búsqueda', + 'search.clear': 'Borrar búsqueda', + 'empty.action': 'Nueva tarea de automatización', + 'statusFilter.label': 'Estado de la tarea', + 'statusFilter.all': 'Todos', + 'status.active': 'Habilitado', + 'status.inactive': 'Inactivo', + 'detail.label': 'Detalles de la tarea', + 'detail.close': 'Cerrar detalles', + 'detail.more': 'Acciones de tareas de automatización', + 'detail.nextRun': 'Próxima ejecución', + 'detail.tabs': 'Vistas de detalles de tareas', + 'detail.rule': 'Reglas', + 'detail.records': 'Registros', + 'delivery.empty': 'No hay registro de entrega disponible', + 'delivery.loading': 'Cargando registros de entrega…', + 'delivery.error': 'No se pudieron cargar los registros de entrega.', + 'delivery.notFound': 'Esta tarea ya no está disponible.', + 'delivery.cursorError': 'Se actualizaron los registros.', + 'delivery.retry': 'Reintentar', + 'delivery.refresh': 'Actualizar', + 'delivery.loadMore': 'Cargar más', + 'delivery.expand': 'Expandir', + 'delivery.collapse': 'Contraer', + 'delivery.pruned': 'Se han borrado los registros de entrega anteriores.', + 'delivery.retention': 'Reglas de retención', + 'delivery.retentionBounds': 'Cada tarea mantiene registros de entrega hasta {records} de los últimos días {days}.', + 'delivery.retentionExplanation': 'Cuando se guarda un nuevo registro, los registros más antiguos que estén fuera de estos límites se borran automáticamente. Borrar registros no detiene la tarea.', + 'detail.name': 'Nombre de la tarea de automatización', + 'detail.instruction': 'Instrucción de tarea de automatización', + 'detail.status': 'Estado', + 'detail.next': 'Próxima hora programada', + 'detail.frequency': 'Frecuencia', + 'detail.id': 'ID de tarea', + 'detail.session': 'Sesión vinculada', + 'detail.openSession': 'Sesión vinculada: abrir sesión original', + 'detail.openSessionTitle': 'Sesión vinculada {title}: abrir sesión original', + 'detail.sessionLoading': 'Cargando información de la sesión original.', + 'detail.sessionArchived': 'La sesión original está archivada.', + 'detail.sessionUnavailable': 'La sesión original no está disponible.', + 'detail.missing': 'Tarea no disponible. Es posible que haya sido eliminado.', + 'delivery.label': 'Registro de entrega guardado', + 'frequency.once': 'una vez', + 'frequency.every': 'Cada {value} {unit}', + ...frequencyEs, + 'timing.date': 'Fecha', + 'timing.time': 'tiempo', + 'timing.hour': 'Hora', + 'timing.minute': 'Minuto', + 'timing.second': 'segundo', + 'timing.prevMonth': 'Mes anterior', + 'timing.nextMonth': 'El próximo mes', + 'timing.zone': 'Zona horaria', + 'timing.zoneSearch': 'Buscar por desplazamiento UTC, ID de IANA o ciudad', + 'timing.zoneNoResults': 'No hay zonas horarias coincidentes', + 'timing.interval': 'Repetir cada', + 'timing.intervalIncrease': 'aumentar el intervalo', + 'timing.intervalDecrease': 'Intervalo de disminución', + 'timing.unit.hour': 'horas', + 'timing.unit.minute': 'minutos', + 'timing.unit.second': 'segundos', + 'timing.intervalHint.hour': 'Al menos 1 hora. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.intervalHint.minute': 'Al menos 1 minuto. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.intervalHint.second': 'Al menos 60 segundos. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.zoneNoStored': 'Una tarea única almacena sólo su momento objetivo, no una zona horaria. La fecha y la hora se interpretan en la zona horaria seleccionada.', + 'timing.inactive': 'Las tareas inactivas son de solo lectura. Su sincronización no se puede editar.', + 'timing.conflict': 'Esta tarea cambió mientras estabas editando. Su borrador se conserva. ' + + 'Cancele y vuelva a abrir después de actualizar para editar la regla más reciente.', + 'timing.notFound': 'Esta tarea ya no está disponible. Su borrador se conserva; cancelar para cerrar el editor.', + 'timing.subagentSession': 'Esta sesión pertenece a un subagente, que nunca recibe recordatorios entregados.', + 'timing.invalid': 'Ingrese una fecha y hora válidas, o verifique los campos de tiempo.', + 'timing.invalidZone': 'Ingrese una zona horaria IANA válida, por ejemplo Asia/Shanghai.', + 'timing.notFuture': 'Elija una fecha y hora en el futuro.', + 'timing.invalidInterval': 'Introduzca un intervalo de al menos 1 minuto.', + 'timing.invalidInterval.hour': 'Introduzca un intervalo de al menos 1 hora.', + 'timing.invalidInterval.minute': 'Introduzca un intervalo de al menos 1 minuto.', + 'timing.invalidInterval.second': 'Introduzca un intervalo de al menos 60 segundos.', + 'timing.error': 'No se pudo confirmar la actualización del tiempo. Su borrador se conserva. Verifique la tarea antes de volver a intentarlo.', + 'rule.title': 'tiempo de ejecución', + 'rule.repeat': 'repetir', + 'rule.weekday': 'Día laborable', + 'rule.weekdayOption': 'Día laborable {weekday}', + 'rule.unsaved': 'Cambios no guardados', + 'rule.save': 'Guardar cambios', + 'rule.saving': 'Guardando…', + 'rule.cancel': 'Cancelar', + 'rule.invalidTitle': 'Introduzca un nombre de tarea de como máximo 120 caracteres.', + 'rule.invalidPrompt': 'Introduzca una instrucción de tarea.', + 'rule.once': 'una vez', + 'rule.everyMinutes': 'Cada N minutos', + 'rule.everyHours': 'Cada N horas', + 'rule.everySeconds': 'Cada N segundos', + 'rule.daily': 'todos los dias', + 'rule.weekdays': 'lunes a viernes', + 'rule.weekly': 'Semanal', + 'rule.cron': 'personalizado', + 'rule.cronLabel': 'expresión cron', + 'rule.cronInvalid': 'Ingrese una expresión cron de cinco campos, por ejemplo 0 9 * * 1-5.', + 'cronForm.frequency': 'Frecuencia', + 'cronForm.monthly': 'Mensual', + 'cronForm.weekly': 'Semanal', + 'cronForm.daily': 'todos los dias', + 'cronForm.dates': 'en fechas', + 'cronForm.dateOption': 'Día {day}', + 'cronForm.atMinute': 'al minuto', + 'cronForm.minuteIncrease': 'aumentar minuto', + 'cronForm.minuteDecrease': 'Disminuir minuto', + 'rule.zone.system': ' (sistema)', + 'rule.error.conflict': 'Esta tarea cambió antes de que se guardara la actualización. La fila muestra la regla guardada; intentar otra vez.', + 'rule.error.notFound': 'Esta tarea ya no está disponible.', + 'rule.error.unknown': 'No se pudo confirmar la actualización de la regla. La fila muestra la regla guardada.', + 'delete.action': 'Eliminar tarea', + 'delete.title': '¿Eliminar esta tarea?', + 'delete.description': 'Esto evita que la tarea se active y la elimina junto con sus registros de entrega guardados. ' + + 'La sesión original y sus mensajes permanecen; los mensajes en cola no se retiran.', + 'delete.confirm': 'Confirmar eliminación', + 'delete.cancel': 'Cancelar', + 'delete.close': 'Cerrar confirmación de eliminación', + 'delete.pending': 'Eliminando…', + 'list.more': 'Más acciones de tareas', + 'clear.action': 'Borrar todas las tareas inactivas', + 'clear.title': '¿Borrar las tareas inactivas ({count})?', + 'clear.description': 'Estas tareas y sus registros de entrega guardados se eliminarán permanentemente. Las tareas habilitadas, las sesiones originales y los mensajes en cola no se verán afectados.', + 'clear.confirm': 'Borrar tareas', + 'clear.pending': 'Borrando…', + 'clear.close': 'Cerrar confirmación de borrado', + 'toast.cleared.one': 'Se ha borrado {count} tarea inactiva.', + 'toast.cleared.other': 'Se han borrado {count} tareas inactivas.', + 'toast.clearedNone': 'No hay tareas inactivas que borrar.', + 'toast.clearFailed': 'No se pudieron borrar las tareas.', + 'toast.deleted': 'Tarea eliminada.', + 'toast.deleteFailed': 'No se pudo eliminar la tarea.', + 'card.open': 'Abrir', + 'card.openLabel': 'Abrir detalles de la tarea: {title}', + 'card.deleted': 'Eliminado', + 'tool.invoked': 'Invocado {name}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'panel': "Tâches d'automatisation", + 'title': "Tâches d'automatisation", + 'new.action': 'Nouveau', + 'list.label': 'Catalogue de tâches', + 'list.nextPrefix': 'Prochaine heure programmée : ', + 'list.loading': 'Chargement des tâches…', + 'list.error': 'Impossible de charger les tâches.', + 'list.retry': 'Réessayer', + 'list.empty': "Aucune tâche pour l'instant. Les tâches créées dans vos sessions apparaissent ici.", + 'list.emptyInactive': "Aucune tâche d'automatisation inactive", + 'list.noMatches': 'Aucune tâche correspondante', + 'search.label': 'Tâches de recherche', + 'search.placeholder': "Tâches d'automatisation de la recherche", + 'search.clear': 'Effacer la recherche', + 'empty.action': "Nouvelle tâche d'automatisation", + 'statusFilter.label': 'Statut de la tâche', + 'statusFilter.all': 'Tout', + 'status.active': 'Activé', + 'status.inactive': 'Inactif', + 'detail.label': 'Détails de la tâche', + 'detail.close': 'Fermer les détails', + 'detail.more': "Actions des tâches d'automatisation", + 'detail.nextRun': 'Prochaine exécution', + 'detail.tabs': 'Vues détaillées des tâches', + 'detail.rule': 'Règles', + 'detail.records': 'Enregistrements', + 'delivery.empty': 'Aucun historique de livraison disponible', + 'delivery.loading': 'Chargement des enregistrements de livraison…', + 'delivery.error': 'Impossible de charger les enregistrements de livraison.', + 'delivery.notFound': "Cette tâche n'est plus disponible.", + 'delivery.cursorError': 'Les dossiers ont été mis à jour.', + 'delivery.retry': 'Réessayer', + 'delivery.refresh': 'Actualiser', + 'delivery.loadMore': 'Charger plus', + 'delivery.expand': 'Développer', + 'delivery.collapse': 'Réduire', + 'delivery.pruned': 'Les enregistrements de livraison antérieurs ont été effacés', + 'delivery.retention': 'Règles de conservation', + 'delivery.retentionBounds': "Chaque tâche conserve jusqu'à {records} enregistrements de livraison des derniers jours {days}.", + 'delivery.retentionExplanation': "Lorsqu'un nouvel enregistrement est enregistré, les enregistrements plus anciens en dehors de ces limites sont automatiquement effacés. La suppression des enregistrements n'arrête pas la tâche.", + 'detail.name': "Nom de la tâche d'automatisation", + 'detail.instruction': "Instruction de tâche d'automatisation", + 'detail.status': 'Statut', + 'detail.next': 'Prochaine heure programmée', + 'detail.frequency': 'Fréquence', + 'detail.id': 'ID de tâche', + 'detail.session': 'Séance liée', + 'detail.openSession': 'Session liée : ouvrir la session originale', + 'detail.openSessionTitle': 'Session liée {title} : ouvrir la session originale', + 'detail.sessionLoading': 'Chargement des informations de session originales.', + 'detail.sessionArchived': 'La session originale est archivée.', + 'detail.sessionUnavailable': "La session originale n'est pas disponible.", + 'detail.missing': 'Tâche indisponible. Il a peut-être été supprimé.', + 'delivery.label': 'Dossier de livraison enregistré', + 'frequency.once': 'Une fois', + 'frequency.every': 'Chaque {value} {unit}', + ...frequencyFr, + 'timing.date': 'Date', + 'timing.time': 'Temps', + 'timing.hour': 'Heure', + 'timing.minute': 'Minutes', + 'timing.second': 'Seconde', + 'timing.prevMonth': 'Mois précédent', + 'timing.nextMonth': 'Le mois prochain', + 'timing.zone': 'Fuseau horaire', + 'timing.zoneSearch': 'Recherche par décalage UTC, ID IANA ou ville', + 'timing.zoneNoResults': 'Aucun fuseau horaire correspondant', + 'timing.interval': 'Répétez tous les', + 'timing.intervalIncrease': "Augmenter l'intervalle", + 'timing.intervalDecrease': "Diminuer l'intervalle", + 'timing.unit.hour': 'heures', + 'timing.unit.minute': 'minutes', + 'timing.unit.second': 'secondes', + 'timing.intervalHint.hour': "Au moins 1 heure. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.intervalHint.minute': "Au moins 1 minute. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.intervalHint.second': "Au moins 60 secondes. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.zoneNoStored': "Une tâche ponctuelle stocke uniquement son moment cible, pas un fuseau horaire. La date et l'heure sont interprétées dans le fuseau horaire sélectionné.", + 'timing.inactive': 'Les tâches inactives sont en lecture seule. Leur timing ne peut pas être modifié.', + 'timing.conflict': 'Cette tâche a changé pendant que vous effectuiez la modification. Votre brouillon est conservé. ' + + "Annulez et rouvrez après l'actualisation pour modifier la dernière règle.", + 'timing.notFound': "Cette tâche n'est plus disponible. Votre brouillon est conservé ; annuler pour fermer l'éditeur.", + 'timing.subagentSession': 'Cette session appartient à un sous-agent, qui ne reçoit jamais de rappels délivrés.', + 'timing.invalid': 'Entrez une date et une heure valides ou vérifiez les champs de synchronisation.', + 'timing.invalidZone': 'Saisissez un fuseau horaire IANA valide, par exemple Asie/Shanghai.', + 'timing.notFuture': 'Choisissez une date et une heure dans le futur.', + 'timing.invalidInterval': "Entrez un intervalle d'au moins 1 minute.", + 'timing.invalidInterval.hour': "Entrez un intervalle d'au moins 1 heure.", + 'timing.invalidInterval.minute': "Entrez un intervalle d'au moins 1 minute.", + 'timing.invalidInterval.second': "Entrez un intervalle d'au moins 60 secondes.", + 'timing.error': 'Impossible de confirmer la mise à jour du timing. Votre brouillon est conservé. Vérifiez la tâche avant de réessayer.', + 'rule.title': "Temps d'exécution", + 'rule.repeat': 'Répéter', + 'rule.weekday': 'Jour de la semaine', + 'rule.weekdayOption': 'Jour de la semaine {weekday}', + 'rule.unsaved': 'Modifications non enregistrées', + 'rule.save': 'Enregistrer les modifications', + 'rule.saving': 'Sauvegarde…', + 'rule.cancel': 'Annuler', + 'rule.invalidTitle': 'Saisissez un nom de tâche comportant au maximum 120 caractères.', + 'rule.invalidPrompt': 'Entrez une instruction de tâche.', + 'rule.once': 'Une fois', + 'rule.everyMinutes': 'Toutes les N minutes', + 'rule.everyHours': 'Toutes les N heures', + 'rule.everySeconds': 'Toutes les N secondes', + 'rule.daily': 'Chaque jour', + 'rule.weekdays': 'Du lundi au vendredi', + 'rule.weekly': 'Hebdomadaire', + 'rule.cron': 'Personnalisé', + 'rule.cronLabel': 'Expression Cron', + 'rule.cronInvalid': 'Entrez une expression cron à cinq champs, par exemple 0 9 * * 1-5.', + 'cronForm.frequency': 'Fréquence', + 'cronForm.monthly': 'Mensuel', + 'cronForm.weekly': 'Hebdomadaire', + 'cronForm.daily': 'Chaque jour', + 'cronForm.dates': 'Aux dates', + 'cronForm.dateOption': 'Jour {day}', + 'cronForm.atMinute': 'À la minute', + 'cronForm.minuteIncrease': 'Augmenter les minutes', + 'cronForm.minuteDecrease': 'Diminuer les minutes', + 'rule.zone.system': ' (système)', + 'rule.error.conflict': "Cette tâche a changé avant l'enregistrement de la mise à jour. La ligne affiche la règle enregistrée ; essayer à nouveau.", + 'rule.error.notFound': "Cette tâche n'est plus disponible.", + 'rule.error.unknown': 'Impossible de confirmer la mise à jour de la règle. La ligne affiche la règle enregistrée.', + 'delete.action': 'Supprimer la tâche', + 'delete.title': 'Supprimer cette tâche ?', + 'delete.description': 'Cela empêche le déclenchement de la tâche et la supprime ainsi que ses enregistrements de livraison enregistrés. ' + + "La session d'origine et ses messages restent conservés ; les messages en file d'attente ne sont pas retirés.", + 'delete.confirm': 'Confirmer la suppression', + 'delete.cancel': 'Annuler', + 'delete.close': 'Fermer la confirmation de suppression', + 'delete.pending': 'Suppression…', + 'list.more': 'Autres actions sur les tâches', + 'clear.action': 'Effacer toutes les tâches inactives', + 'clear.title': 'Effacer les tâches inactives ({count}) ?', + 'clear.description': "Ces tâches et leurs enregistrements de livraison seront définitivement supprimés. Les tâches activées, les sessions originales et les messages en file d'attente ne sont pas affectés.", + 'clear.confirm': 'Effacer les tâches', + 'clear.pending': 'Effacement…', + 'clear.close': "Fermer la confirmation d'effacement", + 'toast.cleared.one': '{count} tâche inactive effacée.', + 'toast.cleared.other': '{count} tâches inactives effacées.', + 'toast.clearedNone': 'Aucune tâche inactive à effacer.', + 'toast.clearFailed': "Impossible d'effacer les tâches.", + 'toast.deleted': 'Tâche supprimée.', + 'toast.deleteFailed': 'Impossible de supprimer la tâche.', + 'card.open': 'Ouvrir', + 'card.openLabel': 'Détails de la tâche ouverte : {title}', + 'card.deleted': 'Supprimé', + 'tool.invoked': '{name} invoqué', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'panel': 'Automatisierungsaufgaben', + 'title': 'Automatisierungsaufgaben', + 'new.action': 'Neu', + 'list.label': 'Aufgabenkatalog', + 'list.nextPrefix': 'Nächste geplante Zeit: ', + 'list.loading': 'Aufgaben werden geladen…', + 'list.error': 'Aufgaben konnten nicht geladen werden.', + 'list.retry': 'Erneut versuchen', + 'list.empty': 'Noch keine Aufgaben. Hier werden die in Ihren Sitzungen erstellten Aufgaben angezeigt.', + 'list.emptyInactive': 'Keine inaktiven Automatisierungsaufgaben', + 'list.noMatches': 'Keine passenden Aufgaben', + 'search.label': 'Suchaufgaben', + 'search.placeholder': 'Suchautomatisierungsaufgaben', + 'search.clear': 'Suche löschen', + 'empty.action': 'Neue Automatisierungsaufgabe', + 'statusFilter.label': 'Aufgabenstatus', + 'statusFilter.all': 'Alle', + 'status.active': 'Aktiviert', + 'status.inactive': 'Inaktiv', + 'detail.label': 'Aufgabendetails', + 'detail.close': 'Details schließen', + 'detail.more': 'Aktionen der Automatisierungsaufgabe', + 'detail.nextRun': 'Nächster Lauf', + 'detail.tabs': 'Aufgabendetailansichten', + 'detail.rule': 'Regeln', + 'detail.records': 'Aufzeichnungen', + 'delivery.empty': 'Kein Lieferprotokoll verfügbar', + 'delivery.loading': 'Lieferdatensätze werden geladen…', + 'delivery.error': 'Lieferdatensätze konnten nicht geladen werden.', + 'delivery.notFound': 'Diese Aufgabe ist nicht mehr verfügbar.', + 'delivery.cursorError': 'Die Datensätze wurden aktualisiert.', + 'delivery.retry': 'Erneut versuchen', + 'delivery.refresh': 'Aktualisieren', + 'delivery.loadMore': 'Mehr laden', + 'delivery.expand': 'Ausklappen', + 'delivery.collapse': 'Einklappen', + 'delivery.pruned': 'Frühere Lieferaufzeichnungen wurden gelöscht', + 'delivery.retention': 'Aufbewahrungsregeln', + 'delivery.retentionBounds': 'Jede Aufgabe verwaltet bis zu {records}-Lieferaufzeichnungen der letzten {days}-Tage.', + 'delivery.retentionExplanation': 'Wenn ein neuer Datensatz gespeichert wird, werden ältere Datensätze außerhalb dieser Grenzen automatisch gelöscht. Durch das Löschen von Datensätzen wird die Aufgabe nicht gestoppt.', + 'detail.name': 'Name der Automatisierungsaufgabe', + 'detail.instruction': 'Anleitung zur Automatisierungsaufgabe', + 'detail.status': 'Status', + 'detail.next': 'Nächste geplante Zeit', + 'detail.frequency': 'Häufigkeit', + 'detail.id': 'Aufgaben-ID', + 'detail.session': 'Verknüpfte Sitzung', + 'detail.openSession': 'Verknüpfte Sitzung: Originalsitzung öffnen', + 'detail.openSessionTitle': 'Verknüpfte Sitzung {title}: Originalsitzung öffnen', + 'detail.sessionLoading': 'Laden der ursprünglichen Sitzungsinformationen.', + 'detail.sessionArchived': 'Die ursprüngliche Sitzung wird archiviert.', + 'detail.sessionUnavailable': 'Die ursprüngliche Sitzung ist nicht verfügbar.', + 'detail.missing': 'Aufgabe nicht verfügbar. Möglicherweise wurde es gelöscht.', + 'delivery.label': 'Lieferdatensatz gespeichert', + 'frequency.once': 'Einmal', + 'frequency.every': 'Jeder {value} {unit}', + ...frequencyDe, + 'timing.date': 'Datum', + 'timing.time': 'Zeit', + 'timing.hour': 'Stunde', + 'timing.minute': 'Minute', + 'timing.second': 'Sekunde', + 'timing.prevMonth': 'Vorheriger Monat', + 'timing.nextMonth': 'Nächsten Monat', + 'timing.zone': 'Zeitzone', + 'timing.zoneSearch': 'Suche nach UTC-Offset, IANA-ID oder Stadt', + 'timing.zoneNoResults': 'Keine passenden Zeitzonen', + 'timing.interval': 'Wiederholen Sie alle', + 'timing.intervalIncrease': 'Intervall erhöhen', + 'timing.intervalDecrease': 'Intervall verringern', + 'timing.unit.hour': 'Stunden', + 'timing.unit.minute': 'Minuten', + 'timing.unit.second': 'Sekunden', + 'timing.intervalHint.hour': 'Mindestens 1 Stunde. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.intervalHint.minute': 'Mindestens 1 Minute. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.intervalHint.second': 'Mindestens 60 Sekunden. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.zoneNoStored': 'Eine einmalige Aufgabe speichert nur ihren Zielzeitpunkt, keine Zeitzone. Datum und Uhrzeit werden in der gewählten Zeitzone interpretiert.', + 'timing.inactive': 'Inaktive Aufgaben sind schreibgeschützt. Ihr Timing kann nicht bearbeitet werden.', + 'timing.conflict': 'Diese Aufgabe hat sich während der Bearbeitung geändert. Ihr Entwurf bleibt erhalten. ' + + 'Abbrechen und nach der Aktualisierung erneut öffnen, um die neueste Regel zu bearbeiten.', + 'timing.notFound': 'Diese Aufgabe ist nicht mehr verfügbar. Ihr Entwurf bleibt erhalten; Abbrechen, um den Editor zu schließen.', + 'timing.subagentSession': 'Diese Sitzung gehört einem Subagenten, der niemals zugestellte Erinnerungen erhält.', + 'timing.invalid': 'Geben Sie ein gültiges Datum und eine gültige Uhrzeit ein oder überprüfen Sie die Zeitfelder.', + 'timing.invalidZone': 'Geben Sie eine gültige IANA-Zeitzone ein, zum Beispiel Asien/Shanghai.', + 'timing.notFuture': 'Wählen Sie ein Datum und eine Uhrzeit in der Zukunft.', + 'timing.invalidInterval': 'Geben Sie ein Intervall von mindestens 1 Minute ein.', + 'timing.invalidInterval.hour': 'Geben Sie ein Intervall von mindestens 1 Stunde ein.', + 'timing.invalidInterval.minute': 'Geben Sie ein Intervall von mindestens 1 Minute ein.', + 'timing.invalidInterval.second': 'Geben Sie ein Intervall von mindestens 60 Sekunden ein.', + 'timing.error': 'Das Timing-Update konnte nicht bestätigt werden. Ihr Entwurf bleibt erhalten. Überprüfen Sie die Aufgabe, bevor Sie es erneut versuchen.', + 'rule.title': 'Laufzeit', + 'rule.repeat': 'Wiederholen', + 'rule.weekday': 'Wochentag', + 'rule.weekdayOption': 'Wochentag {weekday}', + 'rule.unsaved': 'Nicht gespeicherte Änderungen', + 'rule.save': 'Änderungen speichern', + 'rule.saving': 'Speichern…', + 'rule.cancel': 'Abbrechen', + 'rule.invalidTitle': 'Geben Sie einen Aufgabennamen mit maximal 120 Zeichen ein.', + 'rule.invalidPrompt': 'Geben Sie eine Aufgabenanweisung ein.', + 'rule.once': 'Einmal', + 'rule.everyMinutes': 'Alle N Minuten', + 'rule.everyHours': 'Alle N Stunden', + 'rule.everySeconds': 'Alle N Sekunden', + 'rule.daily': 'Jeden Tag', + 'rule.weekdays': 'Montag bis Freitag', + 'rule.weekly': 'Wöchentlich', + 'rule.cron': 'Benutzerdefiniert', + 'rule.cronLabel': 'Cron-Ausdruck', + 'rule.cronInvalid': 'Geben Sie einen Cron-Ausdruck mit fünf Feldern ein, zum Beispiel 0 9 * * 1-5.', + 'cronForm.frequency': 'Häufigkeit', + 'cronForm.monthly': 'Monatlich', + 'cronForm.weekly': 'Wöchentlich', + 'cronForm.daily': 'Jeden Tag', + 'cronForm.dates': 'An Terminen', + 'cronForm.dateOption': 'Tag {day}', + 'cronForm.atMinute': 'Im Moment', + 'cronForm.minuteIncrease': 'Minute erhöhen', + 'cronForm.minuteDecrease': 'Minute verringern', + 'rule.zone.system': ' (System)', + 'rule.error.conflict': 'Diese Aufgabe wurde vor dem Speichern des Updates geändert. Die Zeile zeigt die gespeicherte Regel; versuchen Sie es erneut.', + 'rule.error.notFound': 'Diese Aufgabe ist nicht mehr verfügbar.', + 'rule.error.unknown': 'Die Regelaktualisierung konnte nicht bestätigt werden. Die Zeile zeigt die gespeicherte Regel.', + 'delete.action': 'Aufgabe löschen', + 'delete.title': 'Diese Aufgabe löschen?', + 'delete.description': 'Dadurch wird verhindert, dass die Aufgabe ausgelöst wird, und sie wird zusammen mit den gespeicherten Lieferdatensätzen gelöscht. ' + + 'Die ursprüngliche Sitzung und ihre Nachrichten bleiben erhalten; Nachrichten in der Warteschlange werden nicht zurückgezogen.', + 'delete.confirm': 'Bestätigen Sie den Löschvorgang', + 'delete.cancel': 'Abbrechen', + 'delete.close': 'Löschbestätigung schließen', + 'delete.pending': 'Löschen…', + 'list.more': 'Weitere Aufgabenaktionen', + 'clear.action': 'Alle inaktiven Aufgaben löschen', + 'clear.title': 'Inaktive Aufgaben löschen ({count})?', + 'clear.description': 'Diese Aufgaben und ihre gespeicherten Lieferdatensätze werden dauerhaft gelöscht. Aktivierte Aufgaben, ursprüngliche Sitzungen und Nachrichten in der Warteschlange sind nicht betroffen.', + 'clear.confirm': 'Aufgaben löschen', + 'clear.pending': 'Wird gelöscht…', + 'clear.close': 'Löschbestätigung schließen', + 'toast.cleared.one': '{count} inaktive Aufgabe gelöscht.', + 'toast.cleared.other': '{count} inaktive Aufgaben gelöscht.', + 'toast.clearedNone': 'Keine inaktiven Aufgaben zum Löschen.', + 'toast.clearFailed': 'Aufgaben konnten nicht gelöscht werden.', + 'toast.deleted': 'Aufgabe gelöscht.', + 'toast.deleteFailed': 'Die Aufgabe konnte nicht gelöscht werden.', + 'card.open': 'Öffnen', + 'card.openLabel': 'Aufgabendetails öffnen: {title}', + 'card.deleted': 'Gelöscht', + 'tool.invoked': '{name} aufgerufen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'panel': 'Задачи автоматизации', + 'title': 'Задачи автоматизации', + 'new.action': 'Новый', + 'list.label': 'Каталог задач', + 'list.nextPrefix': 'Следующее запланированное время: ', + 'list.loading': 'Загрузка задач…', + 'list.error': 'Не удалось загрузить задачи.', + 'list.retry': 'Повторить', + 'list.empty': 'Заданий пока нет. Здесь отображаются задачи, созданные в ваших сеансах.', + 'list.emptyInactive': 'Нет неактивных задач автоматизации', + 'list.noMatches': 'Нет подходящих задач', + 'search.label': 'Поиск задач', + 'search.placeholder': 'Задачи автоматизации поиска', + 'search.clear': 'Очистить поиск', + 'empty.action': 'Новая задача автоматизации', + 'statusFilter.label': 'Статус задачи', + 'statusFilter.all': 'Все', + 'status.active': 'Включено', + 'status.inactive': 'Неактивный', + 'detail.label': 'Подробности задачи', + 'detail.close': 'Закрыть детали', + 'detail.more': 'Действия задачи автоматизации', + 'detail.nextRun': 'Следующий запуск', + 'detail.tabs': 'Просмотр сведений о задаче', + 'detail.rule': 'Правила', + 'detail.records': 'Рекорды', + 'delivery.empty': 'Нет записи о доставке', + 'delivery.loading': 'Загрузка записей о доставке…', + 'delivery.error': 'Не удалось загрузить записи о доставке.', + 'delivery.notFound': 'Эта задача больше не доступна.', + 'delivery.cursorError': 'Записи обновлены.', + 'delivery.retry': 'Повторить', + 'delivery.refresh': 'Обновить', + 'delivery.loadMore': 'Загрузить больше', + 'delivery.expand': 'Развернуть', + 'delivery.collapse': 'Свернуть', + 'delivery.pruned': 'Записи о предыдущих поставках удалены.', + 'delivery.retention': 'Правила хранения', + 'delivery.retentionBounds': 'Каждая задача сохраняет записи о доставке до {records} за последние дни {days}.', + 'delivery.retentionExplanation': 'При сохранении новой записи более старые записи, выходящие за эти пределы, автоматически удаляются. Очистка записей не останавливает выполнение задачи.', + 'detail.name': 'Название задачи автоматизации', + 'detail.instruction': 'Инструкция по задаче автоматизации', + 'detail.status': 'Статус', + 'detail.next': 'Следующее запланированное время', + 'detail.frequency': 'Частота', + 'detail.id': 'Идентификатор задачи', + 'detail.session': 'Связанный сеанс', + 'detail.openSession': 'Связанный сеанс: открыть исходный сеанс.', + 'detail.openSessionTitle': 'Связанный сеанс {title}: открыть исходный сеанс.', + 'detail.sessionLoading': 'Загрузка исходной информации о сеансе.', + 'detail.sessionArchived': 'Исходный сеанс архивируется.', + 'detail.sessionUnavailable': 'Исходный сеанс недоступен.', + 'detail.missing': 'Задача недоступна. Возможно, оно было удалено.', + 'delivery.label': 'Сохраненная запись о доставке', + 'frequency.once': 'Однажды', + 'frequency.every': 'Каждый {value} {unit}', + ...frequencyRu, + 'timing.date': 'Дата', + 'timing.time': 'Время', + 'timing.hour': 'Час', + 'timing.minute': 'минута', + 'timing.second': 'Секунда', + 'timing.prevMonth': 'Предыдущий месяц', + 'timing.nextMonth': 'В следующем месяце', + 'timing.zone': 'Часовой пояс', + 'timing.zoneSearch': 'Поиск по смещению UTC, идентификатору IANA или городу.', + 'timing.zoneNoResults': 'Нет подходящих часовых поясов', + 'timing.interval': 'Повторяйте каждые', + 'timing.intervalIncrease': 'Увеличить интервал', + 'timing.intervalDecrease': 'Уменьшить интервал', + 'timing.unit.hour': 'часы', + 'timing.unit.minute': 'минуты', + 'timing.unit.second': 'секунды', + 'timing.intervalHint.hour': 'Минимум 1 час. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.intervalHint.minute': 'Минимум 1 минута. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.intervalHint.second': 'Минимум 60 секунд. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.zoneNoStored': 'Одноразовая задача сохраняет только целевой момент, а не часовой пояс. Дата и время интерпретируются в выбранном часовом поясе.', + 'timing.inactive': 'Неактивные задачи доступны только для чтения. Их время не может быть отредактировано.', + 'timing.conflict': 'Эта задача изменилась во время редактирования. Ваш черновик сохранен. ' + + 'Отмените и снова откройте после обновления, чтобы изменить последнее правило.', + 'timing.notFound': 'Эта задача больше не доступна. Ваш черновик сохраняется; отмените, чтобы закрыть редактор.', + 'timing.subagentSession': 'Этот сеанс принадлежит субагенту, который никогда не получает напоминания.', + 'timing.invalid': 'Введите действительные дату и время или проверьте поля времени.', + 'timing.invalidZone': 'Введите действительный часовой пояс IANA, например Азия/Шанхай.', + 'timing.notFuture': 'Выберите дату и время в будущем.', + 'timing.invalidInterval': 'Введите интервал не менее 1 минуты.', + 'timing.invalidInterval.hour': 'Введите интервал не менее 1 часа.', + 'timing.invalidInterval.minute': 'Введите интервал не менее 1 минуты.', + 'timing.invalidInterval.second': 'Введите интервал не менее 60 секунд.', + 'timing.error': 'Не удалось подтвердить обновление времени. Ваш черновик сохранен. Проверьте задачу, прежде чем повторить попытку.', + 'rule.title': 'Время выполнения', + 'rule.repeat': 'Повторить', + 'rule.weekday': 'Будний день', + 'rule.weekdayOption': 'День недели {weekday}', + 'rule.unsaved': 'Несохраненные изменения', + 'rule.save': 'Сохранить изменения', + 'rule.saving': 'Сохранение…', + 'rule.cancel': 'Отмена', + 'rule.invalidTitle': 'Введите имя задачи длиной не более 120 символов.', + 'rule.invalidPrompt': 'Введите инструкцию задачи.', + 'rule.once': 'Однажды', + 'rule.everyMinutes': 'Каждые N минут', + 'rule.everyHours': 'Каждые N часов', + 'rule.everySeconds': 'Каждые N секунд', + 'rule.daily': 'Каждый день', + 'rule.weekdays': 'С понедельника по пятницу', + 'rule.weekly': 'Еженедельно', + 'rule.cron': 'Пользовательский', + 'rule.cronLabel': 'Выражение Cron', + 'rule.cronInvalid': 'Введите выражение cron из пяти полей, например 0 9 * * 1-5.', + 'cronForm.frequency': 'Частота', + 'cronForm.monthly': 'Ежемесячно', + 'cronForm.weekly': 'Еженедельно', + 'cronForm.daily': 'Каждый день', + 'cronForm.dates': 'По датам', + 'cronForm.dateOption': 'День {day}', + 'cronForm.atMinute': 'В минуту', + 'cronForm.minuteIncrease': 'Увеличить минуту', + 'cronForm.minuteDecrease': 'Уменьшить минуту', + 'rule.zone.system': ' (система)', + 'rule.error.conflict': 'Эта задача изменилась до сохранения обновления. В строке отображается сохраненное правило; Попробуйте еще раз.', + 'rule.error.notFound': 'Эта задача больше не доступна.', + 'rule.error.unknown': 'Не удалось подтвердить обновление правила. В строке отображается сохраненное правило.', + 'delete.action': 'Удалить задачу', + 'delete.title': 'Удалить эту задачу?', + 'delete.description': 'Это останавливает запуск задачи и удаляет ее вместе с сохраненными записями о доставке. ' + + 'Исходный сеанс и его сообщения остаются; сообщения в очереди не изымаются.', + 'delete.confirm': 'Подтвердить удаление', + 'delete.cancel': 'Отмена', + 'delete.close': 'Закрыть подтверждение удаления', + 'delete.pending': 'Удаление…', + 'list.more': 'Другие действия с задачами', + 'clear.action': 'Удалить все неактивные задачи', + 'clear.title': 'Удалить неактивные задачи ({count})?', + 'clear.description': 'Эти задачи и сохранённые записи доставки будут удалены безвозвратно. Включённые задачи, исходные сеансы и сообщения в очереди не затрагиваются.', + 'clear.confirm': 'Удалить задачи', + 'clear.pending': 'Удаление…', + 'clear.close': 'Закрыть подтверждение удаления', + 'toast.cleared.one': 'Удалено неактивных задач: {count}.', + 'toast.cleared.other': 'Удалено неактивных задач: {count}.', + 'toast.clearedNone': 'Нет неактивных задач для удаления.', + 'toast.clearFailed': 'Не удалось удалить задачи.', + 'toast.deleted': 'Задача удалена.', + 'toast.deleteFailed': 'Не удалось удалить задачу.', + 'card.open': 'Открыть', + 'card.openLabel': 'Открыть сведения о задаче: {title}.', + 'card.deleted': 'Удалено', + 'tool.invoked': 'Вызван {name}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'panel': 'Tarefas de automação', + 'title': 'Tarefas de automação', + 'new.action': 'Novo', + 'list.label': 'Catálogo de tarefas', + 'list.nextPrefix': 'Próximo horário agendado: ', + 'list.loading': 'Carregando tarefas…', + 'list.error': 'Não foi possível carregar tarefas.', + 'list.retry': 'Tentar novamente', + 'list.empty': 'Nenhuma tarefa ainda. As tarefas criadas nas suas sessões aparecem aqui.', + 'list.emptyInactive': 'Nenhuma tarefa de automação inativa', + 'list.noMatches': 'Nenhuma tarefa correspondente', + 'search.label': 'Tarefas de pesquisa', + 'search.placeholder': 'Tarefas de automação de pesquisa', + 'search.clear': 'Limpar pesquisa', + 'empty.action': 'Nova tarefa de automação', + 'statusFilter.label': 'Status da tarefa', + 'statusFilter.all': 'Todos', + 'status.active': 'Habilitado', + 'status.inactive': 'Inativo', + 'detail.label': 'Detalhes da tarefa', + 'detail.close': 'Fechar detalhes', + 'detail.more': 'Ações de tarefas de automação', + 'detail.nextRun': 'Próxima execução', + 'detail.tabs': 'Visualizações de detalhes da tarefa', + 'detail.rule': 'Regras', + 'detail.records': 'Registros', + 'delivery.empty': 'Nenhum registro de entrega disponível', + 'delivery.loading': 'Carregando registros de entrega…', + 'delivery.error': 'Não foi possível carregar os registros de entrega.', + 'delivery.notFound': 'Esta tarefa não está mais disponível.', + 'delivery.cursorError': 'Os registros foram atualizados.', + 'delivery.retry': 'Tentar novamente', + 'delivery.refresh': 'Atualizar', + 'delivery.loadMore': 'Carregar mais', + 'delivery.expand': 'Expandir', + 'delivery.collapse': 'Recolher', + 'delivery.pruned': 'Registros de entrega anteriores foram apagados', + 'delivery.retention': 'Regras de retenção', + 'delivery.retentionBounds': 'Cada tarefa mantém até {records} registros de entrega dos últimos {days} dias.', + 'delivery.retentionExplanation': 'Quando um novo registro é salvo, os registros mais antigos fora desses limites são apagados automaticamente. A limpeza de registros não interrompe a tarefa.', + 'detail.name': 'Nome da tarefa de automação', + 'detail.instruction': 'Instrução de tarefa de automação', + 'detail.status': 'Estado', + 'detail.next': 'Próximo horário agendado', + 'detail.frequency': 'Frequência', + 'detail.id': 'ID da tarefa', + 'detail.session': 'Sessão vinculada', + 'detail.openSession': 'Sessão vinculada: sessão original aberta', + 'detail.openSessionTitle': 'Sessão vinculada {title}: sessão original aberta', + 'detail.sessionLoading': 'Carregando informações originais da sessão.', + 'detail.sessionArchived': 'A sessão original é arquivada.', + 'detail.sessionUnavailable': 'A sessão original não está disponível.', + 'detail.missing': 'Tarefa indisponível. Pode ter sido excluído.', + 'delivery.label': 'Registro de entrega salvo', + 'frequency.once': 'Uma vez', + 'frequency.every': 'A cada {value} {unit}', + ...frequencyPt, + 'timing.date': 'Data', + 'timing.time': 'Hora', + 'timing.hour': 'Hora', + 'timing.minute': 'Minuto', + 'timing.second': 'Segundo', + 'timing.prevMonth': 'Mês anterior', + 'timing.nextMonth': 'No próximo mês', + 'timing.zone': 'Fuso horário', + 'timing.zoneSearch': 'Pesquise por deslocamento UTC, ID IANA ou cidade', + 'timing.zoneNoResults': 'Nenhum fuso horário correspondente', + 'timing.interval': 'Repita cada', + 'timing.intervalIncrease': 'Aumentar intervalo', + 'timing.intervalDecrease': 'Diminuir intervalo', + 'timing.unit.hour': 'horas', + 'timing.unit.minute': 'minutos', + 'timing.unit.second': 'segundos', + 'timing.intervalHint.hour': 'Pelo menos 1 hora. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.intervalHint.minute': 'Pelo menos 1 minuto. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.intervalHint.second': 'Pelo menos 60 segundos. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.zoneNoStored': 'Uma tarefa única armazena apenas o momento alvo, não um fuso horário. A data e a hora são interpretadas no fuso horário selecionado.', + 'timing.inactive': 'As tarefas inativas são somente leitura. O tempo deles não pode ser editado.', + 'timing.conflict': 'Esta tarefa mudou enquanto você estava editando. Seu rascunho foi mantido. ' + + 'Cancele e reabra após a atualização para editar a regra mais recente.', + 'timing.notFound': 'Esta tarefa não está mais disponível. Seu rascunho foi retido; cancelar para fechar o editor.', + 'timing.subagentSession': 'Esta sessão pertence a um subagente, que nunca recebe lembretes entregues.', + 'timing.invalid': 'Insira uma data e hora válidas ou verifique os campos de horário.', + 'timing.invalidZone': 'Insira um fuso horário IANA válido, por exemplo Ásia/Xangai.', + 'timing.notFuture': 'Escolha uma data e hora no futuro.', + 'timing.invalidInterval': 'Insira um intervalo de pelo menos 1 minuto.', + 'timing.invalidInterval.hour': 'Insira um intervalo de pelo menos 1 hora.', + 'timing.invalidInterval.minute': 'Insira um intervalo de pelo menos 1 minuto.', + 'timing.invalidInterval.second': 'Insira um intervalo de pelo menos 60 segundos.', + 'timing.error': 'Não foi possível confirmar a atualização de tempo. Seu rascunho foi mantido. Verifique a tarefa antes de tentar novamente.', + 'rule.title': 'Tempo de execução', + 'rule.repeat': 'Repetir', + 'rule.weekday': 'Dia da semana', + 'rule.weekdayOption': 'Dia da semana {weekday}', + 'rule.unsaved': 'Alterações não salvas', + 'rule.save': 'Salvar alterações', + 'rule.saving': 'Salvando…', + 'rule.cancel': 'Cancelar', + 'rule.invalidTitle': 'Insira um nome de tarefa com no máximo 120 caracteres.', + 'rule.invalidPrompt': 'Insira uma instrução de tarefa.', + 'rule.once': 'Uma vez', + 'rule.everyMinutes': 'A cada N minutos', + 'rule.everyHours': 'A cada N horas', + 'rule.everySeconds': 'A cada N segundos', + 'rule.daily': 'Todos os dias', + 'rule.weekdays': 'Segunda a sexta', + 'rule.weekly': 'Semanalmente', + 'rule.cron': 'Personalizado', + 'rule.cronLabel': 'Expressão Cron', + 'rule.cronInvalid': 'Insira uma expressão cron de cinco campos, por exemplo 0 9 * * 1-5.', + 'cronForm.frequency': 'Frequência', + 'cronForm.monthly': 'Mensalmente', + 'cronForm.weekly': 'Semanalmente', + 'cronForm.daily': 'Todos os dias', + 'cronForm.dates': 'Em datas', + 'cronForm.dateOption': 'Dia {day}', + 'cronForm.atMinute': 'No minuto', + 'cronForm.minuteIncrease': 'Aumentar minuto', + 'cronForm.minuteDecrease': 'Diminuir minuto', + 'rule.zone.system': ' (sistema)', + 'rule.error.conflict': 'Esta tarefa foi alterada antes da atualização ser salva. A linha mostra a regra salva; tente novamente.', + 'rule.error.notFound': 'Esta tarefa não está mais disponível.', + 'rule.error.unknown': 'Não foi possível confirmar a atualização da regra. A linha mostra a regra salva.', + 'delete.action': 'Excluir tarefa', + 'delete.title': 'Excluir esta tarefa?', + 'delete.description': 'Isso interrompe o acionamento da tarefa e a exclui junto com seus registros de entrega salvos. ' + + 'A sessão original e suas mensagens permanecem; mensagens enfileiradas não são retiradas.', + 'delete.confirm': 'Confirmar exclusão', + 'delete.cancel': 'Cancelar', + 'delete.close': 'Fechar confirmação de exclusão', + 'delete.pending': 'Excluindo…', + 'list.more': 'Mais ações de tarefas', + 'clear.action': 'Limpar todas as tarefas inativas', + 'clear.title': 'Limpar as tarefas inativas ({count})?', + 'clear.description': 'Estas tarefas e seus registros de entrega salvos serão excluídos permanentemente. As tarefas habilitadas, as sessões originais e as mensagens na fila não serão afetadas.', + 'clear.confirm': 'Limpar tarefas', + 'clear.pending': 'Limpando…', + 'clear.close': 'Fechar confirmação de limpeza', + 'toast.cleared.one': '{count} tarefa inativa limpa.', + 'toast.cleared.other': '{count} tarefas inativas limpas.', + 'toast.clearedNone': 'Não há tarefas inativas para limpar.', + 'toast.clearFailed': 'Não foi possível limpar as tarefas.', + 'toast.deleted': 'Tarefa excluída.', + 'toast.deleteFailed': 'Não foi possível excluir a tarefa.', + 'card.open': 'Abrir', + 'card.openLabel': 'Detalhes da tarefa aberta: {title}', + 'card.deleted': 'Excluído', + 'tool.invoked': 'Invocado {name}', +} satisfies Record diff --git a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts index 768c478bef..5ae0ea5ac2 100644 --- a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts @@ -11,7 +11,9 @@ import { apply as applyNode } from '../src/index.ts' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client' -import type { ScheduleId, ScheduleCatalogEntry, ScheduleUpdateRequest, ScheduleUpdateResult } from '@deepseek-ai/dsh-schedule/client' +import type { + ScheduleId, ScheduleCatalogEntry, ScheduleDeleteInactiveResult, ScheduleUpdateRequest, ScheduleUpdateResult, +} from '@deepseek-ai/dsh-schedule/client' import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { CatalogInjected } from '../src/client/catalog-source.ts' @@ -496,6 +498,83 @@ describe('ui-schedule browser half', () => { } }) + it('clears every ended task from the global page, reads the catalog back, and reports the count', async () => { + const record: ScheduleCatalogEntry = { + id: 'host-ended' as ScheduleId, sessionId: 'cold-original' as SessionId, status: 'inactive', + kind: 'after', title: 'Ended review', prompt: 'Ended review', afterSeconds: 60, + scheduledAt: '2099-01-01T00:00:00.000Z', + } + const catalog = vi.fn() + .mockResolvedValueOnce({ ok: true, value: [record] }) + .mockResolvedValue({ ok: true, value: [] }) + const deleteInactive = vi.fn().mockResolvedValue({ ok: true, value: { deleted: 1 } }) + const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} }) + const owner = ctx.slots.register({ + name: 'root', + children: { + main: { kind: 'keyed', scope: 'root' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + }, + } as never, Empty) + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + try { + const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected + const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit + const dispose = face.hooks.catalog.subscribe(vi.fn()) + try { + await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([record]) }) + const reads = catalog.mock.calls.length + await face.onClearEnded() + expect(deleteInactive).toHaveBeenCalledOnce() + // The acknowledged removal reads the catalog back instead of joining a + // read issued before it, so the page's rows follow the removal. + expect(catalog.mock.calls.length).toBe(reads + 1) + await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([]) }) + expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 1, seq: 1 }) + } finally { + dispose() + } + } finally { + await fiber.dispose() + owner() + await ctx.fiber.dispose() + } + }) + + it('reports a settled bulk removal after the page registration is gone', async () => { + const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] }) + const removal = Promise.withResolvers<{ ok: true; value: ScheduleDeleteInactiveResult }>() + const deleteInactive = vi.fn(() => removal.promise) + const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} }) + const owner = ctx.slots.register({ + name: 'root', + children: { + main: { kind: 'keyed', scope: 'root' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + }, + } as never, Empty) + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + let claimed = false + try { + const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected + const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit + const clearing = face.onClearEnded() + // The page panel can unmount while the Host removal is still in flight; + // the notice store belongs to the plugin, so the outcome still publishes. + owner() + claimed = true + removal.resolve({ ok: true, value: { deleted: 2 } }) + await clearing + expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 2, seq: 1 }) + } finally { + if (!claimed) owner() + await fiber.dispose() + await ctx.fiber.dispose() + } + }) + it('registers the row mark and hover-card seats under the Session row declarations', async () => { const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] }) const ctx = await baseContext({ schedule: { catalog }, $on: () => () => {} }) diff --git a/packages/client/ui-schedule/tests/clear-ended.client.spec.ts b/packages/client/ui-schedule/tests/clear-ended.client.spec.ts new file mode 100644 index 0000000000..ef943688b0 --- /dev/null +++ b/packages/client/ui-schedule/tests/clear-ended.client.spec.ts @@ -0,0 +1,74 @@ +/** The management page's bulk removal: one request, one readback, one settled outcome. */ +import { describe, expect, it, vi } from 'vitest' +import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' +import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' +import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client' +import { createClearEnded } from '../src/client/clear-ended.ts' + +const failure: RemoteResult = { + ok: false, error: new RemoteError('gateway/internal', 'Unavailable', {}), +} + +/** + * Wire the helper to one fake Remote answer and one readback spy. + * @param answer - what the removal call answers, or the reason it rejects. + * @param refresh - the readback the helper calls after an acknowledged removal. + * @returns the callback under test with its dependencies' spies. + */ +function setup( + answer: RemoteResult | Error, + refresh = vi.fn(async () => {}), +) { + const removeInactive = vi.fn(async () => { + if (answer instanceof Error) throw answer + return answer + }) + return { clearEnded: createClearEnded({ removeInactive, refresh }), removeInactive, refresh } +} + +describe('createClearEnded', () => { + it('reads the catalog back before the outcome settles and reports how many rows went', async () => { + const readback = Promise.withResolvers() + const { clearEnded, removeInactive, refresh } = setup( + { ok: true, value: { deleted: 3 } }, + vi.fn(() => readback.promise), + ) + let settled = false + const outcome = clearEnded().then((value) => { + settled = true + return value + }) + + await vi.waitFor(() => { expect(refresh).toHaveBeenCalledOnce() }) + // The list must agree with the count before the notice states it, so the + // outcome waits for the readback rather than publishing alongside it. + expect(settled).toBe(false) + readback.resolve(undefined) + + await expect(outcome).resolves.toEqual({ cleared: true, count: 3 }) + expect(removeInactive).toHaveBeenCalledOnce() + expect(removeInactive.mock.invocationCallOrder[0]) + .toBeLessThan(refresh.mock.invocationCallOrder[0]!) + }) + + it('reports an acknowledged removal that found nothing as a cleared outcome', async () => { + const { clearEnded, refresh } = setup({ ok: true, value: { deleted: 0 } }) + + await expect(clearEnded()).resolves.toEqual({ cleared: true, count: 0 }) + expect(refresh).toHaveBeenCalledOnce() + }) + + it('reports a refused removal and reads nothing back', async () => { + const { clearEnded, refresh } = setup(failure) + + await expect(clearEnded()).resolves.toEqual({ cleared: false }) + expect(refresh).not.toHaveBeenCalled() + }) + + it('reports a rejected removal and reads nothing back', async () => { + const { clearEnded, refresh } = setup(new Error('connection lost')) + + await expect(clearEnded()).resolves.toEqual({ cleared: false }) + expect(refresh).not.toHaveBeenCalled() + }) +}) diff --git a/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx b/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx index 55f6535587..7112670eea 100644 --- a/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx +++ b/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx @@ -48,6 +48,17 @@ describe('delete toast source', () => { source.report('deleted') expect(listener).toHaveBeenCalledTimes(4) }) + + it('publishes each settled bulk removal with its count or failure', () => { + const source = createDeleteToastSource() + source.reportClear({ cleared: true, count: 3 }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 3, seq: 1 }) + // A removal that found nothing keeps its own zero, so the banner can say so. + source.reportClear({ cleared: true, count: 0 }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 0, seq: 2 }) + source.reportClear({ cleared: false }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'clearFailed', seq: 3 }) + }) }) describe('ScheduleDeleteToast', () => { @@ -68,4 +79,25 @@ describe('ScheduleDeleteToast', () => { mount({ kind: 'deleteFailed', seq: 2 }, zh) expect(screen.getByRole('alert').textContent).toBe(zh['toast.deleteFailed']) }) + + it('states how many inactive tasks a bulk removal cleared, singular for one', () => { + vi.useFakeTimers() + const many = mount({ kind: 'cleared', count: 4, seq: 3 }) + expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.other'].replace('{count}', '4')) + act(() => { vi.runAllTimers() }) + expect(many.dismiss).toHaveBeenCalledOnce() + cleanup() + mount({ kind: 'cleared', count: 1, seq: 4 }) + expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.one'].replace('{count}', '1')) + }) + + it('states that a bulk removal found nothing instead of a zero count', () => { + mount({ kind: 'cleared', count: 0, seq: 4 }, zh) + expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearedNone']) + }) + + it('shows the warning banner for a bulk removal that failed', () => { + mount({ kind: 'clearFailed', seq: 5 }, zh) + expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearFailed']) + }) }) diff --git a/packages/client/ui-schedule/tests/task-detail.client.spec.tsx b/packages/client/ui-schedule/tests/task-detail.client.spec.tsx index ff64d41b88..97bef63a33 100644 --- a/packages/client/ui-schedule/tests/task-detail.client.spec.tsx +++ b/packages/client/ui-schedule/tests/task-detail.client.spec.tsx @@ -122,6 +122,7 @@ function mount( onDelete: vi.fn(async () => 'deleted'), onRetry: vi.fn(async () => {}), onNewTask: vi.fn(), + onClearEnded: vi.fn(async () => {}), onUpdateTiming: vi.fn(async ({ expected }) => ({ ok: true, value: { id: expected.id, updated: false, record: expected }, })), diff --git a/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx b/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx index 822b08571b..e185f2d136 100644 --- a/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx +++ b/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx @@ -162,6 +162,7 @@ function mount( initial: Partial> = {}, dictionary: typeof en | typeof zh = en, overrides: Partial = {}, + options: { startAtAll?: boolean } = {}, ) { let snapshot: CatalogSnapshot = { records, status: 'ready', deleting: [], settled: true, readRequest: 0, readSettled: 0, ...initial, @@ -178,6 +179,7 @@ function mount( onDelete: vi.fn(async () => 'deleted'), onRetry: vi.fn(async () => {}), onNewTask: vi.fn(), + onClearEnded: vi.fn(async () => {}), onUpdateTiming: vi.fn(async ({ expected }) => ({ ok: true, value: { id: expected.id, updated: false, record: expected }, })), @@ -193,6 +195,11 @@ function mount( ...overrides, } const view = render() + // The page starts on Active; cases about the full list select All first. The + // default filter itself is covered by its own case below. + if (options.startAtAll !== false) { + fireEvent.click(screen.getByRole('button', { name: dictionary['statusFilter.all'] })) + } return { props, view, updateTiming: vi.mocked(props.onUpdateTiming), @@ -640,9 +647,11 @@ describe('Task manager catalog', () => { expect(within(filters).getAllByRole('group')).toHaveLength(1) const statuses = within(filters).getByRole('group', { name: en['statusFilter.label'] }) expect(statuses.parentElement).toBe(filters) - expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['statusFilter.all']) + expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['status.active']) // The mock's `.searchbox`: the same field carries the leading glyph, the // placeholder, and — once a value exists — the inline clear control. + const searchTools = page.querySelector(`.${css.searchTools!}`)! + expect(searchTools.contains(page.querySelector(`.${css.searchField!}`))).toBe(true) const field = page.querySelector(`.${css.searchField!}`)! const input = screen.getByRole('searchbox', { name: en['search.label'] }) expect(field.contains(input)).toBe(true) @@ -835,6 +844,18 @@ describe('Task manager catalog', () => { expect(taskNames()).toEqual(['Send summary', 'Review release', 'Check metrics']) }) + it.each([en, zh])('starts on the active status filter', (dictionary) => { + mount({ records: [ended, every, after] }, dictionary, {}, { startAtAll: false }) + const list = screen.getByRole('list', { name: dictionary['list.label'] }) + const names = within(list).queryAllByRole('button') + .filter(button => button.closest('li') !== null) + .map(button => button.getAttribute('aria-label')) + const statuses = screen.getByRole('group', { name: dictionary['statusFilter.label'] }) + expect(within(statuses).getByRole('button', { name: dictionary['status.active'] }).getAttribute('aria-pressed')).toBe('true') + expect(within(statuses).getByRole('button', { name: dictionary['statusFilter.all'] }).getAttribute('aria-pressed')).toBe('false') + expect(names).toEqual(['Send summary', 'Check metrics']) + }) + it.each([en, zh])('combines all, active, and inactive status filters with search', (dictionary) => { mount({ records: [ended, every, after] }, dictionary) const list = screen.getByRole('list', { name: dictionary['list.label'] }) @@ -942,6 +963,209 @@ describe('Task manager catalog', () => { }) }) +describe('clearing inactive tasks', () => { + // A distinct identity: the module's `ended` case reuses the active row's id. + const endedTask: ScheduleCatalogEntry = { + ...ended, id: 'task-ended' as ScheduleId, title: 'Ended summary', prompt: 'Ended summary', + } + const anotherEnded: ScheduleCatalogEntry = { + ...ended, id: 'task-ended-two' as ScheduleId, title: 'Ended report', prompt: 'Ended report', + } + + /** The heading's actions; only the primary New control remains there. */ + function creationActions(): HTMLElement { + return document.querySelector(`.${css.creationActions!}`)! + } + + /** The search row's 36px More trigger. */ + function moreTrigger(dictionary: typeof en | typeof zh = en): HTMLElement { + return screen.getByRole('button', { name: dictionary['list.more'] }) + } + + /** + * Open the cleanup confirmation through the More menu. + * @param dictionary - locale under test. + * @param count - ended rows the page's complete catalog holds. + * @returns the confirmation dialog. + */ + function openClear(dictionary: typeof en | typeof zh = en, count = 1): HTMLElement { + fireEvent.click(moreTrigger(dictionary)) + fireEvent.click(screen.getByRole('menuitem', { name: dictionary['clear.action'] })) + return screen.getByRole('dialog', { + name: dictionary['clear.title'].replace('{count}', String(count)), + }) + } + + it('offers the cleanup in the search row only while an ended task is visible', () => { + const h = mount() + expect(creationActions().textContent).toBe(en['new.action']) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + h.update({ records: [...records, endedTask] }) + // The cleanup is the search row's More menu, not a second heading action. + expect(document.querySelector(`.${css.searchTools!}`)?.contains(moreTrigger())).toBe(true) + expect(within(creationActions()).getAllByRole('button')).toHaveLength(1) + // The Active view hides the cleanup even though the catalog holds ended rows. + fireEvent.click(screen.getByRole('button', { name: en['status.active'] })) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: en['statusFilter.all'] })) + expect(moreTrigger()).toBeDefined() + }) + + it('states the complete ended set in the confirmation, not the filtered rows', () => { + mount({ records: [...records, endedTask, anotherEnded] }) + fireEvent.change(screen.getByRole('searchbox', { name: en['search.label'] }), { + target: { value: 'Send summary' }, + }) + expect(taskNames()).toEqual(['Send summary']) + const dialog = openClear(en, 2) + expect(within(dialog).getByText(en['clear.description'])).toBeDefined() + }) + + it('closes the cleanup menu without opening the confirmation', () => { + mount({ records: [at, endedTask] }) + fireEvent.click(moreTrigger()) + expect(screen.getByRole('menuitem', { name: en['clear.action'] })).toBeDefined() + fireEvent.keyDown(document, { key: 'Escape' }) + expect(screen.queryByRole('menuitem', { name: en['clear.action'] })).toBeNull() + expect(screen.queryByRole('dialog')).toBeNull() + }) + + it('removes nothing when the confirmation is dismissed', () => { + const h = mount({ records: [at, endedTask] }) + openClear() + fireEvent.click(screen.getByRole('button', { name: en['delete.cancel'] })) + expect(screen.queryByRole('dialog')).toBeNull() + expect(h.props.onClearEnded).not.toHaveBeenCalled() + }) + + it('keeps the confirmation open until the settled removal, then returns focus', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + const confirm = within(dialog).getByRole('button', { name: en['clear.confirm'] }) + confirm.focus() + fireEvent.click(confirm) + expect(onClearEnded).toHaveBeenCalledOnce() + + const running = screen.getByRole('dialog') + const runningButton = within(running).getByRole('button', { name: en['clear.pending'] }) + // The pressed control keeps the focus and reads as busy; disabling it would + // hand focus to the document body mid-removal. + expect(runningButton.getAttribute('aria-disabled')).toBe('true') + expect(document.activeElement).toBe(runningButton) + expect(within(running).getByRole('button', { name: en['delete.cancel'] }).hasAttribute('disabled')).toBe(true) + // A second press while the removal runs must not start another request. + fireEvent.click(runningButton) + expect(onClearEnded).toHaveBeenCalledOnce() + // The confirmation is the only place that states the removal still runs, so + // the Escape that would close it waits for the outcome. + fireEvent.keyDown(running, { key: 'Escape' }) + expect(screen.getByRole('dialog')).toBe(running) + + await act(async () => { pending.resolve(undefined) }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(trigger) + }) + + it('falls back to the page heading when the settled cleanup hides its trigger', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + const h = mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] })) + // The acknowledged removal's readback drops the ended rows, and with them the + // trigger the Modal captured when it opened. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + await act(async () => { pending.resolve(undefined) }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('keeps focus in the confirmation when the selected task disappears behind it', () => { + const h = mount({ records: [at, endedTask] }) + fireEvent.click(screen.getByRole('button', { name: 'Ended summary' })) + expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBeDefined() + const dialog = openClear() + // A refresh drops the selected task while the confirmation is open; closing + // the detail must not take the focus out of the dialog. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('complementary', { name: en['detail.label'] })).toBeNull() + expect(dialog.contains(document.activeElement)).toBe(true) + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + // The row that held the focus is gone with the detail, so the heading takes it. + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('falls back to the page heading when another cleanup removes the trigger', () => { + const h = mount({ records: [at, endedTask] }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + // Another client's cleanup removes the ended rows while this confirmation + // is open, so the control the Modal captured is unmounted too. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('focuses the rebuilt trigger when the dialog closes', () => { + const h = mount({ records: [at, endedTask] }) + const dialog = openClear() + // Another client clears the ended rows, then a delivery ends one more while + // the confirmation stays open: the trigger unmounts and a new one mounts. + act(() => { h.update({ records: [at] }) }) + act(() => { h.update({ records: [at, anotherEnded] }) }) + const rebuilt = moreTrigger() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(rebuilt) + }) + + it('moves focus to the heading when a late readback removes the trigger', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + const h = mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] })) + // The removal settles while the catalog still lists the ended row: the + // dialog closes and focus returns to the trigger. + await act(async () => { pending.resolve(undefined) }) + expect(document.activeElement).toBe(trigger) + // A readback superseded by `schedule/changed` publishes afterwards and takes + // the trigger with its row; focus must not stay on the document body. + act(() => { h.update({ records: [at] }) }) + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('closes only the confirmation on Escape while a task detail is open', () => { + mount({ records: [at, endedTask] }) + fireEvent.click(screen.getByRole('button', { name: 'Review release' })) + const detail = screen.getByRole('complementary', { name: en['detail.label'] }) + const dialog = openClear() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBe(detail) + }) + + it('states the cleanup and its confirmation in Chinese', () => { + mount({ records: [at, endedTask] }, zh) + const dialog = openClear(zh, 1) + expect(within(dialog).getByText(zh['clear.description'])).toBeDefined() + expect(within(dialog).getByRole('button', { name: zh['clear.confirm'] })).toBeDefined() + }) +}) + describe.each([['English', en], ['Chinese', zh]] as const)('task detail views in %s', (_name, dictionary) => { it('opens Rules under the selected task and separates rule fields from saved deliveries', async () => { const labels = detailViewLabels(dictionary) @@ -1551,7 +1775,7 @@ describe('Task manager details', () => { expect(screen.getByRole('heading', { level: 1, name: '自动化任务' })).toBeDefined() expect(screen.getByRole('searchbox', { name: '搜索任务' })).toBeDefined() const statuses = screen.getByRole('group', { name: '任务状态' }) - expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('全部') + expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('已开启') fireEvent.click(screen.getByRole('button', { name: 'Check metrics' })) const detail = screen.getByRole('complementary', { name: '任务详情' }) expect(within(detail).getByText(zh['rule.everySeconds'])).toBeDefined() @@ -3814,11 +4038,12 @@ describe('Mock detail metrics shared with the task list', () => { ) // The Plugins toolbar's Add-plugin capsule, repeated by the creation action. expect(rule('creationActions')).toMatch(/gap:\s*16px;/) - const newButton = rule('newButton') - expect(newButton).toMatch(/height:\s*32px;/) - expect(newButton).toMatch(/font-size:\s*13px;/) - expect(newButton).toMatch(/padding:\s*0 12px;/) - expect(newButton).toMatch(/border-radius:\s*16px;/) + // The New action and the bulk cleanup share this capsule geometry. + const headerAction = rule('headerAction') + expect(headerAction).toMatch(/height:\s*32px;/) + expect(headerAction).toMatch(/font-size:\s*13px;/) + expect(headerAction).toMatch(/padding:\s*0 12px;/) + expect(headerAction).toMatch(/border-radius:\s*16px;/) const headingTitle = /\.pageHeading h1\s*\{([^}]*)\}/.exec(stylesheet)?.[1] ?? '' // The first-level title step is the Plugins page's `pageTitle`, not the // mock's larger page heading; the equality case below pins that pair. @@ -3834,9 +4059,14 @@ describe('Mock detail metrics shared with the task list', () => { expect(tab).toMatch(/font-size:\s*14px;/) // The selected chip keeps the chip's own weight; only the fill marks it. expect(rule('filterTabActive')).not.toMatch(/font-weight/) - // The mock's `.searchbox` keeps its hairline at the repository's 0.5px. + // The search row fixes the mock's 36px control height for the field and the + // square More trigger, and keeps the hairline at the repository's 0.5px. + expect(rule('searchTools')).toMatch(/--task-search-height:\s*36px;/) + expect(rule('catalogMore')).toMatch(/width:\s*var\(--task-search-height\);/) + expect(rule('catalogMore')).toMatch(/height:\s*var\(--task-search-height\);/) + expect(rule('catalogMore')).toMatch(/padding:\s*0;/) const field = rule('searchField') - expect(field).toMatch(/height:\s*36px;/) + expect(field).toMatch(/height:\s*var\(--task-search-height\);/) expect(field).toMatch(/padding:\s*0 10px;/) expect(field).toMatch(/border:\s*0\.5px solid var\(--dsw-alias-border-l3\);/) expect(field).toMatch(/border-radius:\s*12px;/) diff --git a/packages/client/ui-schedule/tests/task-menu.client.spec.tsx b/packages/client/ui-schedule/tests/task-menu.client.spec.tsx index f93c36c838..9097bcec30 100644 --- a/packages/client/ui-schedule/tests/task-menu.client.spec.tsx +++ b/packages/client/ui-schedule/tests/task-menu.client.spec.tsx @@ -4,9 +4,10 @@ * handover a portaled list performs once it is placed, the row walk, and the * dismissal and focus-return paths the task manager's four menus rely on. */ +import * as uiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' import { useState } from 'react' import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { TaskMenu } from '../src/client/TaskMenu.tsx' import type { TaskMenuEntry } from '../src/client/TaskMenu.tsx' import taskMenuCss from '../src/client/TaskMenu.module.css' @@ -142,6 +143,48 @@ describe('TaskMenu', () => { expect(document.activeElement).toBe(screen.getByRole('textbox', { name: 'Filter' })) }) + it.each([ + { name: 'iPhone', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 Version/18.6 Mobile/15E148 Safari/604.1', platform: 'iPhone', maxTouchPoints: 5 }, + { name: 'Android tablet', userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36', platform: 'Linux armv8l', maxTouchPoints: 5 }, + { name: 'desktop-mode iPad', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.6 Safari/605.1.15', platform: 'MacIntel', maxTouchPoints: 5 }, + ])('leaves header search focus to the user on $name', async ({ userAgent, platform, maxTouchPoints }) => { + for (const [name, value] of Object.entries({ userAgent, platform, maxTouchPoints })) { + const descriptor = Object.getOwnPropertyDescriptor(navigator, name) + Object.defineProperty(navigator, name, { configurable: true, value }) + onTestFinished(() => { + if (descriptor === undefined) Reflect.deleteProperty(navigator, name) + else Object.defineProperty(navigator, name, descriptor) + }) + } + vi.resetModules() + const detection = await import('@deepseek-ai/dsh-client-ui-primitives/src/focus.ts') + const device = vi.spyOn(uiPrimitives, 'isMobileDevice').mockImplementation(detection.isMobileDevice) + onTestFinished(() => { device.mockRestore() }) + const menu = (open: boolean) => trigger} + header={} + items={ITEMS} + onSelect={() => {}} + onClose={() => {}} + /> + const { rerender } = render(menu(false)) + const trigger = screen.getByRole('button', { name: 'trigger' }) + trigger.focus() + rerender(menu(true)) + const field = screen.getByRole('textbox', { name: 'Filter' }) + expect(document.activeElement).toBe(trigger) + field.focus() + expect(document.activeElement).toBe(field) + fireEvent.keyDown(field, { key: 'ArrowDown' }) + expect(document.activeElement).toBe(screen.getByRole('menuitem', { name: 'Alpha' })) + rerender(menu(false)) + trigger.focus() + rerender(menu(true)) + expect(document.activeElement).toBe(trigger) + }) + it('keeps the keyboard on the row when a placed portal list repositions', () => { render( ctx.locale.register(NS, { zh, en, ja }), 'ui-secret-mask: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-secret-mask: dictionaries') ctx.uiConversation.userText.register(createSecretRule()) ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({ name: 'conversation.user.text.node', key: 'secret', priority: 100, locale: NS, diff --git a/packages/client/ui-secret-mask/src/client/locales.ts b/packages/client/ui-secret-mask/src/client/locales.ts index 7bf3c0915b..2034471fe6 100644 --- a/packages/client/ui-secret-mask/src/client/locales.ts +++ b/packages/client/ui-secret-mask/src/client/locales.ts @@ -32,3 +32,33 @@ export const ja = { 'secret.reveal': 'シークレットを表示', 'secret.hide': 'シークレットを隠す', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'secret.reveal': 'Mostrar secreto', + 'secret.hide': 'Ocultar secreto', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'secret.reveal': 'Afficher le secret', + 'secret.hide': 'Cacher le secret', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'secret.reveal': 'Geheimnis zeigen', + 'secret.hide': 'Geheimnis verbergen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'secret.reveal': 'Показать секрет', + 'secret.hide': 'Скрыть секрет', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'secret.reveal': 'Mostrar segredo', + 'secret.hide': 'Ocultar segredo', +} satisfies Record diff --git a/packages/client/ui-session/locale/de.json b/packages/client/ui-session/locale/de.json new file mode 100644 index 0000000000..cae002c97b --- /dev/null +++ b/packages/client/ui-session/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Verbindung zur Sitzungsschnittstelle", + "description": "Verbinden Sie Sitzungssteuerung und Status mit React-Schnittstellenkomponenten." + } +} diff --git a/packages/client/ui-session/locale/es.json b/packages/client/ui-session/locale/es.json new file mode 100644 index 0000000000..0febc1d178 --- /dev/null +++ b/packages/client/ui-session/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conexión de interfaz de sesión", + "description": "Conecte el control y el estado de la sesión a los componentes de la interfaz de React." + } +} diff --git a/packages/client/ui-session/locale/fr.json b/packages/client/ui-session/locale/fr.json new file mode 100644 index 0000000000..e4622ab45d --- /dev/null +++ b/packages/client/ui-session/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Connexion à l'interface de session", + "description": "Connectez le contrôle de session et l’état aux composants de l’interface React." + } +} diff --git a/packages/client/ui-session/locale/pt.json b/packages/client/ui-session/locale/pt.json new file mode 100644 index 0000000000..c5a9625131 --- /dev/null +++ b/packages/client/ui-session/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conexão de interface de sessão", + "description": "Conecte o controle e o estado da sessão aos componentes da interface React." + } +} diff --git a/packages/client/ui-session/locale/ru.json b/packages/client/ui-session/locale/ru.json new file mode 100644 index 0000000000..c4df84e0a3 --- /dev/null +++ b/packages/client/ui-session/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Подключение интерфейса сеанса", + "description": "Подключите управление сеансом и состояние к компонентам интерфейса React." + } +} diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json index ace9d2c5d2..7eaf775e80 100644 --- a/packages/client/ui-session/package.json +++ b/packages/client/ui-session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-session", "description": "Session Controller adapter for React and session-scoped slots", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-account/locale/de.json b/packages/client/ui-settings-account/locale/de.json new file mode 100644 index 0000000000..1f7c9a6c99 --- /dev/null +++ b/packages/client/ui-settings-account/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Benutzeroberfläche für Kontoeinstellungen", + "description": "Melden Sie sich an, um DeepSeek zu verwalten und die Abrechnungsseite der Plattform zu öffnen." + } +} diff --git a/packages/client/ui-settings-account/locale/es.json b/packages/client/ui-settings-account/locale/es.json new file mode 100644 index 0000000000..de8904f3d3 --- /dev/null +++ b/packages/client/ui-settings-account/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración de cuenta", + "description": "Inicie sesión para administrar DeepSeek y abra la página de facturación de la plataforma." + } +} diff --git a/packages/client/ui-settings-account/locale/fr.json b/packages/client/ui-settings-account/locale/fr.json new file mode 100644 index 0000000000..6b64e79108 --- /dev/null +++ b/packages/client/ui-settings-account/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration du compte", + "description": "Connectez-vous pour gérer DeepSeek et ouvrez la page de facturation de la plateforme." + } +} diff --git a/packages/client/ui-settings-account/locale/pt.json b/packages/client/ui-settings-account/locale/pt.json new file mode 100644 index 0000000000..8a4619488e --- /dev/null +++ b/packages/client/ui-settings-account/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de conta", + "description": "Faça login para gerenciar DeepSeek e abra a página de faturamento da plataforma." + } +} diff --git a/packages/client/ui-settings-account/locale/ru.json b/packages/client/ui-settings-account/locale/ru.json new file mode 100644 index 0000000000..a1b2fddd9b --- /dev/null +++ b/packages/client/ui-settings-account/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки учетной записи", + "description": "Войдите в систему для управления DeepSeek и откройте страницу выставления счетов платформы." + } +} diff --git a/packages/client/ui-settings-account/package.json b/packages/client/ui-settings-account/package.json index 41b221656b..b8911c8b2a 100644 --- a/packages/client/ui-settings-account/package.json +++ b/packages/client/ui-settings-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-account", "description": "Manage DeepSeek login and open Platform billing pages", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-account/src/client/index.ts b/packages/client/ui-settings-account/src/client/index.ts index b70e5097a1..45f13a1b97 100644 --- a/packages/client/ui-settings-account/src/client/index.ts +++ b/packages/client/ui-settings-account/src/client/index.ts @@ -24,7 +24,7 @@ import { createPlatformPages, type PlatformPages } from './platform-pages.ts' import { AccountSection, type AccountSnapshot, type AccountSectionInjected } from './AccountSection.tsx' import { createBonusNoticeController } from './bonus-notices.ts' import { accountClientMetadata } from './client-metadata.ts' -import { en, ja, zh, type AccountKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AccountKey } from './locales.ts' import { AccountQuotaNotice, type AccountQuotaNoticeInjected } from './AccountQuotaNotice.tsx' import { DESKTOP_ONBOARDING_NAMESPACE, type OnboardingSettings } from '../onboarding-settings.ts' import { DesktopOnboardingController } from './onboarding-state.ts' @@ -46,7 +46,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.account', 'remote.se /** Register account UI in Desktop and plain Web clients. @param ctx - client plugin context. */ export function apply(ctx: Context): void { const desktop = 'dshDesktop' in globalThis - ctx.effect(() => ctx.locale.register('settings.account', { en, zh, ja }), 'account: dictionaries') + ctx.effect(() => ctx.locale.register('settings.account', { en, zh, ja, es, fr, de, ru, pt }), 'account: dictionaries') const t = ctx.locale.bind('settings.account') const page = globalThis as Partial> const config = ContactConfig(page[CONTACT_CONFIG_GLOBAL] ?? {}) diff --git a/packages/client/ui-settings-account/src/client/locales.ts b/packages/client/ui-settings-account/src/client/locales.ts index 5244c0743f..43ed4d17c1 100644 --- a/packages/client/ui-settings-account/src/client/locales.ts +++ b/packages/client/ui-settings-account/src/client/locales.ts @@ -1,5 +1,5 @@ /** Account settings copy, owned by the account feature. */ -import { onboardingZh, onboardingEn, onboardingJa } from './locales/onboarding.ts' +import { onboardingZh, onboardingEn, onboardingJa, onboardingEs, onboardingFr, onboardingDe, onboardingRu, onboardingPt } from './locales/onboarding.ts' /** English account dictionary. */ export const en = { @@ -131,3 +131,178 @@ export const ja = { 'quotaTopUp': 'チャージ', 'bonusNoticeTitle': 'ボーナスが付与されました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + modelSignInRequired: 'Modelo no disponible. Por favor inicia sesión y vuelve a intentarlo.', + sessionExpired: 'Has cerrado sesión en tu cuenta, inicia sesión nuevamente.', + ...onboardingEs, + close: 'Cerrar', addApiKey: 'Agregar clave API', retry: 'Iniciar sesión nuevamente', + loginTitle: 'empezar', loginDescription: 'Inicie sesión en su cuenta DeepSeek o agregue una clave API para comenzar. Sus proyectos y archivos se almacenan localmente.', + browserTitle: 'Esperando iniciar sesión', browserPrompt: '¿La página no se abrió automáticamente? ', copyLink: 'Copiar enlace de inicio de sesión', copiedLink: 'Enlace copiado', copyFailed: 'Copia fallida', + browserDescription: ', luego ábrelo en tu navegador para terminar de iniciar sesión.', + timeoutTitle: 'Se agotó el tiempo de inicio de sesión', timeoutDescription: 'Inicia sesión nuevamente para continuar.', + failureTitle: 'No se pudo iniciar sesión', + platformFailed: 'No se pudo completar la operación. Intentar otra vez.', platformRetry: 'Reintentar', + loading: 'Cargando…', backToHarness: 'Volver a DeepSeek Harness', + settings: 'Configuración', contactUs: 'Comentarios', menu: 'Menú de cuenta', + nav: 'Cuenta', signedIn: 'Iniciado sesión en DeepSeek', signedOut: 'No iniciado sesión', + signIn: 'Iniciar sesión', signOut: 'Cerrar sesión', + signOutUnknownDescription: 'No se pudieron verificar las tareas en ejecución. Cerrar sesión puede interrumpir las tareas que utilizan esta cuenta. ¿Cerrar sesión ahora?', + signOutDescription: 'Cerrar sesión no eliminará ningún dato. Puede iniciar sesión en esta cuenta nuevamente.', + signOutRunningDescription: 'Las tareas se están ejecutando actualmente. Cerrar sesión los interrumpirá. ¿Cerrar sesión ahora?', cancel: 'Cancelar', open: 'Abrir navegador', + initializing: 'Iniciando sesión...', waiting: 'Continuar en tu navegador', + completing: 'Completando el inicio de sesión…', expired: 'Iniciar sesión caducado. Intentar otra vez.', + failed: 'No se pudo completar la operación. Intentar otra vez.', + noResponse: 'Algo salió mal. Verifique su conexión de red e inténtelo nuevamente.', + settingsSignedOutTitle: 'No has iniciado sesión en DeepSeek Harness', + settingsSignedOutDescription: 'Inicie sesión en DeepSeek Harness para obtener su clave API dedicada', + signInDescription: 'Utilice su cuenta DeepSeek para comenzar.', + profileUnavailable: 'Los detalles de la cuenta aún no están disponibles.', + balance: 'Saldo recargado', bonusBalance: 'Saldo concedido', balanceUnavailable: 'Ver en plataforma', balanceSignedOut: 'Inicia sesión para ver', + accountInfo: 'Más información de la cuenta', more: 'Más', usage: 'Ver uso', topUp: 'Recargar', + quotaTitle: 'No hay saldo disponible', + quotaDescription: 'DeepSeek Harness no puede iniciar una nueva tarea con esta cuenta cuando no hay saldo disponible. ¿Quieres recargar? También puedes recargar más tarde en Configuración → Cuenta.', + quotaTopUp: 'Recargar', + bonusNoticeTitle: 'Bono acreditado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + modelSignInRequired: 'Modèle indisponible. Veuillez vous connecter et réessayer.', + sessionExpired: 'Vous vous êtes déconnecté de votre compte, veuillez vous reconnecter.', + ...onboardingFr, + close: 'Fermer', addApiKey: 'Ajouter une clé API', retry: 'Connectez-vous à nouveau', + loginTitle: 'Commencez', loginDescription: 'Connectez-vous à votre compte DeepSeek ou ajoutez une clé API pour commencer. Vos projets et fichiers sont stockés localement.', + browserTitle: 'En attente de connexion', browserPrompt: "La page ne s'est pas ouverte automatiquement ? ", copyLink: 'Copier le lien de connexion', copiedLink: 'Lien copié', copyFailed: 'Échec de la copie', + browserDescription: ', puis ouvrez-le dans votre navigateur pour terminer la connexion.', + timeoutTitle: 'La connexion a expiré', timeoutDescription: 'Connectez-vous à nouveau pour continuer.', + failureTitle: 'Impossible de se connecter', + platformFailed: "Impossible de terminer l'opération. Essayer à nouveau.", platformRetry: 'Réessayer', + loading: 'Chargement…', backToHarness: 'Retour à DeepSeek Harness', + settings: 'Paramètres', contactUs: 'Commentaires', menu: 'Menu du compte', + nav: 'Compte', signedIn: 'Connecté à DeepSeek', signedOut: 'Non connecté', + signIn: 'Connectez-vous', signOut: 'Se déconnecter', + signOutUnknownDescription: "Impossible de vérifier les tâches en cours d'exécution. La déconnexion peut interrompre les tâches utilisant ce compte. Se déconnecter maintenant ?", + signOutDescription: 'La déconnexion ne supprimera aucune donnée. Vous pouvez vous reconnecter à ce compte.', + signOutRunningDescription: "Les tâches sont actuellement en cours d'exécution. La déconnexion les interrompra. Se déconnecter maintenant ?", cancel: 'Annuler', open: 'Ouvrir le navigateur', + initializing: 'Début de la connexion…', waiting: 'Continuez dans votre navigateur', + completing: 'Connexion terminée…', expired: 'Connexion expirée. Essayer à nouveau.', + failed: "Impossible de terminer l'opération. Essayer à nouveau.", + noResponse: "Quelque chose s'est mal passé. Veuillez vérifier votre connexion réseau et réessayer.", + settingsSignedOutTitle: "Vous n'êtes pas connecté à DeepSeek Harness", + settingsSignedOutDescription: 'Connectez-vous à DeepSeek Harness pour obtenir votre clé API dédiée', + signInDescription: 'Utilisez votre compte DeepSeek pour commencer.', + profileUnavailable: 'Les détails du compte ne sont pas encore disponibles.', + balance: 'Solde rechargé', bonusBalance: 'Solde accordé', balanceUnavailable: 'Afficher sur la plateforme', balanceSignedOut: 'Connectez-vous pour voir', + accountInfo: "Plus d'informations sur le compte", more: 'Plus', usage: "Afficher l'utilisation", topUp: 'Recharger', + quotaTitle: 'Aucun solde disponible', + quotaDescription: "DeepSeek Harness ne peut pas démarrer une nouvelle tâche avec ce compte lorsqu'aucun solde n'est disponible. Souhaitez-vous recharger ? Vous pouvez également recharger plus tard dans Paramètres → Compte.", + quotaTopUp: 'Recharger', + bonusNoticeTitle: 'Bonus crédité', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + modelSignInRequired: 'Modell nicht verfügbar. Bitte melden Sie sich an und versuchen Sie es erneut.', + sessionExpired: 'Sie haben sich von Ihrem Konto abgemeldet. Bitte melden Sie sich erneut an.', + ...onboardingDe, + close: 'Schließen', addApiKey: 'Fügen Sie den API-Schlüssel hinzu', retry: 'Melden Sie sich erneut an', + loginTitle: 'Fangen Sie an', loginDescription: 'Melden Sie sich bei Ihrem DeepSeek-Konto an oder fügen Sie einen API-Schlüssel hinzu, um loszulegen. Ihre Projekte und Dateien werden lokal gespeichert.', + browserTitle: 'Warten auf die Anmeldung', browserPrompt: 'Seite wurde nicht automatisch geöffnet? ', copyLink: 'Anmeldelink kopieren', copiedLink: 'Link kopiert', copyFailed: 'Das Kopieren ist fehlgeschlagen', + browserDescription: ', und öffnen Sie es dann in Ihrem Browser, um die Anmeldung abzuschließen.', + timeoutTitle: 'Zeitüberschreitung bei der Anmeldung', timeoutDescription: 'Melden Sie sich erneut an, um fortzufahren.', + failureTitle: 'Konnte mich nicht anmelden', + platformFailed: 'Der Vorgang konnte nicht abgeschlossen werden. Versuchen Sie es erneut.', platformRetry: 'Erneut versuchen', + loading: 'Laden…', backToHarness: 'Zurück zu DeepSeek Harness', + settings: 'Einstellungen', contactUs: 'Rückmeldung', menu: 'Kontomenü', + nav: 'Konto', signedIn: 'Angemeldet bei DeepSeek', signedOut: 'Nicht angemeldet', + signIn: 'Melden Sie sich an', signOut: 'Abmelden', + signOutUnknownDescription: 'Laufende Aufgaben konnten nicht überprüft werden. Durch das Abmelden werden möglicherweise Aufgaben mit diesem Konto unterbrochen. Jetzt abmelden?', + signOutDescription: 'Durch das Abmelden werden keine Daten gelöscht. Sie können sich erneut bei diesem Konto anmelden.', + signOutRunningDescription: 'Derzeit werden Aufgaben ausgeführt. Durch das Abmelden werden sie unterbrochen. Jetzt abmelden?', cancel: 'Abbrechen', open: 'Browser öffnen', + initializing: 'Anmeldung beginnt…', waiting: 'Fahren Sie in Ihrem Browser fort', + completing: 'Anmeldung wird abgeschlossen…', expired: 'Anmeldung abgelaufen. Versuchen Sie es erneut.', + failed: 'Der Vorgang konnte nicht abgeschlossen werden. Versuchen Sie es erneut.', + noResponse: 'Etwas ist schief gelaufen. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.', + settingsSignedOutTitle: 'Sie sind nicht bei DeepSeek Harness angemeldet', + settingsSignedOutDescription: 'Melden Sie sich bei DeepSeek Harness an, um Ihren dedizierten API-Schlüssel zu erhalten', + signInDescription: 'Verwenden Sie Ihr DeepSeek-Konto, um loszulegen.', + profileUnavailable: 'Kontodaten sind noch nicht verfügbar.', + balance: 'Aufgeladenes Guthaben', bonusBalance: 'Gewährte Balance', balanceUnavailable: 'Auf der Plattform ansehen', balanceSignedOut: 'Melden Sie sich an, um es anzuzeigen', + accountInfo: 'Weitere Kontoinformationen', more: 'Mehr', usage: 'Nutzung anzeigen', topUp: 'Auffüllen', + quotaTitle: 'Kein Guthaben verfügbar', + quotaDescription: 'DeepSeek Harness kann mit diesem Konto keine neue Aufgabe starten, wenn kein Guthaben verfügbar ist. Möchten Sie aufladen? Sie können das Guthaben auch später unter Einstellungen → Konto aufladen.', + quotaTopUp: 'Auffüllen', + bonusNoticeTitle: 'Bonus gutgeschrieben', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + modelSignInRequired: 'Модель недоступна. Пожалуйста, войдите в систему и повторите попытку.', + sessionExpired: 'Вы вышли из своей учетной записи, пожалуйста, войдите снова.', + ...onboardingRu, + close: 'Закрыть', addApiKey: 'Добавить ключ API', retry: 'Войдите снова', + loginTitle: 'Начать', loginDescription: 'Войдите в свою учетную запись DeepSeek или добавьте ключ API, чтобы начать. Ваши проекты и файлы хранятся локально.', + browserTitle: 'Ожидание входа в систему', browserPrompt: 'Страница не открылась автоматически? ', copyLink: 'Скопировать ссылку для входа', copiedLink: 'Ссылка скопирована', copyFailed: 'Не удалось скопировать', + browserDescription: ', затем откройте его в браузере, чтобы завершить вход.', + timeoutTitle: 'Время входа в систему истекло', timeoutDescription: 'Войдите еще раз, чтобы продолжить.', + failureTitle: 'Не удалось войти в систему', + platformFailed: 'Не удалось завершить операцию. Попробуйте еще раз.', platformRetry: 'Повторить', + loading: 'Загрузка…', backToHarness: 'Вернуться к DeepSeek Harness', + settings: 'Настройки', contactUs: 'Обратная связь', menu: 'Меню аккаунта', + nav: 'Аккаунт', signedIn: 'Вошёл в DeepSeek', signedOut: 'Не авторизован', + signIn: 'Войти', signOut: 'Выйти', + signOutUnknownDescription: 'Не удалось проверить запущенные задачи. Выход из системы может прервать выполнение задач с использованием этой учетной записи. Выйти сейчас?', + signOutDescription: 'Выход из системы не приведет к удалению данных. Вы можете снова войти в эту учетную запись.', + signOutRunningDescription: 'Задачи в настоящее время выполняются. Выход из системы прервет их. Выйти сейчас?', cancel: 'Отмена', open: 'Открыть браузер', + initializing: 'Начинаем вход…', waiting: 'Продолжить в браузере', + completing: 'Завершение входа…', expired: 'Срок действия входа истек. Попробуйте еще раз.', + failed: 'Не удалось завершить операцию. Попробуйте еще раз.', + noResponse: 'Что-то пошло не так. Пожалуйста, проверьте сетевое соединение и повторите попытку.', + settingsSignedOutTitle: 'Вы не вошли в DeepSeek Harness.', + settingsSignedOutDescription: 'Войдите в DeepSeek Harness, чтобы получить специальный ключ API.', + signInDescription: 'Используйте свою учетную запись DeepSeek, чтобы начать.', + profileUnavailable: 'Детали аккаунта пока недоступны.', + balance: 'Пополненный баланс', bonusBalance: 'Предоставленный баланс', balanceUnavailable: 'Посмотреть на платформе', balanceSignedOut: 'Войдите, чтобы просмотреть', + accountInfo: 'Дополнительная информация об аккаунте', more: 'Ещё', usage: 'Посмотреть использование', topUp: 'Пополнить', + quotaTitle: 'Нет доступного баланса', + quotaDescription: 'DeepSeek Harness не может запустить новую задачу с этой учетной записью, если на ней нет баланса. Хотите пополнить счет? Вы также можете пополнить счет позже в «Настройки» → «Аккаунт».', + quotaTopUp: 'Пополнить', + bonusNoticeTitle: 'Бонус начислен', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + modelSignInRequired: 'Modelo indisponível. Faça login e tente novamente.', + sessionExpired: 'Você saiu da sua conta, faça login novamente.', + ...onboardingPt, + close: 'Fechar', addApiKey: 'Adicionar chave API', retry: 'Faça login novamente', + loginTitle: 'Comece', loginDescription: 'Faça login em sua conta DeepSeek ou adicione uma chave API para começar. Seus projetos e arquivos são armazenados localmente.', + browserTitle: 'Aguardando login', browserPrompt: 'A página não abriu automaticamente? ', copyLink: 'Copiar link de login', copiedLink: 'Link copiado', copyFailed: 'Falha na cópia', + browserDescription: 'e abra-o no navegador para concluir o login.', + timeoutTitle: 'O tempo limite de login expirou', timeoutDescription: 'Faça login novamente para continuar.', + failureTitle: 'Não foi possível fazer login', + platformFailed: 'Não foi possível concluir a operação. Tente novamente.', platformRetry: 'Tentar novamente', + loading: 'Carregando…', backToHarness: 'Voltar para DeepSeek Harness', + settings: 'Configurações', contactUs: 'Comentários', menu: 'Menu da conta', + nav: 'Conta', signedIn: 'Conectado em DeepSeek', signedOut: 'Não conectado', + signIn: 'Faça login', signOut: 'Sair', + signOutUnknownDescription: 'Não foi possível verificar as tarefas em execução. Sair pode interromper tarefas usando esta conta. Sair agora?', + signOutDescription: 'Sair não excluirá nenhum dado. Você pode fazer login nesta conta novamente.', + signOutRunningDescription: 'As tarefas estão em execução no momento. Sair irá interrompê-los. Sair agora?', cancel: 'Cancelar', open: 'Abra o navegador', + initializing: 'Iniciando login…', waiting: 'Continue no seu navegador', + completing: 'Concluindo login…', expired: 'O login expirou. Tente novamente.', + failed: 'Não foi possível concluir a operação. Tente novamente.', + noResponse: 'Algo deu errado. Verifique sua conexão de rede e tente novamente.', + settingsSignedOutTitle: 'Você não está conectado a DeepSeek Harness', + settingsSignedOutDescription: 'Faça login em DeepSeek Harness para obter sua chave API dedicada', + signInDescription: 'Use sua conta DeepSeek para começar.', + profileUnavailable: 'Os detalhes da conta ainda não estão disponíveis.', + balance: 'Saldo reabastecido', bonusBalance: 'Saldo concedido', balanceUnavailable: 'Ver na plataforma', balanceSignedOut: 'Faça login para ver', + accountInfo: 'Mais informações da conta', more: 'Mais', usage: 'Ver uso', topUp: 'Recarregar', + quotaTitle: 'Nenhum saldo disponível', + quotaDescription: 'DeepSeek Harness não pode iniciar uma nova tarefa com esta conta quando não houver saldo disponível. Você gostaria de recarregar? Você também pode recarregar mais tarde em Configurações → Conta.', + quotaTopUp: 'Recarregar', + bonusNoticeTitle: 'Bônus creditado', +} satisfies Record diff --git a/packages/client/ui-settings-account/src/client/locales/onboarding.ts b/packages/client/ui-settings-account/src/client/locales/onboarding.ts index 977837e933..2fbf063a28 100644 --- a/packages/client/ui-settings-account/src/client/locales/onboarding.ts +++ b/packages/client/ui-settings-account/src/client/locales/onboarding.ts @@ -126,3 +126,218 @@ export const onboardingJa = { 'onboardingRetry': '再試行', 'onboardingLoading': '設定を読み込んでいます…', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const onboardingEs = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bienvenido a', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness funciona en una carpeta local y utiliza herramientas para leer y escribir archivos en su computadora. Puede ayudarle a investigar y organizar información, crear documentos y hojas de cálculo, escribir código, solucionar problemas y más.', + onboardingStart: 'empezar', + onboardingCredit: 'Añadir créditos', + onboardingCreditDescription: 'DeepSeek Harness cobra créditos en función de los tokens utilizados por los modelos y herramientas. Agregue créditos por adelantado para mantener sus tareas ejecutándose sin interrupciones. Su saldo solo se utiliza mientras el agente está trabajando en una tarea.', + onboardingTopUp: 'Añadir créditos', + onboardingLater: 'Siguiente', + onboardingFundedTopUp: 'Añadir créditos', + onboardingPurposePrefix: '¿En qué quieres que ', + onboardingPurposeSuffix: ' te ayude?', + onboardingPurposeDescription: 'Adaptaremos la interfaz y las herramientas para que se adapten a su forma de trabajar.', + onboardingOffice: 'Oficina y trabajo creativo', + onboardingOfficeDescription: 'Edite documentos, organice datos, cree presentaciones y más', + onboardingDevelopment: 'Codificación y desarrollo', + onboardingDevelopmentDescription: 'Edite código, depure problemas, ejecute comandos, administre archivos de proyectos y más', + onboardingContinue: 'Continuar', + onboardingProcess: '¿Cuántos detalles te gustaría ver?', + onboardingProcessDescription: 'Esto solo cambia cómo se muestra el progreso, no lo que puede hacer DeepSeek Harness.', + onboardingCompact: 'Sólo resultados', + onboardingCompactDescription: 'Vea solo los resultados en una interfaz limpia y sencilla', + onboardingStandard: 'Detalles clave', + onboardingStandardDescription: 'Centrarse en los resultados, mostrando solo los pasos y acciones más importantes.', + onboardingDetailed: 'Detalles completos', + onboardingDetailedDescription: 'Vea el proceso completo para facilitar la depuración y la resolución de problemas', + onboardingEnter: 'Abrir aplicación', + onboardingBack: 'Atrás', + onboardingSkip: 'Saltar', + onboardingSkipTitle: '¿Saltar configuración?', + onboardingSkipDescription: 'Puede cambiar cómo se muestran el progreso, el rendimiento y el uso, o habilitar las herramientas de codificación, en cualquier momento en Configuración → General.', + onboardingKeepSetting: 'Continuar con la configuración', + onboardingNoCreditTitle: '¿Omitir agregar créditos?', + onboardingNoCreditDescription: 'DeepSeek Harness no puede iniciar nuevas tareas sin créditos. Puede agregarlos más tarde en Configuración → Cuenta.', + onboardingUnderstood: 'Lo tengo', + onboardingGoTopUp: 'Añadir créditos', + onboardingSaveFailed: 'No se pudo guardar la configuración. Intentar otra vez.', + onboardingRetry: 'Reintentar', + onboardingLoading: 'Cargando configuración…', +} satisfies typeof onboardingEn + +/** French dictionary, checked against the English keys. */ +export const onboardingFr = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bienvenue à', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness fonctionne dans un dossier local et utilise des outils pour lire et écrire des fichiers sur votre ordinateur. Il peut vous aider à rechercher et à organiser des informations, à créer des documents et des feuilles de calcul, à écrire du code, à résoudre des problèmes, etc.', + onboardingStart: 'Commencez', + onboardingCredit: 'Ajouter des crédits', + onboardingCreditDescription: "DeepSeek Harness facture des crédits en fonction des jetons utilisés par les modèles et les outils. Ajoutez des crédits à l'avance pour que vos tâches continuent de s'exécuter sans interruption. Votre solde n'est utilisé que lorsque l'agent travaille sur une tâche.", + onboardingTopUp: 'Ajouter des crédits', + onboardingLater: 'Suivant', + onboardingFundedTopUp: 'Ajouter des crédits', + onboardingPurposePrefix: 'Que souhaitez-vous que ', + onboardingPurposeSuffix: ' fasse pour vous ?', + onboardingPurposeDescription: 'Nous adapterons l’interface et les outils à votre façon de travailler.', + onboardingOffice: 'Bureau et travail créatif', + onboardingOfficeDescription: 'Modifiez des documents, organisez des données, créez des présentations et bien plus encore', + onboardingDevelopment: 'Codage et développement', + onboardingDevelopmentDescription: 'Modifiez le code, déboguez les problèmes, exécutez des commandes, gérez les fichiers de projet, etc.', + onboardingContinue: 'Continuer', + onboardingProcess: 'Combien de détails aimeriez-vous voir ?', + onboardingProcessDescription: 'Cela change uniquement la façon dont la progression est affichée, pas ce que DeepSeek Harness peut faire.', + onboardingCompact: 'Résultats uniquement', + onboardingCompactDescription: 'Visualisez uniquement les résultats dans une interface claire et simple', + onboardingStandard: 'Détails clés', + onboardingStandardDescription: 'Concentrez-vous sur les résultats, en affichant uniquement les étapes et actions les plus importantes.', + onboardingDetailed: 'Tous les détails', + onboardingDetailedDescription: 'Consultez le processus complet pour faciliter le débogage et le dépannage', + onboardingEnter: "Ouvrir l'application", + onboardingBack: 'Retour', + onboardingSkip: 'Sauter', + onboardingSkipTitle: 'Ignorer la configuration ?', + onboardingSkipDescription: "Vous pouvez modifier la façon dont la progression, les performances et l'utilisation sont affichées, ou activer les outils de codage, à tout moment dans Paramètres → Général.", + onboardingKeepSetting: 'Continuer la configuration', + onboardingNoCreditTitle: "Ignorer l'ajout de crédits ?", + onboardingNoCreditDescription: 'DeepSeek Harness ne peut pas démarrer de nouvelles tâches sans crédits. Vous pourrez les ajouter plus tard dans Paramètres → Compte.', + onboardingUnderstood: "Je l'ai compris", + onboardingGoTopUp: 'Ajouter des crédits', + onboardingSaveFailed: "Impossible d'enregistrer les paramètres. Essayer à nouveau.", + onboardingRetry: 'Réessayer', + onboardingLoading: 'Chargement des paramètres…', +} satisfies typeof onboardingEn + +/** German dictionary, checked against the English keys. */ +export const onboardingDe = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Willkommen bei', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness arbeitet in einem lokalen Ordner und verwendet Tools zum Lesen und Schreiben von Dateien auf Ihrem Computer. Es kann Ihnen dabei helfen, Informationen zu recherchieren und zu organisieren, Dokumente und Tabellenkalkulationen zu erstellen, Code zu schreiben, Probleme zu beheben und vieles mehr.', + onboardingStart: 'Fangen Sie an', + onboardingCredit: 'Credits hinzufügen', + onboardingCreditDescription: 'DeepSeek Harness berechnet Credits basierend auf den von Modellen und Tools verwendeten Token. Fügen Sie Credits im Voraus hinzu, damit Ihre Aufgaben ohne Unterbrechung ausgeführt werden können. Ihr Guthaben wird nur verwendet, während der Agent an einer Aufgabe arbeitet.', + onboardingTopUp: 'Credits hinzufügen', + onboardingLater: 'Weiter', + onboardingFundedTopUp: 'Credits hinzufügen', + onboardingPurposePrefix: 'Wobei soll ', + onboardingPurposeSuffix: ' Ihnen helfen?', + onboardingPurposeDescription: 'Wir passen die Benutzeroberfläche und die Tools an Ihre Arbeitsweise an.', + onboardingOffice: 'Büro & kreative Arbeit', + onboardingOfficeDescription: 'Bearbeiten Sie Dokumente, organisieren Sie Daten, erstellen Sie Präsentationen und mehr', + onboardingDevelopment: 'Codierung und Entwicklung', + onboardingDevelopmentDescription: 'Bearbeiten Sie Code, debuggen Sie Probleme, führen Sie Befehle aus, verwalten Sie Projektdateien und mehr', + onboardingContinue: 'Weiter', + onboardingProcess: 'Wie viele Details möchten Sie sehen?', + onboardingProcessDescription: 'Dies ändert nur die Art und Weise, wie der Fortschritt angezeigt wird, nicht jedoch, was DeepSeek Harness tun kann.', + onboardingCompact: 'Nur Ergebnisse', + onboardingCompactDescription: 'Sehen Sie nur die Ergebnisse in einer übersichtlichen, einfachen Benutzeroberfläche', + onboardingStandard: 'Wichtige Details', + onboardingStandardDescription: 'Konzentrieren Sie sich auf die Ergebnisse und zeigen Sie nur die wichtigsten Schritte und Aktionen', + onboardingDetailed: 'Ausführliche Informationen', + onboardingDetailedDescription: 'Sehen Sie sich den vollständigen Prozess an, um das Debuggen und die Fehlerbehebung zu vereinfachen', + onboardingEnter: 'App öffnen', + onboardingBack: 'Zurück', + onboardingSkip: 'Überspringen', + onboardingSkipTitle: 'Einrichtung überspringen?', + onboardingSkipDescription: 'Sie können jederzeit unter Einstellungen → Allgemein ändern, wie Fortschritt, Leistung und Nutzung angezeigt werden, oder Codierungstools aktivieren.', + onboardingKeepSetting: 'Setzen Sie die Einrichtung fort', + onboardingNoCreditTitle: 'Hinzufügen von Credits überspringen?', + onboardingNoCreditDescription: 'DeepSeek Harness kann ohne Credits keine neuen Aufgaben starten. Sie können sie später unter Einstellungen → Konto hinzufügen.', + onboardingUnderstood: 'Verstanden', + onboardingGoTopUp: 'Credits hinzufügen', + onboardingSaveFailed: 'Die Einstellungen konnten nicht gespeichert werden. Versuchen Sie es erneut.', + onboardingRetry: 'Erneut versuchen', + onboardingLoading: 'Einstellungen werden geladen…', +} satisfies typeof onboardingEn + +/** Russian dictionary, checked against the English keys. */ +export const onboardingRu = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Добро пожаловать в', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness работает в локальной папке и использует инструменты для чтения и записи файлов на вашем компьютере. Он может помочь вам исследовать и систематизировать информацию, создавать документы и таблицы, писать код, устранять неполадки и многое другое.', + onboardingStart: 'Начать', + onboardingCredit: 'Добавить кредиты', + onboardingCreditDescription: 'DeepSeek Harness взимает кредиты на основе токенов, используемых моделями и инструментами. Добавляйте кредиты заранее, чтобы ваши задачи выполнялись без перебоев. Ваш баланс используется только пока агент работает над задачей.', + onboardingTopUp: 'Добавить кредиты', + onboardingLater: 'Далее', + onboardingFundedTopUp: 'Добавить кредиты', + onboardingPurposePrefix: 'С чем ', + onboardingPurposeSuffix: ' должен вам помочь?', + onboardingPurposeDescription: 'Мы адаптируем интерфейс и инструменты под ваш стиль работы.', + onboardingOffice: 'Офис и творческая работа', + onboardingOfficeDescription: 'Редактируйте документы, систематизируйте данные, создавайте презентации и многое другое.', + onboardingDevelopment: 'Кодирование и разработка', + onboardingDevelopmentDescription: 'Редактируйте код, устраняйте проблемы, запускайте команды, управляйте файлами проекта и многое другое.', + onboardingContinue: 'Продолжить', + onboardingProcess: 'Насколько подробно показывать процесс работы?', + onboardingProcessDescription: 'Это меняет только способ отображения прогресса, а не возможности DeepSeek Harness.', + onboardingCompact: 'Только результаты', + onboardingCompactDescription: 'Смотрите только результаты в понятном и простом интерфейсе', + onboardingStandard: 'Ключевые детали', + onboardingStandardDescription: 'Сосредоточьтесь на результатах, показывая только самые важные шаги и действия.', + onboardingDetailed: 'Полная информация', + onboardingDetailedDescription: 'Ознакомьтесь с полным процессом для упрощения отладки и устранения неполадок.', + onboardingEnter: 'Открыть приложение', + onboardingBack: 'Назад', + onboardingSkip: 'Пропустить', + onboardingSkipTitle: 'Пропустить настройку?', + onboardingSkipDescription: 'Вы можете изменить способ отображения прогресса, производительности и использования или включить инструменты программирования в любое время в «Настройки» → «Основные».', + onboardingKeepSetting: 'Продолжить настройку', + onboardingNoCreditTitle: 'Пропустить добавление кредитов?', + onboardingNoCreditDescription: 'DeepSeek Harness не может начинать новые задачи без кредитов. Вы можете добавить их позже в «Настройки» → «Аккаунт».', + onboardingUnderstood: 'понял', + onboardingGoTopUp: 'Добавить кредиты', + onboardingSaveFailed: 'Не удалось сохранить настройки. Попробуйте еще раз.', + onboardingRetry: 'Повторить', + onboardingLoading: 'Загрузка настроек…', +} satisfies typeof onboardingEn + +/** Portuguese dictionary, checked against the English keys. */ +export const onboardingPt = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bem-vindo ao', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness funciona em uma pasta local e usa ferramentas para ler e gravar arquivos no seu computador. Ele pode ajudá-lo a pesquisar e organizar informações, criar documentos e planilhas, escrever códigos, solucionar problemas e muito mais.', + onboardingStart: 'Comece', + onboardingCredit: 'Adicionar créditos', + onboardingCreditDescription: 'DeepSeek Harness cobra créditos com base nos tokens usados pelos modelos e ferramentas. Adicione créditos antecipadamente para manter suas tarefas funcionando sem interrupção. Seu saldo só é usado enquanto o agente está trabalhando em uma tarefa.', + onboardingTopUp: 'Adicionar créditos', + onboardingLater: 'Próximo', + onboardingFundedTopUp: 'Adicionar créditos', + onboardingPurposePrefix: 'Em que você quer que ', + onboardingPurposeSuffix: ' ajude?', + onboardingPurposeDescription: 'Adaptaremos a interface e as ferramentas à sua maneira de trabalhar.', + onboardingOffice: 'Escritório e trabalho criativo', + onboardingOfficeDescription: 'Edite documentos, organize dados, crie apresentações e muito mais', + onboardingDevelopment: 'Codificação e desenvolvimento', + onboardingDevelopmentDescription: 'Edite código, depure problemas, execute comandos, gerencie arquivos de projeto e muito mais', + onboardingContinue: 'Continuar', + onboardingProcess: 'Quantos detalhes você gostaria de ver?', + onboardingProcessDescription: 'Isso altera apenas a forma como o progresso é mostrado, e não o que DeepSeek Harness pode fazer.', + onboardingCompact: 'Somente resultados', + onboardingCompactDescription: 'Veja apenas os resultados em uma interface limpa e simples', + onboardingStandard: 'Detalhes principais', + onboardingStandardDescription: 'Foco nos resultados, mostrando apenas as etapas e ações mais importantes', + onboardingDetailed: 'Detalhes completos', + onboardingDetailedDescription: 'Veja o processo completo para depuração e solução de problemas mais fáceis', + onboardingEnter: 'Abrir aplicativo', + onboardingBack: 'Voltar', + onboardingSkip: 'Pular', + onboardingSkipTitle: 'Ignorar configuração?', + onboardingSkipDescription: 'Você pode alterar a forma como o progresso, o desempenho e o uso são exibidos ou ativar as ferramentas de codificação a qualquer momento em Configurações → Geral.', + onboardingKeepSetting: 'Continuar configuração', + onboardingNoCreditTitle: 'Pular a adição de créditos?', + onboardingNoCreditDescription: 'DeepSeek Harness não pode iniciar novas tarefas sem créditos. Você pode adicioná-los posteriormente em Configurações → Conta.', + onboardingUnderstood: 'Entendi', + onboardingGoTopUp: 'Adicionar créditos', + onboardingSaveFailed: 'Não foi possível salvar as configurações. Tente novamente.', + onboardingRetry: 'Tentar novamente', + onboardingLoading: 'Carregando configurações…', +} satisfies typeof onboardingEn diff --git a/packages/client/ui-settings-agent-loop/locale/de.json b/packages/client/ui-settings-agent-loop/locale/de.json new file mode 100644 index 0000000000..10d2a14a6b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen für die Ausführung des Agenten", + "description": "Konfigurieren Sie die maximale Anzahl an Werkzeugaufrufen, die gleichzeitig ausgeführt werden können." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/es.json b/packages/client/ui-settings-agent-loop/locale/es.json new file mode 100644 index 0000000000..816970df3b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de ejecución del agente", + "description": "Configure el número máximo de llamadas a herramientas que se pueden ejecutar simultáneamente." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/fr.json b/packages/client/ui-settings-agent-loop/locale/fr.json new file mode 100644 index 0000000000..0ac781ec2b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres d'exécution de l'agent", + "description": "Configurez le nombre maximum d'appels d'outils pouvant être exécutés simultanément." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/pt.json b/packages/client/ui-settings-agent-loop/locale/pt.json new file mode 100644 index 0000000000..72655d1257 --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de execução do agente", + "description": "Configure o número máximo de chamadas de ferramenta que podem ser executadas simultaneamente." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/ru.json b/packages/client/ui-settings-agent-loop/locale/ru.json new file mode 100644 index 0000000000..d07509407c --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки запуска агента", + "description": "Настройте максимальное количество вызовов инструментов, которые могут выполняться одновременно." + } +} diff --git a/packages/client/ui-settings-agent-loop/package.json b/packages/client/ui-settings-agent-loop/package.json index 81dd20ded1..46cff84c48 100644 --- a/packages/client/ui-settings-agent-loop/package.json +++ b/packages/client/ui-settings-agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-agent-loop", "description": "Settings page of the agent loop on the dsh web client's Plugins page: the parallel tool-call cap of the agent-loop namespace", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-agent-loop/src/client/index.ts b/packages/client/ui-settings-agent-loop/src/client/index.ts index 29ebe0e132..1e5e796bc3 100644 --- a/packages/client/ui-settings-agent-loop/src/client/index.ts +++ b/packages/client/ui-settings-agent-loop/src/client/index.ts @@ -16,7 +16,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { AgentLoopCard } from './AgentLoopCard.tsx' import { AGENT_LOOP_NS, AgentLoopCardController } from './agent-loop-card-controller.ts' -import { en, ja, zh, type AgentLoopSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AgentLoopSettingsLocaleKey } from './locales.ts' export type { AgentLoopCardProps } from './AgentLoopCard.tsx' export type { AgentLoopCardFace, AgentLoopCardState, AgentLoopSettings } from './agent-loop-card-controller.ts' @@ -40,8 +40,8 @@ export const inject = ['slots', 'locale', 'configForms'] * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-agent-loop: dictionaries') const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-agent-loop: dictionaries') const card = new AgentLoopCardController(ctx.configForms.get(AGENT_LOOP_NS)) ctx.effect(() => () => { card.dispose() }, 'ui-settings-agent-loop: form subscription') ctx.effect(() => ctx.configForms.whileServed([AGENT_LOOP_NS], () => ctx.slots.inject('plugins.item', () => ctx.slots.register({ diff --git a/packages/client/ui-settings-agent-loop/src/client/locales.ts b/packages/client/ui-settings-agent-loop/src/client/locales.ts index e00a191c35..e9b79dd83f 100644 --- a/packages/client/ui-settings-agent-loop/src/client/locales.ts +++ b/packages/client/ui-settings-agent-loop/src/client/locales.ts @@ -64,3 +64,83 @@ export const ja = { 'saveFailed': 'デプロイがこれらの値を受け付けませんでした。修正のために残されています。', 'invalidNumber': '数値を入力するか、空欄のままにするとデフォルトが使用されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Bucle de agente', + description: 'Controle cómo el Agente distribuye las llamadas a la herramienta.', + maxParallel: 'Llamadas a herramientas paralelas', + maxParallelHint: 'Límite superior de llamadas seguras en paralelo que se ejecutan al mismo tiempo en un solo paso.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: "Boucle d'agent", + description: "Contrôlez la manière dont l'agent distribue les appels d'outil.", + maxParallel: "Appels d'outils parallèles", + maxParallelHint: 'Limite supérieure des appels parallèles sécurisés exécutés simultanément en une seule étape.', + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Agentenschleife', + description: 'Steuern Sie, wie der Agent Toolaufrufe verteilt.', + maxParallel: 'Parallele Werkzeugaufrufe', + maxParallelHint: 'Obergrenze für parallelsichere Aufrufe, die gleichzeitig innerhalb eines Schritts ausgeführt werden.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Цикл агента', + description: 'Управляйте тем, как агент распределяет вызовы инструментов.', + maxParallel: 'Параллельные вызовы инструментов', + maxParallelHint: 'Верхняя граница параллельных безопасных вызовов, выполняемых одновременно в течение одного шага.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Loop de agente', + description: 'Controle como o Agente despacha chamadas de ferramenta.', + maxParallel: 'Chamadas de ferramentas paralelas', + maxParallelHint: 'Limite superior em chamadas paralelas seguras executadas ao mesmo tempo em uma etapa.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings-general/locale/de.json b/packages/client/ui-settings-general/locale/de.json new file mode 100644 index 0000000000..e2d9ba29cd --- /dev/null +++ b/packages/client/ui-settings-general/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Allgemeine Einstellungen und Willkommenstipps", + "description": "Bietet allgemeine Einstellungen, Einstellungsfenster zum Verfassen von Texten und Tipps für die erstmalige Verwendung." + } +} diff --git a/packages/client/ui-settings-general/locale/es.json b/packages/client/ui-settings-general/locale/es.json new file mode 100644 index 0000000000..2910fb258d --- /dev/null +++ b/packages/client/ui-settings-general/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración general y consejos de bienvenida", + "description": "Proporciona configuraciones generales, configuración de redacción de ventanas y consejos para el primer uso." + } +} diff --git a/packages/client/ui-settings-general/locale/fr.json b/packages/client/ui-settings-general/locale/fr.json new file mode 100644 index 0000000000..7114398623 --- /dev/null +++ b/packages/client/ui-settings-general/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres généraux et conseils de bienvenue", + "description": "Fournit des paramètres généraux, la configuration de la rédaction de la fenêtre et des conseils pour la première utilisation." + } +} diff --git a/packages/client/ui-settings-general/locale/pt.json b/packages/client/ui-settings-general/locale/pt.json new file mode 100644 index 0000000000..2ab4067b6f --- /dev/null +++ b/packages/client/ui-settings-general/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações gerais e dicas de boas-vindas", + "description": "Fornece configurações gerais, configuração de direitos autorais de janela e dicas de uso pela primeira vez." + } +} diff --git a/packages/client/ui-settings-general/locale/ru.json b/packages/client/ui-settings-general/locale/ru.json new file mode 100644 index 0000000000..8755a8fd82 --- /dev/null +++ b/packages/client/ui-settings-general/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Общие настройки и приветственные советы", + "description": "Содержит общие настройки, настройки окна копирайтинга и советы по первому использованию." + } +} diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index 944ce57932..ddea103e9f 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-general", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 180df12f10..0d85892813 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -44,7 +44,7 @@ import { SettingsDocumentAction } from './SettingsDocumentAction.tsx' import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx' import { SettingsDocumentStore } from './settings-document-store.ts' import { HostReleasePrompt, type HostReleasePromptInjected } from './HostReleasePrompt.tsx' -import { en, ja, zh, type SettingsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type SettingsKey } from './locales.ts' export type { CloseLabelProps, HeaderContentProps, TriggerContentProps, @@ -99,7 +99,7 @@ export function apply(ctx: ClientContext): void { ctx.slots.inject('settings.general.item', () => ctx.slots.register({ name: 'settings.general.item', id: 'current-version', order: 100, locale: NS, }, CurrentVersionRow)) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-general: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-general: dictionaries') const connection = ctx.get('connection') as ConnectionHandle const carrier = (globalThis as typeof globalThis & { dshDesktop?: { protocolVersion: number; updates?: DesktopUpdateBridge } }).dshDesktop const desktopUpdate = new DesktopUpdateSource(carrier?.protocolVersion === 1 ? carrier.updates : undefined) diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts index 8fddbcf24a..58afc6bba7 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/packages/client/ui-settings-general/src/client/locales.ts @@ -140,3 +140,233 @@ export const ja = { 'hostRelease.reload': '再読み込みして新しいバージョンを読み込む', 'hostRelease.later': '後で', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'trigger': 'Configuración', + 'shortcut.open': 'Abrir configuración', + 'desktop.update.available': 'Actualizar', + 'desktop.update.checking': 'Buscando actualizaciones...', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Verificando archivos de actualización...', + 'desktop.update.installing': 'Preparándose para reiniciar…', + 'desktop.update.ready': 'Instalar y reiniciar', + 'desktop.update.retry': 'Reintentar actualización', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Descargando la actualización: {percent}%\nVersión de destino: {version}', + 'desktop.update.checkFailed': 'No se pudo buscar actualizaciones. Inténtelo de nuevo más tarde.', + 'desktop.update.downloadFailed': 'No se pudo descargar la actualización. Por favor inténtalo de nuevo.', + 'desktop.update.installFailed': 'No se pudo instalar la actualización. Inténtelo de nuevo más tarde.', + 'desktop.update.checkNetworkFailed': 'No se pudo buscar actualizaciones. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.downloadNetworkFailed': 'No se pudo descargar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.installNetworkFailed': 'No se pudo instalar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.fullscreenTimeout': 'No se pudo salir de la pantalla completa. Salga de la pantalla completa y luego vuelva a intentar la actualización.', + 'desktop.update.stopFailed': 'No se pudieron detener las tareas de forma segura. La actualización no se ha instalado. Inténtelo de nuevo más tarde.', + 'desktop.update.tasksChanged': 'Han comenzado nuevas tareas. Confirme nuevamente para detener las tareas y actualizar.', + 'desktop.update.tasksUnavailable': 'El estado de la tarea no está disponible. Intente actualizar nuevamente cuando el espacio de trabajo esté listo.', + 'title': 'Configuración', + 'close': 'Cerrar', + 'openDocument': 'Abrir archivo de configuración', + 'openDocument.error': 'No se pudo abrir el archivo de configuración', + 'general.nav': 'General', + 'general.currentVersion': 'Versión actual: {version}', + 'developerTools.title': 'Mostrar vista de codificación', + 'developerTools.error': 'No se pudo guardar. Por favor inténtalo de nuevo.', + 'developerTools.description': 'Muestra la trayectoria, las diferencias de código y todos los ajustes preestablecidos del agente.', + 'connection.error': 'desconectado', + 'connection.connecting': 'Reconectando', + 'connection.connected': 'Conectado', + 'connection.reconnect': 'Desconectado, vuelve a conectarte ahora', + 'connection.restart': 'Reconectándonos, reconectándonos ahora', + 'hostRelease.title': 'La versión del servicio cambió', + 'hostRelease.description': 'La versión del servicio cambió. Esta página detuvo las actualizaciones en vivo; recargar para cargar la nueva versión.', + 'hostRelease.service': 'Servicio', + 'hostRelease.page': 'Página', + 'hostRelease.unknown': 'Desconocido', + 'hostRelease.reload': 'Recargar para cargar la nueva versión.', + 'hostRelease.later': 'Ahora no', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'trigger': 'Paramètres', + 'shortcut.open': 'Ouvrir les paramètres', + 'desktop.update.available': 'Mise à jour', + 'desktop.update.checking': 'Vérification des mises à jour…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Vérification des fichiers de mise à jour…', + 'desktop.update.installing': 'Préparation du redémarrage…', + 'desktop.update.ready': 'Installer et redémarrer', + 'desktop.update.retry': 'Réessayez la mise à jour', + 'desktop.update.versionDetail': '{label} : {version}', + 'desktop.update.downloadDetail': 'Téléchargement de la mise à jour : {percent}%\nVersion cible : {version}', + 'desktop.update.checkFailed': 'Impossible de vérifier les mises à jour. Veuillez réessayer plus tard.', + 'desktop.update.downloadFailed': 'Impossible de télécharger la mise à jour. Veuillez réessayer.', + 'desktop.update.installFailed': "Impossible d'installer la mise à jour. Veuillez réessayer plus tard.", + 'desktop.update.checkNetworkFailed': 'Impossible de vérifier les mises à jour. Vérifiez votre connexion et réessayez.', + 'desktop.update.downloadNetworkFailed': 'Impossible de télécharger la mise à jour. Vérifiez votre connexion et réessayez.', + 'desktop.update.installNetworkFailed': "Impossible d'installer la mise à jour. Vérifiez votre connexion et réessayez.", + 'desktop.update.fullscreenTimeout': 'Impossible de quitter le mode plein écran. Quittez le mode plein écran, puis réessayez la mise à jour.', + 'desktop.update.stopFailed': "Impossible d'arrêter les tâches en toute sécurité. La mise à jour n'a pas été installée. Veuillez réessayer plus tard.", + 'desktop.update.tasksChanged': 'De nouvelles tâches ont commencé. Confirmez à nouveau pour arrêter les tâches et mettre à jour.', + 'desktop.update.tasksUnavailable': "L'état de la tâche n'est pas disponible. Essayez à nouveau de mettre à jour lorsque l'espace de travail est prêt.", + 'title': 'Paramètres', + 'close': 'Fermer', + 'openDocument': 'Ouvrir le fichier de configuration', + 'openDocument.error': "Impossible d'ouvrir le fichier de configuration", + 'general.nav': 'Général', + 'general.currentVersion': 'Version actuelle : {version}', + 'developerTools.title': 'Afficher la vue de codage', + 'developerTools.error': "Impossible d'enregistrer. Veuillez réessayer.", + 'developerTools.description': "Affiche la trajectoire, les différences de code et tous les préréglages d'agent", + 'connection.error': 'Déconnecté', + 'connection.connecting': 'Reconnexion', + 'connection.connected': 'Connecté', + 'connection.reconnect': 'Déconnecté, reconnectez-vous maintenant', + 'connection.restart': 'Reconnexion, reconnectez-vous maintenant', + 'hostRelease.title': 'La version du service a été modifiée', + 'hostRelease.description': 'La version du service a changé. Cette page a suspendu les mises à jour en direct ; recharger pour charger la nouvelle version.', + 'hostRelease.service': 'Service', + 'hostRelease.page': 'Pages', + 'hostRelease.unknown': 'Inconnu', + 'hostRelease.reload': 'Recharger pour charger la nouvelle version', + 'hostRelease.later': 'Pas maintenant', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'trigger': 'Einstellungen', + 'shortcut.open': 'Einstellungen öffnen', + 'desktop.update.available': 'Aktualisieren', + 'desktop.update.checking': 'Suche nach Updates…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Aktualisierungsdateien werden überprüft…', + 'desktop.update.installing': 'Neustart wird vorbereitet…', + 'desktop.update.ready': 'Installieren und neu starten', + 'desktop.update.retry': 'Update erneut versuchen', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Update wird heruntergeladen: {percent}%\nZielversion: {version}', + 'desktop.update.checkFailed': 'Es konnte nicht nach Updates gesucht werden. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.downloadFailed': 'Das Update konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.', + 'desktop.update.installFailed': 'Das Update konnte nicht installiert werden. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.checkNetworkFailed': 'Es konnte nicht nach Updates gesucht werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.downloadNetworkFailed': 'Das Update konnte nicht heruntergeladen werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.installNetworkFailed': 'Das Update konnte nicht installiert werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.fullscreenTimeout': 'Der Vollbildmodus konnte nicht beendet werden. Beenden Sie den Vollbildmodus und versuchen Sie dann erneut, das Update durchzuführen.', + 'desktop.update.stopFailed': 'Die Aufgaben konnten nicht sicher gestoppt werden. Das Update wurde nicht installiert. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.tasksChanged': 'Neue Aufgaben haben begonnen. Bestätigen Sie erneut, um die Aufgaben zu stoppen und zu aktualisieren.', + 'desktop.update.tasksUnavailable': 'Der Aufgabenstatus ist nicht verfügbar. Versuchen Sie erneut, die Aktualisierung durchzuführen, wenn der Arbeitsbereich bereit ist.', + 'title': 'Einstellungen', + 'close': 'Schließen', + 'openDocument': 'Konfigurationsdatei öffnen', + 'openDocument.error': 'Die Konfigurationsdatei konnte nicht geöffnet werden', + 'general.nav': 'Allgemein', + 'general.currentVersion': 'Aktuelle Version: {version}', + 'developerTools.title': 'Codieransicht anzeigen', + 'developerTools.error': 'Konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.', + 'developerTools.description': 'Zeigt Ausführungsverlauf, Codeunterschiede und alle Agentenvoreinstellungen an', + 'connection.error': 'Nicht verbunden', + 'connection.connecting': 'Wieder verbinden', + 'connection.connected': 'Verbunden', + 'connection.reconnect': 'Verbindung getrennt, jetzt erneut verbinden', + 'connection.restart': 'Verbindung wiederherstellen, jetzt erneut verbinden', + 'hostRelease.title': 'Dienstversion geändert', + 'hostRelease.description': 'Die Dienstversion hat sich geändert. Diese Seite hat Live-Updates angehalten. Neu laden, um die neue Version zu laden.', + 'hostRelease.service': 'Service', + 'hostRelease.page': 'Seite', + 'hostRelease.unknown': 'Unbekannt', + 'hostRelease.reload': 'Neu laden, um die neue Version zu laden', + 'hostRelease.later': 'Nicht jetzt', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'trigger': 'Настройки', + 'shortcut.open': 'Открыть настройки', + 'desktop.update.available': 'Обновить', + 'desktop.update.checking': 'Проверка обновлений…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Проверка файлов обновлений…', + 'desktop.update.installing': 'Подготовка к перезапуску…', + 'desktop.update.ready': 'Установить и перезапустить', + 'desktop.update.retry': 'Повторить обновление', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Загрузка обновления: {percent}%\n Целевая версия: {version}', + 'desktop.update.checkFailed': 'Не удалось проверить наличие обновлений. Пожалуйста, повторите попытку позже.', + 'desktop.update.downloadFailed': 'Не удалось загрузить обновление. Пожалуйста, попробуйте еще раз.', + 'desktop.update.installFailed': 'Не удалось установить обновление. Пожалуйста, повторите попытку позже.', + 'desktop.update.checkNetworkFailed': 'Не удалось проверить наличие обновлений. Проверьте подключение и повторите попытку.', + 'desktop.update.downloadNetworkFailed': 'Не удалось загрузить обновление. Проверьте подключение и повторите попытку.', + 'desktop.update.installNetworkFailed': 'Не удалось установить обновление. Проверьте подключение и повторите попытку.', + 'desktop.update.fullscreenTimeout': 'Не удалось выйти из полноэкранного режима. Выйдите из полноэкранного режима и повторите попытку обновления.', + 'desktop.update.stopFailed': 'Не удалось безопасно остановить задачи. Обновление не установлено. Пожалуйста, повторите попытку позже.', + 'desktop.update.tasksChanged': 'Начались новые задачи. Подтвердите еще раз, чтобы остановить выполнение задач и выполнить обновление.', + 'desktop.update.tasksUnavailable': 'Статус задачи недоступен. Попробуйте обновить еще раз, когда рабочая область будет готова.', + 'title': 'Настройки', + 'close': 'Закрыть', + 'openDocument': 'Открыть файл конфигурации', + 'openDocument.error': 'Не удалось открыть файл конфигурации', + 'general.nav': 'Общие', + 'general.currentVersion': 'Текущая версия: {version}.', + 'developerTools.title': 'Показать представление кодирования', + 'developerTools.error': 'Не удалось сохранить. Пожалуйста, попробуйте еще раз.', + 'developerTools.description': 'Показывает траекторию, различия в коде и все настройки агента.', + 'connection.error': 'Отключено', + 'connection.connecting': 'Повторное подключение', + 'connection.connected': 'Подключено', + 'connection.reconnect': 'Соединение отключено, подключитесь снова сейчас', + 'connection.restart': 'Повторное подключение, повторное подключение сейчас', + 'hostRelease.title': 'Версия сервиса изменена', + 'hostRelease.description': 'Версия сервиса изменилась. На этой странице приостановлены текущие обновления; перезагрузите, чтобы загрузить новую версию.', + 'hostRelease.service': 'Сервис', + 'hostRelease.page': 'Страница', + 'hostRelease.unknown': 'Неизвестно', + 'hostRelease.reload': 'Перезагрузите, чтобы загрузить новую версию', + 'hostRelease.later': 'Не сейчас', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'trigger': 'Configurações', + 'shortcut.open': 'Abrir configurações', + 'desktop.update.available': 'Atualizar', + 'desktop.update.checking': 'Verificando atualizações…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Verificando arquivos de atualização…', + 'desktop.update.installing': 'Preparando para reiniciar…', + 'desktop.update.ready': 'Instalar e reiniciar', + 'desktop.update.retry': 'Tentar atualizar novamente', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Fazendo download da atualização: {percent}%\nVersão de destino: {version}', + 'desktop.update.checkFailed': 'Não foi possível verificar se há atualizações. Por favor, tente novamente mais tarde.', + 'desktop.update.downloadFailed': 'Não foi possível baixar a atualização. Por favor, tente novamente.', + 'desktop.update.installFailed': 'Não foi possível instalar a atualização. Por favor, tente novamente mais tarde.', + 'desktop.update.checkNetworkFailed': 'Não foi possível verificar se há atualizações. Verifique sua conexão e tente novamente.', + 'desktop.update.downloadNetworkFailed': 'Não foi possível baixar a atualização. Verifique sua conexão e tente novamente.', + 'desktop.update.installNetworkFailed': 'Não foi possível instalar a atualização. Verifique sua conexão e tente novamente.', + 'desktop.update.fullscreenTimeout': 'Não foi possível sair da tela cheia. Saia da tela inteira e tente atualizar novamente.', + 'desktop.update.stopFailed': 'Não foi possível interromper as tarefas com segurança. A atualização não foi instalada. Por favor, tente novamente mais tarde.', + 'desktop.update.tasksChanged': 'Novas tarefas foram iniciadas. Confirme novamente para interromper as tarefas e atualizar.', + 'desktop.update.tasksUnavailable': 'O status da tarefa não está disponível. Tente atualizar novamente quando o espaço de trabalho estiver pronto.', + 'title': 'Configurações', + 'close': 'Fechar', + 'openDocument': 'Abra o arquivo de configuração', + 'openDocument.error': 'Não foi possível abrir o arquivo de configuração', + 'general.nav': 'Geral', + 'general.currentVersion': 'Versão atual: {version}', + 'developerTools.title': 'Mostrar visualização de codificação', + 'developerTools.error': 'Não foi possível salvar. Por favor, tente novamente.', + 'developerTools.description': 'Mostra trajetória, diferenças de código e todas as predefinições do agente', + 'connection.error': 'Desconectado', + 'connection.connecting': 'Reconectando', + 'connection.connected': 'Conectado', + 'connection.reconnect': 'Desconectado, reconecte agora', + 'connection.restart': 'Reconectando, reconecte agora', + 'hostRelease.title': 'Versão do serviço alterada', + 'hostRelease.description': 'A versão do serviço mudou. Esta página pausou as atualizações ao vivo; reload para carregar a nova versão.', + 'hostRelease.service': 'Serviço', + 'hostRelease.page': 'Página', + 'hostRelease.unknown': 'Desconhecido', + 'hostRelease.reload': 'Recarregue para carregar a nova versão', + 'hostRelease.later': 'Agora não', +} satisfies Record diff --git a/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx b/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx index f90d92f53d..a2d3807e5d 100644 --- a/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/settings-root.client.spec.tsx @@ -392,9 +392,9 @@ describe('SettingsPanel navigation', () => { ], }) openPanel() - // Glyphs carry no id of their own, so the drawn paths are what tells them apart. + // Each product glyph names its sprite symbol id in data-icon. const glyphs = ['General', 'Models', 'Agent presets', 'Plugins', 'Archived sessions', 'Contributed'] - .map(name => screen.getByRole('button', { name }).querySelector('svg')?.innerHTML) + .map(name => screen.getByRole('button', { name }).querySelector('svg')?.getAttribute('data-icon')) expect(glyphs.every(glyph => glyph !== undefined && glyph !== '')).toBe(true) // The four ids the shell names get their own glyph; every other section — diff --git a/packages/client/ui-settings-models/locale/de.json b/packages/client/ui-settings-models/locale/de.json new file mode 100644 index 0000000000..e37303da3b --- /dev/null +++ b/packages/client/ui-settings-models/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modelleinstellungsschnittstelle", + "description": "Verwalten Sie Modelldienste und ihre Zugriffskonfigurationen und stellen Sie relevante Anleitungen bereit." + } +} diff --git a/packages/client/ui-settings-models/locale/es.json b/packages/client/ui-settings-models/locale/es.json new file mode 100644 index 0000000000..46272e2878 --- /dev/null +++ b/packages/client/ui-settings-models/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración del modelo", + "description": "Administre los servicios modelo y sus configuraciones de acceso, y proporcione orientación relevante." + } +} diff --git a/packages/client/ui-settings-models/locale/fr.json b/packages/client/ui-settings-models/locale/fr.json new file mode 100644 index 0000000000..66fe99de82 --- /dev/null +++ b/packages/client/ui-settings-models/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration du modèle", + "description": "Gérez les services modèles et leurs configurations d'accès, et fournissez des conseils pertinents." + } +} diff --git a/packages/client/ui-settings-models/locale/pt.json b/packages/client/ui-settings-models/locale/pt.json new file mode 100644 index 0000000000..bcbc2971af --- /dev/null +++ b/packages/client/ui-settings-models/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de modelo", + "description": "Gerencie serviços de modelo e suas configurações de acesso e forneça orientações relevantes." + } +} diff --git a/packages/client/ui-settings-models/locale/ru.json b/packages/client/ui-settings-models/locale/ru.json new file mode 100644 index 0000000000..cb419aea1a --- /dev/null +++ b/packages/client/ui-settings-models/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки модели", + "description": "Управляйте сервисами моделей и конфигурациями доступа к ним, а также предоставляйте соответствующие рекомендации." + } +} diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index b1967ec8e7..2350ad7182 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-settings-models/src/client/index.ts index 37ad6919fe..d0b61802a5 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/packages/client/ui-settings-models/src/client/index.ts @@ -26,7 +26,7 @@ import { WelcomeNoticeStore } from './welcome-store.ts' import { ModelsSettingsStore } from './store.ts' import { createModelsOperations } from './operations.ts' import { createSettingsSchemaOperations } from './schema-operations.ts' -import { en, ja, zh, type ModelsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ModelsKey } from './locales.ts' import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts' import { Config, ONBOARDING_CONFIG_GLOBAL } from '../onboarding-config.ts' @@ -80,7 +80,7 @@ export function apply(ctx: ClientContext): void { const payload = page[ONBOARDING_CONFIG_GLOBAL] const configured = Config(payload === undefined ? {} : payload) const credentialOnboarding = configured.credentialOnboarding && !('dshDesktop' in globalThis) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-models: copy dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-models: copy dictionaries') const schema = createSettingsSchemaOperations(ctx.settingsSchema) // Bound once here, where the Remote namespaces are declared in this plugin's diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index d1bf8aceb3..533b7aed02 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -425,3 +425,708 @@ export const ja = { 'onboardingSaving': '保存中…', 'keyRequired': '続けるにはAPIキーを入力してください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + nav: 'Modelos', + deepSeekAccount: 'Cuenta DeepSeek', + title: 'Modelos', + intro: 'Ingrese sus claves API para usar modelos de los siguientes proveedores.', + edit: 'Editar', + editProvider: 'Editar {provider}', + remove: 'Eliminar', + removeProvider: 'Eliminar {provider}', + deleteTitle: '¿Eliminar {provider}?', + deleteDescription: 'Al eliminar {provider} se elimina su configuración. Cualquier credencial que utilice se administra en otro lugar y se conservará.', + deleteDescriptionWithCredential: 'Al eliminar {provider} se elimina su configuración y la clave API almacenada.', + deleteConfirm: 'Eliminar {provider}', + deleting: 'Eliminando {provider}…', + add: 'Agregar proveedor de modelo', + addMode: 'como agregar', + addCatalog: 'Proveedor de modelos externo', + addCustom: 'Modelo personalizado API', + addCatalogHint: 'Elija OpenAI, Anthropic, Kimi u otro proveedor del catálogo integrado e ingrese su clave API.', + addCustomHint: 'Conecte un repetidor, un servidor autohospedado o cualquier otro punto final compatible con OpenAI o Anthropic mediante su URL base, protocolo y modelos.', + addCatalogExhausted: 'Cada proveedor de catálogo ya está configurado.', + addCustomUnavailable: 'No hay ningún protocolo API disponible para declarar.', + provider: 'Proveedor', + close: 'Cerrar', + cancel: 'Cancelar', + apply: 'Aplicar', + applying: 'Aplicando…', + savedProvider: 'Guardado {provider}.', + credentialConfigured: 'Clave API configurada', + credentialMissing: 'Falta la clave API', + accountSignedIn: 'Iniciado sesión', + readOnly: 'El documento de configuración es de solo lectura en esta implementación.', + loadFailed: 'Error al cargar el directorio de proveedores', + conflict: 'Alguien más cambió esta configuración mientras esta tarjeta estaba abierta. Ciérrelo y vuelva a abrirlo para editar los valores actuales.', + retry: 'Reintentar', + keyInput: 'Clave de API', + keyReveal: 'Mostrar secreto', + keyHide: 'Ocultar secreto', + keyPlaceholder: 'Ingrese su clave API', + keyPlaceholderFallback: 'Déjelo en blanco para usar env como autenticación ({hint})', + keyPlaceholderSaved: 'Déjelo en blanco para usar las credenciales guardadas', + keyFallbackMissing: 'No se detectó la variable de entorno. Ingrese una clave API o configure {hint} en su entorno.', + keyFallbackDetected: 'Se detectó var env. ({hint})', + keyFallbackDetectedHidden: 'Se detectó var env.', + keyFallbackUnchecked: 'El estado de la credencial ambiental no ha sido confirmado.', + keyHelp: 'Las credenciales guardadas se utilizan primero. Las alternativas de entorno se muestran aquí, en orden: {env}. Las fuentes son el entorno de inicio, $cwd/.env, luego $DSH_HOME/.env.', + authHelp: 'Detalles de autenticación', + customized: 'Configuraciones personalizadas', + baseUrl: 'URL base', + baseUrlDefault: 'Sin punto final predeterminado', + baseUrlEnvironment: 'Configuración del punto final del proveedor: {env}', + baseUrlConfigured: 'Punto final configurado por el proveedor', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (Uso predeterminado DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Utilice un punto final API compatible con Anthropic Messages.', + models: 'Modelos', + modelsInherited: 'Usando los valores predeterminados del adaptador', + modelsCustomized: 'Catálogo de modelos personalizado', + resetModels: 'Restaurar valores predeterminados', + model: 'modelo', + modelId: 'ID del modelo', + modelName: 'Nombre para mostrar', + modelNamePlaceholder: 'Utiliza el ID del modelo cuando está vacío', + contextWindow: 'ventana contextual', + contextWindowPlaceholder: 'Utiliza el proveedor predeterminado', + maxTokens: 'Fichas de salida máxima', + maxTokensPlaceholder: 'Utiliza el proveedor predeterminado', + modelAdvanced: 'Opciones de modelo', + modelInputTypes: 'Tipos de entrada', + modelInputText: 'Texto', + modelInputImage: 'Imagen', + modelReasoning: 'Niveles de razonamiento', + modelReasoningLevel: 'Nivel', + modelReasoningValue: 'Valor API', + modelReasoningHelp: 'Acerca de los niveles de razonamiento', + modelReasoningHelpText: 'El valor de la API se envía en el campo de esfuerzo de razonamiento de la solicitud, como reasoning_effort de OpenAI. De forma predeterminada se usa el nombre del nivel; si la documentación de la API del proveedor del modelo especifica otro valor, úselo. Desactivado deshabilita el razonamiento y no necesita un valor; cualquier valor ya guardado en la configuración se conserva y se envía.', + modelReasoningNeedsLevel: 'Agregue al menos un nivel además de Desactivado o desactive los niveles de razonamiento.', + modelReasoningValueRequired: 'Cada nivel además de Off necesita un valor API y ningún valor puede estar vacío.', + modelReasoningAdd: 'Agregar nivel', + modelReasoningRemove: 'Eliminar nivel', + modelReasoningOffValuePlaceholder: 'Desactiva el razonamiento', + addModel: 'Agregar modelo', + removeModel: 'Eliminar modelo', + reorderModel: 'Reordenar modelo', + reorderModelHelp: 'Arrastre para reordenar o use las teclas de flecha hacia arriba y hacia abajo.', + modelsEmpty: 'No se mostrará ningún modelo en el selector. Las identificaciones que no figuran en la lista aún se pueden enviar directamente.', + keyBlank: 'Ingrese la clave API, o deje el campo vacío para conservar la almacenada.', + keyBlankNew: 'Ingrese la clave API o deje el campo vacío si este proveedor se autentica de otra manera.', + keyIllegalCharacters: 'Esta clave API no tiene un formato válido. Por favor compruébalo.', + modelIdRequired: 'Se requiere identificación del modelo.', + modelIdDuplicate: 'El ID del modelo debe ser único.', + modelNameInvalid: 'El nombre para mostrar no puede estar vacío.', + modelContextInvalid: 'La ventana de contexto debe tener un recuento positivo, como 131072, 256K o 1M.', + modelMaxTokensInvalid: 'Los tokens de salida máxima deben tener un recuento positivo, como 8192, 64K o 1M.', + advancedHint: 'Otros campos se encuentran en cordis.patch.yml; edite esa sección directamente.', + modelCapacityInvalid: 'Una capacidad debe ser un número, opcionalmente con el sufijo K o M.', + modelDuplicate: 'Cada ID de modelo puede aparecer una vez.', + fetchModels: 'Buscar modelos disponibles', + fetching: 'Preguntando al proveedor...', + fetchNeedsBaseUrl: 'Ingrese primero la URL base y luego busque.', + fetchEmpty: 'El proveedor no enumeró ningún modelo. Agréguelos a mano.', + fetchTitle: 'Elige modelos', + fetchDescription: 'Estos son los modelos que tiene disponibles este proveedor. Los modelos comprobados componen la lista.', + fetchSearch: 'Buscar modelos', + fetchNoMatches: 'No hay modelos coincidentes.', + fetchSelectAll: 'Seleccionar todo', + fetchDeselectAll: 'Deseleccionar todo', + fetchAdopt: 'Lista de actualización', + fetchKeepOne: 'Mantenga al menos un modelo controlado.', + customTag: 'personalizado', + customRoute: 'ID del proveedor', + customRouteHint: 'Identificador en minúscula, que comienza con una letra, que nombra de forma exclusiva a este proveedor en las solicitudes y como su nombre de credencial.', + customRouteInvalid: 'Comience con una letra minúscula; luego letras minúsculas, dígitos y guiones.', + customRouteTaken: 'Un proveedor ya utiliza esta identificación.', + customDisplayName: 'Nombre para mostrar', + customApi: 'Protocolo API', + customApiUnset: 'No seleccionado', + protocolOpenAiCompletions: 'Finalizaciones del chat OpenAI', + protocolOpenAiResponses: 'Respuestas de OpenAI', + protocolAnthropicMessages: 'Mensajes antrópicos', + customNeedsBaseUrl: 'Un proveedor personalizado necesita una URL base.', + customBaseUrlInvalid: 'Ingrese una URL HTTP o HTTPS válida.', + customNeedsModels: 'Un proveedor personalizado necesita al menos un modelo.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'ruta de configuración irresoluble', + create: 'Crear proveedor', + creating: 'Creando…', + welcomeTitle: 'Aviso previo', + welcomeBody: 'DeepSeek Harness 0.2 aún está en versión preliminar y muchas áreas necesitan mejoras y refinamientos continuos. Agradecemos los comentarios y sugerencias de todos los desarrolladores y usuarios. La nueva aplicación de escritorio ahora está dirigida a una amplia gama de usuarios, mientras que las funciones avanzadas relacionadas con los desarrolladores se pueden habilitar en la configuración. Se espera que las características del producto y las API de complementos de DeepSeek Harness continúen con una rápida iteración y evolución, y se estabilicen gradualmente con el tiempo.\n\nEsperamos explorar los límites de la inteligencia junto con usuarios y desarrolladores de todo el mundo, basándose en una infraestructura de código abierto, reutilizable y componible. Invitamos a todos a dar vida a sus ideas con DeepSeek Harness y participar en la comunidad para enriquecer el ecosistema de complementos.', + welcomeContinue: 'Continuar', + welcomeError: 'El reconocimiento no se pudo guardar. Por favor inténtalo de nuevo.', + onboardingTitle: 'Agregue una clave API para comenzar', + onboardingDescription: 'Configure el proveedor oficial DeepSeek para comenzar a construir.', + onboardingLater: 'Configurar más tarde', + onboardingSave: 'Guardar y continuar', + onboardingSaving: 'Guardando…', + keyRequired: 'Ingrese una clave API para continuar.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + nav: 'Modèles', + deepSeekAccount: 'Compte DeepSeek', + title: 'Modèles', + intro: 'Entrez vos clés API pour utiliser les modèles des fournisseurs suivants.', + edit: 'Modifier', + editProvider: 'Modifier {provider}', + remove: 'Supprimer', + removeProvider: 'Supprimer {provider}', + deleteTitle: 'Supprimer {provider} ?', + deleteDescription: "La suppression de {provider} supprime sa configuration. Tout identifiant qu'il utilise est géré ailleurs et sera conservé.", + deleteDescriptionWithCredential: 'La suppression de {provider} supprime sa configuration et la clé API stockée.', + deleteConfirm: 'Supprimer {provider}', + deleting: 'Suppression de {provider}…', + add: 'Ajouter un fournisseur de modèles', + addMode: 'Comment ajouter', + addCatalog: 'Fournisseur de modèles tiers', + addCustom: 'Modèle personnalisé API', + addCatalogHint: 'Choisissez OpenAI, Anthropic, Kimi ou un autre fournisseur dans le catalogue intégré et entrez sa clé API.', + addCustomHint: 'Connectez un relais, un serveur auto-hébergé ou tout autre point de terminaison compatible OpenAI ou Anthropic par son URL de base, son protocole et ses modèles.', + addCatalogExhausted: 'Chaque fournisseur de catalogue est déjà configuré.', + addCustomUnavailable: "Aucun protocole API n'est disponible à déclarer.", + provider: 'Fournisseur', + close: 'Fermer', + cancel: 'Annuler', + apply: 'Appliquer', + applying: 'Application…', + savedProvider: '{provider} enregistré.', + credentialConfigured: 'Clé API configurée', + credentialMissing: 'Clé API manquante', + accountSignedIn: 'Connecté', + readOnly: 'Le document de paramètres est en lecture seule dans ce déploiement.', + loadFailed: 'Le chargement du répertoire du fournisseur a échoué', + conflict: "Quelqu'un d'autre a modifié ces paramètres alors que cette carte était ouverte. Fermez-le et rouvrez-le pour modifier les valeurs actuelles.", + retry: 'Réessayer', + keyInput: 'Clé API', + keyReveal: 'Afficher le secret', + keyHide: 'Cacher le secret', + keyPlaceholder: 'Entrez votre clé API', + keyPlaceholderFallback: 'Laissez vide pour utiliser env comme authentification ({hint})', + keyPlaceholderSaved: "Laissez vide pour utiliser les informations d'identification enregistrées", + keyFallbackMissing: "Var d'environnement non détectée. Entrez une clé API ou configurez {hint} dans votre environnement.", + keyFallbackDetected: "Var d'environnement détectée. ({hint})", + keyFallbackDetectedHidden: "Var d'environnement détectée.", + keyFallbackUnchecked: 'Le statut des qualifications en environnement n’a pas été confirmé.', + keyHelp: "Les informations d'identification enregistrées sont utilisées en premier. Solutions de secours d’environnement affichées ici, dans l’ordre : {env}. Les sources sont l'environnement de lancement, $cwd/.env, puis $DSH_HOME/.env.", + authHelp: "Détails d'authentification", + customized: 'Paramètres personnalisés', + baseUrl: 'URL de base', + baseUrlDefault: 'Aucun point de terminaison par défaut', + baseUrlEnvironment: 'Configuration du point de terminaison du fournisseur : {env}', + baseUrlConfigured: 'Point de terminaison configuré par le fournisseur', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (par défaut, DEEPSEEK_BASE_URL est utilisé)', + deepSeekEndpointHint: 'Utilisez un point de terminaison API compatible avec Anthropic Messages.', + models: 'Modèles', + modelsInherited: "Utilisation des valeurs par défaut de l'adaptateur", + modelsCustomized: 'Catalogue de modèles personnalisés', + resetModels: 'Restaurer les valeurs par défaut', + model: 'Modèle', + modelId: 'ID du modèle', + modelName: "Nom d'affichage", + modelNamePlaceholder: "Utilise l'ID du modèle lorsqu'il est vide", + contextWindow: 'Fenêtre contextuelle', + contextWindowPlaceholder: 'Utilise la valeur par défaut du fournisseur', + maxTokens: 'Jetons de sortie maximum', + maxTokensPlaceholder: 'Utilise la valeur par défaut du fournisseur', + modelAdvanced: 'Options de modèle', + modelInputTypes: "Types d'entrée", + modelInputText: 'Texte', + modelInputImage: 'Images', + modelReasoning: 'Niveaux de raisonnement', + modelReasoningLevel: 'Niveau', + modelReasoningValue: 'Valeur API', + modelReasoningHelp: 'À propos des niveaux de raisonnement', + modelReasoningHelpText: 'La valeur API est envoyée dans le champ d’effort de raisonnement de la requête, comme reasoning_effort chez OpenAI. Par défaut, elle correspond au nom du niveau ; si la documentation API du fournisseur du modèle indique une autre valeur, utilisez-la. Désactivé coupe le raisonnement et ne nécessite aucune valeur ; toute valeur déjà enregistrée dans la configuration est conservée et envoyée.', + modelReasoningNeedsLevel: 'Ajoutez au moins un niveau en plus de Désactivé ou désactivez les niveaux de raisonnement.', + modelReasoningValueRequired: 'Chaque niveau autre que Off nécessite une valeur API et aucune valeur ne peut être vide.', + modelReasoningAdd: 'Ajouter un niveau', + modelReasoningRemove: 'Supprimer le niveau', + modelReasoningOffValuePlaceholder: 'Désactive le raisonnement', + addModel: 'Ajouter un modèle', + removeModel: 'Supprimer le modèle', + reorderModel: 'Réorganiser le modèle', + reorderModelHelp: 'Faites glisser pour réorganiser ou utilisez les touches fléchées haut et bas.', + modelsEmpty: 'Aucun modèle ne sera affiché dans le sélecteur. Les identifiants non répertoriés peuvent toujours être envoyés directement.', + keyBlank: 'Saisissez la clé API, ou laissez le champ vide pour conserver celle stockée.', + keyBlankNew: "Saisissez la clé API ou laissez le champ vide si ce fournisseur s'authentifie d'une autre manière.", + keyIllegalCharacters: "Cette clé API n'est pas dans un format valide. Veuillez le vérifier.", + modelIdRequired: 'L’ID du modèle est requis.', + modelIdDuplicate: "L'ID du modèle doit être unique.", + modelNameInvalid: 'Le nom d’affichage ne peut pas être vide.', + modelContextInvalid: 'La fenêtre contextuelle doit être un nombre positif, comme 131072, 256K ou 1M.', + modelMaxTokensInvalid: 'Le nombre maximum de jetons de sortie doit être un nombre positif, comme 8192, 64K ou 1M.', + advancedHint: "D'autres champs se trouvent dans cordis.patch.yml ; modifiez cette section directement.", + modelCapacityInvalid: 'Une capacité doit être un nombre, éventuellement suivi du suffixe K ou M.', + modelDuplicate: 'Chaque ID de modèle peut apparaître une fois.', + fetchModels: 'Récupérer les modèles disponibles', + fetching: 'Demander au fournisseur…', + fetchNeedsBaseUrl: "Entrez d'abord l'URL de base, puis récupérez-la.", + fetchEmpty: 'Le fournisseur n’a répertorié aucun modèle. Ajoutez-les à la main.', + fetchTitle: 'Choisir des modèles', + fetchDescription: 'Ce sont les modèles disponibles chez ce fournisseur. Les modèles cochés constituent la liste.', + fetchSearch: 'Rechercher des modèles', + fetchNoMatches: 'Aucun modèle correspondant.', + fetchSelectAll: 'Tout sélectionner', + fetchDeselectAll: 'Tout désélectionner', + fetchAdopt: 'Mettre à jour la liste', + fetchKeepOne: 'Gardez au moins un modèle coché.', + customTag: 'Personnalisé', + customRoute: 'Identifiant du fournisseur', + customRouteHint: "Identifiant minuscule, commençant par une lettre, qui nomme de manière unique ce fournisseur dans les demandes et comme nom d'identification.", + customRouteInvalid: 'Commencez par une lettre minuscule ; puis des lettres minuscules, des chiffres et des tirets.', + customRouteTaken: 'Un fournisseur utilise déjà cet identifiant.', + customDisplayName: "Nom d'affichage", + customApi: 'Protocole API', + customApiUnset: 'Non sélectionné', + protocolOpenAiCompletions: 'Achèvements du chat OpenAI', + protocolOpenAiResponses: 'Réponses OpenAI', + protocolAnthropicMessages: 'Messages anthropiques', + customNeedsBaseUrl: "Un fournisseur personnalisé a besoin d'une URL de base.", + customBaseUrlInvalid: 'Saisissez une URL HTTP ou HTTPS valide.', + customNeedsModels: "Un fournisseur personnalisé a besoin d'au moins un modèle.", + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'chemin des paramètres insolubles', + create: 'Créer un fournisseur', + creating: 'Création…', + welcomeTitle: "Avis d'aperçu", + welcomeBody: "DeepSeek Harness 0.2 est toujours en version préliminaire et de nombreux domaines nécessitent une amélioration et un perfectionnement continus. Nous apprécions les commentaires et suggestions de tous les développeurs et utilisateurs. La nouvelle application de bureau cible désormais un large éventail d'utilisateurs, tandis que les fonctionnalités avancées liées aux développeurs peuvent être activées dans les paramètres. Les fonctionnalités du produit et les API de plug-in de DeepSeek Harness devraient poursuivre leurs itérations et leur évolution rapides, et se stabiliseront progressivement au fil du temps. \n\nNous sommes impatients d'explorer les limites de l'intelligence avec les utilisateurs et les développeurs du monde entier, en nous appuyant sur une infrastructure open source, réutilisable et composable. Nous invitons chacun à donner vie à ses idées avec DeepSeek Harness et à participer à la communauté pour enrichir l'écosystème des plugins.", + welcomeContinue: 'Continuer', + welcomeError: "L'accusé de réception n'a pas pu être enregistré. Veuillez réessayer.", + onboardingTitle: 'Ajoutez une clé API pour commencer', + onboardingDescription: 'Configurez le fournisseur officiel DeepSeek pour commencer la construction.', + onboardingLater: 'Configurer plus tard', + onboardingSave: 'Enregistrer et continuer', + onboardingSaving: 'Sauvegarde…', + keyRequired: 'Entrez une clé API pour continuer.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + nav: 'Modelle', + deepSeekAccount: 'DeepSeek-Konto', + title: 'Modelle', + intro: 'Geben Sie Ihre API-Schlüssel ein, um Modelle der folgenden Anbieter zu verwenden.', + edit: 'Bearbeiten', + editProvider: 'Bearbeiten Sie {provider}', + remove: 'Löschen', + removeProvider: 'Löschen Sie {provider}', + deleteTitle: '{provider} löschen?', + deleteDescription: 'Durch das Löschen von {provider} wird seine Konfiguration entfernt. Alle verwendeten Anmeldeinformationen werden an anderer Stelle verwaltet und aufbewahrt.', + deleteDescriptionWithCredential: 'Durch das Löschen von {provider} werden seine Konfiguration und der gespeicherte API-Schlüssel entfernt.', + deleteConfirm: 'Löschen Sie {provider}', + deleting: '{provider} wird gelöscht…', + add: 'Modellanbieter hinzufügen', + addMode: 'So fügen Sie hinzu', + addCatalog: 'Drittanbieter von Modellen', + addCustom: 'Kundenspezifisches Modell API', + addCatalogHint: 'Wählen Sie OpenAI, Anthropic, Kimi oder einen anderen Anbieter aus dem integrierten Katalog aus und geben Sie den API-Schlüssel ein.', + addCustomHint: 'Verbinden Sie ein Relay, einen selbst gehosteten Server oder einen anderen OpenAI- oder Anthropic-kompatiblen Endpunkt über seine Basis-URL, sein Protokoll und seine Modelle.', + addCatalogExhausted: 'Jeder Kataloganbieter ist bereits konfiguriert.', + addCustomUnavailable: 'Es ist kein API-Protokoll zur Deklaration verfügbar.', + provider: 'Anbieter', + close: 'Schließen', + cancel: 'Abbrechen', + apply: 'Anwenden', + applying: 'Wird angewendet…', + savedProvider: 'Gespeichert {provider}.', + credentialConfigured: 'API-Schlüssel konfiguriert', + credentialMissing: 'API-Schlüssel fehlt', + accountSignedIn: 'Angemeldet', + readOnly: 'Das Einstellungsdokument ist in dieser Bereitstellung schreibgeschützt.', + loadFailed: 'Das Laden des Anbieterverzeichnisses ist fehlgeschlagen', + conflict: 'Jemand anderes hat diese Einstellungen geändert, während diese Karte geöffnet war. Schließen Sie es und öffnen Sie es erneut, um die aktuellen Werte zu bearbeiten.', + retry: 'Erneut versuchen', + keyInput: 'API-Schlüssel', + keyReveal: 'Geheimnis zeigen', + keyHide: 'Geheimnis verbergen', + keyPlaceholder: 'Geben Sie Ihren API-Schlüssel ein', + keyPlaceholderFallback: 'Lassen Sie das Feld leer, um env als Authentifizierung zu verwenden ({hint})', + keyPlaceholderSaved: 'Lassen Sie das Feld leer, um gespeicherte Anmeldeinformationen zu verwenden', + keyFallbackMissing: 'Umgebungsvariable nicht erkannt. Geben Sie einen API-Schlüssel ein oder konfigurieren Sie {hint} in Ihrer Umgebung.', + keyFallbackDetected: 'Umgebungsvariable erkannt. ({hint})', + keyFallbackDetectedHidden: 'Umgebungsvariable erkannt.', + keyFallbackUnchecked: 'Der Status der Umweltberechtigung wurde nicht bestätigt.', + keyHelp: 'Gespeicherte Anmeldeinformationen werden zuerst verwendet. Hier werden Umgebungs-Fallbacks in der Reihenfolge angezeigt: {env}. Quellen sind die Startumgebung, $cwd/.env und dann $DSH_HOME/.env.', + authHelp: 'Authentifizierungsdetails', + customized: 'Benutzerdefinierte Einstellungen', + baseUrl: 'Basis-URL', + baseUrlDefault: 'Kein Standardendpunkt', + baseUrlEnvironment: 'Konfiguration des Anbieterendpunkts: {env}', + baseUrlConfigured: 'Vom Anbieter konfigurierter Endpunkt', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (Standardmäßig wird DEEPSEEK_BASE_URL verwendet)', + deepSeekEndpointHint: 'Verwenden Sie einen API-Endpunkt, der mit Anthropic Messages kompatibel ist.', + models: 'Modelle', + modelsInherited: 'Verwendung der Adapterstandards', + modelsCustomized: 'Maßgeschneiderter Modellkatalog', + resetModels: 'Standardeinstellungen wiederherstellen', + model: 'Modell', + modelId: 'Modell-ID', + modelName: 'Anzeigename', + modelNamePlaceholder: 'Verwendet die Modell-ID, wenn sie leer ist', + contextWindow: 'Kontextfenster', + contextWindowPlaceholder: 'Verwendet die Standardeinstellung des Anbieters', + maxTokens: 'Maximale Anzahl ausgegebener Token', + maxTokensPlaceholder: 'Verwendet die Standardeinstellung des Anbieters', + modelAdvanced: 'Modelloptionen', + modelInputTypes: 'Eingabetypen', + modelInputText: 'Text', + modelInputImage: 'Bild', + modelReasoning: 'Argumentationsebenen', + modelReasoningLevel: 'Ebene', + modelReasoningValue: 'API-Wert', + modelReasoningHelp: 'Über Argumentationsebenen', + modelReasoningHelpText: 'Der API-Wert wird im Feld für den Denkaufwand der Anfrage gesendet, etwa reasoning_effort bei OpenAI. Standardmäßig wird der Stufenname verwendet; gibt die API-Dokumentation des Modellanbieters einen anderen Wert vor, verwenden Sie diesen. Aus deaktiviert das Denken und benötigt keinen Wert; ein bereits konfigurierter Wert bleibt erhalten und wird gesendet.', + modelReasoningNeedsLevel: 'Fügen Sie neben „Aus“ mindestens eine Ebene hinzu oder deaktivieren Sie die Argumentationsebenen.', + modelReasoningValueRequired: 'Jede Ebene außer Off benötigt einen API-Wert und kein Wert darf leer sein.', + modelReasoningAdd: 'Ebene hinzufügen', + modelReasoningRemove: 'Ebene löschen', + modelReasoningOffValuePlaceholder: 'Schaltet das Denken aus', + addModel: 'Modell hinzufügen', + removeModel: 'Modell löschen', + reorderModel: 'Modell neu anordnen', + reorderModelHelp: 'Ziehen Sie, um die Reihenfolge neu anzuordnen, oder verwenden Sie die Aufwärts- und Abwärtspfeiltasten.', + modelsEmpty: 'Im Selektor werden keine Modelle angezeigt. Nicht gelistete Ausweise können weiterhin direkt versendet werden.', + keyBlank: 'Geben Sie den API-Schlüssel ein oder lassen Sie das Feld leer, um den gespeicherten Schlüssel beizubehalten.', + keyBlankNew: 'Geben Sie den API-Schlüssel ein oder lassen Sie das Feld leer, wenn dieser Anbieter auf andere Weise authentifiziert.', + keyIllegalCharacters: 'Dieser API-Schlüssel hat kein gültiges Format. Bitte überprüfen Sie es.', + modelIdRequired: 'Modell-ID ist erforderlich.', + modelIdDuplicate: 'Die Modell-ID muss eindeutig sein.', + modelNameInvalid: 'Der Anzeigename darf nicht leer sein.', + modelContextInvalid: 'Das Kontextfenster muss eine positive Zahl sein, z. B. 131072, 256K oder 1M.', + modelMaxTokensInvalid: 'Die maximale Anzahl an Ausgabetokens muss eine positive Zahl sein, z. B. 8192, 64K oder 1M.', + advancedHint: 'Andere Felder befinden sich in cordis.patch.yml; Bearbeiten Sie diesen Abschnitt direkt.', + modelCapacityInvalid: 'Eine Kapazität muss eine Zahl sein, optional mit dem Suffix K oder M.', + modelDuplicate: 'Jede Modell-ID darf einmal vorkommen.', + fetchModels: 'Verfügbare Modelle abrufen', + fetching: 'Den Anbieter fragen...', + fetchNeedsBaseUrl: 'Geben Sie zuerst die Basis-URL ein und rufen Sie sie dann ab.', + fetchEmpty: 'Der Anbieter listete keine Modelle auf. Fügen Sie sie von Hand hinzu.', + fetchTitle: 'Wählen Sie Modelle', + fetchDescription: 'Dies sind die Modelle, die dieser Anbieter zur Verfügung hat. Geprüfte Modelle bilden die Liste.', + fetchSearch: 'Modelle suchen', + fetchNoMatches: 'Keine passenden Modelle.', + fetchSelectAll: 'Alles auswählen', + fetchDeselectAll: 'Alle abwählen', + fetchAdopt: 'Liste aktualisieren', + fetchKeepOne: 'Lassen Sie mindestens ein Modell überprüft.', + customTag: 'Benutzerdefiniert', + customRoute: 'Anbieter-ID', + customRouteHint: 'Bezeichner in Kleinbuchstaben, beginnend mit einem Buchstaben, der diesen Anbieter in Anfragen und als Anmeldeinformationsname eindeutig benennt.', + customRouteInvalid: 'Beginnen Sie mit einem Kleinbuchstaben; dann Kleinbuchstaben, Ziffern und Bindestriche.', + customRouteTaken: 'Ein Anbieter verwendet diese ID bereits.', + customDisplayName: 'Anzeigename', + customApi: 'API-Protokoll', + customApiUnset: 'Nicht ausgewählt', + protocolOpenAiCompletions: 'OpenAI-Chat-Abschlüsse', + protocolOpenAiResponses: 'OpenAI-Antworten', + protocolAnthropicMessages: 'Anthropische Botschaften', + customNeedsBaseUrl: 'Ein benutzerdefinierter Anbieter benötigt eine Basis-URL.', + customBaseUrlInvalid: 'Geben Sie eine gültige HTTP- oder HTTPS-URL ein.', + customNeedsModels: 'Ein benutzerdefinierter Anbieter benötigt mindestens ein Modell.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'Unauflösbarer Einstellungspfad', + create: 'Anbieter erstellen', + creating: 'Erstellen…', + welcomeTitle: 'Vorschauhinweis', + welcomeBody: 'DeepSeek Harness 0.2 befindet sich noch in der Vorschau und viele Bereiche müssen weiter verbessert und verfeinert werden. Wir freuen uns über Feedback und Vorschläge aller Entwickler und Benutzer. Die neue Desktop-App richtet sich nun an ein breites Nutzerspektrum, während entwicklerbezogene erweiterte Funktionen in den Einstellungen aktiviert werden können. Es wird erwartet, dass die Produktfunktionen und Plugin-APIs von DeepSeek Harness ihre schnelle Iteration und Weiterentwicklung fortsetzen und sich im Laufe der Zeit allmählich stabilisieren. Wir heißen jeden willkommen, seine Ideen mit DeepSeek Harness zum Leben zu erwecken und sich an der Community zu beteiligen, um das Plugin-Ökosystem zu bereichern.', + welcomeContinue: 'Weiter', + welcomeError: 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.', + onboardingTitle: 'Fügen Sie einen API-Schlüssel hinzu, um loszulegen', + onboardingDescription: 'Konfigurieren Sie den offiziellen DeepSeek-Anbieter, um mit der Erstellung zu beginnen.', + onboardingLater: 'Später konfigurieren', + onboardingSave: 'Speichern und fortfahren', + onboardingSaving: 'Speichern…', + keyRequired: 'Geben Sie einen API-Schlüssel ein, um fortzufahren.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + nav: 'Модели', + deepSeekAccount: 'DeepSeek Аккаунт', + title: 'Модели', + intro: 'Введите ключи API, чтобы использовать модели следующих поставщиков.', + edit: 'Редактировать', + editProvider: 'Редактировать {provider}', + remove: 'Удалить', + removeProvider: 'Удалить {provider}', + deleteTitle: 'Удалить {provider}?', + deleteDescription: 'Удаление {provider} удаляет его конфигурацию. Любые учетные данные, которые он использует, управляются где-то еще и будут сохранены.', + deleteDescriptionWithCredential: 'Удаление {provider} удаляет его конфигурацию и сохраненный ключ API.', + deleteConfirm: 'Удалить {provider}', + deleting: 'Удаление {provider}…', + add: 'Добавить поставщика моделей', + addMode: 'Как добавить', + addCatalog: 'Сторонний поставщик моделей', + addCustom: 'Пользовательская модель API', + addCatalogHint: 'Выберите OpenAI, Anthropic, Kimi или другого провайдера из встроенного каталога и введите его ключ API.', + addCustomHint: 'Подключите реле, автономный сервер или любую другую конечную точку, совместимую с OpenAI или Anthropic, по ее базовому URL-адресу, протоколу и модели.', + addCatalogExhausted: 'Каждый поставщик каталога уже настроен.', + addCustomUnavailable: 'Протокол API недоступен для объявления.', + provider: 'Поставщик', + close: 'Закрыть', + cancel: 'Отмена', + apply: 'Применить', + applying: 'Применение…', + savedProvider: 'Сохранено {provider}.', + credentialConfigured: 'Ключ API настроен', + credentialMissing: 'Ключ API отсутствует', + accountSignedIn: 'Вошёл в систему', + readOnly: 'В этом развертывании документ настроек доступен только для чтения.', + loadFailed: 'Не удалось загрузить каталог поставщика', + conflict: 'Кто-то другой изменил эти настройки, пока эта карточка была открыта. Закройте его и откройте снова, чтобы редактировать текущие значения.', + retry: 'Повторить', + keyInput: 'Ключ API', + keyReveal: 'Показать секрет', + keyHide: 'Скрыть секрет', + keyPlaceholder: 'Введите свой ключ API', + keyPlaceholderFallback: 'Оставьте пустым, чтобы использовать env в качестве аутентификации ({hint})', + keyPlaceholderSaved: 'Оставьте поле пустым, чтобы использовать сохраненные учетные данные.', + keyFallbackMissing: 'Переменная Env не обнаружена. Введите ключ API или настройте {hint} в своей среде.', + keyFallbackDetected: 'Обнаружена переменная Env. ({hint})', + keyFallbackDetectedHidden: 'Обнаружена переменная Env.', + keyFallbackUnchecked: 'Статус экологической сертификации не подтвержден.', + keyHelp: 'Сохраненные учетные данные используются в первую очередь. Здесь показаны резервные варианты среды в порядке: {env}. Источниками являются среда запуска, $cwd/.env, затем $DSH_HOME/.env.', + authHelp: 'Детали аутентификации', + customized: 'Индивидуальные настройки', + baseUrl: 'Базовый URL', + baseUrlDefault: 'Нет конечной точки по умолчанию', + baseUrlEnvironment: 'Конфигурация конечной точки поставщика: {env}.', + baseUrlConfigured: 'Конечная точка, настроенная поставщиком', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (по умолчанию используется DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Используйте конечную точку API, совместимую с антропными сообщениями.', + models: 'Модели', + modelsInherited: 'Использование настроек адаптера по умолчанию', + modelsCustomized: 'Индивидуальный каталог моделей', + resetModels: 'Восстановить настройки по умолчанию', + model: 'Модель', + modelId: 'Идентификатор модели', + modelName: 'Отображаемое имя', + modelNamePlaceholder: 'Использует идентификатор модели, когда он пуст', + contextWindow: 'Контекстное окно', + contextWindowPlaceholder: 'Использует поставщика по умолчанию', + maxTokens: 'Максимальное количество выходных токенов', + maxTokensPlaceholder: 'Использует поставщика по умолчанию', + modelAdvanced: 'Варианты модели', + modelInputTypes: 'Типы ввода', + modelInputText: 'Текст', + modelInputImage: 'Изображение', + modelReasoning: 'Уровни рассуждения', + modelReasoningLevel: 'Уровень', + modelReasoningValue: 'Значение API', + modelReasoningHelp: 'Об уровнях рассуждения', + modelReasoningHelpText: 'Значение API передаётся в поле уровня рассуждений запроса, например reasoning_effort у OpenAI. По умолчанию используется название уровня; если документация API поставщика модели указывает другое значение, используйте его. Выкл. отключает рассуждения и не требует значения; уже записанное в конфигурации значение сохраняется и отправляется.', + modelReasoningNeedsLevel: 'Добавьте хотя бы один уровень помимо «Выкл.» или отключите уровни рассуждения.', + modelReasoningValueRequired: 'Для каждого уровня, кроме Off, требуется значение API, и ни одно значение не может быть пустым.', + modelReasoningAdd: 'Добавить уровень', + modelReasoningRemove: 'Удалить уровень', + modelReasoningOffValuePlaceholder: 'Отключает рассуждения', + addModel: 'Добавить модель', + removeModel: 'Удалить модель', + reorderModel: 'Изменить порядок модели', + reorderModelHelp: 'Перетащите, чтобы изменить порядок, или используйте клавиши со стрелками вверх и вниз.', + modelsEmpty: 'В селекторе не будут показаны модели. Неуказанные идентификаторы по-прежнему можно отправлять напрямую.', + keyBlank: 'Введите ключ API или оставьте поле пустым, чтобы сохранить сохраненный.', + keyBlankNew: 'Введите ключ API или оставьте поле пустым, если этот провайдер выполняет аутентификацию другим способом.', + keyIllegalCharacters: 'Этот ключ API имеет недопустимый формат. Пожалуйста, проверьте это.', + modelIdRequired: 'Требуется идентификатор модели.', + modelIdDuplicate: 'Идентификатор модели должен быть уникальным.', + modelNameInvalid: 'Отображаемое имя не может быть пустым.', + modelContextInvalid: 'Контекстное окно должно иметь положительное значение, например 131072, 256K или 1M.', + modelMaxTokensInvalid: 'Максимальное количество выходных токенов должно иметь положительное значение, например 8192, 64K или 1M.', + advancedHint: 'Остальные поля находятся в файле cordis.patch.yml; отредактируйте этот раздел напрямую.', + modelCapacityInvalid: 'Емкость должна быть числом, возможно с суффиксом K или M.', + modelDuplicate: 'Каждый идентификатор модели может появиться один раз.', + fetchModels: 'Получить доступные модели', + fetching: 'Спросил у провайдера...', + fetchNeedsBaseUrl: 'Сначала введите базовый URL-адрес, а затем извлеките его.', + fetchEmpty: 'Поставщик не указал моделей. Добавьте их вручную.', + fetchTitle: 'Выберите модели', + fetchDescription: 'Это модели, доступные у этого поставщика. Проверенные модели составляют список.', + fetchSearch: 'Поиск моделей', + fetchNoMatches: 'Нет подходящих моделей.', + fetchSelectAll: 'Выбрать все', + fetchDeselectAll: 'Отменить выбор всех', + fetchAdopt: 'Обновить список', + fetchKeepOne: 'Проверьте хотя бы одну модель.', + customTag: 'Пользовательский', + customRoute: 'Идентификатор поставщика', + customRouteHint: 'Идентификатор в нижнем регистре, начинающийся с буквы, который уникально называет этого поставщика в запросах и в качестве его учетного имени.', + customRouteInvalid: 'Начните со строчной буквы; затем строчные буквы, цифры и тире.', + customRouteTaken: 'Поставщик уже использует этот идентификатор.', + customDisplayName: 'Отображаемое имя', + customApi: 'Протокол API', + customApiUnset: 'Не выбрано', + protocolOpenAiCompletions: 'Завершения чата OpenAI', + protocolOpenAiResponses: 'Ответы OpenAI', + protocolAnthropicMessages: 'Антропные сообщения', + customNeedsBaseUrl: 'Пользовательскому поставщику нужен базовый URL-адрес.', + customBaseUrlInvalid: 'Введите действительный URL-адрес HTTP или HTTPS.', + customNeedsModels: 'Пользовательскому поставщику нужна хотя бы одна модель.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'неразрешимый путь настроек', + create: 'Создать поставщика', + creating: 'Создание…', + welcomeTitle: 'Уведомление о предварительном просмотре', + welcomeBody: 'DeepSeek Harness 0.2 все еще находится в предварительной версии, и многие области нуждаются в дальнейшем улучшении и доработке. Мы приветствуем отзывы и предложения от всех разработчиков и пользователей. Новое настольное приложение теперь ориентировано на широкий круг пользователей, а расширенные функции, связанные с разработчиком, можно включить в настройках. Ожидается, что функции продукта DeepSeek Harness и API-интерфейсы плагинов будут продолжать быстро меняться и развиваться и со временем будут постепенно стабилизироваться. \n\nМы надеемся изучить пределы интеллекта вместе с пользователями и разработчиками по всему миру, опираясь на многоразовую и компонуемую инфраструктуру с открытым исходным кодом. Мы приглашаем всех воплотить свои идеи в жизнь с помощью DeepSeek Harness и принять участие в сообществе, чтобы обогатить экосистему плагинов.', + welcomeContinue: 'Продолжить', + welcomeError: 'Не удалось сохранить подтверждение. Пожалуйста, попробуйте еще раз.', + onboardingTitle: 'Добавьте ключ API, чтобы начать работу.', + onboardingDescription: 'Настройте официального поставщика DeepSeek, чтобы начать сборку.', + onboardingLater: 'Настроить позже', + onboardingSave: 'Сохранить и продолжить', + onboardingSaving: 'Сохранение…', + keyRequired: 'Введите ключ API, чтобы продолжить.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + nav: 'Modelos', + deepSeekAccount: 'DeepSeek Conta', + title: 'Modelos', + intro: 'Insira suas chaves API para usar modelos dos seguintes fornecedores.', + edit: 'Editar', + editProvider: 'Editar {provider}', + remove: 'Excluir', + removeProvider: 'Excluir {provider}', + deleteTitle: 'Excluir {provider}?', + deleteDescription: 'Excluir {provider} remove sua configuração. Qualquer credencial usada é gerenciada em outro lugar e será mantida.', + deleteDescriptionWithCredential: 'A exclusão de {provider} remove sua configuração e a chave API armazenada.', + deleteConfirm: 'Excluir {provider}', + deleting: 'Excluindo {provider}…', + add: 'Adicionar provedor de modelo', + addMode: 'Como adicionar', + addCatalog: 'Provedor de modelo terceirizado', + addCustom: 'Modelo personalizado API', + addCatalogHint: 'Escolha OpenAI, Anthropic, Kimi ou outro provedor no catálogo integrado e insira sua chave API.', + addCustomHint: 'Conecte um retransmissor, um servidor auto-hospedado ou qualquer outro endpoint compatível com OpenAI ou Anthropic por meio de URL, protocolo e modelos básicos.', + addCatalogExhausted: 'Cada provedor de catálogo já está configurado.', + addCustomUnavailable: 'Nenhum protocolo API está disponível para declaração.', + provider: 'Provedor', + close: 'Fechar', + cancel: 'Cancelar', + apply: 'Aplicar', + applying: 'Aplicando…', + savedProvider: 'Salvo {provider}.', + credentialConfigured: 'API chave configurada', + credentialMissing: 'API chave faltando', + accountSignedIn: 'Conectado', + readOnly: 'O documento de configurações é somente leitura nesta implantação.', + loadFailed: 'Falha ao carregar o diretório do provedor', + conflict: 'Outra pessoa alterou essas configurações enquanto este cartão estava aberto. Feche-o e abra-o novamente para editar os valores atuais.', + retry: 'Tentar novamente', + keyInput: 'API chave', + keyReveal: 'Mostrar segredo', + keyHide: 'Ocultar segredo', + keyPlaceholder: 'Insira sua chave API', + keyPlaceholderFallback: 'Deixe em branco para usar env como autenticação ({hint})', + keyPlaceholderSaved: 'Deixe em branco para usar credenciais salvas', + keyFallbackMissing: 'Env var não detectado. Insira uma chave API ou configure {hint} em seu ambiente.', + keyFallbackDetected: 'Variável de ambiente detectada. ({hint})', + keyFallbackDetectedHidden: 'Variável de ambiente detectada.', + keyFallbackUnchecked: 'O status da credencial ambiental não foi confirmado.', + keyHelp: 'As credenciais salvas são usadas primeiro. Fallbacks de ambiente mostrados aqui, em ordem: {env}. As fontes são o ambiente de inicialização, $cwd/.env e, em seguida, $DSH_HOME/.env.', + authHelp: 'Detalhes de autenticação', + customized: 'Configurações personalizadas', + baseUrl: 'URL base', + baseUrlDefault: 'Nenhum endpoint padrão', + baseUrlEnvironment: 'Configuração do endpoint do provedor: {env}', + baseUrlConfigured: 'Endpoint configurado pelo provedor', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (padrão para usar DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Use um endpoint API compatível com Anthropic Messages.', + models: 'Modelos', + modelsInherited: 'Usando os padrões do adaptador', + modelsCustomized: 'Catálogo de modelos personalizados', + resetModels: 'Restaurar padrões', + model: 'Modelo', + modelId: 'ID do modelo', + modelName: 'Nome de exibição', + modelNamePlaceholder: 'Usa o ID do modelo quando vazio', + contextWindow: 'Janela de contexto', + contextWindowPlaceholder: 'Usa o padrão do provedor', + maxTokens: 'Máximo de tokens de saída', + maxTokensPlaceholder: 'Usa o padrão do provedor', + modelAdvanced: 'Opções de modelo', + modelInputTypes: 'Tipos de entrada', + modelInputText: 'Texto', + modelInputImage: 'Imagem', + modelReasoning: 'Níveis de raciocínio', + modelReasoningLevel: 'Nível', + modelReasoningValue: 'valor API', + modelReasoningHelp: 'Sobre níveis de raciocínio', + modelReasoningHelpText: 'O valor da API é enviado no campo de esforço de raciocínio da solicitação, como reasoning_effort da OpenAI. Por padrão, usa-se o nome do nível; se a documentação da API do provedor do modelo especificar outro valor, use-o. Desativado desliga o raciocínio e não exige valor; qualquer valor já salvo na configuração é mantido e enviado.', + modelReasoningNeedsLevel: 'Adicione pelo menos um nível além de Desativado ou desative os níveis de raciocínio.', + modelReasoningValueRequired: 'Cada nível além de Desligado precisa de um valor API e nenhum valor pode ficar vazio.', + modelReasoningAdd: 'Adicionar nível', + modelReasoningRemove: 'Excluir nível', + modelReasoningOffValuePlaceholder: 'Desativa o raciocínio', + addModel: 'Adicionar modelo', + removeModel: 'Excluir modelo', + reorderModel: 'Reordenar modelo', + reorderModelHelp: 'Arraste para reordenar ou use as teclas de seta para cima e para baixo.', + modelsEmpty: 'Nenhum modelo será mostrado no seletor. IDs não listados ainda podem ser enviados diretamente.', + keyBlank: 'Insira a chave API ou deixe o campo vazio para manter a chave armazenada.', + keyBlankNew: 'Insira a chave API ou deixe o campo vazio se este provedor autenticar de outra forma.', + keyIllegalCharacters: 'Esta chave API não está em um formato válido. Por favor, verifique.', + modelIdRequired: 'O ID do modelo é obrigatório.', + modelIdDuplicate: 'O ID do modelo deve ser exclusivo.', + modelNameInvalid: 'O nome de exibição não pode ficar vazio.', + modelContextInvalid: 'A janela de contexto deve ter uma contagem positiva, como 131072, 256K ou 1M.', + modelMaxTokensInvalid: 'Os tokens de saída máximos devem ter uma contagem positiva, como 8192, 64K ou 1M.', + advancedHint: 'Outros campos residem em cordis.patch.yml; edite essa seção diretamente.', + modelCapacityInvalid: 'Uma capacidade deve ser um número, opcionalmente com o sufixo K ou M.', + modelDuplicate: 'Cada ID de modelo pode aparecer uma vez.', + fetchModels: 'Buscar modelos disponíveis', + fetching: 'Perguntando ao provedor…', + fetchNeedsBaseUrl: 'Insira o URL base primeiro e depois busque.', + fetchEmpty: 'O fornecedor não listou nenhum modelo. Adicione-os manualmente.', + fetchTitle: 'Escolha modelos', + fetchDescription: 'Estes são os modelos que este fornecedor tem disponíveis. Os modelos verificados compõem a lista.', + fetchSearch: 'Pesquisar modelos', + fetchNoMatches: 'Não há modelos correspondentes.', + fetchSelectAll: 'Selecionar tudo', + fetchDeselectAll: 'Desmarcar tudo', + fetchAdopt: 'Lista de atualização', + fetchKeepOne: 'Mantenha pelo menos um modelo verificado.', + customTag: 'Personalizado', + customRoute: 'ID do provedor', + customRouteHint: 'Identificador minúsculo, começando com uma letra, que nomeia exclusivamente esse provedor nas solicitações e como seu nome de credencial.', + customRouteInvalid: 'Comece com uma letra minúscula; em seguida, letras minúsculas, dígitos e travessões.', + customRouteTaken: 'Um provedor já usa esse ID.', + customDisplayName: 'Nome de exibição', + customApi: 'API protocolo', + customApiUnset: 'Não selecionado', + protocolOpenAiCompletions: 'Conclusões do bate-papo OpenAI', + protocolOpenAiResponses: 'Respostas OpenAI', + protocolAnthropicMessages: 'Mensagens Antrópicas', + customNeedsBaseUrl: 'Um provedor personalizado precisa de um URL base.', + customBaseUrlInvalid: 'Insira um URL HTTP ou HTTPS válido.', + customNeedsModels: 'Um provedor personalizado precisa de pelo menos um modelo.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'caminho de configurações insolúveis', + create: 'Criar provedor', + creating: 'Criando…', + welcomeTitle: 'Aviso de visualização', + welcomeBody: 'DeepSeek Harness 0.2 ainda está em versão prévia e muitas áreas precisam de melhorias e refinamentos contínuos. Agradecemos comentários e sugestões de todos os desenvolvedores e usuários. O novo aplicativo de desktop agora é direcionado a uma ampla gama de usuários, enquanto recursos avançados relacionados ao desenvolvedor podem ser habilitados nas configurações. Espera-se que os recursos do produto e as APIs de plug-in do DeepSeek Harness continuem com rápida iteração e evolução e se estabilizem gradualmente ao longo do tempo.\n\nEsperamos explorar os limites da inteligência junto com usuários e desenvolvedores em todo o mundo, desenvolvendo uma infraestrutura de código aberto, reutilizável e combinável. Convidamos todos a dar vida às suas ideias com DeepSeek Harness e participar da comunidade para enriquecer o ecossistema de plug-ins.', + welcomeContinue: 'Continuar', + welcomeError: 'A confirmação não pôde ser salva. Por favor, tente novamente.', + onboardingTitle: 'Adicione uma chave API para começar', + onboardingDescription: 'Configure o provedor DeepSeek oficial para começar a compilar.', + onboardingLater: 'Configurar mais tarde', + onboardingSave: 'Salvar e continuar', + onboardingSaving: 'Salvando…', + keyRequired: 'Insira uma chave de API para continuar.', +} satisfies Record diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index da48676b6f..08ce75a428 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -1,3 +1,4 @@ +import { es, fr, de, ru, pt } from '../src/client/locales.ts' // @vitest-environment jsdom /** Section, setup-card, and hand-written editor behavior over a scripted wire face. */ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' @@ -2423,3 +2424,16 @@ it('renders the localized account row as signed in without an edit action', asyn expect(within(item).getByText(en.accountSignedIn)).toBeTruthy() expect(within(item).queryByText(en.edit)).toBeNull() }) + +it.each([es, fr, de, ru, pt])('keeps capacity examples in translated validation messages parseable', (dictionary) => { + for (const [key, examples] of [ + ['modelContextInvalid', ['131072', '256K', '1M']], + ['modelMaxTokensInvalid', ['8192', '64K', '1M']], + ] as const) { + for (const example of examples) { + const token = dictionary[key].match(new RegExp(`(? ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-plugin-inventory: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-plugin-inventory: dictionaries') const t = ctx.locale.bind(NS) const list: PluginInventorySettingsTabInjected['list'] = async () => { diff --git a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts index 391896bcc0..d0d7480472 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts +++ b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts @@ -125,3 +125,208 @@ export const ja = { 'failed': '起動に失敗', 'unloading': 'アンロード中', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + tab: 'Lista de complementos', + loading: 'Leyendo complementos…', + clientSyncing: 'Sincronizando complementos en esta página…', + clientSyncFailed: 'Algunos complementos no se pudieron sincronizar en esta página. La habilitación del host no ha cambiado.', + clientSyncRetry: 'Reintentar esta página', + error: 'Los complementos no están disponibles temporalmente.', + metadataError: 'Error de metadatos del paquete: {error}', + retry: 'Reintentar', + search: 'Complementos de búsqueda', + empty: 'No hay complementos disponibles.', + emptySearch: 'No hay complementos coincidentes.', + presetTitle: 'Complementos de sesión', + presetSubtitle: 'Compuesto por sesión por ajustes preestablecidos de agente', + switcherLabel: 'Elija el agente preestablecido para inspeccionar', + presetOptionDefault: '{name} (predeterminado)', + presetOptionBroken: '{name} (no se pudo cargar)', + globalTitle: 'Complementos globales', + globalSubtitle: 'Compartido por el sistema y cada sesión.', + presetProvidedDetail: 'Discapacitados a nivel mundial; Los ajustes preestablecidos del agente lo proporcionan por sesión.', + enabledIn: 'Habilitado en', + matchesInOtherPresets: '{count} más coincidencias en otros ajustes preestablecidos: ', + enabledTag: 'Habilitado', + disabledTag: 'Discapacitado', + conditionalTag: 'Condicional', + presetEnabledTag: 'A través de ajustes preestablecidos', + failedTag: 'Fallido', + moduleLabel: 'Módulo', + fromPreset: 'De', + condition: 'Deshabilitado cuando', + configuration: 'Configuración', + runtime: 'Estado', + unobserved: 'no corriendo', + pending: 'Esperando dependencias', + loadingPhase: 'Cargando', + active: 'corriendo', + failed: 'No se pudo iniciar', + unloading: 'Descarga', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + tab: 'Liste des plugins', + loading: 'Plugins de lecture…', + clientSyncing: 'Synchronisation des plugins sur cette page…', + clientSyncFailed: "Certains plugins n'ont pas pu se synchroniser sur cette page. L'activation de l'hôte est inchangée.", + clientSyncRetry: 'Réessayez cette page', + error: 'Les plugins sont temporairement indisponibles.', + metadataError: 'Erreur de métadonnées du package : {error}', + retry: 'Réessayer', + search: 'Plugins de recherche', + empty: "Aucun plugin n'est disponible.", + emptySearch: 'Aucun plugin correspondant.', + presetTitle: 'Plugins de sessions', + presetSubtitle: "Composé par session par préréglages d'agent", + switcherLabel: "Choisissez le préréglage d'agent à inspecter", + presetOptionDefault: '{name} (par défaut)', + presetOptionBroken: '{name} (échec du chargement)', + globalTitle: 'Plugins globaux', + globalSubtitle: 'Partagé par le système et à chaque session', + presetProvidedDetail: "Handicapé à l'échelle mondiale ; les préréglages d'agent le fournissent par session", + enabledIn: 'Activé dans', + matchesInOtherPresets: "{count} plus de correspondances dans d'autres préréglages : ", + enabledTag: 'Activé', + disabledTag: 'Désactivé', + conditionalTag: 'Conditionnel', + presetEnabledTag: 'Via les préréglages', + failedTag: 'Échec', + moduleLabel: 'Module', + fromPreset: 'De', + condition: 'Désactivé quand', + configuration: 'Configuration', + runtime: 'Statut', + unobserved: 'Ne fonctionne pas', + pending: 'En attente de dépendances', + loadingPhase: 'Chargement', + active: 'Courir', + failed: 'Échec du démarrage', + unloading: 'Déchargement', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + tab: 'Plugin-Liste', + loading: 'Plugins lesen…', + clientSyncing: 'Plugins auf dieser Seite werden synchronisiert…', + clientSyncFailed: 'Einige Plugins konnten auf dieser Seite nicht synchronisiert werden. Die Host-Aktivierung bleibt unverändert.', + clientSyncRetry: 'Versuchen Sie es erneut mit dieser Seite', + error: 'Plugins sind vorübergehend nicht verfügbar.', + metadataError: 'Paketmetadatenfehler: {error}', + retry: 'Erneut versuchen', + search: 'Such-Plugins', + empty: 'Es sind keine Plugins verfügbar.', + emptySearch: 'Keine passenden Plugins.', + presetTitle: 'Sitzungs-Plugins', + presetSubtitle: 'Zusammengestellt pro Sitzung nach Agentenvoreinstellungen', + switcherLabel: 'Wählen Sie die zu überprüfende Agentenvoreinstellung aus', + presetOptionDefault: '{name} (Standard)', + presetOptionBroken: '{name} (Laden fehlgeschlagen)', + globalTitle: 'Globale Plugins', + globalSubtitle: 'Wird vom System und jeder Sitzung geteilt', + presetProvidedDetail: 'Weltweit deaktiviert; Agentenvoreinstellungen stellen es pro Sitzung bereit', + enabledIn: 'Aktiviert in', + matchesInOtherPresets: '{count} weitere Übereinstimmungen in anderen Voreinstellungen: ', + enabledTag: 'Aktiviert', + disabledTag: 'Deaktiviert', + conditionalTag: 'Bedingt', + presetEnabledTag: 'Über Voreinstellungen', + failedTag: 'Fehlgeschlagen', + moduleLabel: 'Modul', + fromPreset: 'Von', + condition: 'Deaktiviert, wenn', + configuration: 'Konfiguration', + runtime: 'Status', + unobserved: 'Läuft nicht', + pending: 'Warten auf Abhängigkeiten', + loadingPhase: 'Laden', + active: 'Laufen', + failed: 'Start fehlgeschlagen', + unloading: 'Entladen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + tab: 'Список плагинов', + loading: 'Чтение плагинов…', + clientSyncing: 'Синхронизация плагинов на этой странице…', + clientSyncFailed: 'Некоторые плагины не смогли синхронизироваться на этой странице. Включение хоста не изменилось.', + clientSyncRetry: 'Повторить эту страницу', + error: 'Плагины временно недоступны.', + metadataError: 'Ошибка метаданных пакета: {error}.', + retry: 'Повторить', + search: 'Поиск плагинов', + empty: 'Нет доступных плагинов.', + emptySearch: 'Нет подходящих плагинов.', + presetTitle: 'Плагины сеансов', + presetSubtitle: 'Составляется за сеанс с помощью предустановок агента', + switcherLabel: 'Выберите предустановку агента для проверки', + presetOptionDefault: '{name} (по умолчанию)', + presetOptionBroken: '{name} (не удалось загрузить)', + globalTitle: 'Глобальные плагины', + globalSubtitle: 'Совместно используется системой и каждым сеансом', + presetProvidedDetail: 'Отключено глобально; предустановки агента предусматривают это за сеанс', + enabledIn: 'Включено в', + matchesInOtherPresets: '{count} больше совпадений в других пресетах: ', + enabledTag: 'Включено', + disabledTag: 'Отключено', + conditionalTag: 'Условный', + presetEnabledTag: 'Через пресеты', + failedTag: 'Не удалось', + moduleLabel: 'Модуль', + fromPreset: 'От', + condition: 'Отключено, когда', + configuration: 'Конфигурация', + runtime: 'Статус', + unobserved: 'Не работает', + pending: 'Ожидание зависимостей', + loadingPhase: 'Загрузка', + active: 'Бег', + failed: 'Не удалось запустить', + unloading: 'Разгрузка', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + tab: 'Lista de plug-ins', + loading: 'Lendo plug-ins…', + clientSyncing: 'Sincronizando plug-ins nesta página…', + clientSyncFailed: 'Alguns plug-ins não puderam ser sincronizados nesta página. A ativação do host permanece inalterada.', + clientSyncRetry: 'Tente esta página novamente', + error: 'Os plug-ins estão temporariamente indisponíveis.', + metadataError: 'Erro de metadados do pacote: {error}', + retry: 'Tentar novamente', + search: 'Plug-ins de pesquisa', + empty: 'Nenhum plug-in está disponível.', + emptySearch: 'Nenhum plug-in correspondente.', + presetTitle: 'Plug-ins de sessão', + presetSubtitle: 'Composto por sessão por predefinições de agente', + switcherLabel: 'Escolha a predefinição do agente para inspecionar', + presetOptionDefault: '{name} (padrão)', + presetOptionBroken: '{name} (falha ao carregar)', + globalTitle: 'Plug-ins globais', + globalSubtitle: 'Compartilhado pelo sistema e por cada sessão', + presetProvidedDetail: 'Desativado globalmente; predefinições de agente fornecem isso por sessão', + enabledIn: 'Habilitado em', + matchesInOtherPresets: '{count} mais correspondências em outras predefinições: ', + enabledTag: 'Habilitado', + disabledTag: 'Desativado', + conditionalTag: 'Condicional', + presetEnabledTag: 'Por meio de predefinições', + failedTag: 'Falha', + moduleLabel: 'Módulo', + fromPreset: 'De', + condition: 'Desativado quando', + configuration: 'Configuração', + runtime: 'Estado', + unobserved: 'Não está em execução', + pending: 'Esperando por dependências', + loadingPhase: 'Carregando', + active: 'Correndo', + failed: 'Falha ao iniciar', + unloading: 'Descarregando', +} satisfies Record diff --git a/packages/client/ui-settings-plugins/locale/de.json b/packages/client/ui-settings-plugins/locale/de.json new file mode 100644 index 0000000000..9b3e3dbeb2 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Integrierter Plug-in-Einstellungseingang", + "description": "Bietet den integrierten Plug-in-Eingang und seine Funktionsregisterkarte in den Einstellungen." + } +} diff --git a/packages/client/ui-settings-plugins/locale/es.json b/packages/client/ui-settings-plugins/locale/es.json new file mode 100644 index 0000000000..f06cce9794 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrada de configuración de complementos incorporados", + "description": "Proporciona la entrada del complemento integrado y su pestaña de función en la configuración." + } +} diff --git a/packages/client/ui-settings-plugins/locale/fr.json b/packages/client/ui-settings-plugins/locale/fr.json new file mode 100644 index 0000000000..762ba220ef --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrée des paramètres du plug-in intégré", + "description": "Fournit l'entrée du plug-in intégré et sa page à onglet de fonction dans les paramètres." + } +} diff --git a/packages/client/ui-settings-plugins/locale/pt.json b/packages/client/ui-settings-plugins/locale/pt.json new file mode 100644 index 0000000000..4af280898e --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrada de configurações de plug-in integradas", + "description": "Fornece a entrada do plug-in integrada e sua guia de função nas configurações." + } +} diff --git a/packages/client/ui-settings-plugins/locale/ru.json b/packages/client/ui-settings-plugins/locale/ru.json new file mode 100644 index 0000000000..4fdd139aa0 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Вход в настройки встроенного плагина", + "description": "Обеспечивает вход встроенного плагина и вкладку его функций в настройках." + } +} diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index 023e38a5c2..2d9ec1b082 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Built-in plugins settings section for the dsh web client: the Settings navigation entry and the tab chrome feature-owned tabs register into", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/src/client/index.ts b/packages/client/ui-settings-plugins/src/client/index.ts index 4897b7c503..b9db73a850 100644 --- a/packages/client/ui-settings-plugins/src/client/index.ts +++ b/packages/client/ui-settings-plugins/src/client/index.ts @@ -18,7 +18,7 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { PluginsSettingsSection } from './PluginsSettingsSection.tsx' import type { PluginsSettingsSectionInjected, PluginsSettingsTabEntry } from './PluginsSettingsSection.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { PluginsSettingsSectionInjected, PluginsSettingsSectionProps } from './PluginsSettingsSection.tsx' @@ -34,7 +34,7 @@ export const inject = ['slots', 'locale'] */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-plugins: section dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-plugins: section dictionaries') let tabsVersion = -1 let tabsRevision = -1 diff --git a/packages/client/ui-settings-plugins/src/client/locales.ts b/packages/client/ui-settings-plugins/src/client/locales.ts index 9d32663ee5..723b596309 100644 --- a/packages/client/ui-settings-plugins/src/client/locales.ts +++ b/packages/client/ui-settings-plugins/src/client/locales.ts @@ -29,3 +29,48 @@ export const ja = { 'tabs': 'プラグインビュー', 'empty': 'このデプロイメントにはプラグインビューがありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + nav: 'Complementos integrados', + title: 'Complementos integrados', + intro: 'Inspeccione los complementos que incluye esta implementación.', + tabs: 'Vistas de complementos', + empty: 'Esta implementación no expone vistas de complementos.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + nav: 'Plugins intégrés', + title: 'Plugins intégrés', + intro: 'Inspectez les plugins fournis par ce déploiement.', + tabs: 'Vues des plugins', + empty: "Ce déploiement n'expose aucune vue de plugin.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + nav: 'Integrierte Plugins', + title: 'Integrierte Plugins', + intro: 'Überprüfen Sie die Plugins, die mit dieser Bereitstellung geliefert werden.', + tabs: 'Plugin-Ansichten', + empty: 'Diese Bereitstellung stellt keine Plugin-Ansichten bereit.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + nav: 'Встроенные плагины', + title: 'Встроенные плагины', + intro: 'Проверьте плагины, поставляемые в этом развертывании.', + tabs: 'Просмотры плагинов', + empty: 'Это развертывание не предоставляет никаких представлений плагинов.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + nav: 'Plug-ins integrados', + title: 'Plug-ins integrados', + intro: 'Inspecione os plug-ins fornecidos por esta implantação.', + tabs: 'Visualizações de plug-in', + empty: 'Esta implantação não expõe visualizações de plug-ins.', +} satisfies Record diff --git a/packages/client/ui-settings-session-log/locale/de.json b/packages/client/ui-settings-session-log/locale/de.json new file mode 100644 index 0000000000..4fcbe1e3dc --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen zum Hochladen des Sitzungsprotokolls", + "description": "Legt fest, ob Sitzungsprotokolle mit DeepSeekAnfragen des offiziellen Modells API hochgeladen werden." + } +} diff --git a/packages/client/ui-settings-session-log/locale/es.json b/packages/client/ui-settings-session-log/locale/es.json new file mode 100644 index 0000000000..58eedd3600 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de carga del registro de sesión", + "description": "Establece si los registros de sesión se cargan con las solicitudes del DeepSeekmodelo oficial API." + } +} diff --git a/packages/client/ui-settings-session-log/locale/fr.json b/packages/client/ui-settings-session-log/locale/fr.json new file mode 100644 index 0000000000..eaf0616333 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres de téléchargement du journal de session", + "description": "Définit si les journaux de session sont téléchargés avec les requêtes DeepSeek de modèle officiel API." + } +} diff --git a/packages/client/ui-settings-session-log/locale/pt.json b/packages/client/ui-settings-session-log/locale/pt.json new file mode 100644 index 0000000000..cfe80f4cc6 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de upload de log de sessão", + "description": "Define se os logs de sessão serão carregados com solicitações de DeepSeekmodelo oficialAPI." + } +} diff --git a/packages/client/ui-settings-session-log/locale/ru.json b/packages/client/ui-settings-session-log/locale/ru.json new file mode 100644 index 0000000000..acba5b0b17 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки загрузки журнала сеанса", + "description": "Определяет, загружаются ли журналы сеансов с запросами DeepSeek официальной модели API." + } +} diff --git a/packages/client/ui-settings-session-log/package.json b/packages/client/ui-settings-session-log/package.json index f4ed749062..c268254b01 100644 --- a/packages/client/ui-settings-session-log/package.json +++ b/packages/client/ui-settings-session-log/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-session-log", "description": "General settings control for Session-log upload with DeepSeek API requests", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-session-log/src/client/index.ts b/packages/client/ui-settings-session-log/src/client/index.ts index 6e749043cb..c959d1a64d 100644 --- a/packages/client/ui-settings-session-log/src/client/index.ts +++ b/packages/client/ui-settings-session-log/src/client/index.ts @@ -6,7 +6,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import { UploadPreference, type UploadSettings } from './upload-preference.ts' import { UploadRow, UploadToast, type UploadInjected } from './UploadRow.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -25,7 +25,7 @@ export const inject = ['slots', 'locale', 'configForms'] export function apply(ctx: Context): void { const locale = 'settings.sessionLog' const namespace = 'session-log-deepseek' - ctx.effect(() => ctx.locale.register(locale, { en, zh, ja })) + ctx.effect(() => ctx.locale.register(locale, { en, zh, ja, es, fr, de, ru, pt })) const form = ctx.configForms.get(namespace) const preference = new UploadPreference(form) const face = (): UploadInjected => ({ diff --git a/packages/client/ui-settings-session-log/src/client/locales.ts b/packages/client/ui-settings-session-log/src/client/locales.ts index 99b1d2f815..aeb0d22bbd 100644 --- a/packages/client/ui-settings-session-log/src/client/locales.ts +++ b/packages/client/ui-settings-session-log/src/client/locales.ts @@ -21,3 +21,43 @@ export const ja = { 'saved': '設定を保存しました', 'failed': '設定を保存できませんでした', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Cargar registro de sesión cuando utilice el modelo oficial API', + description: 'Ayude a mejorar los modelos y productos DeepSeek.', + saved: 'Preferencia guardada', + failed: 'No se pudo guardar la preferencia', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: "Téléchargez le journal de session lors de l'utilisation du modèle officiel API", + description: 'Aidez à améliorer les modèles et produits DeepSeek.', + saved: 'Préférence enregistrée', + failed: "Impossible d'enregistrer la préférence", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Laden Sie das Sitzungsprotokoll hoch, wenn Sie das offizielle Modell API verwenden', + description: 'Helfen Sie mit, DeepSeek-Modelle und -Produkte zu verbessern.', + saved: 'Präferenz gespeichert', + failed: 'Präferenz konnte nicht gespeichert werden', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Загрузить журнал сеансов при использовании официальной модели API', + description: 'Помогите улучшить модели и продукты DeepSeek.', + saved: 'Предпочтение сохранено.', + failed: 'Не удалось сохранить предпочтения.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Carregar log de sessão ao usar o modelo oficial API', + description: 'Ajude a melhorar DeepSeek modelos e produtos.', + saved: 'Preferência salva', + failed: 'Não foi possível salvar a preferência', +} satisfies Record diff --git a/packages/client/ui-settings-shell/locale/de.json b/packages/client/ui-settings-shell/locale/de.json new file mode 100644 index 0000000000..74ac0a1feb --- /dev/null +++ b/packages/client/ui-settings-shell/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen zur Befehlsausführung", + "description": "Konfigurieren Sie das Zeitlimit für die Befehlsausführung und die Begrenzung der Ausgabelänge." + } +} diff --git a/packages/client/ui-settings-shell/locale/es.json b/packages/client/ui-settings-shell/locale/es.json new file mode 100644 index 0000000000..ea799af8ec --- /dev/null +++ b/packages/client/ui-settings-shell/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de ejecución de comandos", + "description": "Configure el tiempo de espera de ejecución del comando y el límite de longitud de salida." + } +} diff --git a/packages/client/ui-settings-shell/locale/fr.json b/packages/client/ui-settings-shell/locale/fr.json new file mode 100644 index 0000000000..d1afc02893 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres d'exécution des commandes", + "description": "Configurez le délai d'expiration de l'exécution de la commande et la limite de longueur de sortie." + } +} diff --git a/packages/client/ui-settings-shell/locale/pt.json b/packages/client/ui-settings-shell/locale/pt.json new file mode 100644 index 0000000000..929cead7d8 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de execução de comando", + "description": "Configure o tempo limite de execução do comando e o limite de comprimento de saída." + } +} diff --git a/packages/client/ui-settings-shell/locale/ru.json b/packages/client/ui-settings-shell/locale/ru.json new file mode 100644 index 0000000000..9d95f4b7a2 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки выполнения команд", + "description": "Настройте тайм-аут выполнения команды и ограничение длины вывода." + } +} diff --git a/packages/client/ui-settings-shell/package.json b/packages/client/ui-settings-shell/package.json index 1ff33eb641..0f13b113e1 100644 --- a/packages/client/ui-settings-shell/package.json +++ b/packages/client/ui-settings-shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-shell", "description": "Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-shell/src/client/index.ts b/packages/client/ui-settings-shell/src/client/index.ts index 55f69d6b09..a20afd04de 100644 --- a/packages/client/ui-settings-shell/src/client/index.ts +++ b/packages/client/ui-settings-shell/src/client/index.ts @@ -17,7 +17,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { ShellCard } from './ShellCard.tsx' import { BASH_NS, PWSH_NS, ShellCardController } from './shell-card-controller.ts' -import { en, ja, zh, type ShellSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ShellSettingsLocaleKey } from './locales.ts' export type { ShellCardProps } from './ShellCard.tsx' export type { ShellCardFace, ShellCardState, ShellSettings } from './shell-card-controller.ts' @@ -42,7 +42,7 @@ export const inject = ['slots', 'locale', 'configForms'] */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-shell: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-shell: dictionaries') // The base bundle composes exactly one shell executor per platform; the page binds the served one. const bash = new ShellCardController(ctx.configForms.get(BASH_NS)) const pwsh = new ShellCardController(ctx.configForms.get(PWSH_NS)) diff --git a/packages/client/ui-settings-shell/src/client/locales.ts b/packages/client/ui-settings-shell/src/client/locales.ts index 74980e77fc..2054ba8025 100644 --- a/packages/client/ui-settings-shell/src/client/locales.ts +++ b/packages/client/ui-settings-shell/src/client/locales.ts @@ -71,3 +71,93 @@ export const ja = { 'saveFailed': 'デプロイがこれらの値を受け付けませんでした。修正のため残されています。', 'invalidNumber': '数値を入力するか、空欄のままにするとデフォルトが使用されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Concha', + description: 'Limite cuánto tiempo puede ejecutarse cada comando y cuánto puede generar.', + timeoutMs: 'Tiempo de espera del comando (ms)', + timeoutMsHint: 'Cuánto tiempo puede ejecutarse un comando antes de finalizar.', + maxOutputBytes: 'Límite de salida por flujo (bytes)', + maxOutputBytesHint: 'La salida más allá de esto se transfiere a un archivo temporal en lugar de perderse.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Coquille', + description: "Limitez la durée d'exécution de chaque commande et le volume de résultats qu'elle peut générer.", + timeoutMs: "Délai d'expiration de la commande (ms)", + timeoutMsHint: "Combien de temps une commande peut s'exécuter avant d'être terminée.", + maxOutputBytes: 'Limite de sortie par flux (octets)', + maxOutputBytesHint: "La sortie au-delà de cela se déverse dans un fichier temporaire plutôt que d'être perdue.", + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Muschel', + description: 'Begrenzen Sie, wie lange jeder Befehl ausgeführt werden darf und wie viel er ausgeben darf.', + timeoutMs: 'Befehls-Timeout (ms)', + timeoutMsHint: 'Wie lange ein Befehl ausgeführt werden darf, bevor er beendet wird.', + maxOutputBytes: 'Ausgabeobergrenze pro Stream (Byte)', + maxOutputBytesHint: 'Darüber hinausgehende Ausgaben werden in eine temporäre Datei übertragen und gehen nicht verloren.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Ракушка', + description: 'Ограничьте продолжительность выполнения каждой команды и объем вывода.', + timeoutMs: 'Таймаут команды (мс)', + timeoutMsHint: 'Как долго может выполняться одна команда до ее завершения.', + maxOutputBytes: 'Ограничение вывода на поток (байты)', + maxOutputBytesHint: 'Вывод, выходящий за пределы этого, сохраняется во временный файл, а не теряется.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Concha', + description: 'Limite quanto tempo cada comando pode ser executado e quanto ele pode gerar.', + timeoutMs: 'Tempo limite do comando (ms)', + timeoutMsHint: 'Quanto tempo um comando pode ser executado antes de ser encerrado.', + maxOutputBytes: 'Limite de saída por stream (bytes)', + maxOutputBytesHint: 'A saída além disso é transferida para um arquivo temporário em vez de ser perdida.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings-subagent/locale/de.json b/packages/client/ui-settings-subagent/locale/de.json new file mode 100644 index 0000000000..ecce9cc69e --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Subagenteneinstellungen", + "description": "Konfigurieren Sie die Delegationstiefe, die Anzahl der Parallelitäten und optionale Modelle." + } +} diff --git a/packages/client/ui-settings-subagent/locale/es.json b/packages/client/ui-settings-subagent/locale/es.json new file mode 100644 index 0000000000..3323c6f124 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de subagente", + "description": "Configure la profundidad de la delegación, el número de simultaneidades y modelos opcionales." + } +} diff --git a/packages/client/ui-settings-subagent/locale/fr.json b/packages/client/ui-settings-subagent/locale/fr.json new file mode 100644 index 0000000000..aec77b780f --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres du sous-agent", + "description": "Configurez la profondeur de la délégation, le nombre de simultanéités et les modèles facultatifs." + } +} diff --git a/packages/client/ui-settings-subagent/locale/pt.json b/packages/client/ui-settings-subagent/locale/pt.json new file mode 100644 index 0000000000..00b8568382 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações do subagente", + "description": "Configure a profundidade da delegação, o número de simultaneidades e os modelos opcionais." + } +} diff --git a/packages/client/ui-settings-subagent/locale/ru.json b/packages/client/ui-settings-subagent/locale/ru.json new file mode 100644 index 0000000000..7c74680dd0 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки субагента", + "description": "Настройте глубину делегирования, количество параллелизма и дополнительные модели." + } +} diff --git a/packages/client/ui-settings-subagent/package.json b/packages/client/ui-settings-subagent/package.json index 30b6fe6fcb..0242eb0d5a 100644 --- a/packages/client/ui-settings-subagent/package.json +++ b/packages/client/ui-settings-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-subagent", "description": "Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-subagent/src/client/index.ts b/packages/client/ui-settings-subagent/src/client/index.ts index 42811181ee..107e206b5b 100644 --- a/packages/client/ui-settings-subagent/src/client/index.ts +++ b/packages/client/ui-settings-subagent/src/client/index.ts @@ -23,7 +23,7 @@ import { SubagentLimitsCardController } from './subagent-limits-card-controller. import { SUBAGENT_MODEL_SELECTION_NS, SubagentModelSelectionCardController, } from './subagent-model-selection-card-controller.ts' -import { en, ja, zh, type SubagentSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type SubagentSettingsLocaleKey } from './locales.ts' export type { SubagentCardProps } from './SubagentCard.tsx' export type { SubagentCardFace } from './subagent-card-controller.ts' @@ -58,7 +58,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.session', 'configFor */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-subagent: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-subagent: dictionaries') const limits = new SubagentLimitsCardController(ctx.configForms.get(SUBAGENT_NS)) ctx.effect(() => () => { limits.dispose() }, 'ui-settings-subagent: limits form subscription') const models = new SubagentModelSelectionCardController( diff --git a/packages/client/ui-settings-subagent/src/client/locales.ts b/packages/client/ui-settings-subagent/src/client/locales.ts index d92185f3dd..6a575814d8 100644 --- a/packages/client/ui-settings-subagent/src/client/locales.ts +++ b/packages/client/ui-settings-subagent/src/client/locales.ts @@ -146,3 +146,198 @@ export const ja = { 'subagentModelSelectionConflict': '設定が別の場所で変更されました。下書きを破棄して再試行してください。', 'subagentModelSelectionOff': 'サブエージェントは設定済みのデフォルトを使用するか、親エージェントのモデルを継承します。保存済みのモデル選択は保持されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + subagentTitle: 'Subagente', + subagentDescription: 'Establezca la profundidad, el recuento y los modelos de recursividad del subagente.', + subagentLimitsTitle: 'Límites', + subagentMaxDepth: 'Profundidad máxima de recursividad', + subagentDepthHelpLabel: 'Acerca de la profundidad máxima de recursividad', + subagentDepthHelp: 'Limita cuántos niveles de Subagentes puede crear un Agente.', + subagentDepthZero: 'Deshabilitar subagentes', + subagentDepthOne: 'Sólo el Agente principal puede crear Subagentes', + subagentDepthOverride: 'Si una herramienta define su propia profundidad de recursividad máxima, esa configuración tiene prioridad.', + subagentMaxActive: 'Límite de paralelismo de subagente', + subagentCapacityHelpLabel: 'Acerca del límite de paralelismo del subagente', + subagentCapacityHelp: 'Total de subagentes activos bajo el mismo agente principal, en todos los niveles de recursividad. El Agente principal está excluido. Las nuevas solicitudes de inicio se rechazan cuando se alcanza el límite.', + subagentDepthInvalid: 'Introduzca un número entero de 0 o más.', + subagentCapacityInvalid: 'Introduzca un número entero de 1 o más.', + subagentModelSelectionTitle: 'Selección de modelo', + subagentModelSelectionToggle: 'Permitir a los agentes elegir modelos para Subagentes', + subagentModelSelectionChoose: 'Cuando está habilitado, los agentes pueden elegir un proveedor, modelo y esfuerzo de razonamiento para cada subagente entre los modelos autorizados a continuación. Aplica sólo para sesiones nuevas.', + subagentModelSelectionAllowed: 'Los agentes modelos pueden elegir', + subagentModelSelectionLoading: 'Cargando modelos…', + subagentModelSelectionLoadFailed: 'No se pudieron cargar los modelos.', + subagentModelSelectionRetry: 'Reintentar', + subagentModelSelectionPartial: 'No se pudieron cargar algunos proveedores de modelos; Las opciones guardadas siguen siendo extraíbles.', + subagentModelSelectionUnavailable: 'Actualmente no disponible', + subagentModelSelectionUnavailableGroup: 'Guardado pero actualmente no disponible', + subagentModelSelectionEmpty: 'Ningún proveedor de modelos anuncia actualmente un modelo.', + subagentModelSelectionRequired: 'Seleccione al menos un modelo antes de guardar.', + subagentModelSelectionConflict: 'La configuración cambió en otros lugares. Descarta tu borrador y vuelve a intentarlo.', + subagentModelSelectionOff: 'Los subagentes utilizan valores predeterminados configurados o heredan el modelo del agente principal. Las opciones de modelo guardadas se conservan.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + subagentTitle: 'Sous-agent', + subagentDescription: 'Définissez la profondeur, le nombre et les modèles de récursion du sous-agent.', + subagentLimitsTitle: 'Limites', + subagentMaxDepth: 'Profondeur de récursion maximale', + subagentDepthHelpLabel: 'À propos de la profondeur de récursion maximale', + subagentDepthHelp: "Limite le nombre de niveaux de sous-agents qu'un agent peut créer.", + subagentDepthZero: 'Désactiver les sous-agents', + subagentDepthOne: "Seul l'agent principal peut créer des sous-agents", + subagentDepthOverride: 'Si un outil définit sa propre profondeur de récursion maximale, ce paramètre est prioritaire.', + subagentMaxActive: 'Limite de parallélisme des sous-agents', + subagentCapacityHelpLabel: 'À propos de la limite de parallélisme des sous-agents', + subagentCapacityHelp: "Total des sous-agents actifs sous le même agent principal, à tous les niveaux de récursion. L'Agent principal est exclu. Les nouvelles demandes de démarrage sont rejetées lorsque la limite est atteinte.", + subagentDepthInvalid: 'Entrez un nombre entier de 0 ou plus.', + subagentCapacityInvalid: 'Entrez un nombre entier de 1 ou plus.', + subagentModelSelectionTitle: 'Sélection du modèle', + subagentModelSelectionToggle: 'Autoriser les agents à choisir des modèles pour les sous-agents', + subagentModelSelectionChoose: "Lorsque cette option est activée, les agents peuvent choisir un fournisseur, un modèle et un effort de raisonnement pour chaque sous-agent parmi les modèles autorisés ci-dessous. S'applique uniquement aux nouvelles sessions.", + subagentModelSelectionAllowed: 'Les agents modèles peuvent choisir', + subagentModelSelectionLoading: 'Chargement des modèles…', + subagentModelSelectionLoadFailed: "Les modèles n'ont pas pu être chargés.", + subagentModelSelectionRetry: 'Réessayer', + subagentModelSelectionPartial: "Certains fournisseurs de modèles n'ont pas pu être chargés ; les choix enregistrés restent amovibles.", + subagentModelSelectionUnavailable: 'Actuellement indisponible', + subagentModelSelectionUnavailableGroup: 'Enregistré mais actuellement indisponible', + subagentModelSelectionEmpty: "Aucun fournisseur de modèles n'annonce actuellement un modèle.", + subagentModelSelectionRequired: 'Sélectionnez au moins un modèle avant de sauvegarder.', + subagentModelSelectionConflict: 'Paramètres modifiés ailleurs. Supprimez votre brouillon et réessayez.', + subagentModelSelectionOff: "Les sous-agents utilisent les valeurs par défaut configurées ou héritent du modèle de l'agent parent. Les choix de modèles enregistrés sont conservés.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + subagentTitle: 'Subagent', + subagentDescription: 'Legen Sie die Rekursionstiefe, Anzahl und Modelle des Subagenten fest.', + subagentLimitsTitle: 'Grenzen', + subagentMaxDepth: 'Maximale Rekursionstiefe', + subagentDepthHelpLabel: 'Über maximale Rekursionstiefe', + subagentDepthHelp: 'Begrenzt die Anzahl der Ebenen von Subagenten, die ein Agent erstellen kann.', + subagentDepthZero: 'Subagenten deaktivieren', + subagentDepthOne: 'Nur der Hauptagent kann Unteragenten erstellen', + subagentDepthOverride: 'Wenn ein Werkzeug seine eigene maximale Rekursionstiefe definiert, hat diese Einstellung Vorrang.', + subagentMaxActive: 'Grenzwert für die Parallelität des Subagenten', + subagentCapacityHelpLabel: 'Informationen zum Subagenten-Parallelitätslimit', + subagentCapacityHelp: 'Gesamtzahl der Live-Subagenten unter demselben Hauptagenten, über alle Rekursionsebenen hinweg. Der Hauptvertreter ist ausgeschlossen. Neue Startanfragen werden abgelehnt, wenn das Limit erreicht ist.', + subagentDepthInvalid: 'Geben Sie eine ganze Zahl von 0 oder mehr ein.', + subagentCapacityInvalid: 'Geben Sie eine ganze Zahl von 1 oder mehr ein.', + subagentModelSelectionTitle: 'Modellauswahl', + subagentModelSelectionToggle: 'Erlauben Sie Agenten, Modelle für Subagenten auszuwählen', + subagentModelSelectionChoose: 'Wenn diese Option aktiviert ist, können Agenten aus den unten aufgeführten autorisierten Modellen einen Anbieter, ein Modell und einen Argumentationsaufwand für jeden Subagenten auswählen. Gilt nur für neue Sitzungen.', + subagentModelSelectionAllowed: 'Modelagenten können wählen', + subagentModelSelectionLoading: 'Modelle werden geladen…', + subagentModelSelectionLoadFailed: 'Modelle konnten nicht geladen werden.', + subagentModelSelectionRetry: 'Erneut versuchen', + subagentModelSelectionPartial: 'Einige Modellanbieter konnten nicht geladen werden; Gespeicherte Auswahlmöglichkeiten bleiben entfernbar.', + subagentModelSelectionUnavailable: 'Derzeit nicht verfügbar', + subagentModelSelectionUnavailableGroup: 'Gespeichert, aber derzeit nicht verfügbar', + subagentModelSelectionEmpty: 'Derzeit bewirbt kein Modelanbieter ein Model.', + subagentModelSelectionRequired: 'Wählen Sie vor dem Speichern mindestens ein Modell aus.', + subagentModelSelectionConflict: 'Einstellungen wurden an anderer Stelle geändert. Verwerfen Sie Ihren Entwurf und versuchen Sie es erneut.', + subagentModelSelectionOff: 'Subagenten verwenden konfigurierte Standardwerte oder erben das Modell des übergeordneten Agenten. Gespeicherte Modellauswahlen bleiben erhalten.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + subagentTitle: 'Субагент', + subagentDescription: 'Установите глубину рекурсии субагента, количество и модели.', + subagentLimitsTitle: 'Пределы', + subagentMaxDepth: 'Максимальная глубина рекурсии', + subagentDepthHelpLabel: 'О максимальной глубине рекурсии', + subagentDepthHelp: 'Ограничивает количество уровней субагентов, которые может создать агент.', + subagentDepthZero: 'Отключить субагенты', + subagentDepthOne: 'Только главный агент может создавать субагентов.', + subagentDepthOverride: 'Если инструмент определяет собственную максимальную глубину рекурсии, этот параметр имеет приоритет.', + subagentMaxActive: 'Предел параллелизма субагентов', + subagentCapacityHelpLabel: 'О пределе параллелизма субагентов', + subagentCapacityHelp: 'Общее количество действующих субагентов под одним и тем же основным агентом на всех уровнях рекурсии. Главный Агент исключен. Новые запросы на запуск отклоняются при достижении предела.', + subagentDepthInvalid: 'Введите целое число от 0 или более.', + subagentCapacityInvalid: 'Введите целое число от 1 или более.', + subagentModelSelectionTitle: 'Выбор модели', + subagentModelSelectionToggle: 'Разрешить агентам выбирать модели для субагентов', + subagentModelSelectionChoose: 'Если этот параметр включен, агенты могут выбирать поставщика, модель и усилия по обоснованию для каждого субагента из авторизованных моделей, указанных ниже. Применяется только к новым сеансам.', + subagentModelSelectionAllowed: 'Модели агенты могут выбирать', + subagentModelSelectionLoading: 'Загрузка моделей…', + subagentModelSelectionLoadFailed: 'Не удалось загрузить модели.', + subagentModelSelectionRetry: 'Повторить', + subagentModelSelectionPartial: 'Некоторые поставщики моделей не удалось загрузить; сохраненные варианты остаются съемными.', + subagentModelSelectionUnavailable: 'В настоящее время недоступен', + subagentModelSelectionUnavailableGroup: 'Сохранено, но сейчас недоступно', + subagentModelSelectionEmpty: 'Ни один поставщик моделей в настоящее время не рекламирует модель.', + subagentModelSelectionRequired: 'Прежде чем сохранить, выберите хотя бы одну модель.', + subagentModelSelectionConflict: 'Настройки изменены в другом месте. Отмените черновик и повторите попытку.', + subagentModelSelectionOff: 'Субагенты используют настроенные значения по умолчанию или наследуют модель родительского агента. Сохраненные варианты моделей сохраняются.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + subagentTitle: 'Subagente', + subagentDescription: 'Defina a profundidade, a contagem e os modelos de recursão do subagente.', + subagentLimitsTitle: 'Limites', + subagentMaxDepth: 'Profundidade máxima de recursão', + subagentDepthHelpLabel: 'Sobre a profundidade máxima de recursão', + subagentDepthHelp: 'Limita quantos níveis de Subagentes um Agente pode criar.', + subagentDepthZero: 'Desabilitar subagentes', + subagentDepthOne: 'Somente o Agente principal pode criar Subagentes', + subagentDepthOverride: 'Se uma ferramenta definir sua própria profundidade máxima de recursão, essa configuração terá precedência.', + subagentMaxActive: 'Limite de paralelismo de subagentes', + subagentCapacityHelpLabel: 'Sobre o limite de paralelismo do subagente', + subagentCapacityHelp: 'Total de subagentes ativos sob o mesmo agente principal, em todos os níveis de recursão. O Agente principal é excluído. Novas solicitações de início serão rejeitadas quando o limite for atingido.', + subagentDepthInvalid: 'Insira um número inteiro de 0 ou mais.', + subagentCapacityInvalid: 'Insira um número inteiro de 1 ou mais.', + subagentModelSelectionTitle: 'Seleção de modelo', + subagentModelSelectionToggle: 'Permitir que os agentes escolham modelos para subagentes', + subagentModelSelectionChoose: 'Quando ativado, os agentes podem escolher um provedor, um modelo e um esforço de raciocínio para cada subagente dentre os modelos autorizados abaixo. Aplica-se apenas a novas sessões.', + subagentModelSelectionAllowed: 'Os agentes de modelos podem escolher', + subagentModelSelectionLoading: 'Carregando modelos…', + subagentModelSelectionLoadFailed: 'Não foi possível carregar os modelos.', + subagentModelSelectionRetry: 'Tentar novamente', + subagentModelSelectionPartial: 'Alguns provedores de modelo não puderam ser carregados; as escolhas salvas permanecem removíveis.', + subagentModelSelectionUnavailable: 'Atualmente indisponível', + subagentModelSelectionUnavailableGroup: 'Salvo, mas atualmente indisponível', + subagentModelSelectionEmpty: 'Atualmente, nenhum fornecedor de modelos anuncia um modelo.', + subagentModelSelectionRequired: 'Selecione pelo menos um modelo antes de salvar.', + subagentModelSelectionConflict: 'Configurações alteradas em outro lugar. Descarte seu rascunho e tente novamente.', + subagentModelSelectionOff: 'Os subagentes usam padrões configurados ou herdam o modelo do agente pai. As opções de modelo salvas são mantidas.', +} satisfies Record diff --git a/packages/client/ui-settings-web-search/locale/de.json b/packages/client/ui-settings-web-search/locale/de.json new file mode 100644 index 0000000000..5aa238488d --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen für die Websuche", + "description": "Konfigurieren Sie den Schlüssel, die Dienstadresse und das Anforderungsbudget für die DeepSeek-Websuche." + } +} diff --git a/packages/client/ui-settings-web-search/locale/es.json b/packages/client/ui-settings-web-search/locale/es.json new file mode 100644 index 0000000000..1e1d3f5e7b --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de búsqueda web", + "description": "Configure la clave, la dirección del servicio y solicite el presupuesto para la búsqueda web DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/locale/fr.json b/packages/client/ui-settings-web-search/locale/fr.json new file mode 100644 index 0000000000..6c20e4212a --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres de recherche sur le Web", + "description": "Configurez la clé, l'adresse du service et le budget de demande pour la recherche sur le Web DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/locale/pt.json b/packages/client/ui-settings-web-search/locale/pt.json new file mode 100644 index 0000000000..ce7d68d86c --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de pesquisa na web", + "description": "Configure a chave, o endereço do serviço e solicite o orçamento para DeepSeek pesquisa na web." + } +} diff --git a/packages/client/ui-settings-web-search/locale/ru.json b/packages/client/ui-settings-web-search/locale/ru.json new file mode 100644 index 0000000000..b8bd1cd774 --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки веб-поиска", + "description": "Настройте ключ, адрес службы и бюджет запроса для веб-поиска DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/package.json b/packages/client/ui-settings-web-search/package.json index 1d1b50fa4d..d0914d7b31 100644 --- a/packages/client/ui-settings-web-search/package.json +++ b/packages/client/ui-settings-web-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-web-search", "description": "Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-web-search/src/client/index.ts b/packages/client/ui-settings-web-search/src/client/index.ts index 461237a165..b4ed1c6779 100644 --- a/packages/client/ui-settings-web-search/src/client/index.ts +++ b/packages/client/ui-settings-web-search/src/client/index.ts @@ -22,7 +22,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { WebSearchCard } from './WebSearchCard.tsx' import { WEB_API_NS, WEB_NS, WEB_SEARCH_NS, WebSearchCardController } from './web-search-card-controller.ts' -import { en, ja, zh, type WebSearchSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type WebSearchSettingsLocaleKey } from './locales.ts' export type { WebSearchCardProps } from './WebSearchCard.tsx' export type { @@ -49,7 +49,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.credentials', 'confi */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-web-search: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-web-search: dictionaries') const card = new WebSearchCardController({ web: ctx.configForms.get(WEB_NS), messages: ctx.configForms.get(WEB_SEARCH_NS), api: ctx.configForms.get(WEB_API_NS), }, ctx) diff --git a/packages/client/ui-settings-web-search/src/client/locales.ts b/packages/client/ui-settings-web-search/src/client/locales.ts index 65d21ae8f0..1290fb9000 100644 --- a/packages/client/ui-settings-web-search/src/client/locales.ts +++ b/packages/client/ui-settings-web-search/src/client/locales.ts @@ -111,3 +111,158 @@ export const ja = { 'invalidNumber': '数値を入力するか、デフォルトを使用するには空欄にしてください。', 'invalidCount': '1〜50 の整数を入力するか、デフォルトを使用するには空欄にしてください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'búsqueda web', + description: 'Configure el proveedor de búsqueda DeepSeek.', + useSearchApi: 'Utilice la nueva búsqueda web API', + useSearchApiHint: 'Desactivado de forma predeterminada: la búsqueda se ejecuta a través de Mensajes DeepSeek. Para conocer las características y los precios del nuevo Web Search API, consulte el ', + searchGuide: 'Guía de búsqueda y recuperación', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Clave de API', + apiKeyHint: 'Almacenado fuera del archivo de configuración. Déjelo en blanco para conservar la clave actual.', + apiKeySet: 'Se configura una clave.', + apiKeyUnset: 'No hay ninguna clave configurada; solo se pueden buscar conversaciones que utilicen un modelo de cuenta DeepSeek, a través del punto final predeterminado.', + baseUrl: 'Mensajes API URL base', + baseUrlHint: 'Incluya el prefijo API, como https://api.deepseek.com/anthropic/v1.. El proveedor agrega /messages. Déjelo en blanco para restaurar el valor predeterminado.', + searchApiBaseUrl: 'Buscar/obtener URL base API', + searchApiBaseUrlHint: 'Incluya el prefijo API, como https://api.deepseek.com/v1/tools.. El proveedor agrega /search o /fetch. Déjelo en blanco para restaurar el valor predeterminado.', + maxUses: 'Búsquedas máximas por solicitud', + maxUsesHint: 'Cuántas búsquedas puede ejecutar el modelo auxiliar dentro de una solicitud de Mensajes. Aplica sólo para Mensajes API.', + count: 'Resultados por búsqueda', + countHint: 'Resultados devueltos por consulta: un número entero del 1 al 50, valor predeterminado 10. Esto controla el recuento de resultados, no las llamadas de búsqueda. web_search no guarda más fuentes que su propio límite de resultados.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', + invalidCount: 'Ingrese un número entero del 1 al 50 o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Recherche sur le Web', + description: 'Configurez le moteur de recherche DeepSeek.', + useSearchApi: 'Utilisez la nouvelle recherche Web API', + useSearchApiHint: "Désactivé par défaut : la recherche s'effectue dans les messages DeepSeek. Pour connaître les fonctionnalités et les tarifs du nouveau Web Search API, consultez le ", + searchGuide: 'Guide de recherche et de récupération', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Clé API', + apiKeyHint: 'Stocké en dehors du fichier de paramètres. Laissez vide pour conserver la clé actuelle.', + apiKeySet: 'Une clé est configurée.', + apiKeyUnset: "Aucune clé n'est configurée ; seules les conversations utilisant un modèle de compte DeepSeek peuvent effectuer une recherche, via le point de terminaison par défaut.", + baseUrl: 'URL de base des messages API', + baseUrlHint: 'Incluez le préfixe API, tel que https://api.deepseek.com/anthropic/v1.. Le fournisseur ajoute /messages. Laissez vide pour restaurer la valeur par défaut.', + searchApiBaseUrl: "Rechercher/récupérer l'URL de base de API", + searchApiBaseUrlHint: 'Incluez le préfixe API, tel que https://api.deepseek.com/v1/tools.. Le fournisseur ajoute /search ou /fetch. Laissez vide pour restaurer la valeur par défaut.', + maxUses: 'Nombre maximum de recherches par requête', + maxUsesHint: "Combien de recherches le modèle auxiliaire peut exécuter dans une requête de messages. S'applique uniquement aux messages API.", + count: 'Résultats par recherche', + countHint: 'Résultats renvoyés par requête : un nombre entier de 1 à 50, 10 par défaut. Cela contrôle le nombre de résultats, pas les appels de recherche. web_search ne conserve pas plus de sources que sa propre limite de résultats.', + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', + invalidCount: 'Entrez un nombre entier compris entre 1 et 50 ou laissez vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Websuche', + description: 'Richten Sie den Suchanbieter DeepSeek ein.', + useSearchApi: 'Nutzen Sie die neue Websuche API', + useSearchApiHint: 'Standardmäßig deaktiviert: Die Suche erfolgt über DeepSeek-Nachrichten. Informationen zu den Funktionen und Preisen der neuen Websuche API finden Sie im ', + searchGuide: 'Leitfaden zum Suchen und Abrufen', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'API-Schlüssel', + apiKeyHint: 'Außerhalb der Einstellungsdatei gespeichert. Lassen Sie das Feld leer, um den aktuellen Schlüssel beizubehalten.', + apiKeySet: 'Ein Schlüssel ist konfiguriert.', + apiKeyUnset: 'Es ist kein Schlüssel konfiguriert; Nur Konversationen, die ein DeepSeek-Kontomodell verwenden, können über den Standardendpunkt suchen.', + baseUrl: 'Nachrichten API Basis-URL', + baseUrlHint: 'Fügen Sie das Präfix API ein, z. B. https://api.deepseek.com/anthropic/v1.. Der Anbieter hängt /messages an. Lassen Sie das Feld leer, um die Standardeinstellung wiederherzustellen.', + searchApiBaseUrl: 'Suchen/Abrufen der API-Basis-URL', + searchApiBaseUrlHint: 'Fügen Sie das Präfix API ein, z. B. https://api.deepseek.com/v1/tools.. Der Anbieter hängt /search oder /fetch an. Lassen Sie das Feld leer, um die Standardeinstellung wiederherzustellen.', + maxUses: 'Maximale Anzahl an Suchanfragen pro Anfrage', + maxUsesHint: 'Wie viele Suchvorgänge das Hilfsmodell innerhalb einer Nachrichtenanforderung ausführen darf. Gilt nur für die Nachrichten API.', + count: 'Ergebnisse pro Suche', + countHint: 'Pro Abfrage zurückgegebene Ergebnisse: eine Ganzzahl von 1 bis 50, Standard 10. Dies steuert die Ergebnisanzahl, nicht Suchaufrufe. web_search behält nicht mehr Quellen als sein eigenes Ergebnislimit.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', + invalidCount: 'Geben Sie eine Ganzzahl zwischen 1 und 50 ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Веб-поиск', + description: 'Настройте поисковую систему DeepSeek.', + useSearchApi: 'Используйте новый веб-поиск API', + useSearchApiHint: 'По умолчанию отключено: поиск осуществляется по сообщениям DeepSeek. Информацию о новых функциях и ценах веб-поиска API см. ', + searchGuide: 'Руководство по поиску и получению', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Ключ API', + apiKeyHint: 'Хранится вне файла настроек. Оставьте пустым, чтобы сохранить текущий ключ.', + apiKeySet: 'Ключ настроен.', + apiKeyUnset: 'Ключ не настроен; только разговоры, использующие модель учетной записи DeepSeek, могут выполнять поиск через конечную точку по умолчанию.', + baseUrl: 'Базовый URL-адрес сообщений API', + baseUrlHint: 'Включите префикс API, например https://api.deepseek.com/anthropic/v1.. Поставщик добавляет /messages. Оставьте пустым, чтобы восстановить значение по умолчанию.', + searchApiBaseUrl: 'Найдите/получите базовый URL-адрес API', + searchApiBaseUrlHint: 'Включите префикс API, например https://api.deepseek.com/v1/tools.. Поставщик добавляет /search или /fetch. Оставьте пустым, чтобы восстановить значение по умолчанию.', + maxUses: 'Максимальное количество запросов на запрос', + maxUsesHint: 'Сколько поисков вспомогательная модель может выполнить в рамках одного запроса сообщений. Применяется только к сообщениям API.', + count: 'Результаты по поиску', + countHint: 'Результаты, возвращаемые по запросу: целое число от 1 до 50, по умолчанию 10. Это контролирует количество результатов, а не поисковые вызовы. web_search хранит не больше источников, чем его собственный лимит результатов.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', + invalidCount: 'Введите целое число от 1 до 50 или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Pesquisa na web', + description: 'Configure o provedor de pesquisa DeepSeek.', + useSearchApi: 'Use a nova Pesquisa na Web API', + useSearchApiHint: 'Desativado por padrão: a pesquisa é executada em DeepSeek Mensagens. Para conhecer os recursos e preços da nova Pesquisa na Web API, consulte o ', + searchGuide: 'Guia de pesquisa e busca', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'API chave', + apiKeyHint: 'Armazenado fora do arquivo de configurações. Deixe em branco para manter a chave atual.', + apiKeySet: 'Uma chave está configurada.', + apiKeyUnset: 'Nenhuma chave está configurada; somente conversas que usam um modelo de conta DeepSeek podem pesquisar, por meio do endpoint padrão.', + baseUrl: 'URL base de mensagens API', + baseUrlHint: 'Inclua o prefixo API, como https://api.deepseek.com/anthropic/v1. O provedor anexa /messages. Deixe em branco para restaurar o padrão.', + searchApiBaseUrl: 'Pesquisar/buscar API URL base', + searchApiBaseUrlHint: 'Inclua o prefixo API, como https://api.deepseek.com/v1/tools. O provedor anexa /search ou /fetch. Deixe em branco para restaurar o padrão.', + maxUses: 'Máximo de pesquisas por solicitação', + maxUsesHint: 'Quantas pesquisas o modelo auxiliar pode executar em uma solicitação de Mensagens. Aplica-se somente às Mensagens API.', + count: 'Resultados por pesquisa', + countHint: 'Resultados retornados por consulta: um número inteiro de 1 a 50, padrão 10. Isso controla a contagem de resultados, não as chamadas de pesquisa. web_search não mantém mais fontes do que seu próprio limite de resultados.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', + invalidCount: 'Insira um número inteiro de 1 a 50 ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings/locale/de.json b/packages/client/ui-settings/locale/de.json new file mode 100644 index 0000000000..c0a32190bd --- /dev/null +++ b/packages/client/ui-settings/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstellenrahmen festlegen", + "description": "Stellt das Grundformular und den Plug-in-Erweiterungsspeicherort der Einstellungsseite bereit." + } +} diff --git a/packages/client/ui-settings/locale/es.json b/packages/client/ui-settings/locale/es.json new file mode 100644 index 0000000000..f703fc3c6e --- /dev/null +++ b/packages/client/ui-settings/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Establecer marco de interfaz", + "description": "Proporciona el formulario básico y la ubicación de la extensión del complemento de la página de configuración." + } +} diff --git a/packages/client/ui-settings/locale/fr.json b/packages/client/ui-settings/locale/fr.json new file mode 100644 index 0000000000..4e500b24d0 --- /dev/null +++ b/packages/client/ui-settings/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Définir le cadre de l'interface", + "description": "Fournit le formulaire de base et l’emplacement de l’extension du plug-in de la page des paramètres." + } +} diff --git a/packages/client/ui-settings/locale/pt.json b/packages/client/ui-settings/locale/pt.json new file mode 100644 index 0000000000..4aebe79237 --- /dev/null +++ b/packages/client/ui-settings/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Definir quadro de interface", + "description": "Fornece o formulário básico e a localização da extensão do plug-in da página de configurações." + } +} diff --git a/packages/client/ui-settings/locale/ru.json b/packages/client/ui-settings/locale/ru.json new file mode 100644 index 0000000000..bd60ab3572 --- /dev/null +++ b/packages/client/ui-settings/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Установить фрейм интерфейса", + "description": "Предоставляет базовую форму и расположение расширения подключаемого модуля на странице настроек." + } +} diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index 44193c54ff..a8ac4e05bc 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: shared configuration forms and the canonical settings slot-type contract", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-shortcuts/locale/de.json b/packages/client/ui-shortcuts/locale/de.json new file mode 100644 index 0000000000..f6fef238f7 --- /dev/null +++ b/packages/client/ui-shortcuts/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zum Festlegen von Tastenkombinationen", + "description": "Tastaturkürzel anzeigen, eingeben und ändern." + } +} diff --git a/packages/client/ui-shortcuts/locale/es.json b/packages/client/ui-shortcuts/locale/es.json new file mode 100644 index 0000000000..e01ec944f6 --- /dev/null +++ b/packages/client/ui-shortcuts/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración de teclas de acceso directo", + "description": "Ver, ingresar y modificar atajos de teclado." + } +} diff --git a/packages/client/ui-shortcuts/locale/fr.json b/packages/client/ui-shortcuts/locale/fr.json new file mode 100644 index 0000000000..bbaa18731d --- /dev/null +++ b/packages/client/ui-shortcuts/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration des touches de raccourci", + "description": "Afficher, saisir et modifier les raccourcis clavier." + } +} diff --git a/packages/client/ui-shortcuts/locale/pt.json b/packages/client/ui-shortcuts/locale/pt.json new file mode 100644 index 0000000000..85767dbdff --- /dev/null +++ b/packages/client/ui-shortcuts/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de teclas de atalho", + "description": "Visualize, insira e modifique atalhos de teclado." + } +} diff --git a/packages/client/ui-shortcuts/locale/ru.json b/packages/client/ui-shortcuts/locale/ru.json new file mode 100644 index 0000000000..b9152041ee --- /dev/null +++ b/packages/client/ui-shortcuts/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки сочетаний клавиш", + "description": "Просмотр, ввод и изменение сочетаний клавиш." + } +} diff --git a/packages/client/ui-shortcuts/package.json b/packages/client/ui-shortcuts/package.json index 04cb270923..b6fa63255a 100644 --- a/packages/client/ui-shortcuts/package.json +++ b/packages/client/ui-shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-shortcuts", "description": "Keyboard shortcut reference, recording, and local preference editing", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-shortcuts/src/client/index.ts b/packages/client/ui-shortcuts/src/client/index.ts index 9e0d508833..1de049aec6 100644 --- a/packages/client/ui-shortcuts/src/client/index.ts +++ b/packages/client/ui-shortcuts/src/client/index.ts @@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import { createShortcutsStore } from './store.ts' import { ShortcutReference, ShortcutsRow } from './Reference.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { fixedCommands } from './fixed.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -26,7 +26,7 @@ export const inject = ['shortcuts', 'locale', 'slots'] * @param ctx - plugin-owned client context. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('shortcuts', { zh, en, ja }), 'shortcuts: dictionaries') + ctx.effect(() => ctx.locale.register('shortcuts', { zh, en, ja, es, fr, de, ru, pt }), 'shortcuts: dictionaries') const t = ctx.locale.bind('shortcuts') const handle = createShortcutsStore() const instance = handle.create() diff --git a/packages/client/ui-shortcuts/src/client/locales.ts b/packages/client/ui-shortcuts/src/client/locales.ts index e96e4457da..6261df423a 100644 --- a/packages/client/ui-shortcuts/src/client/locales.ts +++ b/packages/client/ui-shortcuts/src/client/locales.ts @@ -128,3 +128,183 @@ export const ja = { 'select': 'メニュー項目を選択', 'dismiss': 'メニューまたは最上位ダイアログを閉じる', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'edit-label': 'Editar acceso directo para {command}', + record: 'Presione un atajo', 'record-help': 'Suelta las teclas para guardar. La pestaña se mueve entre acciones; Esc se cancela.', + 'web-help': 'Combinaciones de navegador: Mod+/, Mod+Shift+, Mod+Shift+... Mod es Comando en Mac y Ctrl en otros lugares.', + 'unsupported-key': 'Esta clave no es compatible.', reserved: 'Esta combinación está reservada para acciones de edición de texto o del sistema.', + 'modifier-required': 'Incluya Comando, Ctrl o Alt en la combinación.', + 'too-many-keys': 'Mantenga presionadas como máximo dos teclas no modificadoras. Suelta las teclas para volver a intentarlo.', + 'macos-web-help': 'Utilice Comando+/, Comando+, Comando+barra invertida, Control+comillas invertidas, Comando+Opción+tecla o Comando+Mayús+tecla. También se admiten combinaciones con tres o cuatro modificadores distintos. Es posible que los accesos directos del navegador o del sistema no lleguen a la página.', + 'windows-web-help': 'Utilice Ctrl+/, Ctrl+, Ctrl+Alt+tecla o Ctrl+Shift+tecla. También se admiten combinaciones con tres o cuatro modificadores distintos. Es posible que los accesos directos del navegador o del sistema no lleguen a la página.', + 'unsupported-browser': 'Este navegador aún no admite esta combinación.', conflict: 'Ya utilizado por “{commands}”', + saved: 'Modificado', clear: 'Quitar', 'retry-save': 'Reintentar guardar', reset: 'Restaurar valor predeterminado', + 'reset-all': 'Restaurar todos los valores predeterminados', 'modified-count': '{count} personalizado', + 'reset-title': '¿Restaurar todos los atajos predeterminados?', + 'reset-description': 'Restaure los accesos directos predeterminados para esta plataforma. Se restaurarán todos los accesos directos modificados o eliminados. Otras plataformas no se ven afectadas.', + cancel: 'Cancelar', 'reset-saved': 'Accesos directos predeterminados restaurados', + 'close-confirmation': 'Cerrar confirmación', 'reset-failed': 'No se pudieron restaurar los valores predeterminados. Tus atajos no han cambiado. Vuelva a intentarlo.', + review: 'He revisado la última configuración.', + stale: 'La configuración de accesos directos o los comandos disponibles cambiaron. Revise los enlaces más recientes antes de guardar.', + 'write-failed': 'No se pudo guardar. Se conservan sus atajos anteriores y el borrador actual. Vuelva a intentarlo.', + 'not-ready': 'Los atajos no están listos. Por favor inténtalo de nuevo.', + read: 'No se pudo leer {location}. Verifique los permisos de acceso, luego {reload}.', + invalid: 'La configuración de acceso directo en {location} está dañada. Haga una copia de seguridad y repare esta configuración, luego {reload}.', + future: 'La configuración de acceso directo en {location} fue creada por una versión más reciente. Actualice el arnés e inténtelo de nuevo.', + 'web-document': 'la entrada de almacenamiento local de este sitio dsh.keybindings.v1', 'desktop-document': 'datos de usuario/bindings.json', + 'web-reload': 'recargar la pagina', 'desktop-reload': 'reiniciar el arnés', + 'using-defaults': 'Los enlaces predeterminados están activos.', 'using-accepted': 'Los últimos enlaces leídos correctamente permanecen activos.', + 'native-failed': 'No se pudo proteger la grabación de claves del escritorio. Salga de la grabación y vuelva a intentarlo.', + 'global-hint': 'Abierto desde cualquier lugar', 'clear-search': 'Borrar búsqueda', + title: 'Atajos de teclado', open: 'Abrir atajos de teclado', settings: 'Atajos de teclado', view: 'Editar atajos', + description: 'Ver y editar atajos disponibles y acciones de entrada', search: 'Buscar atajos', close: 'Cerrar atajos de teclado', + application: 'Solicitud', input: 'Entrada de mensaje', menus: 'Menús y cuadros de diálogo', approval: 'Área de aprobación', unbound: 'Sin atajos', + empty: 'No hay atajos coincidentes', + move: 'Mover selección de menú', select: 'Seleccionar elemento del menú', dismiss: 'Cerrar menú o cuadro de diálogo superior', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'edit-label': 'Modifier le raccourci pour {command}', + record: 'Appuyez sur un raccourci', 'record-help': "Relâchez les touches pour enregistrer. L'onglet se déplace entre les actions ; Esc annule.", + 'web-help': 'Combinaisons de navigateur : Mod+/, Mod+Shift+,, Mod+Shift+.. Mod est Command sur Mac et Ctrl ailleurs.', + 'unsupported-key': "Cette clé n'est pas prise en charge.", reserved: "Cette combinaison est réservée aux actions système ou d'édition de texte.", + 'modifier-required': 'Incluez Commande, Ctrl ou Alt dans la combinaison.', + 'too-many-keys': 'Tenez au plus deux clés non modificatrices. Relâchez les touches pour réessayer.', + 'macos-web-help': 'Utilisez Command+/, Command+,, Command+Backslash, Control+Backquote, Command+Option+key ou Command+Shift+key. Les combinaisons avec trois ou quatre modificateurs distincts sont également prises en charge. Les raccourcis du navigateur ou du système peuvent ne pas atteindre la page.', + 'windows-web-help': 'Utilisez Ctrl+/, Ctrl+,, Ctrl+Alt+touche ou Ctrl+Shift+touche. Les combinaisons avec trois ou quatre modificateurs distincts sont également prises en charge. Les raccourcis du navigateur ou du système peuvent ne pas atteindre la page.', + 'unsupported-browser': 'Ce navigateur ne prend pas encore en charge cette combinaison.', conflict: 'Déjà utilisé par « {commands} »', + saved: 'Modifié', clear: 'Supprimer', 'retry-save': "Réessayez d'enregistrer", reset: 'Restaurer les valeurs par défaut', + 'reset-all': 'Restaurer tous les paramètres par défaut', 'modified-count': '{count} personnalisé', + 'reset-title': 'Restaurer tous les raccourcis par défaut ?', + 'reset-description': 'Restaurez les raccourcis par défaut pour cette plateforme. Tous les raccourcis modifiés ou supprimés seront restaurés. Les autres plateformes ne sont pas affectées.', + cancel: 'Annuler', 'reset-saved': 'Raccourcis par défaut restaurés', + 'close-confirmation': 'Fermer la confirmation', 'reset-failed': 'Impossible de restaurer les valeurs par défaut. Vos raccourcis restent inchangés. Veuillez réessayer.', + review: "J'ai examiné la dernière configuration", + stale: 'La configuration des raccourcis ou les commandes disponibles ont été modifiées. Vérifiez les dernières liaisons avant de les enregistrer.', + 'write-failed': "Impossible d'enregistrer. Vos raccourcis précédents et votre brouillon actuel sont conservés. Veuillez réessayer.", + 'not-ready': 'Les raccourcis ne sont pas prêts. Veuillez réessayer.', + read: "Impossible de lire {location}. Vérifiez les autorisations d'accès, puis {reload}.", + invalid: 'La configuration des raccourcis dans {location} est endommagée. Sauvegardez et réparez cette configuration, puis {reload}.', + future: 'La configuration des raccourcis dans {location} a été créée par une version plus récente. Mettez à niveau le harnais et réessayez.', + 'web-document': "l'entrée localStorage de ce site dsh.keybindings.v1", 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'recharger la page', 'desktop-reload': 'redémarrer le harnais', + 'using-defaults': 'Les liaisons par défaut sont actives.', 'using-accepted': 'Les dernières liaisons lues avec succès restent actives.', + 'native-failed': "Impossible de protéger l'enregistrement des clés du bureau. Quittez l'enregistrement et réessayez.", + 'global-hint': "Ouvert de n'importe où", 'clear-search': 'Effacer la recherche', + title: 'Raccourcis clavier', open: 'Ouvrir les raccourcis clavier', settings: 'Raccourcis clavier', view: 'Modifier les raccourcis', + description: 'Afficher et modifier les raccourcis et les actions de saisie disponibles', search: 'Raccourcis de recherche', close: 'Fermer les raccourcis clavier', + application: 'Demande', input: 'Saisie des messages', menus: 'Menus et boîtes de dialogue', approval: "Zone d'approbation", unbound: 'Pas de raccourci', + empty: 'Aucun raccourci correspondant', + move: 'Déplacer la sélection du menu', select: 'Sélectionner un élément de menu', dismiss: 'Fermer le menu ou la boîte de dialogue supérieure', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'edit-label': 'Verknüpfung für {command} bearbeiten', + record: 'Drücken Sie eine Verknüpfung', 'record-help': 'Lassen Sie die Tasten los, um zu speichern. Tab bewegt sich zwischen Aktionen; Esc bricht ab.', + 'web-help': 'Browserkombinationen: Mod+/, Mod+Umschalt+,, Mod+Umschalt+.. Mod ist Befehl auf dem Mac und Strg anderswo.', + 'unsupported-key': 'Dieser Schlüssel wird nicht unterstützt.', reserved: 'Diese Kombination ist für System- oder Textbearbeitungsaktionen reserviert.', + 'modifier-required': 'Fügen Sie Befehl, Strg oder Alt in die Kombination ein.', + 'too-many-keys': 'Halten Sie höchstens zwei nicht modifizierbare Tasten gedrückt. Lassen Sie die Tasten los, um es erneut zu versuchen.', + 'macos-web-help': 'Verwenden Sie Befehl+/, Befehl+,, Befehl+Backslash, Strg+Backquote, Befehl+Wahl+Taste oder Befehl+Umschalt+Taste. Kombinationen mit drei oder vier unterschiedlichen Modifikatoren werden ebenfalls unterstützt. Browser- oder Systemverknüpfungen erreichen die Seite möglicherweise nicht.', + 'windows-web-help': 'Verwenden Sie Strg+/, Strg+,, Strg+Alt+Taste oder Strg+Umschalt+Taste. Kombinationen mit drei oder vier unterschiedlichen Modifikatoren werden ebenfalls unterstützt. Browser- oder Systemverknüpfungen erreichen die Seite möglicherweise nicht.', + 'unsupported-browser': 'Dieser Browser unterstützt diese Kombination noch nicht.', conflict: 'Bereits verwendet von „{commands}“', + saved: 'Geändert', clear: 'Entfernen', 'retry-save': 'Speichern Sie erneut', reset: 'Standard wiederherstellen', + 'reset-all': 'Alle Standardeinstellungen wiederherstellen', 'modified-count': '{count} angepasst', + 'reset-title': 'Alle Standardverknüpfungen wiederherstellen?', + 'reset-description': 'Stellen Sie die Standardverknüpfungen für diese Plattform wieder her. Alle geänderten oder entfernten Verknüpfungen werden wiederhergestellt. Andere Plattformen sind davon nicht betroffen.', + cancel: 'Abbrechen', 'reset-saved': 'Standardverknüpfungen wiederhergestellt', + 'close-confirmation': 'Bestätigung schließen', 'reset-failed': 'Die Standardeinstellungen konnten nicht wiederhergestellt werden. Ihre Verknüpfungen bleiben unverändert. Bitte versuchen Sie es erneut.', + review: 'Ich habe die neueste Konfiguration überprüft', + stale: 'Verknüpfungskonfiguration oder verfügbare Befehle geändert. Überprüfen Sie vor dem Speichern die neuesten Bindungen.', + 'write-failed': 'Konnte nicht gespeichert werden. Ihre vorherigen Verknüpfungen und Ihr aktueller Entwurf bleiben erhalten. Bitte versuchen Sie es erneut.', + 'not-ready': 'Verknüpfungen sind nicht bereit. Bitte versuchen Sie es erneut.', + read: '{location} konnte nicht gelesen werden. Zugriffsberechtigungen prüfen, dann {reload}.', + invalid: 'Die Verknüpfungskonfiguration in {location} ist beschädigt. Sichern und reparieren Sie diese Konfiguration, dann {reload}.', + future: 'Die Verknüpfungskonfiguration in {location} wurde von einer neueren Version erstellt. Aktualisieren Sie Harness und versuchen Sie es erneut.', + 'web-document': 'Der localStorage-Eintrag dieser Site dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'Laden Sie die Seite neu', 'desktop-reload': 'Starten Sie Harness neu', + 'using-defaults': 'Standardbindungen sind aktiv.', 'using-accepted': 'Die zuletzt erfolgreich gelesenen Bindungen bleiben aktiv.', + 'native-failed': 'Die Desktop-Schlüsselaufzeichnung konnte nicht geschützt werden. Beenden Sie die Aufnahme und versuchen Sie es erneut.', + 'global-hint': 'Von überall aus öffnen', 'clear-search': 'Suche löschen', + title: 'Tastaturkürzel', open: 'Öffnen Sie Tastaturkürzel', settings: 'Tastaturkürzel', view: 'Verknüpfungen bearbeiten', + description: 'Verfügbare Verknüpfungen und Eingabeaktionen anzeigen und bearbeiten', search: 'Suchverknüpfungen', close: 'Tastaturkürzel schließen', + application: 'Anwendung', input: 'Nachrichteneingabe', menus: 'Menüs und Dialoge', approval: 'Genehmigungsbereich', unbound: 'Keine Abkürzung', + empty: 'Keine passenden Verknüpfungen', + move: 'Menüauswahl verschieben', select: 'Menüpunkt auswählen', dismiss: 'Menü oder oberen Dialog schließen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'edit-label': 'Изменить ярлык для {command}', + record: 'Нажмите ярлык', 'record-help': 'Отпустите клавиши для сохранения. Tab перемещается между действиями; Esc отменяет.', + 'web-help': 'Комбинации браузера: Mod+/, Mod+Shift+, Mod+Shift+.. Mod — это команда на Mac, а в других местах — Ctrl.', + 'unsupported-key': 'Этот ключ не поддерживается.', reserved: 'Эта комбинация зарезервирована для системных действий или действий по редактированию текста.', + 'modifier-required': 'Включите в комбинацию Command, Ctrl или Alt.', + 'too-many-keys': 'Удерживайте не более двух немодифицирующих клавиш. Отпустите клавиши, чтобы повторить попытку.', + 'macos-web-help': 'Используйте Command+/, Command+,, Command+обратная косая черта, Control+обратная кавычка, Command+Option+клавиша или Command+Shift+клавиша. Также поддерживаются комбинации с тремя или четырьмя различными модификаторами. Ярлыки браузера или системы могут не доходить до страницы.', + 'windows-web-help': 'Используйте Ctrl+/, Ctrl+, Ctrl+Alt+клавиша или Ctrl+Shift+клавиша. Также поддерживаются комбинации с тремя или четырьмя различными модификаторами. Ярлыки браузера или системы могут не доходить до страницы.', + 'unsupported-browser': 'Этот браузер пока не поддерживает эту комбинацию.', conflict: 'Уже используется «{commands}»', + saved: 'Модифицированный', clear: 'Удалить', 'retry-save': 'Повторить попытку сохранения', reset: 'Восстановить настройки по умолчанию', + 'reset-all': 'Восстановить все настройки по умолчанию', 'modified-count': '{count} по индивидуальному заказу', + 'reset-title': 'Восстановить все ярлыки по умолчанию?', + 'reset-description': 'Восстановите ярлыки по умолчанию для этой платформы. Все измененные или удаленные ярлыки будут восстановлены. Другие платформы не затронуты.', + cancel: 'Отмена', 'reset-saved': 'Ярлыки по умолчанию восстановлены.', + 'close-confirmation': 'Закрыть подтверждение', 'reset-failed': 'Не удалось восстановить настройки по умолчанию. Ваши ярлыки не изменились. Пожалуйста, повторите попытку.', + review: 'Я рассмотрел последнюю конфигурацию', + stale: 'Конфигурация ярлыка или доступные команды изменены. Перед сохранением просмотрите последние привязки.', + 'write-failed': 'Не удалось сохранить. Ваши предыдущие ярлыки и текущий черновик сохраняются. Пожалуйста, повторите попытку.', + 'not-ready': 'Ярлыки не готовы. Пожалуйста, попробуйте еще раз.', + read: 'Не удалось прочитать {location}. Проверьте права доступа, затем {reload}.', + invalid: 'Конфигурация ярлыка в {location} повреждена. Создайте резервную копию и исправьте эту конфигурацию, затем {reload}.', + future: 'Конфигурация ярлыка в {location} была создана более новой версией. Обновите Harness и повторите попытку.', + 'web-document': 'запись localStorage этого сайта dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'перезагрузить страницу', 'desktop-reload': 'перезапустить Harness', + 'using-defaults': 'Привязки по умолчанию активны.', 'using-accepted': 'Последние успешно прочитанные привязки остаются активными.', + 'native-failed': 'Не удалось защитить запись клавиш рабочего стола. Выйдите из записи и повторите попытку.', + 'global-hint': 'Открыто из любого места', 'clear-search': 'Очистить поиск', + title: 'Сочетания клавиш', open: 'Открытие сочетаний клавиш', settings: 'Сочетания клавиш', view: 'Редактировать ярлыки', + description: 'Просмотр и редактирование доступных ярлыков и действий ввода', search: 'Поиск ярлыков', close: 'Закрыть сочетания клавиш', + application: 'Приложение', input: 'Ввод сообщения', menus: 'Меню и диалоги', approval: 'Область утверждения', unbound: 'Нет ярлыка', + empty: 'Нет подходящих ярлыков', + move: 'Переместить выбор меню', select: 'Выберите пункт меню', dismiss: 'Закрыть меню или верхний диалог', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'edit-label': 'Editar atalho para {command}', + record: 'Pressione um atalho', 'record-help': 'Solte as teclas para salvar. A guia se move entre as ações; Esc cancela.', + 'web-help': 'Combinações de navegador: Mod+/, Mod+Shift+,, Mod+Shift+.. Mod é Command no Mac e Ctrl em outros lugares.', + 'unsupported-key': 'Esta chave não é suportada.', reserved: 'Esta combinação é reservada para ações de sistema ou edição de texto.', + 'modifier-required': 'Inclua Command, Ctrl ou Alt na combinação.', + 'too-many-keys': 'Segure no máximo duas teclas não modificadoras. Solte as teclas para tentar novamente.', + 'macos-web-help': 'Use Command+/, Command+,, Command+Barra invertida, Control+Backquote, Command+Option+key ou Command+Shift+key. Combinações com três ou quatro modificadores distintos também são suportadas. Os atalhos do navegador ou do sistema podem não chegar à página.', + 'windows-web-help': 'Use Ctrl+/, Ctrl+,, Ctrl+Alt+tecla ou Ctrl+Shift+tecla. Combinações com três ou quatro modificadores distintos também são suportadas. Os atalhos do navegador ou do sistema podem não chegar à página.', + 'unsupported-browser': 'Este navegador ainda não suporta esta combinação.', conflict: 'Já usado por “{commands}”', + saved: 'Modificado', clear: 'Remover', 'retry-save': 'Tentar salvar novamente', reset: 'Restaurar padrão', + 'reset-all': 'Restaurar todos os padrões', 'modified-count': '{count} personalizado', + 'reset-title': 'Restaurar todos os atalhos padrão?', + 'reset-description': 'Restaure os atalhos padrão desta plataforma. Todos os atalhos modificados ou removidos serão restaurados. Outras plataformas não são afetadas.', + cancel: 'Cancelar', 'reset-saved': 'Atalhos padrão restaurados', + 'close-confirmation': 'Fechar confirmação', 'reset-failed': 'Não foi possível restaurar os padrões. Seus atalhos permanecem inalterados. Por favor, tente novamente.', + review: 'Eu revisei a configuração mais recente', + stale: 'Configuração de atalhos ou comandos disponíveis alterados. Revise as ligações mais recentes antes de salvar.', + 'write-failed': 'Não foi possível salvar. Seus atalhos anteriores e o rascunho atual serão preservados. Por favor, tente novamente.', + 'not-ready': 'Os atalhos não estão prontos. Por favor, tente novamente.', + read: 'Não foi possível ler {location}. Verifique as permissões de acesso e depois {reload}.', + invalid: 'A configuração do atalho em {location} está danificada. Faça backup e repare esta configuração e então {reload}.', + future: 'A configuração do atalho em {location} foi criada por uma versão mais recente. Atualize o Harness e tente novamente.', + 'web-document': 'a entrada localStorage deste site dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'recarregar a página', 'desktop-reload': 'reinicie o chicote', + 'using-defaults': 'As ligações padrão estão ativas.', 'using-accepted': 'As últimas ligações lidas com sucesso permanecem ativas.', + 'native-failed': 'Não foi possível proteger a gravação de teclas da área de trabalho. Saia da gravação e tente novamente.', + 'global-hint': 'Abra de qualquer lugar', 'clear-search': 'Limpar pesquisa', + title: 'Atalhos de teclado', open: 'Abra atalhos de teclado', settings: 'Atalhos de teclado', view: 'Editar atalhos', + description: 'Visualize e edite atalhos e ações de entrada disponíveis', search: 'Atalhos de pesquisa', close: 'Fechar atalhos de teclado', + application: 'Aplicação', input: 'Entrada de mensagem', menus: 'Menus e caixas de diálogo', approval: 'Área de aprovação', unbound: 'Sem atalho', + empty: 'Nenhum atalho correspondente', + move: 'Mover seleção de menu', select: 'Selecione o item do menu', dismiss: 'Fechar menu ou caixa de diálogo superior', +} satisfies Record diff --git a/packages/client/ui-sidebar-browser/locale/de.json b/packages/client/ui-sidebar-browser/locale/de.json new file mode 100644 index 0000000000..0e2de49b34 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Browser-Panel", + "description": "Öffnen und durchsuchen Sie Webseiten im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/es.json b/packages/client/ui-sidebar-browser/locale/es.json new file mode 100644 index 0000000000..aab1b5b129 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel del navegador", + "description": "Abra y explore páginas web en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/fr.json b/packages/client/ui-sidebar-browser/locale/fr.json new file mode 100644 index 0000000000..1d11175a16 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau de navigateur", + "description": "Ouvrez et parcourez les pages Web dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/pt.json b/packages/client/ui-sidebar-browser/locale/pt.json new file mode 100644 index 0000000000..88074aa070 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel do navegador", + "description": "Abra e navegue nas páginas da web no painel direito." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/ru.json b/packages/client/ui-sidebar-browser/locale/ru.json new file mode 100644 index 0000000000..5ef851d55d --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "панель браузера", + "description": "Открывайте и просматривайте веб-страницы на правой панели." + } +} diff --git a/packages/client/ui-sidebar-browser/package.json b/packages/client/ui-sidebar-browser/package.json index cf21af3052..90d9b9719b 100644 --- a/packages/client/ui-sidebar-browser/package.json +++ b/packages/client/ui-sidebar-browser/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-browser", "description": "Sandboxed Web browser tabs for the right Sidebar", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-browser/src/client/index.ts b/packages/client/ui-sidebar-browser/src/client/index.ts index e3ac92a9db..29219212bc 100644 --- a/packages/client/ui-sidebar-browser/src/client/index.ts +++ b/packages/client/ui-sidebar-browser/src/client/index.ts @@ -16,7 +16,7 @@ import type { DesktopBrowserBridge } from '../types.ts' import { browserWorkspace } from './electron/workspace.ts' import type { BrowserPageFactory } from './browser/BrowserPage.ts' import { BROWSER_ID, browserDefinition } from './definition.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { createBrowserStore } from './browser/store.ts' export type { BrowserBodyProps } from './view/BrowserBody.tsx' @@ -70,7 +70,7 @@ export function apply(ctx: Context): void { dshDesktop?: { readonly protocolVersion: number; readonly browser?: DesktopBrowserBridge } }).dshDesktop const desktop = carrier?.protocolVersion === 1 ? carrier.browser : undefined - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'ui-sidebar-browser.copy') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-browser.copy') ctx.effect(() => ctx.sidebarRightTabs.register({ ...browserDefinition(t), keepMounted: desktop !== undefined }), 'ui-sidebar-browser.type') const installFrames = (scope: Context, factory: (sessionId: BrowserBodyProps['sessionId']) => BrowserPageFactory): void => { const controllers = new Map() diff --git a/packages/client/ui-sidebar-browser/src/client/locales.ts b/packages/client/ui-sidebar-browser/src/client/locales.ts index a5222248ae..fad29893e0 100644 --- a/packages/client/ui-sidebar-browser/src/client/locales.ts +++ b/packages/client/ui-sidebar-browser/src/client/locales.ts @@ -97,3 +97,153 @@ export const ja = { 'load.failed.detail': 'ページの読み込みに失敗しました({code}): {description}', 'address.unknown': 'ページが遷移しました。このキャリアは新しいURLを読み取れません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'type.label': 'Navegador', + 'guide.title': 'Navegador', + 'guide.description': 'Navegar por páginas web', + 'shortcut.noSession': 'Primero abra una sesión', + 'address.placeholder': 'Ingrese una dirección HTTP(S)', + 'address.changed': 'URL cambiada', + back: 'Atrás', + forward: 'Adelante', + reload: 'recargar', + go: 'ir', + external: 'Abrir en el navegador del sistema', + 'sandbox.disable': 'Deshabilitar restricciones de zona de pruebas', + 'sandbox.enable': 'Restaurar restricciones de zona de pruebas', + 'sandbox.warning': 'Las restricciones de la zona de pruebas están deshabilitadas; la página puede navegar por la aplicación de nivel superior y usar descargas, cuadros de diálogo modales y bloqueos de entrada.', + start: 'Ingrese una dirección HTTP(S) para comenzar a navegar', + loading: 'Abriendo…', + 'restore.previous': 'Abierto previamente', + 'restore.action': 'Restaurar página', + 'error.empty': 'Introduzca una dirección.', + 'error.invalid': 'Esa dirección no es válida o es demasiado larga.', + 'error.protocol': 'Sólo se admiten las direcciones HTTP y HTTPS; utilice la vista previa del documento para archivos locales.', + 'error.credentials': 'Las direcciones no pueden contener un nombre de usuario o contraseña.', + 'error.application-origin': 'El navegador integrado no puede abrir la aplicación DSH.', + 'load.failed': 'La página no se pudo cargar; recargarlo o abrirlo en el navegador del sistema.', + 'load.failed.detail': 'Error al cargar la página ({code}): {description}', + 'address.unknown': 'La página navegó; este operador no puede leer su nueva URL.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'type.label': 'Navigateur', + 'guide.title': 'Navigateur', + 'guide.description': 'Parcourir les pages Web', + 'shortcut.noSession': "Ouvrir d'abord une session", + 'address.placeholder': 'Saisissez une adresse HTTP(S)', + 'address.changed': 'URL modifiée', + back: 'Retour', + forward: 'En avant', + reload: 'Recharger', + go: 'Allez', + external: 'Ouvrir dans le navigateur système', + 'sandbox.disable': 'Désactiver les restrictions du bac à sable', + 'sandbox.enable': 'Restaurer les restrictions du bac à sable', + 'sandbox.warning': "Les restrictions du bac à sable sont désactivées ; la page peut naviguer dans l'application de niveau supérieur et utiliser les téléchargements, les boîtes de dialogue modales et les verrous de saisie.", + start: 'Entrez une adresse HTTP(S) pour commencer la navigation', + loading: 'Ouverture…', + 'restore.previous': 'Déjà ouvert', + 'restore.action': 'Page de restauration', + 'error.empty': 'Entrez une adresse.', + 'error.invalid': "Cette adresse n'est pas valide ou est trop longue.", + 'error.protocol': "Seules les adresses HTTP et HTTPS sont prises en charge ; utilisez l'aperçu du document pour les fichiers locaux.", + 'error.credentials': "Les adresses ne peuvent pas contenir de nom d'utilisateur ou de mot de passe.", + 'error.application-origin': "Le navigateur intégré ne peut pas ouvrir l'application DSH elle-même.", + 'load.failed': "La page n'a pas pu se charger ; rechargez-le ou ouvrez-le dans le navigateur système.", + 'load.failed.detail': 'Échec du chargement de la page ({code}) : {description}', + 'address.unknown': 'La page parcourue ; cet opérateur ne peut pas lire sa nouvelle URL.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'type.label': 'Browser', + 'guide.title': 'Browser', + 'guide.description': 'Durchsuchen Sie Webseiten', + 'shortcut.noSession': 'Öffnen Sie zunächst eine Sitzung', + 'address.placeholder': 'Geben Sie eine HTTP(S)-Adresse ein', + 'address.changed': 'URL geändert', + back: 'Zurück', + forward: 'Vorwärts', + reload: 'Neu laden', + go: 'Geh', + external: 'Im Systembrowser öffnen', + 'sandbox.disable': 'Sandbox-Einschränkungen deaktivieren', + 'sandbox.enable': 'Sandbox-Einschränkungen wiederherstellen', + 'sandbox.warning': 'Sandbox-Einschränkungen sind deaktiviert; Die Seite kann durch die App der obersten Ebene navigieren und Downloads, modale Dialoge und Eingabesperren verwenden.', + start: 'Geben Sie eine HTTP(S)-Adresse ein, um mit dem Surfen zu beginnen', + loading: 'Eröffnung…', + 'restore.previous': 'Zuvor geöffnet', + 'restore.action': 'Seite wiederherstellen', + 'error.empty': 'Geben Sie eine Adresse ein.', + 'error.invalid': 'Diese Adresse ist ungültig oder zu lang.', + 'error.protocol': 'Es werden nur die Adressen HTTP und HTTPS unterstützt. Verwenden Sie die Dokumentvorschau für lokale Dateien.', + 'error.credentials': 'Adressen dürfen weder einen Benutzernamen noch ein Passwort enthalten.', + 'error.application-origin': 'Der eingebettete Browser kann die DSH-Anwendung selbst nicht öffnen.', + 'load.failed': 'Die Seite konnte nicht geladen werden; neu laden oder im Systembrowser öffnen.', + 'load.failed.detail': 'Laden der Seite fehlgeschlagen ({code}): {description}', + 'address.unknown': 'Die Seite wurde navigiert; Dieser Netzbetreiber kann seine neue URL nicht lesen.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'type.label': 'Браузер', + 'guide.title': 'Браузер', + 'guide.description': 'Просмотр веб-страниц', + 'shortcut.noSession': 'Сначала откройте сеанс', + 'address.placeholder': 'Введите адрес HTTP(S)', + 'address.changed': 'URL-адрес изменен', + back: 'Назад', + forward: 'Вперед', + reload: 'Перезагрузить', + go: 'Иди', + external: 'Открыть в системном браузере', + 'sandbox.disable': 'Отключить ограничения песочницы', + 'sandbox.enable': 'Восстановить ограничения песочницы', + 'sandbox.warning': 'Ограничения песочницы отключены; страница может перемещаться по приложению верхнего уровня и использовать загрузки, модальные диалоговые окна и блокировки ввода.', + start: 'Введите адрес HTTP(S), чтобы начать просмотр.', + loading: 'Открытие…', + 'restore.previous': 'Ранее открыто', + 'restore.action': 'Восстановить страницу', + 'error.empty': 'Введите адрес.', + 'error.invalid': 'Этот адрес недействителен или слишком длинный.', + 'error.protocol': 'Поддерживаются только адреса HTTP и HTTPS; используйте предварительный просмотр документа для локальных файлов.', + 'error.credentials': 'Адреса не могут содержать имя пользователя или пароль.', + 'error.application-origin': 'Встроенный браузер не может открыть само приложение DSH.', + 'load.failed': 'Страница не может загрузиться; перезагрузите или откройте его в системном браузере.', + 'load.failed.detail': 'Ошибка загрузки страницы ({code}): {description}', + 'address.unknown': 'Страница перемещалась; этот оператор связи не может прочитать свой новый URL-адрес.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'type.label': 'Navegador', + 'guide.title': 'Navegador', + 'guide.description': 'Navegar em páginas da web', + 'shortcut.noSession': 'Abra uma sessão primeiro', + 'address.placeholder': 'Insira um endereço HTTP(S)', + 'address.changed': 'URL alterado', + back: 'Voltar', + forward: 'Avançar', + reload: 'Recarregar', + go: 'Vá', + external: 'Abrir no navegador do sistema', + 'sandbox.disable': 'Desativar restrições de sandbox', + 'sandbox.enable': 'Restaurar restrições de sandbox', + 'sandbox.warning': 'As restrições do sandbox estão desativadas; a página pode navegar no aplicativo de nível superior e usar downloads, caixas de diálogo modais e bloqueios de entrada.', + start: 'Insira um endereço HTTP(S) para começar a navegar', + loading: 'Abrindo…', + 'restore.previous': 'Aberto anteriormente', + 'restore.action': 'Restaurar página', + 'error.empty': 'Insira um endereço.', + 'error.invalid': 'Esse endereço é inválido ou muito longo.', + 'error.protocol': 'Somente endereços HTTP e HTTPS são suportados; use a visualização do documento para arquivos locais.', + 'error.credentials': 'Os endereços não podem conter um nome de usuário ou senha.', + 'error.application-origin': 'O navegador incorporado não pode abrir o aplicativo DSH sozinho.', + 'load.failed': 'A página não pôde carregar; recarregue ou abra-o no navegador do sistema.', + 'load.failed.detail': 'Falha no carregamento da página ({code}): {description}', + 'address.unknown': 'A página navegada; esta operadora não consegue ler seu novo URL.', +} satisfies Record diff --git a/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts b/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts index 68f338a40d..c2a7078d60 100644 --- a/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts @@ -11,7 +11,7 @@ import { BrowserTitle } from '../src/client/view/BrowserTitle.tsx' import type { BrowserInjected } from '../src/client/browser/BrowserController.ts' import { BROWSER_ID, BROWSER_KIND } from '../src/client/definition.tsx' import { apply, inject } from '../src/client/index.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' import { createBrowserStore } from '../src/client/browser/store.ts' import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' import type { DesktopBrowserBridge, DesktopBrowserLeaseId } from '../src/types.ts' @@ -125,7 +125,7 @@ describe('ui-sidebar-browser apply', () => { expect(definition?.title('sidebar://browser')).toBe('type.label') expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])) .toEqual([[30, 'guide.title', 'guide.description']]) - expect(dictionaries.get('sidebarBrowser')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarBrowser')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(dictionaries.get('sidebarBrowser')).toMatchObject({ zh: { 'guide.description': '浏览网页' }, en: { 'guide.description': 'Browse web pages' }, diff --git a/packages/client/ui-sidebar-documentpreview/locale/de.json b/packages/client/ui-sidebar-documentpreview/locale/de.json new file mode 100644 index 0000000000..076c2d117b --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Dokumentvorschaufenster", + "description": "Vorschau von Dateien wie Dokumenten, Tabellen, Codes, Bildern und Webseiten im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/es.json b/packages/client/ui-sidebar-documentpreview/locale/es.json new file mode 100644 index 0000000000..67b4b65fb1 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panel de vista previa del documento", + "description": "Obtenga una vista previa de archivos como documentos, tablas, códigos, imágenes y páginas web en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/fr.json b/packages/client/ui-sidebar-documentpreview/locale/fr.json new file mode 100644 index 0000000000..c06734b675 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panneau d'aperçu du document", + "description": "Prévisualisez les fichiers tels que les documents, les tableaux, les codes, les images et les pages Web sur le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/pt.json b/packages/client/ui-sidebar-documentpreview/locale/pt.json new file mode 100644 index 0000000000..a2dc23158d --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Painel de visualização do documento", + "description": "Visualize arquivos como documentos, tabelas, códigos, imagens e páginas da web no painel direito." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/ru.json b/packages/client/ui-sidebar-documentpreview/locale/ru.json new file mode 100644 index 0000000000..42277eca20 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Панель предварительного просмотра документа", + "description": "Предварительный просмотр файлов, таких как документы, таблицы, коды, изображения и веб-страницы, на правой панели." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index b52bfe1c21..cc791ac9cb 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts index 3008802e74..eeceda36c4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts @@ -3,14 +3,14 @@ import type { Context } from '@deepseek-ai/cordis' import { CODE_HIGHLIGHT_EXTENSIONS } from '@deepseek-ai/dsh-client-ui-primitives' import type {} from '../index.ts' import { CodeBody } from './CodeBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' const ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/code' const NS = 'sidebarCodePreview' /** @param ctx - owning plugin context. Register localized metadata and the matching keyed document body. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja })) + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind(NS) ctx.effect(() => ctx.documentPreviews.register({ id: ID, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts index 0173b40bff..04f2e115ed 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts @@ -28,3 +28,38 @@ export const ja = { 'copy': 'コピー', 'copied': 'コピーしました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Código', + copy: 'Copiar', + copied: 'Copiado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Coder', + copy: 'Copier', + copied: 'Copié', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Code', + copy: 'Kopieren', + copied: 'Kopiert', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Код', + copy: 'Копировать', + copied: 'Скопировано', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Código', + copy: 'Copiar', + copied: 'Copiado', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts index 4a2771723f..28a2336475 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts @@ -3,7 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '../index.ts' import type { Config } from '../../config.ts' import { LazyExcelBody } from './LazyExcelBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** * Register the browser Excel viewer and its lifecycle-owned slot. @@ -12,7 +12,7 @@ import { en, ja, zh } from './locales.ts' */ export function apply(ctx: Context, limits: Config['excel']): void { const id = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/excel' - ctx.effect(() => ctx.locale.register('sidebarExcel', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarExcel', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarExcel') ctx.effect(() => ctx.documentPreviews.register({ id, extensions: ['xlsx', 'xls', 'csv', 'tsv'], binaryExtensions: ['xlsx', 'xls'], priority: 'builtin', diff --git a/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts index 2a448c0660..c45ad60880 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts @@ -50,3 +50,63 @@ export const ja = { 'featureSeparator': '、', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Hoja de cálculo', language: 'en', loading: 'Representando documento...', + invalid: 'Esta hoja de cálculo no se pudo abrir. Comprueba su formato, contenidos o protección con contraseña.', + tooLarge: 'Este libro excede el límite de tamaño de vista previa.', timeout: 'Se agotó el tiempo de apertura de este libro. Pruebe con un archivo más pequeño.', + encoding: 'Esta codificación de texto no se pudo leer. Guarde el archivo como UTF-8 o UTF-16 con una lista de materiales y vuelva a intentarlo.', + formulaWarning: 'Este libro contiene fórmulas. Los resultados mostrados pueden faltar o ser inexactos.', + unsupportedNotice: 'Esta vista previa no es compatible con {features} en este libro. Ábralo en una aplicación del sistema para disfrutar de la experiencia completa.', + charts: 'gráficos', images: 'imágenes', shapes: 'formas', conditionalFormatting: 'formato condicional', featureSeparator: ', ', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Feuille de calcul', language: 'en', loading: 'Rendu du document...', + invalid: "Cette feuille de calcul n'a pas pu être ouverte. Vérifiez son format, son contenu ou sa protection par mot de passe.", + tooLarge: "Ce classeur dépasse la limite de taille d'aperçu.", timeout: "L'ouverture de ce classeur a expiré. Essayez un fichier plus petit.", + encoding: "Cet encodage de texte n'a pas pu être lu. Enregistrez le fichier au format UTF-8 ou UTF-16 avec une nomenclature et réessayez.", + formulaWarning: 'Ce classeur contient des formules. Les résultats affichés peuvent être manquants ou inexacts.', + unsupportedNotice: 'Cet aperçu ne prend pas en charge {features} dans ce classeur. Ouvrez-le dans une application système pour une expérience complète.', + charts: 'graphiques', images: 'photos', shapes: 'formes', conditionalFormatting: 'mise en forme conditionnelle', featureSeparator: ', ', + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Tabellenkalkulation', language: 'en', loading: 'Dokument wird gerendert...', + invalid: 'Diese Tabelle konnte nicht geöffnet werden. Überprüfen Sie Format, Inhalt oder Passwortschutz.', + tooLarge: 'Diese Arbeitsmappe überschreitet die Größenbeschränkung für die Vorschau.', timeout: 'Beim Öffnen dieser Arbeitsmappe ist eine Zeitüberschreitung aufgetreten. Versuchen Sie es mit einer kleineren Datei.', + encoding: 'Diese Textkodierung konnte nicht gelesen werden. Speichern Sie die Datei als UTF-8 oder UTF-16 mit einer Stückliste und versuchen Sie es erneut.', + formulaWarning: 'Diese Arbeitsmappe enthält Formeln. Die angezeigten Ergebnisse fehlen möglicherweise oder sind ungenau.', + unsupportedNotice: 'Diese Vorschau unterstützt {features} in dieser Arbeitsmappe nicht. Öffnen Sie es in einer Systemanwendung, um das volle Erlebnis zu genießen.', + charts: 'Diagramme', images: 'Bilder', shapes: 'Formen', conditionalFormatting: 'bedingte Formatierung', featureSeparator: ', ', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'электронная таблица', language: 'en', loading: 'Оформление документа...', + invalid: 'Эту таблицу невозможно открыть. Проверьте его формат, содержимое или защиту паролем.', + tooLarge: 'Размер этой книги превышает ограничение на размер предварительного просмотра.', timeout: 'Время открытия этой книги истекло. Попробуйте файл меньшего размера.', + encoding: 'Эту текстовую кодировку невозможно прочитать. Сохраните файл как UTF-8 или UTF-16 со спецификацией и повторите попытку.', + formulaWarning: 'В этой рабочей тетради есть формулы. Отображаемые результаты могут отсутствовать или быть неточными.', + unsupportedNotice: 'Эта предварительная версия не поддерживает {features} в этой книге. Откройте его в системном приложении, чтобы получить полную информацию.', + charts: 'графики', images: 'изображения', shapes: 'формы', conditionalFormatting: 'условное форматирование', featureSeparator: ', ', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Planilha', language: 'en', loading: 'Renderizando documento...', + invalid: 'Não foi possível abrir esta planilha. Verifique seu formato, conteúdo ou proteção por senha.', + tooLarge: 'Esta pasta de trabalho excede o limite de tamanho de visualização.', timeout: 'A abertura desta pasta de trabalho expirou. Experimente um arquivo menor.', + encoding: 'Esta codificação de texto não pôde ser lida. Salve o arquivo como UTF-8 ou UTF-16 com uma BOM e tente novamente.', + formulaWarning: 'Esta pasta de trabalho contém fórmulas. Os resultados exibidos podem estar ausentes ou imprecisos.', + unsupportedNotice: 'Esta visualização não oferece suporte a {features} nesta pasta de trabalho. Abra-o em um aplicativo do sistema para ter uma experiência completa.', + charts: 'gráficos', images: 'imagens', shapes: 'formas', conditionalFormatting: 'formatação condicional', featureSeparator: ', ', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts index 056efb2de9..a731edabdf 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts @@ -6,7 +6,7 @@ import { hostFileOf } from '../rpc.ts' import { HtmlBody } from './HtmlBody.tsx' import type { HtmlBodyInjected } from './HtmlBody.tsx' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** HTML implementation identity, shared by metadata and the keyed slot. */ export const HTML_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/html' @@ -26,7 +26,7 @@ export function htmlBodyDefinition(title: () => string): DocumentPreviewDefiniti */ export function apply(ctx: Context): void { const t = ctx.locale.bind('documentHtml') - ctx.effect(() => ctx.locale.register('documentHtml', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('documentHtml', { zh, en, ja, es, fr, de, ru, pt })) ctx.effect(() => ctx.documentPreviews.register(htmlBodyDefinition(() => t('title')))) ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register( { diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts index 1617bd42d7..1751249d3b 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts @@ -31,3 +31,43 @@ export const ja = { 'loading': 'ドキュメントをレンダリング中...', 'failed': 'このHTMLドキュメントをプレビューできませんでした。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'HTML', + frame: 'Vista previa del documento HTML', + loading: 'Representando documento...', + failed: 'No se pudo obtener una vista previa de este documento HTML.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'HTML', + frame: 'Aperçu du document HTML', + loading: 'Rendu du document...', + failed: "Ce document HTML n'a pas pu être prévisualisé.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'HTML', + frame: 'Vorschau des HTML-Dokuments', + loading: 'Dokument wird gerendert...', + failed: 'Dieses HTML-Dokument konnte nicht in der Vorschau angezeigt werden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'HTML', + frame: 'Предварительный просмотр HTML-документа', + loading: 'Оформление документа...', + failed: 'Этот HTML-документ не удалось просмотреть.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'HTML', + frame: 'Pré-visualização do documento HTML', + loading: 'Renderizando documento...', + failed: 'Este documento HTML não pôde ser visualizado.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts index 014598add0..a123cff8ae 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts @@ -8,7 +8,7 @@ import type { DocumentPreviewDefinition } from '../document/registry.ts' import { retainDocumentTabs } from '../document/tab-lifetime.ts' import { createZoomStore, type ZoomStore } from '../zoom/store.ts' import { ImageBody, type ImageBodyInjected } from './ImageBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Image implementation identity, shared by metadata and the keyed slot. */ export const IMAGE_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/image' @@ -42,7 +42,7 @@ export function imageBodyDefinition(title: () => string): DocumentPreviewDefinit */ export function apply(ctx: Context): void { const t = ctx.locale.bind('sidebarImage') - ctx.effect(() => ctx.locale.register('sidebarImage', { zh, en, ja }), 'document-image: dictionaries') + ctx.effect(() => ctx.locale.register('sidebarImage', { zh, en, ja, es, fr, de, ru, pt }), 'document-image: dictionaries') ctx.effect(() => ctx.documentPreviews.register(imageBodyDefinition(() => t('title'))), 'document-image: metadata') const store = createZoomStore() const retainTab = retainDocumentTabs(ctx) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts index 852635dcca..605e6d9e88 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts @@ -1,4 +1,4 @@ -import { zoomEn, zoomJa, zoomZh } from '../zoom/locales.ts' +import { zoomEn, zoomJa, zoomEs, zoomFr, zoomDe, zoomRu, zoomPt, zoomZh } from '../zoom/locales.ts' /** Locale-owned image renderer labels and status text. */ export const zh = { @@ -39,3 +39,53 @@ export const ja = { 'failed': 'この画像を表示できませんでした。', 'unsupported': '画像プレビューには完全なファイル内容が必要です。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...zoomEs, + title: 'Imagen', + preview: 'Vista previa de la imagen: {name}', + loading: 'Representando documento...', + failed: 'Esta imagen no se pudo mostrar.', + unsupported: 'La vista previa de la imagen requiere el contenido completo del archivo.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...zoomFr, + title: 'Images', + preview: "Aperçu de l'image : {name}", + loading: 'Rendu du document...', + failed: "Cette image n'a pas pu être affichée.", + unsupported: "L'aperçu de l'image nécessite le contenu complet du fichier.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...zoomDe, + title: 'Bild', + preview: 'Bildvorschau: {name}', + loading: 'Dokument wird gerendert...', + failed: 'Dieses Bild konnte nicht angezeigt werden.', + unsupported: 'Für die Bildvorschau ist der vollständige Dateiinhalt erforderlich.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...zoomRu, + title: 'Изображение', + preview: 'Предварительный просмотр изображения: {name}', + loading: 'Оформление документа...', + failed: 'Это изображение не может быть отображено.', + unsupported: 'Для предварительного просмотра изображения требуется полное содержимое файла.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...zoomPt, + title: 'Imagem', + preview: 'Visualização da imagem: {name}', + loading: 'Renderizando documento...', + failed: 'Esta imagem não pôde ser exibida.', + unsupported: 'A visualização da imagem requer o conteúdo completo do arquivo.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/index.ts index 1da0150e46..670ba33d6e 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/index.ts @@ -27,7 +27,7 @@ import { TEXTPREVIEW_ID, textDefinition } from './definition.ts' import { textFace } from './face.ts' import { createReadPage } from './rpc.ts' import { createTextStore } from './store.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { DocumentPreviewRegistry } from './document/registry.ts' import { documentTabInfoFactory } from './document/contract.ts' import { apply as registerText } from './text/index.ts' @@ -91,7 +91,7 @@ export function apply(ctx: ClientContext): void { const disposePreviews = ctx.reflect.provide('documentPreviews', previews) ctx.effect(() => disposePreviews) ctx.effect(() => ctx.sidebarRightTabs.register(textDefinition()), 'ui-sidebar-documentpreview: text type') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-documentpreview: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-documentpreview: dictionaries') const store = createTextStore() const face = textFace( diff --git a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts index 400118427e..fa99836610 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts @@ -86,3 +86,133 @@ export const ja = { 'error.unavailable': '読み込みに失敗しました: {message}', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + loading: 'Representando documento...', + loadMore: 'Cargar más', + changed: 'El archivo ha cambiado, mostrando el contenido anterior.', + reloadNow: 'recargar', + reload: 'Leer el archivo nuevamente', + autoRefresh: 'Actualización automática', + 'autoRefresh.enable': 'Habilitar actualización automática', + 'autoRefresh.disable': 'Deshabilitar la actualización automática', + 'wrap.enable': 'Activar ajuste de línea', + 'wrap.disable': 'Desactivar ajuste de línea', + 'wrap.aria': 'Ajuste de línea', + openWith: 'abrir con', + 'viewer.text': 'Texto sin formato', + resourceUnavailable: 'El servicio de recursos de archivos no está disponible.', + rendererUnavailable: 'La vista previa de {name} no está disponible.', + unsupportedFile: 'La vista previa aún no está disponible para este tipo de archivo.', + 'error.notFound': 'Archivo no encontrado. Es posible que se haya movido o eliminado.', + 'error.tooLarge': 'Esta página excede el límite {limit} y no se puede leer.', + 'error.notText': 'La vista previa aún no está disponible para este tipo de archivo.', + 'error.notRegularFile': 'No es un archivo normal, no hay nada que mostrar.', + 'error.unavailable': 'Error de lectura: {message}', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + loading: 'Rendu du document...', + loadMore: 'Charger plus', + changed: 'Le fichier a changé, affichant le contenu précédent.', + reloadNow: 'Recharger', + reload: 'Relisez le fichier', + autoRefresh: 'Actualisation automatique', + 'autoRefresh.enable': "Activer l'actualisation automatique", + 'autoRefresh.disable': "Désactiver l'actualisation automatique", + 'wrap.enable': 'Activer le retour à la ligne', + 'wrap.disable': 'Désactiver le retour à la ligne', + 'wrap.aria': 'Retour à la ligne', + openWith: 'Ouvrir avec', + 'viewer.text': 'Texte brut', + resourceUnavailable: "Le service de ressources de fichiers n'est pas disponible.", + rendererUnavailable: "L'aperçu {name} n'est pas disponible.", + unsupportedFile: "L'aperçu n'est pas encore disponible pour ce type de fichier.", + 'error.notFound': 'Fichier introuvable. Il a peut-être été déplacé ou supprimé.', + 'error.tooLarge': 'Cette page dépasse la limite {limit} et ne peut pas être lue.', + 'error.notText': "L'aperçu n'est pas encore disponible pour ce type de fichier.", + 'error.notRegularFile': "Ce n'est pas un fichier normal, rien à afficher.", + 'error.unavailable': 'Échec de lecture : {message}', + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + loading: 'Dokument wird gerendert...', + loadMore: 'Mehr laden', + changed: 'Die Datei wurde geändert und zeigt den vorherigen Inhalt an.', + reloadNow: 'Neu laden', + reload: 'Lesen Sie die Datei noch einmal', + autoRefresh: 'Automatische Aktualisierung', + 'autoRefresh.enable': 'Aktivieren Sie die automatische Aktualisierung', + 'autoRefresh.disable': 'Deaktivieren Sie die automatische Aktualisierung', + 'wrap.enable': 'Aktivieren Sie den Zeilenumbruch', + 'wrap.disable': 'Schalten Sie den Zeilenumbruch aus', + 'wrap.aria': 'Zeilenumbruch', + openWith: 'Öffnen mit', + 'viewer.text': 'Klartext', + resourceUnavailable: 'Der Dateiressourcendienst ist nicht verfügbar.', + rendererUnavailable: 'Die {name}-Vorschau ist nicht verfügbar.', + unsupportedFile: 'Für diesen Dateityp ist noch keine Vorschau verfügbar.', + 'error.notFound': 'Datei nicht gefunden. Möglicherweise wurde es verschoben oder gelöscht.', + 'error.tooLarge': 'Diese Seite überschreitet das {limit}-Limit und kann nicht gelesen werden.', + 'error.notText': 'Für diesen Dateityp ist noch keine Vorschau verfügbar.', + 'error.notRegularFile': 'Keine normale Datei, nichts zum Anzeigen.', + 'error.unavailable': 'Lesen fehlgeschlagen: {message}', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + loading: 'Оформление документа...', + loadMore: 'Загрузить больше', + changed: 'Файл изменился, показывая предыдущее содержимое.', + reloadNow: 'Перезагрузить', + reload: 'Прочтите файл еще раз', + autoRefresh: 'Автоматическое обновление', + 'autoRefresh.enable': 'Включить автоматическое обновление', + 'autoRefresh.disable': 'Отключить автоматическое обновление', + 'wrap.enable': 'Включить перенос строк', + 'wrap.disable': 'Отключить перенос строк', + 'wrap.aria': 'Перенос строки', + openWith: 'Открыть с помощью', + 'viewer.text': 'Обычный текст', + resourceUnavailable: 'Служба файловых ресурсов недоступна.', + rendererUnavailable: 'Предварительный просмотр {name} недоступен.', + unsupportedFile: 'Предварительный просмотр для этого типа файлов пока недоступен.', + 'error.notFound': 'Файл не найден. Возможно, оно было перемещено или удалено.', + 'error.tooLarge': 'Эта страница превышает лимит {limit} и не может быть прочитана.', + 'error.notText': 'Предварительный просмотр для этого типа файлов пока недоступен.', + 'error.notRegularFile': 'Не обычный файл, отображать нечего.', + 'error.unavailable': 'Не удалось прочитать: {message}.', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + loading: 'Renderizando documento...', + loadMore: 'Carregar mais', + changed: 'O arquivo foi alterado, mostrando o conteúdo anterior.', + reloadNow: 'Recarregar', + reload: 'Leia o arquivo novamente', + autoRefresh: 'Atualização automática', + 'autoRefresh.enable': 'Ativar atualização automática', + 'autoRefresh.disable': 'Desativar atualização automática', + 'wrap.enable': 'Ativar quebra de linha', + 'wrap.disable': 'Desativar quebra de linha', + 'wrap.aria': 'Quebra de linha', + openWith: 'Abrir com', + 'viewer.text': 'Texto simples', + resourceUnavailable: 'O serviço de recurso de arquivo não está disponível.', + rendererUnavailable: 'A visualização {name} não está disponível.', + unsupportedFile: 'A visualização ainda não está disponível para este tipo de arquivo.', + 'error.notFound': 'Arquivo não encontrado. Pode ter sido movido ou excluído.', + 'error.tooLarge': 'Esta página excede o limite de {limit} e não pode ser lida.', + 'error.notText': 'A visualização ainda não está disponível para este tipo de arquivo.', + 'error.notRegularFile': 'Não é um arquivo normal, nada para exibir.', + 'error.unavailable': 'Falha na leitura: {message}', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts index a3d68990c0..2412dda05d 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts @@ -3,7 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '../index.ts' import type { DocumentPreviewDefinition } from '../document/registry.ts' import { MarkdownBody } from './MarkdownBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Implementation identity shared by metadata and the document slot. */ export const MARKDOWN_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/markdown' @@ -23,7 +23,7 @@ export function markdownDefinition(title: () => string): DocumentPreviewDefiniti */ export function apply(ctx: Context): void { const t = ctx.locale.bind('documentMarkdown') - ctx.effect(() => ctx.locale.register('documentMarkdown', { zh, en, ja }), 'document-markdown: dictionaries') + ctx.effect(() => ctx.locale.register('documentMarkdown', { zh, en, ja, es, fr, de, ru, pt }), 'document-markdown: dictionaries') ctx.effect(() => ctx.documentPreviews.register(markdownDefinition(() => t('viewer.label'))), 'document-markdown: metadata') ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register( { name: 'sidebar.right.tab.document', key: MARKDOWN_BODY_ID, locale: 'documentMarkdown' }, MarkdownBody, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts index 41554ace8b..4405800591 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts @@ -31,3 +31,43 @@ export const ja = { 'code.copied': 'コピーしました', 'footnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copiar', + 'code.copied': 'Copiado', + 'footnotes': 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copier', + 'code.copied': 'Copié', + 'footnotes': 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'viewer.label': 'Markdown', + 'code.copy': 'Kopieren', + 'code.copied': 'Kopiert', + 'footnotes': 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'viewer.label': 'Markdown', + 'code.copy': 'Копировать', + 'code.copied': 'Скопировано', + 'footnotes': 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copiar', + 'code.copied': 'Copiado', + 'footnotes': 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts index 350585f7ed..832787aaf2 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts @@ -7,7 +7,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-api-workspace-files/remote' import { failureLine } from '../failure-line.ts' import { documentTabInfoFactory } from '../document/contract.ts' -import { en, ja, zh, type OfficePreviewKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type OfficePreviewKey } from './locales.ts' import { OfficePreviewCache, type ReadOfficeBytes, type ReadOfficeDocument } from './cache.ts' import { pdfBodyRegistration } from '../pdf/index.ts' import { LazyPdfBody } from '../pdf/LazyPdfBody.tsx' @@ -31,7 +31,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { export function apply(ctx: Context, config: Config['office']): void { const id = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office' const extensions = ['doc', 'docx', 'ppt', 'pptx'] - ctx.effect(() => ctx.locale.register('sidebarOffice', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarOffice', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarOffice') const unavailable: ReadOfficeDocument = (_file, signal) => { signal.throwIfAborted() diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts index fbab4f4913..5c224c81e4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts @@ -57,3 +57,98 @@ export const ja = { 'busy': 'Office プレビューは混雑しています。しばらくしてから再試行してください。', 'changed': '読み込み中にファイルが変更されました。プレビューを開き直してください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'documento de oficina', + loading: 'Representando documento...', + retry: 'Reintentar', + viewMissingFonts: 'Fuentes faltantes: {count}. Haga clic para ver.', + missingFontsTitle: 'Fuentes faltantes', + missingFontsDescription: 'Estas fuentes no están disponibles para esta vista previa. El texto y el diseño pueden diferir del documento original.', + missingFontsCount: 'Fuentes: {count}', + closeDetails: 'Cerrar detalles de fuente', + unavailable: 'Las vistas previas de Office no están disponibles. Habilite el servicio de vista previa de documentos en la computadora que ejecuta DeepSeek Harness.', + invalid: 'No se puede obtener una vista previa de este archivo de Office. Puede que esté dañado, protegido con contraseña o que tenga la extensión incorrecta.', + tooLarge: 'El archivo de Office o el PDF convertido supera el límite de tamaño de vista previa. Reduzca el tamaño del archivo o ajuste la configuración de vista previa.', + failed: 'La conversión de Office no produjo un PDF utilizable. Verifique el archivo e inténtelo nuevamente.', + timeout: 'Se agotó el tiempo de espera para la conversión de Office. Intentar otra vez.', + busy: 'La vista previa de Office está ocupada. Vuelve a intentarlo en breve.', + changed: 'El archivo cambió mientras se leía. Vuelva a abrir la vista previa.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Document de bureau', + loading: 'Rendu du document...', + retry: 'Réessayer', + viewMissingFonts: 'Polices manquantes : {count}. Cliquez pour voir.', + missingFontsTitle: 'Polices manquantes', + missingFontsDescription: 'Ces polices ne sont pas disponibles pour cet aperçu. Le texte et la mise en page peuvent différer du document original.', + missingFontsCount: 'Polices : {count}', + closeDetails: 'Fermer les détails de la police', + unavailable: "Les aperçus Office ne sont pas disponibles. Activez le service de prévisualisation des documents sur l'ordinateur exécutant DeepSeek Harness.", + invalid: 'Ce fichier Office ne peut pas être prévisualisé. Il peut être endommagé, protégé par mot de passe ou avoir une extension incorrecte.', + tooLarge: "Le fichier Office ou le PDF converti dépasse la limite de taille d'aperçu. Réduisez la taille du fichier ou ajustez la configuration de l’aperçu.", + failed: "La conversion Office n'a pas produit de PDF utilisable. Vérifiez le fichier et réessayez.", + timeout: 'La conversion du bureau a expiré. Essayer à nouveau.', + busy: 'L’aperçu Office est occupé. Réessayez sous peu.', + changed: "Le fichier a changé lors de sa lecture. Rouvrez l'aperçu.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Office-Dokument', + loading: 'Dokument wird gerendert...', + retry: 'Erneut versuchen', + viewMissingFonts: 'Fehlende Schriftarten: {count}. Klicken Sie zum Anzeigen.', + missingFontsTitle: 'Fehlende Schriftarten', + missingFontsDescription: 'Diese Schriftarten sind für diese Vorschau nicht verfügbar. Text und Layout können vom Originaldokument abweichen.', + missingFontsCount: 'Schriftarten: {count}', + closeDetails: 'Schriftartdetails schließen', + unavailable: 'Office-Vorschauen sind nicht verfügbar. Aktivieren Sie den Dokumentvorschaudienst auf dem Computer, auf dem DeepSeek Harness ausgeführt wird.', + invalid: 'Diese Office-Datei kann nicht in der Vorschau angezeigt werden. Möglicherweise ist es beschädigt, passwortgeschützt oder hat die falsche Erweiterung.', + tooLarge: 'Die Office-Datei oder das konvertierte PDF überschreitet die Größenbeschränkung für die Vorschau. Reduzieren Sie die Dateigröße oder passen Sie die Vorschaukonfiguration an.', + failed: 'Die Office-Konvertierung ergab kein brauchbares PDF. Überprüfen Sie die Datei und versuchen Sie es erneut.', + timeout: 'Zeitüberschreitung bei der Office-Konvertierung. Versuchen Sie es erneut.', + busy: 'Die Office-Vorschau ist beschäftigt. Versuchen Sie es in Kürze noch einmal.', + changed: 'Die Datei wurde beim Lesen geändert. Öffnen Sie die Vorschau erneut.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Офисный документ', + loading: 'Оформление документа...', + retry: 'Повторить', + viewMissingFonts: 'Отсутствуют шрифты: {count}. Нажмите, чтобы просмотреть.', + missingFontsTitle: 'Отсутствуют шрифты', + missingFontsDescription: 'Эти шрифты недоступны для этой предварительной версии. Текст и оформление могут отличаться от исходного документа.', + missingFontsCount: 'Шрифты: {count}', + closeDetails: 'Закрыть сведения о шрифте', + unavailable: 'Предварительные версии Office недоступны. Включите службу предварительного просмотра документов на компьютере под управлением DeepSeek Harness.', + invalid: 'Этот файл Office нельзя просмотреть. Возможно, он поврежден, защищен паролем или имеет неправильное расширение.', + tooLarge: 'Размер файла Office или преобразованного PDF-файла превышает ограничение на размер предварительного просмотра. Уменьшите размер файла или настройте конфигурацию предварительного просмотра.', + failed: 'Преобразование Office не привело к созданию пригодного для использования PDF-файла. Проверьте файл и повторите попытку.', + timeout: 'Время преобразования Office истекло. Попробуйте еще раз.', + busy: 'Предварительная версия Office занята. Повторите попытку через некоторое время.', + changed: 'Файл изменился во время чтения. Снова откройте предварительный просмотр.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Documento de escritório', + loading: 'Renderizando documento...', + retry: 'Tentar novamente', + viewMissingFonts: 'Fontes ausentes: {count}. Clique para visualizar.', + missingFontsTitle: 'Fontes ausentes', + missingFontsDescription: 'Essas fontes não estão disponíveis para esta visualização. O texto e o layout podem diferir do documento original.', + missingFontsCount: 'Fontes: {count}', + closeDetails: 'Fechar detalhes da fonte', + unavailable: 'As visualizações do Office não estão disponíveis. Ative o serviço de visualização de documentos no computador que executa DeepSeek Harness.', + invalid: 'Este arquivo do Office não pode ser visualizado. Ele pode estar danificado, protegido por senha ou ter a extensão errada.', + tooLarge: 'O arquivo Office ou PDF convertido excede o limite de tamanho de visualização. Reduza o tamanho do arquivo ou ajuste a configuração de visualização.', + failed: 'A conversão do Office não produziu um PDF utilizável. Verifique o arquivo e tente novamente.', + timeout: 'A conversão do Office expirou. Tente novamente.', + busy: 'A visualização do Office está ocupada. Tente novamente em breve.', + changed: 'O arquivo foi alterado durante a leitura. Reabra a visualização.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts index ee099bb22a..c49e52b683 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts @@ -9,7 +9,7 @@ import type { BoundActions } from '@deepseek-ai/dsh-client-store' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { createPdfStore, type PdfStore } from './store.ts' import { ZoomViewport, zoomSurfaceClass } from '../zoom/ZoomViewport.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** PDF metadata and keyed body share this package-local implementation identity. */ export const PDF_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/pdf' @@ -25,7 +25,7 @@ export function pdfBodyDefinition(title: () => string): DocumentPreviewDefinitio /** @param ctx - context carrying the locale, document registry, and slot registry. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('sidebarPdf', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarPdf', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarPdf') ctx.effect(() => ctx.documentPreviews.register(pdfBodyDefinition(() => t('title')))) const presentation = pdfBodyRegistration(ctx) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts index e9de97b4d3..55722d8888 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts @@ -1,4 +1,4 @@ -import { zoomEn, zoomJa, zoomZh } from '../zoom/locales.ts' +import { zoomEn, zoomJa, zoomEs, zoomFr, zoomDe, zoomRu, zoomPt, zoomZh } from '../zoom/locales.ts' /** Copy owned by the PDF renderer. */ export const zh = { @@ -51,3 +51,73 @@ export const ja = { 'unsupported': 'PDFのプレビューには完全なファイル内容が必要です。', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...zoomEs, + title: 'PDF', + pageImage: 'Página PDF {page}', + loading: 'Representando documento...', + rendering: 'Página de renderizado...', + failed: 'No se puede mostrar PDF: {message}', + password: 'Este PDF requiere una contraseña; No se admiten vistas previas protegidas con contraseña.', + workerFailed: 'El proceso de renderizado de PDF no pudo continuar. Vuelva a intentarlo.', + unsupported: 'La vista previa de PDF requiere el contenido completo del archivo.', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...zoomFr, + title: 'PDF', + pageImage: 'Page PDF {page}', + loading: 'Rendu du document...', + rendering: 'Page de rendu…', + failed: "Impossible d'afficher le PDF : {message}", + password: 'Ce PDF nécessite un mot de passe ; les aperçus protégés par mot de passe ne sont pas pris en charge.', + workerFailed: "Le processus de rendu PDF n'a pas pu continuer. Veuillez réessayer.", + unsupported: "L'aperçu PDF nécessite le contenu complet du fichier.", + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...zoomDe, + title: 'PDF', + pageImage: 'PDF-Seite {page}', + loading: 'Dokument wird gerendert...', + rendering: 'Rendering-Seite…', + failed: 'PDF kann nicht angezeigt werden: {message}', + password: 'Für dieses PDF ist ein Passwort erforderlich. Passwortgeschützte Vorschauen werden nicht unterstützt.', + workerFailed: 'Der PDF-Rendering-Prozess konnte nicht fortgesetzt werden. Bitte versuchen Sie es erneut.', + unsupported: 'Für die PDF-Vorschau ist der vollständige Dateiinhalt erforderlich.', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...zoomRu, + title: 'PDF', + pageImage: 'PDF-страница {page}', + loading: 'Оформление документа...', + rendering: 'Отображение страницы…', + failed: 'Невозможно отобразить PDF: {message}', + password: 'Для этого PDF-файла требуется пароль; предварительный просмотр, защищенный паролем, не поддерживается.', + workerFailed: 'Процесс рендеринга PDF не может быть продолжен. Пожалуйста, повторите попытку.', + unsupported: 'Для предварительного просмотра PDF требуется полное содержимое файла.', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...zoomPt, + title: 'PDF', + pageImage: 'Página PDF {page}', + loading: 'Renderizando documento...', + rendering: 'Página de renderização…', + failed: 'Não é possível exibir PDF: {message}', + password: 'Este PDF requer uma senha; visualizações protegidas por senha não são suportadas.', + workerFailed: 'O processo de renderização do PDF não pôde continuar. Por favor, tente novamente.', + unsupported: 'A visualização do PDF requer o conteúdo completo do arquivo.', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts index ce263fccfd..6edb1573b4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts @@ -30,3 +30,53 @@ export const zoomJa = { 'zoomFitWidth': '幅に合わせる', 'zoomValue': '{percent}%', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const zoomEs = { + zoomControls: 'Controles de zoom', + zoomMenu: 'Elige zoom', + zoomOut: 'alejar', + zoomIn: 'Acercar', + zoomFitWidth: 'Ancho de ajuste', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** French dictionary, checked against the English keys. */ +export const zoomFr = { + zoomControls: 'Commandes de zoom', + zoomMenu: 'Choisissez le zoom', + zoomOut: 'Zoom arrière', + zoomIn: 'Zoomer', + zoomFitWidth: 'Ajuster la largeur', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** German dictionary, checked against the English keys. */ +export const zoomDe = { + zoomControls: 'Zoom-Steuerelemente', + zoomMenu: 'Wählen Sie Zoom', + zoomOut: 'Herauszoomen', + zoomIn: 'Vergrößern', + zoomFitWidth: 'Breite anpassen', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** Russian dictionary, checked against the English keys. */ +export const zoomRu = { + zoomControls: 'Элементы управления масштабированием', + zoomMenu: 'Выберите масштаб', + zoomOut: 'Уменьшить масштаб', + zoomIn: 'Увеличить масштаб', + zoomFitWidth: 'Подходит по ширине', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** Portuguese dictionary, checked against the English keys. */ +export const zoomPt = { + zoomControls: 'Controles de zoom', + zoomMenu: 'Escolha o zoom', + zoomOut: 'Diminuir zoom', + zoomIn: 'Ampliar', + zoomFitWidth: 'Ajustar largura', + zoomValue: '{percent}%', +} satisfies typeof zoomEn diff --git a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts index e1f236c578..b1a8caac0a 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts @@ -31,7 +31,7 @@ import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx' import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx' import { PDF_BODY_ID } from '../src/client/pdf/index.ts' import { CodeBody } from '../src/client/code/CodeBody.tsx' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' import type { textFace } from '../src/client/face.ts' import type { TextStore } from '../src/client/store.ts' import { FILE, SESSION, TAB_ID, byteResult, createResources, page } from './fixtures.client.ts' @@ -86,7 +86,7 @@ describe('ui-sidebar-documentpreview apply', () => { const { tabs, registered, dictionaries } = await boot() expect(tabs.get(TEXTPREVIEW_KIND)?.priority).toBe('fallback') expect(tabs.get(TEXTPREVIEW_KIND)?.id).toBe(TEXTPREVIEW_ID) - expect(dictionaries.get('sidebarDocumentPreview')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarDocumentPreview')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) // The seat key is the implementation's id, not the kind: an extension may // take the kind over, and the seat must still find this body. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([ diff --git a/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts index b5df1bdaaf..9d8feb861b 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts @@ -6,7 +6,7 @@ import { binaryDocumentPath, DocumentPreviewRegistry } from '../src/client/docum import { Config } from '../src/config.ts' import { apply } from '../src/client/excel/index.ts' import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx' -import { en, ja, zh } from '../src/client/excel/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/excel/locales.ts' it('registers complete binary reads and removes the slot and locale on disposal', async () => { const ctx = new Context() @@ -28,7 +28,7 @@ it('registers complete binary reads and removes the slot and locale on disposal' expect(candidate.title()).toBe('Spreadsheet') expect(binaryDocumentPath(registry.getSnapshot(), path)).toBe(/\.(xlsx|xls)$/iu.test(path)) } - expect(registerLocale).toHaveBeenCalledWith('sidebarExcel', { zh, en, ja }) + expect(registerLocale).toHaveBeenCalledWith('sidebarExcel', { zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledOnce() expect(register.mock.calls[0]![1]).toBe(LazyExcelBody) const options = register.mock.calls[0]![0] as { inject: () => unknown } diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts index a77c9b7c6f..eaf1b73f01 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts @@ -7,7 +7,7 @@ import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path' import { DocumentPreviewRegistry } from '../src/client/document/registry.ts' import { apply, HTML_BODY_ID, htmlBodyDefinition } from '../src/client/html/index.ts' import { HtmlBody } from '../src/client/html/HtmlBody.tsx' -import { en, ja, zh } from '../src/client/html/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/html/locales.ts' import type { HtmlBodyProps } from '../src/client/html/HtmlBody.tsx' type Registration = { @@ -54,7 +54,7 @@ describe('HTML registration', () => { await fiber.await() expect(registry.candidates('INDEX.HTM').map(entry => entry.id)).toEqual([HTML_BODY_ID]) expect(registry.getSnapshot()[0]?.title()).toBe(en.title) - expect(dictionaries.get('documentHtml')).toEqual({ zh, en, ja }) + expect(dictionaries.get('documentHtml')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledOnce() const registration = register.mock.calls[0]?.[0] expect(registration).toMatchObject({ name: 'sidebar.right.tab.document', key: HTML_BODY_ID, locale: 'documentHtml' }) diff --git a/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts index 63e428e35f..dfacf0e27d 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts @@ -6,7 +6,7 @@ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' import { DocumentPreviewRegistry } from '../src/client/document/registry.ts' import { ImageBody } from '../src/client/image/ImageBody.tsx' import { apply, BINARY_IMAGE_EXTENSIONS, IMAGE_BODY_ID, IMAGE_EXTENSIONS, imageBodyDefinition } from '../src/client/image/index.ts' -import { en, ja, zh } from '../src/client/image/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/image/locales.ts' import type { ZoomInjected, ZoomStore } from '../src/client/zoom/store.ts' let dispose: (() => Promise) | undefined @@ -64,7 +64,7 @@ describe('image registration', () => { expect(registry.candidates(`ASSET.${extension}`).map(entry => entry.id)).toEqual([IMAGE_BODY_ID]) } expect(registry.getSnapshot()[0]?.title()).toBe(en.title) - expect(dictionaries.get('sidebarImage')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarImage')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ name: 'sidebar.right.tab.document', key: IMAGE_BODY_ID, locale: 'sidebarImage', }), ImageBody) diff --git a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts index 334beb3da1..7e30ba921a 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts @@ -13,7 +13,7 @@ import { Config } from '../src/config.ts' import { OfficeBody, type OfficeBodyInjected } from '../src/client/office/OfficeBody.tsx' import type { OfficeStore } from '../src/client/office/store.ts' import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' -import { en, ja, zh } from '../src/client/office/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/office/locales.ts' import { en as documentEn } from '../src/client/locales.ts' vi.mock('../src/client/office/face.ts', async (importOriginal) => { @@ -91,7 +91,7 @@ async function harness(config: Partial = {}, missing?: 'remote it.each(['remote', 'render', 'files'] as const)('keeps Word and PowerPoint registration and guidance when %s is absent', async (missing) => { const h = await harness(undefined, missing) try { - expect(h.locale.register).toHaveBeenCalledWith('sidebarOffice', { zh, en, ja }) + expect(h.locale.register).toHaveBeenCalledWith('sidebarOffice', { zh, en, ja, es, fr, de, ru, pt }) for (const path of ['a.DOC', 'b.DOCX', 'c.PPT', 'd.pptx']) { expect(h.registry.candidates(path)[0]!.binaryExtensions).toEqual(['doc', 'docx', 'ppt', 'pptx']) expect(h.registry.candidates(path)[0]!.title()).toBe(en.title) diff --git a/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts index a55c457e4c..965269c977 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts @@ -16,7 +16,7 @@ const renderedPdf = vi.hoisted(() => vi.fn(() => null)) vi.mock('../src/client/pdf/pdf.tsx', () => ({ PdfBody: renderedPdf })) import { apply, PDF_BODY_ID } from '../src/client/pdf/index.ts' import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx' -import { en, ja, zh } from '../src/client/pdf/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/pdf/locales.ts' afterEach(() => { cleanup(); renderedPdf.mockClear() }) @@ -58,7 +58,7 @@ describe('PDF registration', () => { { id: PDF_BODY_ID, extensions: ['pdf'], priority: 'builtin', loading: 'bytes-complete', wrap: false }, ]) expect(previews.candidates('report.pdf')[0]!.title()).toBe('PDF') - expect(dictionaries.get('sidebarPdf')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarPdf')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(entries[0]).toMatchObject({ name: 'sidebar.right.tab.document', key: PDF_BODY_ID, locale: 'sidebarPdf' }) const instance = entries[0]!.store.create() const face = entries[0]!.inject('s1' as SessionId, instance.actions) diff --git a/packages/client/ui-sidebar-files/locale/de.json b/packages/client/ui-sidebar-files/locale/de.json new file mode 100644 index 0000000000..996a4a4a75 --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Bedienfeld zum Durchsuchen von Dateien", + "description": "Durchsuchen Sie das Arbeitsbereichsverzeichnis und öffnen Sie die Datei im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-files/locale/es.json b/packages/client/ui-sidebar-files/locale/es.json new file mode 100644 index 0000000000..d50b1818de --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panel de exploración de archivos", + "description": "Explore el directorio del espacio de trabajo y abra el archivo en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-files/locale/fr.json b/packages/client/ui-sidebar-files/locale/fr.json new file mode 100644 index 0000000000..7d3f2a9917 --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panneau de navigation dans les fichiers", + "description": "Parcourez le répertoire de l'espace de travail et ouvrez le fichier dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-files/locale/pt.json b/packages/client/ui-sidebar-files/locale/pt.json new file mode 100644 index 0000000000..27cf88ec0d --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Painel de navegação de arquivos", + "description": "Navegue no diretório do espaço de trabalho e abra o arquivo no painel direito." + } +} diff --git a/packages/client/ui-sidebar-files/locale/ru.json b/packages/client/ui-sidebar-files/locale/ru.json new file mode 100644 index 0000000000..8376a67afe --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Панель просмотра файлов", + "description": "Просмотрите каталог рабочей области и откройте файл на правой панели." + } +} diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index d093121243..f3cc2f36f8 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/src/client/index.ts b/packages/client/ui-sidebar-files/src/client/index.ts index 0b140c0e4e..0a03f63f3d 100644 --- a/packages/client/ui-sidebar-files/src/client/index.ts +++ b/packages/client/ui-sidebar-files/src/client/index.ts @@ -20,7 +20,7 @@ import { FILES_ID, filesDefinition } from './definition.tsx' import { createList, createWatch, filesFace } from './face.ts' import { FilesBody } from './FilesBody.tsx' import { FilesTitle } from './FilesTitle.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { createFilesStore } from './store.ts' export type { SidebarFilesKey } from './locales.ts' @@ -79,7 +79,7 @@ export function apply(ctx: ClientContext): void { }), 'ui-sidebar-files: shortcut') }) ctx.effect(() => ctx.sidebarRightTabs.register(filesDefinition(t)), 'ui-sidebar-files: files type') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-files: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-files: dictionaries') const store = createFilesStore() const inject = filesFace(createList(ctx.remote), createWatch(ctx.remote)) diff --git a/packages/client/ui-sidebar-files/src/client/locales.ts b/packages/client/ui-sidebar-files/src/client/locales.ts index 69bf81f8ce..f646b7ea74 100644 --- a/packages/client/ui-sidebar-files/src/client/locales.ts +++ b/packages/client/ui-sidebar-files/src/client/locales.ts @@ -83,3 +83,108 @@ export const ja = { 'error.notDirectory': 'それはディレクトリではありません。', 'error.unavailable': '読み取りに失敗しました: {message}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'shortcut.noSession': 'Seleccione una sesión primero', + 'type.label': 'Archivos', + 'guide.title': 'Archivos de espacio de trabajo', + 'guide.description': 'Explorar archivos en el espacio de trabajo de esta sesión', + loading: 'Leyendo…', + empty: 'directorio vacio', + truncated: 'Demasiadas entradas y solo se muestran algunas de ellas.', + noWorkspace: 'Esta sesión no tiene ningún directorio de espacio de trabajo.', + reload: 'recargar', + autoRefresh: 'Actualización automática', + 'autoRefresh.enable': 'Habilitar actualización automática', + 'autoRefresh.disable': 'Deshabilitar la actualización automática', + 'entry.other': 'No es un archivo ni un directorio, por lo que no se puede abrir.', + 'error.notFound': 'Ese directorio desapareció. Es posible que se haya movido o eliminado.', + 'error.outsideWorkspace': 'Ese directorio está fuera del espacio de trabajo, por lo que la barra lateral no lo leerá.', + 'error.notDirectory': 'Eso no es un directorio.', + 'error.unavailable': 'Error de lectura: {message}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'shortcut.noSession': "Sélectionnez d'abord une session", + 'type.label': 'Fichiers', + 'guide.title': "Fichiers de l'espace de travail", + 'guide.description': "Parcourir les fichiers dans l'espace de travail de cette session", + loading: 'Lecture…', + empty: 'Répertoire vide', + truncated: "Trop d'entrées, n'en affichant que certaines.", + noWorkspace: "Cette session n'a pas de répertoire d'espace de travail.", + reload: 'Recharger', + autoRefresh: 'Actualisation automatique', + 'autoRefresh.enable': "Activer l'actualisation automatique", + 'autoRefresh.disable': "Désactiver l'actualisation automatique", + 'entry.other': "Il ne s'agit pas d'un fichier ou d'un répertoire, il ne peut donc pas être ouvert.", + 'error.notFound': 'Ce répertoire a disparu. Il a peut-être été déplacé ou supprimé.', + 'error.outsideWorkspace': 'Ce répertoire se trouve en dehors de l’espace de travail, donc la barre latérale ne le lira pas.', + 'error.notDirectory': "Ce n'est pas un répertoire.", + 'error.unavailable': 'Échec de lecture : {message}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'shortcut.noSession': 'Wählen Sie zunächst eine Sitzung aus', + 'type.label': 'Dateien', + 'guide.title': 'Arbeitsbereichsdateien', + 'guide.description': 'Durchsuchen Sie die Dateien im Arbeitsbereich dieser Sitzung', + loading: 'Lesen…', + empty: 'Leeres Verzeichnis', + truncated: 'Zu viele Einträge, es werden nur einige davon angezeigt.', + noWorkspace: 'Diese Sitzung hat kein Arbeitsbereichsverzeichnis.', + reload: 'Neu laden', + autoRefresh: 'Automatische Aktualisierung', + 'autoRefresh.enable': 'Aktivieren Sie die automatische Aktualisierung', + 'autoRefresh.disable': 'Deaktivieren Sie die automatische Aktualisierung', + 'entry.other': 'Keine Datei oder kein Verzeichnis, daher kann sie nicht geöffnet werden.', + 'error.notFound': 'Dieses Verzeichnis ist weg. Möglicherweise wurde es verschoben oder gelöscht.', + 'error.outsideWorkspace': 'Dieses Verzeichnis liegt außerhalb des Arbeitsbereichs und wird daher von der Seitenleiste nicht gelesen.', + 'error.notDirectory': 'Das ist kein Verzeichnis.', + 'error.unavailable': 'Lesen fehlgeschlagen: {message}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'shortcut.noSession': 'Сначала выберите сеанс', + 'type.label': 'Файлы', + 'guide.title': 'Файлы рабочей области', + 'guide.description': 'Просмотр файлов в рабочей области этого сеанса', + loading: 'Чтение…', + empty: 'Пустой каталог', + truncated: 'Слишком много записей, показаны только некоторые из них.', + noWorkspace: 'В этом сеансе нет каталога рабочей области.', + reload: 'Перезагрузить', + autoRefresh: 'Автоматическое обновление', + 'autoRefresh.enable': 'Включить автоматическое обновление', + 'autoRefresh.disable': 'Отключить автоматическое обновление', + 'entry.other': 'Это не файл и не каталог, поэтому его нельзя открыть.', + 'error.notFound': 'Этот каталог исчез. Возможно, оно было перемещено или удалено.', + 'error.outsideWorkspace': 'Этот каталог находится за пределами рабочей области, поэтому боковая панель его не читает.', + 'error.notDirectory': 'Это не каталог.', + 'error.unavailable': 'Не удалось прочитать: {message}.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'shortcut.noSession': 'Selecione uma sessão primeiro', + 'type.label': 'Arquivos', + 'guide.title': 'Arquivos do espaço de trabalho', + 'guide.description': 'Procure arquivos no espaço de trabalho desta sessão', + loading: 'Lendo…', + empty: 'Diretório vazio', + truncated: 'Muitas entradas, mostrando apenas algumas delas.', + noWorkspace: 'Esta sessão não possui diretório de espaço de trabalho.', + reload: 'Recarregar', + autoRefresh: 'Atualização automática', + 'autoRefresh.enable': 'Ativar atualização automática', + 'autoRefresh.disable': 'Desativar atualização automática', + 'entry.other': 'Não é um arquivo ou diretório, portanto não pode ser aberto.', + 'error.notFound': 'Esse diretório desapareceu. Pode ter sido movido ou excluído.', + 'error.outsideWorkspace': 'Esse diretório está fora da área de trabalho, portanto a barra lateral não o lerá.', + 'error.notDirectory': 'Isso não é um diretório.', + 'error.unavailable': 'Falha na leitura: {message}', +} satisfies Record diff --git a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts index 003df8b934..53f5e51ebd 100644 --- a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts @@ -16,7 +16,7 @@ import { apply, inject } from '../src/client/index.ts' import { apply as hostApply } from '../src/index.ts' import { FilesBody } from '../src/client/FilesBody.tsx' import { FilesTitle } from '../src/client/FilesTitle.tsx' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' interface Recorded { name: string @@ -94,7 +94,7 @@ describe('ui-sidebar-files apply', () => { expect(definition?.title('sidebar://files')).toBe('type.label') expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])) .toEqual([[10, 'guide.title', 'guide.description']]) - expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) // The seat key is the implementation's id, not the kind: an extension may // take the kind over, and the seat must still find this body. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([ diff --git a/packages/client/ui-sidebar-right/README.i18n.yaml b/packages/client/ui-sidebar-right/README.i18n.yaml index c781146562..cedfe4f4e4 100644 --- a/packages/client/ui-sidebar-right/README.i18n.yaml +++ b/packages/client/ui-sidebar-right/README.i18n.yaml @@ -36,8 +36,8 @@ en: 717b86e05b44ebdd zh: 0f4f694d0ea1d7f8 /deepseek-ai-dsh-client-ui-sidebar-right/the-guide: - en: c67935eddef999fe - zh: 0648664ded92ee82 + en: 5fcda0a3a4453b52 + zh: d7818c63bf3371f2 /deepseek-ai-dsh-client-ui-sidebar-right/copy: en: 4315a1c03ca5fef3 zh: c09a09f1d9b99107 diff --git a/packages/client/ui-sidebar-right/README.md b/packages/client/ui-sidebar-right/README.md index b5839bd05d..01fbf0a30e 100644 --- a/packages/client/ui-sidebar-right/README.md +++ b/packages/client/ui-sidebar-right/README.md @@ -114,7 +114,7 @@ Close and refresh commands resolve the focused pane and tab occurrence. Page bod Each `guide` entry has an `id` unique within its provider. The guide renders `sidebar.right.tab.guide.entry` by the active definition id, forwarding the entry id, resolved title and optional description, plus the framework `useTabInfo` hook. A provider can register a custom card under its definition id; otherwise the standard icon/title button renders. The sidebar owns width and placement, while custom components own their markup and interaction. Guide cards use the shared R20 radius, including hover fills and provider-owned split buttons. Entry identity remains stable across sorting and unrelated registrations. Unboxed shortcut text aligns at the right. The terminal shell menu sits beside its entry title. -While the root-scoped `sidebar.right.tab.guide.actions` list has registrations, a **More tools** card follows the guide cards. Its menu end-aligns to the card, is at least as wide as the card, and grows with its rows up to 480px or the viewport width minus 24px. The menu rows are that list's registrations. Each receives `entries` (cards in registry order with `id`, `label`, and `pinned`; kind `files` is never listed), `onOpen(id)`, which opens the card's tab type in the guide's place, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The guide keeps one root list of unpinned card ids, each `kind:entry id`, in browser `localStorage` under `dsh.slot.manual-order.sidebar.right.tab.guide.list.v1`, stored as `{ "unpinned": string[] }`; the key reserves the name of a future guide list slot. The kind identifies a card instead of the provider id, so a pin choice survives an extension taking over that kind with the same entry id. Cards of kind `files` always show. Unpinning hides only the card; it closes no tab and changes neither tab-type registration nor the default page. Description density follows the visible card count. Ids of cards that are not registered stay stored; stored content other than a string array shows every card. +While the root-scoped `sidebar.right.tab.guide.actions` list has registrations, a **More tools** card follows the guide cards. Its menu end-aligns to the card, is at least as wide as the card, and grows with its rows up to 480px or the viewport width minus 24px. The menu rows are that list's registrations. Each receives `entries` (cards in registry order with `id`, `label`, and `pinned`; kind `files` is never listed), `onOpen(id)`, which opens the card's tab type in the guide's place, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The guide keeps an in-memory list of unpinned card ids, each `kind:entry id`. Plugin reload resets pin choices; legacy localStorage values are ignored. The default More menu only offers plugin browsing and creation; pin callbacks remain available to other registrations. The kind identifies a card instead of the provider id, so a pin choice survives an extension taking over that kind with the same entry id. Cards of kind `files` always show. Unpinning hides only the card; it closes no tab and changes neither tab-type registration nor the default page. Description density follows the visible card count. Unregistered card ids remain in memory until plugin reload. A tab type can declare `multiple: true` for independent instances such as terminals. Each open receives a distinct content address, so placement and docking preserve those instances; ordinary pages retain per-pane uniqueness. diff --git a/packages/client/ui-sidebar-right/README.zh.md b/packages/client/ui-sidebar-right/README.zh.md index cfb16e3e53..96f12af1a6 100644 --- a/packages/client/ui-sidebar-right/README.zh.md +++ b/packages/client/ui-sidebar-right/README.zh.md @@ -114,7 +114,7 @@ Tab 域按(Session,Tab id)保留品牌化 occurrence id、导航、中止 每个 `guide` 入口具有 provider 内唯一的 `id`。开始页按当前生效的定义 id 渲染 `sidebar.right.tab.guide.entry`,传入入口 id、解析后的标题、可选描述,以及框架提供的 `useTabInfo` hook。provider 可按自己的定义 id 注册自定义卡片;未注册时使用默认图标与标题按钮。侧栏负责宽度和排列,自定义组件负责内部结构和交互。引导卡片统一使用 R20,包含悬停底色与提供方自有的分体按钮。排序或其他注册变化不会改变入口身份。无背景的快捷键文字右对齐。终端 Shell 菜单位于入口标题旁。 -root 作用域的 `sidebar.right.tab.guide.actions` list 有注册项时,引导卡片之后显示**更多工具**卡片。它的菜单与卡片末端对齐,不窄于卡片,并随菜单行增宽,最大为 480px 与视口宽度减 24px 中的较小值。菜单行为该 list 的注册项。每个注册项接收 `entries`(按注册表顺序的卡片,含 `id`、`label` 与 `pinned`;不含 kind 为 `files` 的卡片)、在引导页位置打开卡片标签类型的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。引导页在浏览器 `localStorage` 的 `dsh.slot.manual-order.sidebar.right.tab.guide.list.v1` 下保存一份 root 级的已收起卡片 id 列表,每个 id 为 `kind:入口 id`,格式为 `{ "unpinned": string[] }`;该键名为以后的引导入口 list slot 预留。卡片由 kind 而不是提供方 id 标识,因此扩展接管某个 kind 且保持入口 id 时,Pin 选择仍然有效。kind 为 `files` 的卡片始终显示。收起只隐藏卡片,不关闭标签,也不改变标签类型注册或默认页。描述的显示密度按可见卡片数计算。未注册卡片的 id 保留在列表中;保存内容不是字符串数组时,所有卡片都显示。 +root 作用域的 `sidebar.right.tab.guide.actions` list 有注册项时,引导卡片之后显示**更多工具**卡片。它的菜单与卡片末端对齐,不窄于卡片,并随菜单行增宽,最大为 480px 与视口宽度减 24px 中的较小值。菜单行为该 list 的注册项。每个注册项接收 `entries`(按注册表顺序的卡片,含 `id`、`label` 与 `pinned`;不含 kind 为 `files` 的卡片)、在引导页位置打开卡片标签类型的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。引导页在内存中保存已收起卡片 id 列表,每个 id 为 `kind:入口 id`。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。默认更多菜单只提供插件浏览和创建;其他注册项仍可使用 Pin 回调。卡片由 kind 而不是提供方 id 标识,因此扩展接管某个 kind 且保持入口 id 时,Pin 选择仍然有效。kind 为 `files` 的卡片始终显示。收起只隐藏卡片,不关闭标签,也不改变标签类型注册或默认页。描述的显示密度按可见卡片数计算。未注册卡片的 id 保留在内存中,直到插件重新加载。 终端等需要独立实例的 tab 类型可声明 `multiple: true`。每次打开都会获得独立内容地址,因此移动和停靠会保留这些实例;普通页面仍在每个格内去重。 diff --git a/packages/client/ui-sidebar-right/locale/de.json b/packages/client/ui-sidebar-right/locale/de.json new file mode 100644 index 0000000000..e94663c445 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "rechtes Panel", + "description": "Verwalten Sie den Erweiterungsstatus, den Inhalt und die Navigation des rechten Bereichs." + } +} diff --git a/packages/client/ui-sidebar-right/locale/es.json b/packages/client/ui-sidebar-right/locale/es.json new file mode 100644 index 0000000000..a7d89d3719 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel derecho", + "description": "Administre el estado de expansión, el contenido y la navegación del panel derecho." + } +} diff --git a/packages/client/ui-sidebar-right/locale/fr.json b/packages/client/ui-sidebar-right/locale/fr.json new file mode 100644 index 0000000000..39057044ef --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau droit", + "description": "Gérez l'état d'expansion, le contenu et la navigation du panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-right/locale/pt.json b/packages/client/ui-sidebar-right/locale/pt.json new file mode 100644 index 0000000000..53a65eeae1 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel direito", + "description": "Gerencie o estado da expansão, o conteúdo e a navegação do painel direito." + } +} diff --git a/packages/client/ui-sidebar-right/locale/ru.json b/packages/client/ui-sidebar-right/locale/ru.json new file mode 100644 index 0000000000..cf55dfcadd --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "правая панель", + "description": "Управляйте состоянием расширения, содержимым и навигацией правой панели." + } +} diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 286461dec0..30943ba233 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/src/client/index.ts b/packages/client/ui-sidebar-right/src/client/index.ts index cb6a2f6e3f..99b800b6c8 100644 --- a/packages/client/ui-sidebar-right/src/client/index.ts +++ b/packages/client/ui-sidebar-right/src/client/index.ts @@ -42,7 +42,7 @@ import { SidebarSessionViews } from './session-views.ts' import { createSidebarRightController, type SidebarRightController } from './service.ts' import { SidebarRightTabRegistry } from './tab-registry.ts' import { createSidebarRightStore } from './stores.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { GUIDE_ID, guideDefinition } from './tabs/guide/definition.ts' import { createGuidePins } from './tabs/guide/guide-pins.ts' import { guideTabInfoFactory, tabInfoFactory } from './tab-info.ts' @@ -161,7 +161,7 @@ export function apply(ctx: ClientContext): void { void disposeRegistry() }, 'ui-sidebar-right: service faces') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-right: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-right: dictionaries') ctx.effect(() => registerSidebarShortcuts(ctx.shortcuts, controller, t, () => { void ctx.shortcuts.closeWindow().catch((error: unknown) => { console.error('Window close failed', error) }) }), 'ui-sidebar-right: shortcuts') diff --git a/packages/client/ui-sidebar-right/src/client/locales.ts b/packages/client/ui-sidebar-right/src/client/locales.ts index f6b70b8a88..1f4b190402 100644 --- a/packages/client/ui-sidebar-right/src/client/locales.ts +++ b/packages/client/ui-sidebar-right/src/client/locales.ts @@ -125,3 +125,198 @@ export const ja = { 'tab.guide.more': 'その他のツール', 'tab.unavailable': 'この種類のコンテンツを表示できるものはまだありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'command.close': 'Cerrar la página o ventana actual', + 'command.refresh': 'Actualizar la página actual', + 'command.noRefresh': 'Esta página no se puede actualizar', + 'command.toggle': 'Alternar barra lateral derecha', + 'command.fullscreen': 'Alternar panel de pantalla completa', + 'command.noSession': 'Seleccione una sesión primero', + 'command.noFocus': 'Enfoque primero un panel de la barra lateral derecha', + 'command.stale': 'La página cambió; enfocarlo de nuevo', + 'command.collapsed': 'Expande primero la barra lateral derecha', + 'command.float': 'Esta acción no está disponible en un panel flotante', + 'command.empty': 'Abra una página primero', + 'command.budget': 'Dos paneles es el límite', + 'command.width': 'No hay suficiente ancho para dividir, ampliar la barra lateral', + 'chrome.expand': 'Abrir barra lateral', + 'chrome.expandAria': 'Abrir barra lateral derecha', + 'chrome.collapse': 'Contraer barra lateral', + 'chrome.collapseAria': 'Contraer barra lateral derecha', + 'chrome.toFullscreen': 'Pantalla completa', + 'chrome.exitFullscreen': 'Salir de pantalla completa', + 'dock.emptyPane': 'Panel vacío', + 'dock.splitPane': 'dividir', + 'dock.splitPaneDisabled': 'Dos paneles es el límite', + 'dock.splitPaneNarrow': 'No hay suficiente ancho para dividir, ampliar la barra lateral', + 'dock.closeTab': 'Cerrar', + 'dock.addTab': 'Nueva pestaña', + 'dock.dockFloat': 'Enviar de vuelta a la barra lateral', + 'dock.closeFloat': 'Cerrar', + 'dock.drop.center': 'Muévete aquí', + 'dock.drop.left': 'Agregar división izquierda', + 'dock.drop.right': 'Agregar división a la derecha', + 'dock.drop.top': 'Agregar división superior', + 'dock.drop.bottom': 'Agregar división inferior', + 'tab.guide.title': 'Empezar', + 'tab.guide.more': 'Más herramientas', + 'tab.unavailable': 'Nada aquí puede ver este tipo de contenido todavía.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'command.close': 'Fermer la page ou la fenêtre actuelle', + 'command.refresh': 'Actualiser la page actuelle', + 'command.noRefresh': 'Cette page ne peut pas être actualisée', + 'command.toggle': 'Basculer la barre latérale droite', + 'command.fullscreen': 'Basculer le panneau en plein écran', + 'command.noSession': "Sélectionnez d'abord une session", + 'command.noFocus': "Concentrer d'abord un volet de la barre latérale droite", + 'command.stale': 'La page a changé ; concentre-le à nouveau', + 'command.collapsed': "Développez d'abord la barre latérale droite", + 'command.float': "Cette action n'est pas disponible dans un panneau flottant", + 'command.empty': "Ouvrir d'abord une page", + 'command.budget': 'Deux panneaux sont la limite', + 'command.width': 'Pas assez de largeur pour diviser, élargir la barre latérale', + 'chrome.expand': 'Ouvrir la barre latérale', + 'chrome.expandAria': 'Ouvrir la barre latérale droite', + 'chrome.collapse': 'Réduire la barre latérale', + 'chrome.collapseAria': 'Réduire la barre latérale droite', + 'chrome.toFullscreen': 'Plein écran', + 'chrome.exitFullscreen': 'Quitter le plein écran', + 'dock.emptyPane': 'Volet vide', + 'dock.splitPane': 'Diviser', + 'dock.splitPaneDisabled': 'Deux panneaux sont la limite', + 'dock.splitPaneNarrow': 'Pas assez de largeur pour diviser, élargir la barre latérale', + 'dock.closeTab': 'Fermer', + 'dock.addTab': 'Nouvel onglet', + 'dock.dockFloat': 'Renvoyer à la barre latérale', + 'dock.closeFloat': 'Fermer', + 'dock.drop.center': 'Déplacez-vous ici', + 'dock.drop.left': 'Ajouter une division à gauche', + 'dock.drop.right': 'Ajouter une division à droite', + 'dock.drop.top': 'Ajouter une division supérieure', + 'dock.drop.bottom': 'Ajouter une division inférieure', + 'tab.guide.title': 'Commencer', + 'tab.guide.more': "Plus d'outils", + 'tab.unavailable': 'Rien ici ne peut encore afficher ce type de contenu.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'command.close': 'Aktuelle Seite oder Fenster schließen', + 'command.refresh': 'Aktuelle Seite aktualisieren', + 'command.noRefresh': 'Diese Seite kann nicht aktualisiert werden', + 'command.toggle': 'Rechte Seitenleiste umschalten', + 'command.fullscreen': 'Panel-Vollbild umschalten', + 'command.noSession': 'Wählen Sie zunächst eine Sitzung aus', + 'command.noFocus': 'Fokussieren Sie zuerst einen rechten Seitenleistenbereich', + 'command.stale': 'Die Seite hat sich geändert; Konzentrieren Sie es erneut', + 'command.collapsed': 'Erweitern Sie zunächst die rechte Seitenleiste', + 'command.float': 'Diese Aktion ist in einem schwebenden Panel nicht verfügbar', + 'command.empty': 'Öffnen Sie zunächst eine Seite', + 'command.budget': 'Zwei Scheiben sind die Grenze', + 'command.width': 'Die Breite reicht nicht aus, um die Seitenleiste zu teilen. Verbreitern Sie die Seitenleiste', + 'chrome.expand': 'Seitenleiste öffnen', + 'chrome.expandAria': 'Rechte Seitenleiste öffnen', + 'chrome.collapse': 'Seitenleiste einklappen', + 'chrome.collapseAria': 'Rechte Seitenleiste einklappen', + 'chrome.toFullscreen': 'Vollbild', + 'chrome.exitFullscreen': 'Beenden Sie den Vollbildmodus', + 'dock.emptyPane': 'Leerer Bereich', + 'dock.splitPane': 'Geteilt', + 'dock.splitPaneDisabled': 'Zwei Scheiben sind die Grenze', + 'dock.splitPaneNarrow': 'Die Breite reicht nicht aus, um die Seitenleiste zu teilen. Verbreitern Sie die Seitenleiste', + 'dock.closeTab': 'Schließen', + 'dock.addTab': 'Neuer Tab', + 'dock.dockFloat': 'Zurück zur Seitenleiste senden', + 'dock.closeFloat': 'Schließen', + 'dock.drop.center': 'Bewegen Sie sich hierher', + 'dock.drop.left': 'Linke Teilung hinzufügen', + 'dock.drop.right': 'Rechte Teilung hinzufügen', + 'dock.drop.top': 'Oberen Split hinzufügen', + 'dock.drop.bottom': 'Untere Teilung hinzufügen', + 'tab.guide.title': 'Starten', + 'tab.guide.more': 'Weitere Werkzeuge', + 'tab.unavailable': 'Diese Art von Inhalten kann hier noch nicht angezeigt werden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'command.close': 'Закрыть текущую страницу или окно', + 'command.refresh': 'Обновить текущую страницу', + 'command.noRefresh': 'Эту страницу невозможно обновить', + 'command.toggle': 'Переключить правую боковую панель', + 'command.fullscreen': 'Переключить панель в полноэкранный режим', + 'command.noSession': 'Сначала выберите сеанс', + 'command.noFocus': 'Сначала сфокусируйтесь на правой боковой панели', + 'command.stale': 'Страница изменилась; сосредоточься еще раз', + 'command.collapsed': 'Сначала разверните правую боковую панель', + 'command.float': 'Это действие недоступно на плавающей панели.', + 'command.empty': 'Сначала откройте страницу', + 'command.budget': 'Две панели — это предел', + 'command.width': 'Недостаточно ширины для разделения, расширьте боковую панель', + 'chrome.expand': 'Открыть боковую панель', + 'chrome.expandAria': 'Открыть правую боковую панель', + 'chrome.collapse': 'Свернуть боковую панель', + 'chrome.collapseAria': 'Свернуть правую боковую панель', + 'chrome.toFullscreen': 'Полноэкранный режим', + 'chrome.exitFullscreen': 'Выйти из полноэкранного режима', + 'dock.emptyPane': 'Пустая панель', + 'dock.splitPane': 'Сплит', + 'dock.splitPaneDisabled': 'Две панели — это предел', + 'dock.splitPaneNarrow': 'Недостаточно ширины для разделения, расширьте боковую панель', + 'dock.closeTab': 'Закрыть', + 'dock.addTab': 'Новая вкладка', + 'dock.dockFloat': 'Отправить обратно на боковую панель', + 'dock.closeFloat': 'Закрыть', + 'dock.drop.center': 'Переместитесь сюда', + 'dock.drop.left': 'Добавить левый сплит', + 'dock.drop.right': 'Добавить правое разделение', + 'dock.drop.top': 'Добавить верхнее разделение', + 'dock.drop.bottom': 'Добавить нижний разрез', + 'tab.guide.title': 'Старт', + 'tab.guide.more': 'Дополнительные инструменты', + 'tab.unavailable': 'Ничто здесь пока не может просматривать такой контент.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'command.close': 'Fechar a página ou janela atual', + 'command.refresh': 'Atualizar página atual', + 'command.noRefresh': 'Esta página não pode ser atualizada', + 'command.toggle': 'Alternar barra lateral direita', + 'command.fullscreen': 'Alternar painel para tela cheia', + 'command.noSession': 'Selecione uma sessão primeiro', + 'command.noFocus': 'Foque primeiro um painel da barra lateral direita', + 'command.stale': 'A página mudou; concentre-se novamente', + 'command.collapsed': 'Expanda a barra lateral direita primeiro', + 'command.float': 'Esta ação não está disponível em um painel flutuante', + 'command.empty': 'Abra uma página primeiro', + 'command.budget': 'Dois painéis é o limite', + 'command.width': 'Largura insuficiente para dividir. Amplie a barra lateral', + 'chrome.expand': 'Abrir barra lateral', + 'chrome.expandAria': 'Abrir barra lateral direita', + 'chrome.collapse': 'Recolher barra lateral', + 'chrome.collapseAria': 'Recolher barra lateral direita', + 'chrome.toFullscreen': 'Tela cheia', + 'chrome.exitFullscreen': 'Sair da tela cheia', + 'dock.emptyPane': 'Painel vazio', + 'dock.splitPane': 'Dividir', + 'dock.splitPaneDisabled': 'Dois painéis é o limite', + 'dock.splitPaneNarrow': 'Largura insuficiente para dividir. Amplie a barra lateral', + 'dock.closeTab': 'Fechar', + 'dock.addTab': 'Nova guia', + 'dock.dockFloat': 'Enviar de volta para a barra lateral', + 'dock.closeFloat': 'Fechar', + 'dock.drop.center': 'Mova-se aqui', + 'dock.drop.left': 'Adicionar divisão à esquerda', + 'dock.drop.right': 'Adicionar divisão à direita', + 'dock.drop.top': 'Adicionar divisão superior', + 'dock.drop.bottom': 'Adicionar divisão inferior', + 'tab.guide.title': 'Começar', + 'tab.guide.more': 'Mais ferramentas', + 'tab.unavailable': 'Nada aqui pode visualizar esse tipo de conteúdo ainda.', +} satisfies Record diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css index 853e24953a..0c9f9046cb 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css @@ -67,6 +67,18 @@ transition: transform var(--ds-transition-duration-slow) var(--ds-ease-in-out); } +/* An item mounted by the opening has no closed style to slide from; it starts + at the closed position like the items already mounted. Only an opening + marks the panel, so items that mount while it stays open, or that a + Session switch shows, appear in place. */ +@starting-style { + .panel[data-sidebar-right-opening] :global([data-dockkit-host='dock']), + .panel[data-sidebar-right-opening] :global([data-dockkit-empty]), + .panel[data-sidebar-right-opening] :global([data-dockkit-divider]) { + transform: translateX(var(--dsh-sidebar-width)); + } +} + .panel[data-sidebar-right-panel='fullscreen'] { --dsh-dockkit-dock-layer: 40; } @@ -110,7 +122,10 @@ @media (prefers-reduced-motion: reduce) { .panel :global([data-dockkit-host='dock']), .panel :global([data-dockkit-empty]), - .panel :global([data-dockkit-divider]) { + .panel :global([data-dockkit-divider]), + .panel[data-sidebar-right-open] :global([data-dockkit-host='dock']), + .panel[data-sidebar-right-open] :global([data-dockkit-empty]), + .panel[data-sidebar-right-open] :global([data-dockkit-divider]) { transition: none; } } diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx index 484dfe458b..0f39af5d9f 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx @@ -8,9 +8,11 @@ * * Docked content stays mounted while collapsed, translated off the frame's right * edge, so opening and closing are one gesture in both presentations: a slide - * from and to that edge. Normal presentation moves the frame's tracks with - * the panel. A fullscreen opening reserves its underlying track only after - * the panel covers the frame, without animating those hidden columns. + * from and to that edge. An item mounted by the opening, such as the first tab + * of an empty column, starts at that edge, so it joins the slide. Normal + * presentation moves the frame's tracks with the panel. A fullscreen opening + * reserves its underlying track only after the panel covers the frame, without + * animating those hidden columns. * * The panel has no header of its own: its two controls — presentation switch * and collapse — ride the docking kit's chrome seat at the end of the top-right @@ -27,7 +29,7 @@ * signal, actions — is read through the slot-owned useTabInfo hook. The Tab * domain follows each session's store commits, including sessions off screen. */ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { CSSProperties, ReactNode, RefObject } from 'react' import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client' import { IconPanelLeftOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' @@ -303,6 +305,17 @@ function SidebarPanel(panel: PanelProps & { width: number; panelRef: RefObject value) + // True from the render that opens the on-screen panel until a transition in + // it ends, the panel closes, or it leaves the screen, which cancels the + // slide; the stylesheet starts items mounted meanwhile at the closed position. + const [opened, setOpened] = useState(expanded) + const [opening, setOpening] = useState(false) + if (opened !== expanded) { + setOpened(expanded) + setOpening(expanded && panel.active) + } else if (opening && !panel.active) { + setOpening(false) + } return (
{ setOpening(false) } : undefined} // Dockkit hides individual docked hosts; floating descendants remain accessible. aria-hidden={(!expanded && surface.layout.floats.length === 0) || undefined} > diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx index 4d40f86e01..febe9f1954 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx @@ -19,8 +19,7 @@ * open. */ import { useState } from 'react' -import clsx from 'clsx' -import { IconAppsAddOutlineRegular, Menu, ShortcutKeys } from '@deepseek-ai/dsh-client-ui-primitives' +import { Menu, ShortcutKeys } from '@deepseek-ai/dsh-client-ui-primitives' import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client' import type { ReactNode } from 'react' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' @@ -30,6 +29,7 @@ import type { import type { SidebarRightGuideActionsOwnerProps } from '../../contract/slots.ts' import type { SidebarRightGuideBox } from '../../tab-registry.ts' import { CompassGlyph, CubeGlyph } from './GuideTitle.tsx' +import { GuideMoreIcon } from './GuideMoreIcon.tsx' import { FIXED_GUIDE_KIND, guideEntryId } from './guide-pins.ts' import css from './GuideBody.module.css' @@ -39,7 +39,7 @@ export interface GuideInjected { readonly hooks: { readonly shortcuts: ObservableSnapshot readonly guideEntries: ObservableSnapshot - /** Unpinned `kind:id` card ids, persisted across reloads. */ + /** Unpinned `kind:id` card ids held in memory until plugin reload. */ readonly entryPins: ObservableSnapshot /** Whether `sidebar.right.tab.guide.actions` has registrations. */ readonly entryActions: ObservableSnapshot @@ -106,7 +106,7 @@ function GuideMoreMenu({ label, entries, onOpen, onPinnedChange, renderSlot }: G anchor={( )}> diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx new file mode 100644 index 0000000000..61f7913aa3 --- /dev/null +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx @@ -0,0 +1,15 @@ +/** Designer-supplied artwork used only by the right-sidebar More tools card. */ + +/** + * Render the More tools glyph at its authored size and ink. + * @returns Decorative 22px More tools artwork. + */ +export function GuideMoreIcon() { + return +} diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts b/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts index 3614a6778b..f43ee7503d 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts @@ -1,12 +1,10 @@ -/** Persisted pin choices for the guide's tool cards. */ +/** In-memory pin choices for the guide's tool cards. */ import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { SidebarRightGuideBox } from '../../tab-registry.ts' /** Tab kind whose card always shows and never enters the customization list. */ export const FIXED_GUIDE_KIND = 'files' -/** Root-level key reserved for the future guide list slot's per-list preferences. */ -const STORAGE_KEY = 'dsh.slot.manual-order.sidebar.right.tab.guide.list.v1' const NO_CARDS: readonly string[] = [] /** @@ -23,30 +21,23 @@ export interface GuidePins { /** Unpinned `kind:id` card ids; ids of cards that are not registered stay listed. */ readonly unpinned: ObservableSnapshot /** - * Show or hide one guide card; cards of the fixed kind are ignored. + * Pin or unpin one guide card; cards of the fixed kind are ignored. * @param id - `kind:id` card id. - * @param pinned - Whether the card shows on the guide. + * @param pinned - Whether the card is manually pinned. */ readonly setPinned: (id: string, pinned: boolean) => void } -/** Stored content is unchecked; anything but a string array leaves every card pinned. */ -function unpinnedCards(value: unknown): readonly string[] { - if (typeof value !== 'object' || value === null || !('unpinned' in value)) return NO_CARDS - const { unpinned } = value - return Array.isArray(unpinned) && unpinned.every((id): id is string => typeof id === 'string') ? unpinned : NO_CARDS -} - /** - * Create the persisted guide pin list; the snapshot keeps its reference until the list changes. - * @returns Validated unpinned card ids and the pin mutation. + * Create the in-memory guide pin list; the snapshot keeps its reference until the list changes. + * @returns Unpinned card ids and the pin mutation. */ export function createGuidePins(): GuidePins { - const store = createSnapshotStore({ unpinned: NO_CARDS }, { persist: { name: STORAGE_KEY } }) + const store = createSnapshotStore<{ readonly unpinned: readonly string[] }>({ unpinned: NO_CARDS }) return { - unpinned: { getSnapshot: () => unpinnedCards(store.getSnapshot()), subscribe: listener => store.subscribe(listener) }, + unpinned: { getSnapshot: () => store.getSnapshot().unpinned, subscribe: listener => store.subscribe(listener) }, setPinned: (id, pinned) => { - const current = unpinnedCards(store.getSnapshot()) + const current = store.getSnapshot().unpinned if (id.startsWith(`${FIXED_GUIDE_KIND}:`) || pinned !== current.includes(id)) return store.set({ unpinned: pinned ? current.filter(entry => entry !== id) : [...current, id] }) }, diff --git a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts index 59c620cd3b..6b40b7e4cb 100644 --- a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts @@ -27,7 +27,7 @@ import { ExpandButton } from '../src/client/shell/ExpandButton.tsx' import { GuideBody } from '../src/client/tabs/guide/GuideBody.tsx' import { GuideTitle } from '../src/client/tabs/guide/GuideTitle.tsx' import { GUIDE_ID } from '../src/client/tabs/guide/definition.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' const SHORTCUT_CATALOG: readonly never[] = [] @@ -124,7 +124,7 @@ describe('ui-sidebar-right apply', () => { expect(ctx.sidebarRightTabs).toBeInstanceOf(SidebarRightTabRegistry) expect(ctx.sidebarRight).toBeInstanceOf(SidebarRightController) expect('adopt' in ctx.sidebarRight).toBe(false) - expect(dictionaries.get('sidebarRight')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarRight')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) const guide = ctx.sidebarRightTabs.get('guide') expect(guide?.id).toBe(GUIDE_ID) expect(guide?.priority).toBe('builtin') diff --git a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx index 34429ce6db..82b8d386e0 100644 --- a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx @@ -119,6 +119,12 @@ describe('GuideBody', () => { const guide = b.view.container.querySelector('[data-sidebar-right-guide]') expect(guide?.lastElementChild?.contains(more)).toBe(true) expect(more.getAttribute('aria-haspopup')).toBe('menu') + const glyph = more.querySelector('svg') + expect(glyph?.getAttribute('width')).toBe('22') + expect(glyph?.getAttribute('height')).toBe('22') + expect(glyph?.getAttribute('stroke')).toBe('#A6B2C7') + expect(glyph?.getAttribute('aria-hidden')).toBe('true') + expect(glyph?.querySelectorAll('path')).toHaveLength(4) fireEvent.click(more) expect(more.getAttribute('aria-expanded')).toBe('true') expect(b.actionsOwner().entries).toEqual([ diff --git a/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts b/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts index 1bc8994b94..5c775816fd 100644 --- a/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** Guide card pins persist per `kind:id` and ignore every Files card. */ +/** Guide card pins stay in memory per `kind:id` and ignore every Files card. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { createGuidePins, guideEntryId } from '../src/client/tabs/guide/guide-pins.ts' @@ -11,15 +11,15 @@ describe('guide card pins', () => { expect(guideEntryId({ kind: 'terminal', id: 'shell' })).toBe('terminal:shell') }) - it('persists unpinned card ids and restores them in a new instance', () => { + it('keeps unpinned card ids in memory without restoring them in a new instance', () => { const pins = createGuidePins() const initial = pins.unpinned.getSnapshot() expect(initial).toEqual([]) expect(pins.unpinned.getSnapshot()).toBe(initial) pins.setPinned('terminal:shell', false) pins.setPinned('browser:page', false) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['terminal:shell', 'browser:page'] }) - expect(createGuidePins().unpinned.getSnapshot()).toEqual(['terminal:shell', 'browser:page']) + expect(localStorage.getItem(KEY)).toBeNull() + expect(createGuidePins().unpinned.getSnapshot()).toEqual([]) pins.setPinned('terminal:shell', true) expect(pins.unpinned.getSnapshot()).toEqual(['browser:page']) }) @@ -38,20 +38,14 @@ describe('guide card pins', () => { }) it('keeps ids of cards that are not registered', () => { - localStorage.setItem(KEY, JSON.stringify({ unpinned: ['removed:card'] })) const pins = createGuidePins() + pins.setPinned('removed:card', false) pins.setPinned('terminal:shell', false) expect(pins.unpinned.getSnapshot()).toEqual(['removed:card', 'terminal:shell']) }) - it.each([ - ['a non-array list', { unpinned: 'terminal:shell' }], - ['non-string ids', { unpinned: [false] }], - ['a missing list', {}], - ['null', null], - ['a number', 7], - ])('reads %s as no unpinned cards with a stable snapshot', (_name, stored) => { - localStorage.setItem(KEY, JSON.stringify(stored)) + it('ignores a legacy stored list instead of restoring it', () => { + localStorage.setItem(KEY, JSON.stringify({ unpinned: ['terminal:shell'] })) const pins = createGuidePins() const snapshot = pins.unpinned.getSnapshot() expect(snapshot).toEqual([]) diff --git a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx index facd9870b2..9d20ce5ba2 100644 --- a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx @@ -557,6 +557,33 @@ describe('RightbarSeat presentation', () => { expect(h.frame.closeRightbar).toHaveBeenCalled() }) + it('marks the panel as opening from the opening render until its slide ends', async () => { + const h = await mountSeat() + const panel = element(h.view.container, '[data-sidebar-right-panel]') + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + h.open() + // The tab the opening seeds mounts under the mark, so it starts at the closed position. + const host = element(h.view.container, "[data-dockkit-host='dock']") + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(true) + fireEvent.transitionEnd(host) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + act(() => { h.actions.splitPane(SESSION) }) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + fireEvent.click(element(h.view.container, '[data-sidebar-right-toggle]')) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + act(() => { h.actions.setExpanded(SESSION, true) }) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(true) + // Closing before the slide ends clears the mark. + fireEvent.click(element(h.view.container, '[data-sidebar-right-toggle]')) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + // Leaving the screen cancels the slide, so returning shows the items in place. + act(() => { h.actions.setExpanded(SESSION, true) }) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(true) + act(() => { h.runtime.panelInfo.set({ activePanelId: 'other-panel' as MainPanelId }) }) + act(() => { h.runtime.panelInfo.set({ activePanelId: null }) }) + expect(panel.hasAttribute('data-sidebar-right-opening')).toBe(false) + }) + it('fills the viewport without replacing the content tree or releasing the wide track', async () => { const h = await mountSeat() const tab = h.open() diff --git a/packages/client/ui-sidebar-right/tests/sidebar-right-styles.client.spec.ts b/packages/client/ui-sidebar-right/tests/sidebar-right-styles.client.spec.ts new file mode 100644 index 0000000000..378c72834e --- /dev/null +++ b/packages/client/ui-sidebar-right/tests/sidebar-right-styles.client.spec.ts @@ -0,0 +1,20 @@ +/** Right sidebar slide style contracts. */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const css = readFileSync(fileURLToPath(new URL('../src/client/shell/SidebarRight.module.css', import.meta.url)), 'utf8') + .replace(/\/\*[\s\S]*?\*\//g, ' ') +const host = String.raw`:global\(\[data-dockkit-host='dock'\]\)` + +describe('SidebarRight.module.css', () => { + it('starts docked items mounted by an opening at the closed position', () => { + expect(css).toMatch(new RegExp(String.raw`@starting-style\s*\{\s*\.panel\[data-sidebar-right-opening\] ${host},[^{}]*\{` + + String.raw`\s*transform: translateX\(var\(--dsh-sidebar-width\)\);\s*}\s*}`)) + }) + + it('removes the open slide as well as the close slide for reduced motion', () => { + expect(css).toMatch(new RegExp(String.raw`@media \(prefers-reduced-motion: reduce\)\s*\{[^{}]*` + + String.raw`\.panel\[data-sidebar-right-open\] ${host}[^{}]*\{\s*transition: none;\s*}`)) + }) +}) diff --git a/packages/client/ui-sidebar-terminal/locale/de.json b/packages/client/ui-sidebar-terminal/locale/de.json new file mode 100644 index 0000000000..bd4022653e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anschlussfeld", + "description": "Verwenden Sie das interaktive Befehlsterminal im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/es.json b/packages/client/ui-sidebar-terminal/locale/es.json new file mode 100644 index 0000000000..4e2351855e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel de terminales", + "description": "Utilice la terminal de comando interactiva en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/fr.json b/packages/client/ui-sidebar-terminal/locale/fr.json new file mode 100644 index 0000000000..a6b35dd180 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau à bornes", + "description": "Utilisez le terminal de commande interactif dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/pt.json b/packages/client/ui-sidebar-terminal/locale/pt.json new file mode 100644 index 0000000000..373527231e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel de terminais", + "description": "Use o terminal de comando interativo no painel direito." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/ru.json b/packages/client/ui-sidebar-terminal/locale/ru.json new file mode 100644 index 0000000000..8eaf7dd35e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "терминальная панель", + "description": "Используйте интерактивный командный терминал на правой панели." + } +} diff --git a/packages/client/ui-sidebar-terminal/package.json b/packages/client/ui-sidebar-terminal/package.json index 2b44e6e162..1b666b52fa 100644 --- a/packages/client/ui-sidebar-terminal/package.json +++ b/packages/client/ui-sidebar-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-terminal", "description": "Interactive shell tabs for the right Sidebar", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-terminal/src/client/index.ts b/packages/client/ui-sidebar-terminal/src/client/index.ts index e355b4844c..0390d3f7b2 100644 --- a/packages/client/ui-sidebar-terminal/src/client/index.ts +++ b/packages/client/ui-sidebar-terminal/src/client/index.ts @@ -17,7 +17,7 @@ import { TerminalTitle } from './TerminalTitle.tsx' // import { TerminalRecovery, type TerminalRecoveryInjected } from './TerminalRecovery.tsx' // import { TerminalCleanup, type TerminalCleanupInjected } from './TerminalCleanup.tsx' import type { TerminalBodyInjected, TerminalInjected } from './face.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Services needed by the terminal's two sidebar seats. */ export const inject = ['slots', 'locale', 'sidebarRight', 'sidebarRightTabs', 'webTerminals', 'theme', 'shortcuts'] @@ -67,7 +67,7 @@ export function apply(ctx: Context): void { return { status: 'handled', run: () => { ctx.sidebarRight.openTabFromTarget('terminal', target) } } }, }), 'ui-sidebar-terminal: shortcut') - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'ui-sidebar-terminal.copy') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-terminal.copy') ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'terminal', multiple: true, priority: 'builtin', title: () => t('title'), guide: [{ id: 'new', order: 20, title: () => t('new'), description: () => t('description'), icon: PluginArtworkTerminal }], diff --git a/packages/client/ui-sidebar-terminal/src/client/locales.ts b/packages/client/ui-sidebar-terminal/src/client/locales.ts index 7c822d6216..28059f5876 100644 --- a/packages/client/ui-sidebar-terminal/src/client/locales.ts +++ b/packages/client/ui-sidebar-terminal/src/client/locales.ts @@ -74,3 +74,93 @@ export const ja = { 'invalidOutput': 'ターミナル画面を受信できませんでした。再接続して復元してください。', 'terminalLimit': 'ターミナルの上限に達しました。未使用のターミナルを閉じて再試行してください。終了したターミナルも上限に含まれます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'shortcut.noSession': 'Seleccione una sesión primero', + recoveryFailed: 'Falló la recuperación del terminal: {message}', retryRecovery: 'Reintentar la recuperación del terminal', + shell: 'Elige concha', shellLoading: 'Cargando proyectiles…', shellEmpty: 'No hay conchas disponibles', description: 'Ejecutar comandos en el espacio de trabajo de sesión', + title: 'terminales', new: 'Nueva terminal', loading: 'Entorno de terminal de lectura...', creating: 'Empezando…', + connecting: 'Conectando…', disconnected: 'Desconectado.', reconnect: 'Reconectar', + readonly: 'Esta vista es de sólo lectura.', control: 'toma el control', + closed: 'Terminal cerrada.', exited: 'Proceso cerrado ({code})', failed: 'Error de terminal: {message}', + rename: 'Nombre del terminal', unavailable: 'No disponible', retry: 'Reintentar', + cleanupFailed: 'No se pudo finalizar el terminal “{title}”: {message}', + missingTerminal: 'Esta terminal ya no existe. Abra una nueva terminal.', + inputFull: 'El búfer de entrada está lleno. Vuelve a conectarte y vuelve a intentarlo.', + attachmentEnded: 'La conexión del terminal terminó. Vuelva a conectarse para continuar.', + invalidOutput: 'No se pudo recibir la pantalla del terminal. Vuelva a conectarse para recuperarlo.', + terminalLimit: 'Se ha alcanzado el límite terminal. Cierre los terminales no utilizados y vuelva a intentarlo. Las terminales salidas también cuentan para el límite.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'shortcut.noSession': "Sélectionnez d'abord une session", + recoveryFailed: 'Échec de la récupération du terminal : {message}', retryRecovery: 'Réessayez la récupération du terminal', + shell: 'Choisissez la coque', shellLoading: 'Chargement des obus…', shellEmpty: 'Aucune coquille disponible', description: 'Exécuter des commandes dans l’espace de travail de la session', + title: 'Borne', new: 'Nouvelle borne', loading: 'Environnement du terminal de lecture…', creating: 'Commencer…', + connecting: 'Connexion…', disconnected: 'Déconnecté.', reconnect: 'Reconnecter', + readonly: 'Cette vue est en lecture seule.', control: 'Prenez le contrôle', + closed: 'Borne fermée.', exited: 'Processus terminé ({code})', failed: 'Erreur de terminal : {message}', + rename: 'Nom du terminal', unavailable: 'Indisponible', retry: 'Réessayer', + cleanupFailed: "Le terminal « {title} » n'a pas pu être terminé : {message}", + missingTerminal: "Ce terminal n'existe plus. Ouvrez un nouveau terminal.", + inputFull: "Le tampon d'entrée est plein. Reconnectez-vous et réessayez.", + attachmentEnded: 'La connexion du terminal est terminée. Reconnectez-vous pour continuer.', + invalidOutput: "L'écran du terminal n'a pas pu être reçu. Reconnectez-vous pour le récupérer.", + terminalLimit: 'La limite terminale a été atteinte. Fermez les terminaux inutilisés et réessayez. Les terminaux sortis comptent également dans la limite.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'shortcut.noSession': 'Wählen Sie zunächst eine Sitzung aus', + recoveryFailed: 'Terminalwiederherstellung fehlgeschlagen: {message}', retryRecovery: 'Versuchen Sie die Terminalwiederherstellung erneut', + shell: 'Wählen Sie Shell', shellLoading: 'Granaten werden geladen…', shellEmpty: 'Keine Muscheln verfügbar', description: 'Führen Sie Befehle im Sitzungsarbeitsbereich aus', + title: 'Terminal', new: 'Neues Terminal', loading: 'Terminalumgebung lesen…', creating: 'Beginnend mit …', + connecting: 'Verbinden…', disconnected: 'Nicht verbunden.', reconnect: 'Wieder verbinden', + readonly: 'Diese Ansicht ist schreibgeschützt.', control: 'Übernehmen Sie die Kontrolle', + closed: 'Terminal geschlossen.', exited: 'Prozess beendet ({code})', failed: 'Terminalfehler: {message}', + rename: 'Terminalname', unavailable: 'Nicht verfügbar', retry: 'Erneut versuchen', + cleanupFailed: 'Terminal „{title}“ konnte nicht beendet werden: {message}', + missingTerminal: 'Dieses Terminal existiert nicht mehr. Öffnen Sie ein neues Terminal.', + inputFull: 'Der Eingabepuffer ist voll. Stellen Sie die Verbindung wieder her und versuchen Sie es erneut.', + attachmentEnded: 'Die Terminalverbindung wurde beendet. Stellen Sie die Verbindung wieder her, um fortzufahren.', + invalidOutput: 'Der Terminalbildschirm konnte nicht empfangen werden. Stellen Sie die Verbindung wieder her, um es wiederherzustellen.', + terminalLimit: 'Das Endlimit wurde erreicht. Schließen Sie nicht verwendete Terminals und versuchen Sie es erneut. Auch verlassene Terminals werden auf das Limit angerechnet.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'shortcut.noSession': 'Сначала выберите сеанс', + recoveryFailed: 'Восстановление терминала не удалось: {message}', retryRecovery: 'Повторить попытку восстановления терминала', + shell: 'Выбрать оболочку', shellLoading: 'Загрузка снарядов…', shellEmpty: 'Нет доступных ракушек', description: 'Запуск команд в рабочей области сеанса', + title: 'Терминал', new: 'Новый терминал', loading: 'Чтение среды терминала…', creating: 'Начало…', + connecting: 'Подключение…', disconnected: 'Отключено.', reconnect: 'Восстановить соединение', + readonly: 'Это представление доступно только для чтения.', control: 'Возьмите под свой контроль', + closed: 'Терминал закрыт.', exited: 'Процесс завершен ({code})', failed: 'Ошибка терминала: {message}', + rename: 'Имя терминала', unavailable: 'Недоступно', retry: 'Повторить', + cleanupFailed: 'Терминал «{title}» не удалось завершить: {message}', + missingTerminal: 'Этого терминала больше не существует. Откройте новый терминал.', + inputFull: 'Входной буфер заполнен. Подключитесь и повторите попытку.', + attachmentEnded: 'Терминальное соединение завершилось. Подключитесь повторно, чтобы продолжить.', + invalidOutput: 'Не удалось получить экран терминала. Повторно подключитесь, чтобы восстановить его.', + terminalLimit: 'Достигнут лимит терминала. Закройте неиспользуемые терминалы и повторите попытку. Вышедшие терминалы также учитываются в лимите.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'shortcut.noSession': 'Selecione uma sessão primeiro', + recoveryFailed: 'Falha na recuperação do terminal: {message}', retryRecovery: 'Tentar novamente a recuperação do terminal', + shell: 'Escolha a concha', shellLoading: 'Carregando conchas…', shellEmpty: 'Não há conchas disponíveis', description: 'Execute comandos no espaço de trabalho Sessão', + title: 'Terminais', new: 'Novo terminal', loading: 'Lendo o ambiente do terminal…', creating: 'Começando…', + connecting: 'Conectando…', disconnected: 'Desconectado.', reconnect: 'Reconectar', + readonly: 'Esta visualização é somente leitura.', control: 'Assuma o controle', + closed: 'Terminal fechado.', exited: 'Processo encerrado ({code})', failed: 'Erro de terminal: {message}', + rename: 'Nome do terminal', unavailable: 'Indisponível', retry: 'Tentar novamente', + cleanupFailed: 'O terminal “{title}” não pôde ser finalizado: {message}', + missingTerminal: 'Este terminal não existe mais. Abra um novo terminal.', + inputFull: 'O buffer de entrada está cheio. Reconecte e tente novamente.', + attachmentEnded: 'A conexão do terminal terminou. Reconecte para continuar.', + invalidOutput: 'A tela do terminal não pôde ser recebida. Reconecte para recuperá-lo.', + terminalLimit: 'O limite terminal foi atingido. Feche os terminais não utilizados e tente novamente. Os terminais encerrados também contam para o limite.', +} satisfies Record diff --git a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts index 9161768ab2..618ad175cb 100644 --- a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts @@ -18,7 +18,7 @@ import { TerminalGuide, type TerminalGuideInjected } from '../src/client/Termina import { LazyTerminalBody } from '../src/client/LazyTerminalBody.tsx' import { TerminalTitle } from '../src/client/TerminalTitle.tsx' import type { TerminalBodyInjected } from '../src/client/face.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' vi.mock('@xterm/xterm', () => ({ Terminal: vi.fn() })) const SHORTCUT_CATALOG: readonly never[] = [] @@ -97,7 +97,7 @@ it('registers terminal views without recovery or cleanup slots, then releases co expect(renderToStaticMarkup(createElement(Icon, { size: 22 }))).toContain('width="22"') expect(renderToStaticMarkup(createElement(Icon))).toContain('width="36"') expect(definition.multiple).toBe(true) - expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh, ja }) + expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh, ja, es, fr, de, ru, pt }) expect(h.entries.map(entry => [entry.name, entry.component, entry.locale])).toEqual([ ['sidebar.right.tab.guide.entry', TerminalGuide, 'sidebarTerminal'], ['sidebar.right.pane.tab', LazyTerminalBody, 'sidebarTerminal'], diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index 8bf7b111e1..9f24479203 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/README.i18n.yaml @@ -18,11 +18,11 @@ en: a970f0547496634f zh: 36ef9a85b26e19f1 /deepseek-ai-dsh-client-ui-sidebar/use-this-package/brand-and-new-session: - en: f2c2254325cea5db - zh: c9ef643b431f8328 + en: 44db2d448bc2ea6d + zh: cbb1143afb524d5b /deepseek-ai-dsh-client-ui-sidebar/use-this-package/global-panel-entries: - en: b109498d88367117 - zh: 6d407f265bacdb0d + en: e7f316023445511b + zh: 6271c8c011533877 /deepseek-ai-dsh-client-ui-sidebar/use-this-package/collapse-behavior: en: 20e744360b4be2cc zh: a90be7e8526bbb21 @@ -33,11 +33,11 @@ en: 9e7b31acee759f2b zh: 3a6beb63b23d26ae /deepseek-ai-dsh-client-ui-sidebar/use-this-package/click-analytics: - en: bb3f4a199ed49039 - zh: cc29413fd31fe2e7 + en: 8855ad013285a27e + zh: f19b3e75c38c5b0a /deepseek-ai-dsh-client-ui-sidebar/understand-the-implementation: - en: bc51c446d8b8e098 - zh: c0c7bbfb50541d33 + en: 39b5e3ca8ce8c5c2 + zh: 1d652b7554772dad /deepseek-ai-dsh-client-ui-sidebar/understand-the-implementation/slot-discipline: en: 6eefc5c45abe8a8f zh: 20b651e48a0ea903 diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 049673a2ce..5a21b27987 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -31,13 +31,13 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co ### Brand and New Session -The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. Clicking the fallback mark or label of such a build copies that badge text instead of starting a session, also on macOS desktop, and a successful write shows a Version copied toast. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on keyboard focus regardless of hover capability, and on hover when the device supports it. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`. +The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. Clicking the fallback mark or label of such a build copies that badge text instead of starting a session, also on macOS desktop, and a successful write shows a Version copied toast. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on keyboard focus regardless of hover capability, and on hover when the device supports it. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`. While the global access snapshot excludes `session/create`, the brand row stays inert and neither the New Session button nor the window-chrome New Session control renders; the Host still decides every create request. ### Global panel entries Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot; selecting a missing main entry throws without changing the current selection. The label supplies plain visible text, the accessible name, and the collapsed tooltip. Expanded navigation rows have a 2px gap. Each row reads its own selected state through `usePanelInfo`; moving DOM focus to search or a directory picker does not change the displayed panel or its selected row. With no panel registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel. -While the root-scoped `sidebar.panellist.actions` list has registrations, a **More** button styled as a panel row follows the rows inside the navigation landmark; the collapsed rail shows its label as a tooltip. It opens a menu whose rows are that list's registrations. Each receives `entries` (customizable panels in registration order with `id`, `label`, and `pinned`; Plugins is never listed), `onOpen(id)`, which selects the panel, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The sidebar keeps one root list of unpinned panel ids in browser `localStorage` under `dsh.slot.manual-order.sidebar.panellist.v1`, stored as `{ "unpinned": string[] }` and shared by every Session; the rows omit unpinned panels except the selected one, which keeps its row and `aria-current` until another panel is selected, and Plugins always shows. Unpinning hides only the row; the main panel, its registration, and the current selection stay unchanged. Ids of panels that are not registered stay stored, so a panel that registers again stays unpinned; stored content other than a string array shows every row. +While the root-scoped `sidebar.panellist.actions` list has registrations, a **More** button styled as a panel row follows the rows inside the navigation landmark; the collapsed rail shows its label as a tooltip. It opens a menu whose rows are that list's registrations. Each receives `entries` (customizable panels in registration order with `id`, `label`, `pinned`, and `active`; Plugins is never listed), `onOpen(id)`, which selects the panel, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The sidebar keeps one in-memory list of unpinned panel ids shared by every Session; Plugins always shows. The default More menu only offers plugin browsing and creation, without panel rows or pin controls. Pin callbacks remain available to other registrations, but choices reset when the plugin reloads and legacy localStorage values are ignored. Unpinning hides only the row; the main panel, its registration, and the current selection stay unchanged. Unregistered panel ids remain in the in-memory list until the plugin reloads. ### Collapse behavior @@ -64,7 +64,7 @@ On Desktop, the official navigation controls report `sidebar_menu_click` with `m - The expanded brand and the New Session button report `menu_name=new_session` with `position=sidebar`. While the Windows caption hosts the collapsed New Session button, that button reports `position=titlebar`; the macOS New Session control beside the traffic lights also reports `position=titlebar`. - The **Plugins** and **Automation tasks** rows report `menu_name=plugin` and `menu_name=cron` with `position=sidebar`. Other panel rows report nothing. - Each **More** button click reports `menu_name=more` with `position=sidebar`, whether it opens or closes the menu. -- A More menu row that opens Plugins or Automation tasks through `onOpen(id)` reports `menu_name=plugin` or `menu_name=cron` with `position=sidebar_more`. The report precedes the registration check, so a request for a panel that has unregistered still counts although nothing is selected. +- A third-party More menu row that calls `onOpen(id)` for Plugins or Automation tasks reports `menu_name=plugin` or `menu_name=cron` with `position=sidebar_more`. The shipped More menu only browses and creates plugins, so it never calls `onOpen`. The report precedes the registration check, so a request for a panel that has unregistered still counts although nothing is selected. ----- @@ -74,7 +74,7 @@ On Desktop, the official navigation controls report `sidebar_menu_click` with `m
Implementation internals — click to expand -The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. Panel entries and their optional titles use the same composition path. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store. The injected `trackEntryClick(attributes)` forwards to `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`, and `selectPanel` only selects, so each control supplies its own entry and position. +The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. The sidebar owns `sidebar.panellist` and renders each panel's registered icon directly. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store. The injected `trackEntryClick(attributes)` forwards to `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`, and `selectPanel` only selects, so each control supplies its own entry and position. ### Slot discipline diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index c9acb9aa78..08eb1f1648 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -31,13 +31,13 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将 ### 品牌与 New Session -展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。此类构建中点击回退的标记或标签会复制该徽标文本而非新建会话,macOS 桌面端同样适用;写入成功后弹出“版本号复制成功”toast。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在键盘聚焦时于右侧以灰色文字显示当前有效绑定,不受设备悬停能力限制;支持悬停的设备也会在悬停时显示。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。 +展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。此类构建中点击回退的标记或标签会复制该徽标文本而非新建会话,macOS 桌面端同样适用;写入成功后弹出“版本号复制成功”toast。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在键盘聚焦时于右侧以灰色文字显示当前有效绑定,不受设备悬停能力限制;支持悬停的设备也会在悬停时显示。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。全局访问快照不含 `session/create` 时,品牌行不可点击,New Session 按钮和窗口标题栏中的 New Session 控件都不渲染;每个新建请求仍由 Host 判定。 ### 全局面板入口 插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或 locale-aware 的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件;选择不存在的主面板条目会抛错,并保留当前选中态。标签提供普通可见文字、无障碍名称和折叠提示。展开态导航行的间距为 2px。每一行通过 `usePanelInfo` 读取自己的选中态;DOM 焦点移到搜索框或目录选择器时,显示的面板及其列表项选中态不变。没有面板注册项时,列表及其间距均不渲染。产品随附的组合不注册示例面板。 -root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区域的面板行之后显示面板行样式的**更多**按钮;收起的窄栏以提示框显示其名称。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可自定义面板,含 `id`、`label` 与 `pinned`;不含插件管理)、选中面板的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。侧边栏在浏览器 `localStorage` 的 `dsh.slot.manual-order.sidebar.panellist.v1` 下保存一份 root 级的已收起面板 id 列表,格式为 `{ "unpinned": string[] }`,所有会话共用;面板行不显示已收起的面板,但当前选中的面板保留其行和 `aria-current`,直到选中其他面板;插件管理入口始终显示。收起只隐藏导航行,主面板、其注册和当前选中态不变。未注册面板的 id 保留在列表中,因此重新注册的面板保持收起;保存内容不是字符串数组时,所有行都显示。 +root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区域的面板行之后显示面板行样式的**更多**按钮;收起的窄栏以提示框显示其名称。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可自定义面板,含 `id`、`label`、`pinned` 与 `active`;不含插件管理)、选中面板的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。侧边栏在内存中保存所有会话共用的已收起面板 id 列表;插件管理入口始终显示。默认更多菜单只提供插件浏览和创建,不列出面板或 Pin 控件。其他注册项仍可调用 Pin 回调,但插件重新加载时选择重置,不读取旧 localStorage 值。收起只隐藏导航行,主面板、其注册和当前选中态不变。未注册面板的 id 保留在内存列表中,直到插件重新加载。 ### 折叠行为 @@ -64,7 +64,7 @@ root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区 - 展开态品牌与新建会话按钮上报 `menu_name=new_session`、`position=sidebar`。Windows 顶栏承载收起态新建会话按钮时,该按钮上报 `position=titlebar`;macOS 红绿灯旁的新建会话控件同样上报 `position=titlebar`。 - **插件**与**自动化任务**面板行分别上报 `menu_name=plugin` 与 `menu_name=cron`,`position=sidebar`。其他面板行不上报。 - 每次点击**更多**按钮都上报 `menu_name=more`、`position=sidebar`,无论打开还是关闭菜单。 -- 更多菜单中的行通过 `onOpen(id)` 打开插件或自动化任务时,上报 `menu_name=plugin` 或 `menu_name=cron`,`position=sidebar_more`。上报先于注册检查,因此所请求的面板已注销时仍会计数,但不会选中任何面板。 +- 第三方注册的更多菜单行通过 `onOpen(id)` 打开插件或自动化任务时,上报 `menu_name=plugin` 或 `menu_name=cron`,`position=sidebar_more`。随产品提供的更多菜单只浏览和创建插件,不会调用 `onOpen`。上报先于注册检查,因此所请求的面板已注销时仍会计数,但不会选中任何面板。 ----- @@ -74,7 +74,7 @@ root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区
实现细节——点击展开 -外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。面板入口及其可选标题使用相同的组合方式。面板元数据由列表注册和 locale 变化派生;选中态属于布局存储。注入的 `trackEntryClick(attributes)` 转发到 `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`;`selectPanel` 只负责选中,因此由各控件自行提供入口与位置。 +外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。侧边栏声明 `sidebar.panellist`,并直接渲染各面板注册的图标。面板元数据由列表注册和 locale 变化派生;选中态属于布局存储。注入的 `trackEntryClick(attributes)` 转发到 `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`;`selectPanel` 只负责选中,因此由各控件自行提供入口与位置。 ### slot 纪律 diff --git a/packages/client/ui-sidebar/locale/de.json b/packages/client/ui-sidebar/locale/de.json new file mode 100644 index 0000000000..b48c3793d0 --- /dev/null +++ b/packages/client/ui-sidebar/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Konversationsseitenleiste", + "description": "Konversationsliste anzeigen, Such-, Gruppierungs- und Statusaufforderungen bereitstellen." + } +} diff --git a/packages/client/ui-sidebar/locale/es.json b/packages/client/ui-sidebar/locale/es.json new file mode 100644 index 0000000000..48de49b659 --- /dev/null +++ b/packages/client/ui-sidebar/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barra lateral de conversación", + "description": "Muestre la lista de conversaciones, proporcione indicaciones de búsqueda, agrupación y estado." + } +} diff --git a/packages/client/ui-sidebar/locale/fr.json b/packages/client/ui-sidebar/locale/fr.json new file mode 100644 index 0000000000..0dd77b45eb --- /dev/null +++ b/packages/client/ui-sidebar/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barre latérale des conversations", + "description": "Affichez la liste des conversations, fournissez des invites de recherche, de regroupement et d’état." + } +} diff --git a/packages/client/ui-sidebar/locale/pt.json b/packages/client/ui-sidebar/locale/pt.json new file mode 100644 index 0000000000..dafc152209 --- /dev/null +++ b/packages/client/ui-sidebar/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barra lateral de conversa", + "description": "Exibir lista de conversas, fornecer prompts de pesquisa, agrupamento e status." + } +} diff --git a/packages/client/ui-sidebar/locale/ru.json b/packages/client/ui-sidebar/locale/ru.json new file mode 100644 index 0000000000..a9df979adc --- /dev/null +++ b/packages/client/ui-sidebar/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Боковая панель беседы", + "description": "Отобразить список разговоров, предоставить поиск, группировку и подсказки о статусе." + } +} diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index 9277d4aa87..527adba101 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx index 3bb05369cb..618bdd26e7 100644 --- a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx +++ b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx @@ -20,10 +20,14 @@ export type HeaderLeadingControlsProps = * controls off screen; this occupant puts them back beside the traffic * lights. The frame mounts the seat only in that state and owns its * placement, so the occupant renders unconditionally. - * @param props - Injected sidebar actions plus the sidebar locale seat. - * @returns the two window-chrome controls. + * @param props - Injected sidebar actions, the global access Hook, and the sidebar locale seat. + * @returns the sidebar toggle, plus the New Session control while the access snapshot allows `session/create`. */ -export function HeaderLeadingControls({ toggleSidebar, startSession, trackEntryClick, useShortcuts, t }: HeaderLeadingControlsProps) { +export function HeaderLeadingControls({ + toggleSidebar, startSession, trackEntryClick, useRemoteAccess, useShortcuts, t, +}: HeaderLeadingControlsProps) { + // A page whose access snapshot excludes starting a Session keeps only the sidebar toggle here. + const canStartSession = useRemoteAccess('session/create') const shortcut = useShortcuts(rows => rows.find(row => row.id === 'sidebar.left.toggle')) const newShortcut = useShortcuts(rows => rows.find(row => row.id === 'session.new')) return ( @@ -39,20 +43,22 @@ export function HeaderLeadingControls({ toggleSidebar, startSession, trackEntryC - - - + {canStartSession && ( + + + + )}
) } diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index cf8bcc1167..c4083d223d 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -135,6 +135,7 @@ export function SidebarRoot({ selectPanel, setPinned, trackEntryClick, + useRemoteAccess, usePanels, useEntryPins, useEntryActions, @@ -149,6 +150,8 @@ export function SidebarRoot({ const hasEntryActions = useEntryActions(registered => registered) const shortcut = useShortcuts(rows => rows.find(row => row.id === 'sidebar.left.toggle')) const newShortcut = useShortcuts(rows => rows.find(row => row.id === 'session.new')) + // A page whose access snapshot excludes starting a Session offers no New Session affordance. + const canStartSession = useRemoteAccess('session/create') const toggleLabel = collapsed ? t('toggle.open') : t('toggle.collapse') // Wide content stays mounted while the collapse animates (fading via // .collapsed .wide), unmounts at settle, and remounts right away on expand. @@ -278,7 +281,8 @@ export function SidebarRoot({ traffic lights and keeps the toggle at the sidebar's top-right. */} {darwinDesktop &&
{toggle}
}
- {/* Expanded, the brand doubles as a New Session shortcut — except on + {/* Expanded, the brand doubles as a New Session shortcut while the + access snapshot allows `session/create` — except on macOS, where it stays part of the logo row's window-drag surface (a button would subtract itself through the global no-drag rule); the collapsed rail's logo is the expand toggle below instead. @@ -307,7 +311,7 @@ export function SidebarRoot({ ) - return darwinDesktop + return darwinDesktop || !canStartSession ? {identity} : ( - + {canStartSession && ( + + + + )} {panels.length > 0 && (
@@ -599,13 +572,10 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo > { const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() expect(b.slots.entries('sidebar')).toHaveLength(0) - const disposeRoot = b.slots.register({ + expect(b.slots.spec('sidebar.panellist')).toBeUndefined() + const declareSidebar = () => b.slots.register({ name: 'root', children: { sidebar: { kind: 'single', scope: 'root' } }, }, SidebarFrame) + const disposeRoot = declareSidebar() expect(b.slots.entries('sidebar')).toHaveLength(1) + expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' }) disposeRoot() + expect(b.slots.entries('sidebar')).toEqual([]) + expect(b.slots.spec('sidebar.panellist')).toBeUndefined() + const disposeReplacement = declareSidebar() + expect(b.slots.entries('sidebar')).toHaveLength(1) + expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' }) await fiber.dispose() + expect(b.slots.spec('sidebar.panellist')).toBeUndefined() + disposeReplacement() }) it('projects plugin-owned panel entries and removes them with their owner', async () => { diff --git a/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx b/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx index f4bda913fa..68e15ed3f5 100644 --- a/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/panel-list.client.spec.tsx @@ -91,7 +91,8 @@ function ProbeActions({ entries, onOpen, onPinnedChange, onDismiss }: PropsRunti <> {entries.map(entry => ( - + @@ -323,21 +324,28 @@ describe('sidebar global panels', () => { expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() act(() => { locale.setLocale('zh') }) expect(view.queryByRole('navigation')).toBeNull() + await runtime.mount({ inject: [...inject], apply }) + const restored = await view.findByRole('button', { name: '甲面板' }) + expect(restored.getAttribute('aria-current')).toBe('page') + const icon = within(restored).getByTestId(`${ALPHA}-icon`) + expect(icon.getAttribute('data-active')).toBe('true') + expect(icon.querySelector('svg')?.getAttribute('width')).toBe('16') + expect(runtime.slots.entries('sidebar.panellist').map(entry => entry.options.id)).toEqual([BETA, ALPHA]) }) }) describe('sidebar launcher pins', () => { const PINS_KEY = 'dsh.slot.manual-order.sidebar.panellist.v1' - it('hides stored unpinned panels, keeps Plugins, and keeps the list while panels are registered', async () => { + it('ignores stored unpinned panels and shows all registered launchers', async () => { localStorage.setItem(PINS_KEY, JSON.stringify({ unpinned: [ALPHA, BETA, PLUGINS, 'removed-plugin'] })) const { runtime, locale, view } = await bench() await mountPanels(runtime, locale) const navigation = await view.findByRole('navigation', { name: 'Global panels' }) - expect(within(navigation).queryAllByRole('button')).toEqual([]) + expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual(['Beta panel', 'Alpha panel']) await mountPanel(runtime, { id: PLUGINS, heading: 'Plugins content', label: 'Plugins', order: 0 }) await waitFor(() => { - expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual(['Plugins']) + expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual(['Plugins', 'Beta panel', 'Alpha panel']) }) expect(runtime.slots.entries('main').map(entry => entry.options.key)).toEqual(['conversation', ALPHA, BETA, PLUGINS]) }) @@ -377,16 +385,25 @@ describe('sidebar launcher pins', () => { .toEqual(['Open Beta panel', 'Unpin Beta panel', 'Open Alpha panel', 'Unpin Alpha panel', 'Dismiss']) fireEvent.click(menu.getByRole('menuitem', { name: 'Unpin Alpha panel' })) - expect(JSON.parse(localStorage.getItem(PINS_KEY) ?? 'null')).toEqual({ unpinned: [ALPHA] }) + expect(localStorage.getItem(PINS_KEY)).toBeNull() await waitFor(() => { expect(within(navigation).queryByRole('button', { name: 'Alpha panel' })).toBeNull() }) expect(menu.getByRole('menuitem', { name: 'Pin Alpha panel' })).toBeTruthy() fireEvent.click(menu.getByRole('menuitem', { name: 'Open Alpha panel' })) expect(layout.selectPanel).toHaveBeenLastCalledWith(ALPHA) expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() - // The open panel keeps its unpinned row as the current page until another panel is selected. + await waitFor(() => { expect(within(navigation).queryByRole('button', { name: 'Alpha panel' })).toBeNull() }) + fireEvent.click(menu.getByRole('menuitem', { name: 'Pin Alpha panel' })) const alphaRow = await within(navigation).findByRole('button', { name: 'Alpha panel' }) expect(alphaRow.getAttribute('aria-current')).toBe('page') + fireEvent.click(menu.getByRole('menuitem', { name: 'Unpin Alpha panel' })) + await waitFor(() => { expect(within(navigation).queryByRole('button', { name: 'Alpha panel' })).toBeNull() }) + expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy() + expect(menu.getByRole('menuitem', { name: 'Open Alpha panel' }).getAttribute('aria-current')).toBe('page') act(() => { layout.selectPanel(BETA) }) + await waitFor(() => { + expect(menu.getByRole('menuitem', { name: 'Open Alpha panel' }).hasAttribute('aria-current')).toBe(false) + expect(menu.getByRole('menuitem', { name: 'Open Beta panel' }).getAttribute('aria-current')).toBe('page') + }) await waitFor(() => { expect(within(navigation).queryByRole('button', { name: 'Alpha panel' })).toBeNull() }) fireEvent.click(menu.getByRole('menuitem', { name: 'Pin Alpha panel' })) diff --git a/packages/client/ui-sidebar/tests/panel-pins.client.spec.ts b/packages/client/ui-sidebar/tests/panel-pins.client.spec.ts index a870fefed0..245a2a4b34 100644 --- a/packages/client/ui-sidebar/tests/panel-pins.client.spec.ts +++ b/packages/client/ui-sidebar/tests/panel-pins.client.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** Sidebar launcher pins persist per panel id and ignore the fixed Plugins panel. */ +/** Sidebar launcher pins stay in memory per panel id and ignore the fixed Plugins panel. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { createPanelPins } from '../src/client/panel-pins.ts' @@ -7,7 +7,7 @@ const KEY = 'dsh.slot.manual-order.sidebar.panellist.v1' afterEach(() => { localStorage.clear(); vi.restoreAllMocks() }) describe('sidebar panel pins', () => { - it('persists unpinned panel ids and restores them in a new instance', () => { + it('keeps unpinned panel ids in memory without restoring them in a new instance', () => { const pins = createPanelPins() const listener = vi.fn() pins.unpinned.subscribe(listener) @@ -17,12 +17,12 @@ describe('sidebar panel pins', () => { pins.setPinned('notes', false) pins.setPinned('tasks', false) expect(pins.unpinned.getSnapshot()).toEqual(['notes', 'tasks']) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['notes', 'tasks'] }) + expect(localStorage.getItem(KEY)).toBeNull() expect(listener).toHaveBeenCalledTimes(2) - expect(createPanelPins().unpinned.getSnapshot()).toEqual(['notes', 'tasks']) + expect(createPanelPins().unpinned.getSnapshot()).toEqual([]) pins.setPinned('notes', true) expect(pins.unpinned.getSnapshot()).toEqual(['tasks']) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['tasks'] }) + expect(localStorage.getItem(KEY)).toBeNull() }) it('ignores Plugins and repeated choices without republishing', () => { @@ -39,20 +39,14 @@ describe('sidebar panel pins', () => { }) it('keeps ids of panels that are not registered', () => { - localStorage.setItem(KEY, JSON.stringify({ unpinned: ['removed-plugin'] })) const pins = createPanelPins() + pins.setPinned('removed-plugin', false) pins.setPinned('notes', false) expect(pins.unpinned.getSnapshot()).toEqual(['removed-plugin', 'notes']) }) - it.each([ - ['a non-array list', { unpinned: 'notes' }], - ['non-string ids', { unpinned: ['notes', 1] }], - ['a missing list', { order: ['notes'] }], - ['null', null], - ['a string', 'notes'], - ])('reads %s as no unpinned panels with a stable snapshot', (_name, stored) => { - localStorage.setItem(KEY, JSON.stringify(stored)) + it('ignores a legacy stored list instead of restoring it', () => { + localStorage.setItem(KEY, JSON.stringify({ unpinned: ['notes'] })) const pins = createPanelPins() const snapshot = pins.unpinned.getSnapshot() expect(snapshot).toEqual([]) diff --git a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx index 8b3b77265b..4dedd711db 100644 --- a/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx @@ -157,6 +157,37 @@ describe('SidebarRoot shell', () => { expect(b.trackEntryClick).toHaveBeenCalledExactlyOnceWith({ menu_name: 'new_session', position: 'sidebar' }) }) + it('offers no New Session control while the access snapshot excludes session/create', () => { + const toggleSidebar = vi.fn() + const startSession = vi.fn() + const useRemoteAccess = vi.fn((action: string) => action !== 'session/create') + render( {}} usePanels={selector => selector([])} useEntryPins={selector => selector([])} + useEntryActions={selector => selector(false)} setPinned={vi.fn()} + useShortcuts={selector => selector([])} + useResource={useResource} useWorkspaces={neverHook} + {...allowAllAccess} + useRemoteAccess={useRemoteAccess} + startSession={startSession} toggleSidebar={toggleSidebar} trackEntryClick={vi.fn()} t={t} + renderSlot={((key: string) => { + if (key === 'sidebar.brand.mark') return M + if (key === 'sidebar.brand.name') return Brand + return null + }) as SidebarRootComponentProps['renderSlot']} + />) + + // Neither the brand row nor the dedicated button starts a Session; the brand still renders. + expect(useRemoteAccess).toHaveBeenCalledWith('session/create') + expect(screen.queryByRole('button', { name: 'New session' })).toBeNull() + expect(screen.getByTestId('brand-mark')).toBeTruthy() + expect(screen.getByTestId('brand-name')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' })) + expect(toggleSidebar).toHaveBeenCalledOnce() + expect(startSession).not.toHaveBeenCalled() + }) + it('renders generic brand fallbacks when no package fills the slots', () => { vi.stubEnv('DSH_CLIENT_COMMIT_HASH', '0123456') vi.stubEnv('DSH_CLIENT_GIT_DIRTY', 'true') @@ -359,8 +390,8 @@ it('wires the shell.leading controls to the shared sidebar actions', () => { const toggleSidebar = vi.fn() const startSession = vi.fn() const trackEntryClick = vi.fn() - // This occupant only consumes its actions and locale, not Session hooks. - const props = { toggleSidebar, startSession, trackEntryClick, t, useShortcuts: select => select([{ id: 'sidebar.left.toggle' as ShortcutCommandId, label: 'Toggle sidebar', aliases: [], binding: null, modified: false, conflicts: [], issue: null, keys: ['⌘', 'B'], aria: 'Meta+B' }]) } as HeaderLeadingControlsProps + // This occupant only consumes its actions, the global access Hook, and locale, not Session hooks. + const props = { toggleSidebar, startSession, trackEntryClick, t, useRemoteAccess: allowAllAccess.useRemoteAccess, useShortcuts: select => select([{ id: 'sidebar.left.toggle' as ShortcutCommandId, label: 'Toggle sidebar', aliases: [], binding: null, modified: false, conflicts: [], issue: null, keys: ['⌘', 'B'], aria: 'Meta+B' }]) } as HeaderLeadingControlsProps render() fireEvent.click(screen.getByRole('button', { name: en['toggle.open'] })) expect(toggleSidebar).toHaveBeenCalledOnce() @@ -370,6 +401,12 @@ it('wires the shell.leading controls to the shared sidebar actions', () => { // The window-chrome New Session control reports a titlebar click before starting the Session. expect(trackEntryClick).toHaveBeenCalledExactlyOnceWith({ menu_name: 'new_session', position: 'titlebar' }) expect(trackEntryClick.mock.invocationCallOrder[0]).toBeLessThan(startSession.mock.invocationCallOrder[0]!) + cleanup() + + // A page whose access snapshot excludes starting a Session keeps only the sidebar toggle. + render( action !== 'session/create'} />) + expect(screen.getByRole('button', { name: en['toggle.open'] })).toBeTruthy() + expect(screen.queryByRole('button', { name: en['session.new.label'] })).toBeNull() }) describe('Windows caption New Session analytics', () => { diff --git a/packages/client/ui-skill/locale/de.json b/packages/client/ui-skill/locale/de.json new file mode 100644 index 0000000000..3c2bcc04bb --- /dev/null +++ b/packages/client/ui-skill/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Referenz und Anzeige der Fertigkeiten", + "description": "Stellt Fertigkeitsreferenzen und Fertigkeitsaufrufanzeigen in Sitzungen bereit." + } +} diff --git a/packages/client/ui-skill/locale/es.json b/packages/client/ui-skill/locale/es.json new file mode 100644 index 0000000000..1e9055f12c --- /dev/null +++ b/packages/client/ui-skill/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Referencia y visualización de habilidades.", + "description": "Proporciona referencias de habilidades y visualizaciones de invocación de habilidades en las sesiones." + } +} diff --git a/packages/client/ui-skill/locale/fr.json b/packages/client/ui-skill/locale/fr.json new file mode 100644 index 0000000000..2404a96e66 --- /dev/null +++ b/packages/client/ui-skill/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Référence et affichage des compétences", + "description": "Fournit des références de compétences et des affichages d’invocation de compétences dans les sessions." + } +} diff --git a/packages/client/ui-skill/locale/pt.json b/packages/client/ui-skill/locale/pt.json new file mode 100644 index 0000000000..e4763a6ba6 --- /dev/null +++ b/packages/client/ui-skill/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Referência e exibição de habilidades", + "description": "Fornece referências de habilidades e exibições de invocação de habilidades nas sessões." + } +} diff --git a/packages/client/ui-skill/locale/ru.json b/packages/client/ui-skill/locale/ru.json new file mode 100644 index 0000000000..68d9539f50 --- /dev/null +++ b/packages/client/ui-skill/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Справочник и отображение навыков", + "description": "Предоставляет ссылки на навыки и отображение вызовов навыков в сеансах." + } +} diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index 1f7fe22db5..afdc304eb1 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/src/client/index.ts b/packages/client/ui-skill/src/client/index.ts index 0356035c26..db9b746dba 100644 --- a/packages/client/ui-skill/src/client/index.ts +++ b/packages/client/ui-skill/src/client/index.ts @@ -52,7 +52,7 @@ import { SkillRow } from './SkillRow.tsx' import { SkillReference, type SkillReferenceInjected } from './SkillReference.tsx' import { skillReferenceRule } from './user-text-rules.ts' export type {} from './user-text-rules.ts' -import { en, ja, NS, zh, type SkillKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type SkillKey } from './locales.ts' declare module '@deepseek-ai/dsh-api-session-controller/client' { interface SessionReferenceSourceMap { @@ -84,7 +84,7 @@ export const inject = ['inputTriggers', 'sessions', 'slots', 'locale', 'remote', * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-skill: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-skill: dictionaries') ctx.uiConversation.userText.register(skillReferenceRule) ctx.slots.inject('tool.call.toolview', () => ctx.slots.register( { name: 'tool.call.toolview', key: 'skill', locale: NS }, diff --git a/packages/client/ui-skill/src/client/locales.ts b/packages/client/ui-skill/src/client/locales.ts index be188303d6..4100aec6f0 100644 --- a/packages/client/ui-skill/src/client/locales.ts +++ b/packages/client/ui-skill/src/client/locales.ts @@ -41,3 +41,63 @@ export const ja = { 'row.inspect': '詳細を確認', 'menu.userOnly': 'ユーザー専用', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'row.title': 'Habilidad', + 'row.running': 'Habilidad de carga', + 'row.preparing': 'Preparándose para cargar una habilidad', + 'row.failed': 'La carga de habilidades falló', + 'row.stopped': 'Carga de habilidades detenida', + 'row.instructions': 'Instrucciones', + 'row.inspect': 'inspeccionar', + 'menu.userOnly': 'solo usuario', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'row.title': 'Compétence', + 'row.running': 'Compétence de chargement', + 'row.preparing': 'Préparation du chargement d’une compétence', + 'row.failed': 'Échec du chargement des compétences', + 'row.stopped': 'Chargement de compétences arrêté', + 'row.instructions': 'Instructions', + 'row.inspect': 'Inspecter', + 'menu.userOnly': 'utilisateur uniquement', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'row.title': 'Geschicklichkeit', + 'row.running': 'Ladefähigkeit', + 'row.preparing': 'Das Laden eines Skills wird vorbereitet', + 'row.failed': 'Das Laden des Skills ist fehlgeschlagen', + 'row.stopped': 'Das Laden der Fertigkeiten wurde gestoppt', + 'row.instructions': 'Anweisungen', + 'row.inspect': 'Inspizieren', + 'menu.userOnly': 'Nur für Benutzer', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'row.title': 'Навык', + 'row.running': 'Навык загрузки', + 'row.preparing': 'Подготовка к загрузке навыка', + 'row.failed': 'Не удалось загрузить навык', + 'row.stopped': 'Загрузка навыков остановлена', + 'row.instructions': 'Инструкции', + 'row.inspect': 'Осмотреть', + 'menu.userOnly': 'только для пользователей', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'row.title': 'Habilidade', + 'row.running': 'Habilidade de carregamento', + 'row.preparing': 'Preparando-se para carregar uma habilidade', + 'row.failed': 'Falha no carregamento da habilidade', + 'row.stopped': 'Carga de habilidade interrompida', + 'row.instructions': 'Instruções', + 'row.inspect': 'Inspecionar', + 'menu.userOnly': 'somente usuário', +} satisfies Record diff --git a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts index 8d589aecca..b160c954ad 100644 --- a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts @@ -1,3 +1,4 @@ +import { es, fr, de, ru, pt } from '../src/client/locales.ts' /** * ui-skill browser half: source and keyed toolview registration + * locale dictionaries + source duplicate-name proof + @@ -141,6 +142,7 @@ describe('apply', () => { expect(entry?.component).toBe(SkillToolRow) expect(presentation.dictionaries).toEqual([{ namespace: 'skill', dictionaries: { + es, fr, de, ru, pt, zh: { 'row.title': '加载技能', 'row.running': '正在加载 skill', diff --git a/packages/client/ui-skill/tests/skill-row.client.spec.tsx b/packages/client/ui-skill/tests/skill-row.client.spec.tsx index 81d043f13d..29b95aaa3a 100644 --- a/packages/client/ui-skill/tests/skill-row.client.spec.tsx +++ b/packages/client/ui-skill/tests/skill-row.client.spec.tsx @@ -102,7 +102,7 @@ describe('SkillRow', () => { expect(row.getAttribute('role')).toBeNull() expect(view.container.textContent).toContain('正在加载 skill') expect(view.container.textContent).toContain('dsh-manage-issues') - expect(view.container.querySelector('svg [fill="currentColor"]')).not.toBeNull() + expect(view.container.querySelector('svg[data-icon="dsw-icon-skill-outline-regular"]')).not.toBeNull() }) it('uses the first failure line in the summary and exposes the full error', () => { diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index 0765810448..ee1df6a92d 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/locale/de.json b/packages/client/ui-subagent/locale/de.json new file mode 100644 index 0000000000..5d03994ab8 --- /dev/null +++ b/packages/client/ui-subagent/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Subagenten-Konversationsschnittstelle", + "description": "Sehen Sie sich Unterhaltungen untergeordneter Agenten an und springen Sie zu ihnen, mit Unterstützung für fortgesetzte Unterhaltungen und @quotes." + } +} diff --git a/packages/client/ui-subagent/locale/es.json b/packages/client/ui-subagent/locale/es.json new file mode 100644 index 0000000000..85dc3ba25f --- /dev/null +++ b/packages/client/ui-subagent/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz conversacional de subagente", + "description": "Vea y salte a conversaciones de agentes secundarios, con soporte para conversaciones continuas y @quotes." + } +} diff --git a/packages/client/ui-subagent/locale/fr.json b/packages/client/ui-subagent/locale/fr.json new file mode 100644 index 0000000000..353bd27b22 --- /dev/null +++ b/packages/client/ui-subagent/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface conversationnelle du sous-agent", + "description": "Affichez et accédez aux conversations des agents enfants, avec prise en charge des conversations continues et des @quotes." + } +} diff --git a/packages/client/ui-subagent/locale/pt.json b/packages/client/ui-subagent/locale/pt.json new file mode 100644 index 0000000000..80fbe76051 --- /dev/null +++ b/packages/client/ui-subagent/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de conversação do subagente", + "description": "Visualize e acesse conversas de agentes filhos, com suporte para conversas contínuas e @quotes." + } +} diff --git a/packages/client/ui-subagent/locale/ru.json b/packages/client/ui-subagent/locale/ru.json new file mode 100644 index 0000000000..c76c162aff --- /dev/null +++ b/packages/client/ui-subagent/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Диалоговый интерфейс субагента", + "description": "Просматривайте разговоры дочерних агентов и переходите к ним с поддержкой продолжения разговоров и @quotes." + } +} diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index 80e533e46c..5a51deb500 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css index 94b87097d1..1abef93da0 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css @@ -285,14 +285,19 @@ grid-row: 2; } -.sidebarButton { - display: inline-flex; +.sidebarButton, +.sidebarSpace { + box-sizing: border-box; flex: none; - align-items: center; - justify-content: center; width: 28px; height: 28px; margin: 4px 0; +} + +.sidebarButton { + display: inline-flex; + align-items: center; + justify-content: center; padding: 6px; border: 0; border-radius: var(--dsw-radius-sm); diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx index 6cd38d6d93..e3d87c3267 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx @@ -353,24 +353,26 @@ function CatalogRows({ )} )} - {!isCurrent && !external && ( - - - - )} + {isCurrent || external + ? + : ( + + + + )}
{isExpanded && ( diff --git a/packages/client/ui-subagent/src/client/index.ts b/packages/client/ui-subagent/src/client/index.ts index 51f052672e..117b0b0605 100644 --- a/packages/client/ui-subagent/src/client/index.ts +++ b/packages/client/ui-subagent/src/client/index.ts @@ -14,7 +14,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' -import { en, ja, NS, zh, type SubagentKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type SubagentKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -53,7 +53,7 @@ function selectReadOnlySubagent(owner: ComposerChainProps): SubagentReadOnlyMatc * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-subagent: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-subagent: dictionaries') ctx.inject(['resources', 'sidebarRightTabs'], (scope) => { registerSidebarChat(scope, ctx.locale.bind(NS)) }) diff --git a/packages/client/ui-subagent/src/client/locales.ts b/packages/client/ui-subagent/src/client/locales.ts index 9cd5f6d30e..385572dad2 100644 --- a/packages/client/ui-subagent/src/client/locales.ts +++ b/packages/client/ui-subagent/src/client/locales.ts @@ -140,3 +140,228 @@ export const ja = { 'readonly.oneShot.body': '単発タスクは追加のメッセージを受け付けません。ここで実行記録の全体を確認できます。', 'readonly.body': '親セッションがオフラインです。再度開いてメッセージの送信を続けてください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Duración total activa: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': 'Tok {value}', + 'loading.label': 'Cargando subagentes…', + 'load.error': 'No se pueden cargar subagentes', + 'catalog.migrationRequired': 'Migración requerida', + 'retry': 'Reintentar', + 'mode.external': 'tarea externa', + 'mode.oneShot': 'un solo disparo', + 'mode.continuable': 'continuable', + 'mode.unknown': 'modo desconocido', + 'readonly.unknown.body': 'Lea la sesión secundaria para determinar si se puede continuar.', + 'activity.running': 'corriendo', + 'activity.completed': 'completado', + 'activity.inactive': 'no corriendo', + 'branch.collapse': 'Contraer descendientes de {label}', + 'branch.expand': 'Ampliar descendientes de {label}', + 'count.total.one': 'Subagente {count}', + 'count.total.other': 'Subagentes {count}', + 'count.running.one': 'Subagente {count} ejecutándose', + 'count.running.other': 'Subagentes {count} ejecutándose', + 'switcher.aria': 'Cambiar subagente: {title}', + 'tree.aria': 'Sesiones de subagente', + 'open.sidebar': 'Abrir en la barra lateral', + 'open.sidebar.aria': 'Abra {label} en la barra lateral', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Registro de subagente de una sola vez', + 'readonly.title': 'Este subagente es de solo lectura por ahora', + 'readonly.oneShot.body': 'Las tareas únicas no aceptan seguimientos; revise el registro de ejecución completo aquí.', + 'readonly.body': 'La sesión para padres está fuera de línea; Vuelva a abrirlo para continuar enviando mensajes.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': "Durée totale d'activité : {duration}", + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': 'Jeton {value}', + 'loading.label': 'Chargement des sous-agents…', + 'load.error': 'Impossible de charger les sous-agents', + 'catalog.migrationRequired': 'Migration requise', + 'retry': 'Réessayer', + 'mode.external': 'Tâche externe', + 'mode.oneShot': 'un coup', + 'mode.continuable': 'continu', + 'mode.unknown': 'mode inconnu', + 'readonly.unknown.body': 'Lisez la séance enfant pour déterminer si elle peut être poursuivie.', + 'activity.running': 'courir', + 'activity.completed': 'terminé', + 'activity.inactive': 'ne fonctionne pas', + 'branch.collapse': 'Réduire les descendants de {label}', + 'branch.expand': 'Développer les descendants de {label}', + 'count.total.one': 'Sous-agent {count}', + 'count.total.other': 'Sous-agents {count}', + 'count.running.one': "Sous-agent {count} en cours d'exécution", + 'count.running.other': "Sous-agents {count} en cours d'exécution", + 'switcher.aria': 'Changer de sous-agent : {title}', + 'tree.aria': 'Sessions de sous-agents', + 'open.sidebar': 'Ouvrir dans la barre latérale', + 'open.sidebar.aria': 'Ouvrir {label} dans la barre latérale', + 'sidebar.chat': 'Discussion', + 'readonly.oneShot.title': 'Enregistrement de sous-agent unique', + 'readonly.title': 'Ce sous-agent est actuellement en lecture seule', + 'readonly.oneShot.body': 'Les tâches ponctuelles n’acceptent pas de messages de suivi ; consultez leur journal d’exécution complet ici.', + 'readonly.body': 'La session parente est hors ligne ; rouvrez-la pour continuer à envoyer des messages.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Gesamte aktive Dauer: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value}-Token', + 'loading.label': 'Subagenten werden geladen…', + 'load.error': 'Subagenten können nicht geladen werden', + 'catalog.migrationRequired': 'Migration erforderlich', + 'retry': 'Erneut versuchen', + 'mode.external': 'Externe Aufgabe', + 'mode.oneShot': 'One-Shot', + 'mode.continuable': 'fortsetzbar', + 'mode.unknown': 'Unbekannter Modus', + 'readonly.unknown.body': 'Lesen Sie die untergeordnete Sitzung, um festzustellen, ob sie fortgesetzt werden kann.', + 'activity.running': 'laufen', + 'activity.completed': 'abgeschlossen', + 'activity.inactive': 'läuft nicht', + 'branch.collapse': '{label}-Nachkommen reduzieren', + 'branch.expand': 'Erweitern Sie {label}-Nachkommen', + 'count.total.one': '{count}-Subagent', + 'count.total.other': '{count}-Subagenten', + 'count.running.one': '{count}-Subagent läuft', + 'count.running.other': '{count} Subagenten laufen', + 'switcher.aria': 'Subagent wechseln: {title}', + 'tree.aria': 'Subagentensitzungen', + 'open.sidebar': 'In der Seitenleiste öffnen', + 'open.sidebar.aria': 'Öffnen Sie {label} in der Seitenleiste', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Einmaliger Subagentendatensatz', + 'readonly.title': 'Dieser Subagent ist derzeit schreibgeschützt', + 'readonly.oneShot.body': 'Einmalige Aufgaben akzeptieren keine Folgeaufgaben; Sehen Sie sich hier den vollständigen Ausführungsbericht an.', + 'readonly.body': 'Die übergeordnete Sitzung ist offline. Öffnen Sie es erneut, um weiterhin Nachrichten zu senden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Общая активная продолжительность: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value} ток', + 'loading.label': 'Загрузка субагентов…', + 'load.error': 'Невозможно загрузить субагенты', + 'catalog.migrationRequired': 'Требуется миграция', + 'retry': 'Повторить', + 'mode.external': 'Внешняя задача', + 'mode.oneShot': 'одноразовый', + 'mode.continuable': 'продолжаемый', + 'mode.unknown': 'неизвестный режим', + 'readonly.unknown.body': 'Прочтите дочерний сеанс, чтобы определить, можно ли его продолжить.', + 'activity.running': 'бег', + 'activity.completed': 'завершено', + 'activity.inactive': 'не работает', + 'branch.collapse': 'Свернуть потомков {label}', + 'branch.expand': 'Развернуть потомков {label}', + 'count.total.one': 'Субагент {count}', + 'count.total.other': 'Субагенты {count}', + 'count.running.one': 'Субагент {count} работает', + 'count.running.other': 'Субагенты {count} работают', + 'switcher.aria': 'Переключить субагент: {title}', + 'tree.aria': 'Сеансы субагента', + 'open.sidebar': 'Открыть в боковой панели', + 'open.sidebar.aria': 'Откройте {label} на боковой панели.', + 'sidebar.chat': 'Чат', + 'readonly.oneShot.title': 'Разовая запись субагента', + 'readonly.title': 'Этот субагент пока доступен только для чтения.', + 'readonly.oneShot.body': 'Одноразовые задачи не допускают последующих действий; просмотреть полную запись выполнения можно здесь.', + 'readonly.body': 'Родительский сеанс находится в автономном режиме; откройте его повторно, чтобы продолжить отправку сообщений.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months} mês', + 'duration.monthsDays': '~{months}mês {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}a {months}mês', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Duração total ativa: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value} valor', + 'loading.label': 'Carregando subagentes…', + 'load.error': 'Não foi possível carregar subagentes', + 'catalog.migrationRequired': 'Migração necessária', + 'retry': 'Tentar novamente', + 'mode.external': 'Tarefa externa', + 'mode.oneShot': 'tiro único', + 'mode.continuable': 'continuável', + 'mode.unknown': 'modo desconhecido', + 'readonly.unknown.body': 'Leia a sessão secundária para determinar se ela pode continuar.', + 'activity.running': 'correndo', + 'activity.completed': 'concluído', + 'activity.inactive': 'não está funcionando', + 'branch.collapse': 'Recolher {label} descendentes', + 'branch.expand': 'Expandir {label} descendentes', + 'count.total.one': '{count} subagente', + 'count.total.other': '{count} subagentes', + 'count.running.one': '{count} subagente em execução', + 'count.running.other': '{count} subagentes em execução', + 'switcher.aria': 'Alternar subagente: {title}', + 'tree.aria': 'Sessões de subagente', + 'open.sidebar': 'Abrir na barra lateral', + 'open.sidebar.aria': 'Abra {label} na barra lateral', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Registro de subagente único', + 'readonly.title': 'Este subagente é somente leitura por enquanto', + 'readonly.oneShot.body': 'Tarefas únicas não aceitam acompanhamentos; revise o registro completo da execução aqui.', + 'readonly.body': 'A sessão pai está offline; reabra-o para continuar enviando mensagens.', +} satisfies Record diff --git a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx index 7b1262cc11..56e34dc371 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx @@ -1099,6 +1099,8 @@ describe('SubagentHeaderLineage', () => { const current = screen.getByRole('treeitem', { name: /worker/ }) expect(current.getAttribute('aria-current')).toBe('true') expect(within(current).getByText('worker').className).toContain('currentLabel') + expect(within(current).queryByRole('button', { name: '在侧边栏打开 worker' })).toBeNull() + expect(within(screen.getByRole('treeitem', { name: /reviewer/ })).getByRole('button', { name: '在侧边栏打开 reviewer' })).toBeTruthy() fireEvent.click(screen.getByRole('treeitem', { name: /reviewer/ })) expect(input.openChild).toHaveBeenCalledWith({ parentSessionId: PARENT, diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index 8856253c0b..1b86d7c1dd 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -24,8 +24,8 @@ en: b45d5b48ca799c3a zh: af65514b3759204f /deepseek-ai-dsh-client-ui-theme/use-this-package/registering-a-theme: - en: 2cd32ccaba33b3e9 - zh: 53ea78d19929ea02 + en: 5244fc168d0a03f8 + zh: c7ab12fdf8c6c55b /deepseek-ai-dsh-client-ui-theme/use-this-package/pre-plugin-palette: en: 203523ddd7927c34 zh: 31dd89da78f9ab4f @@ -51,8 +51,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-theme/known-limitations-and-deferred-work: - en: 0d328dbced31a041 - zh: 2951de356ba6bb82 + en: 2388061abb1dd40f + zh: 1a4ad9a8e12472df /deepseek-ai-dsh-client-ui-theme/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index d67099cb8e..2e65e8be04 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -41,7 +41,7 @@ Three rows set the text font (interface and conversation text), the code font (c ### Registering a theme -A composition can register a third-party theme id with alias-token overrides through `ctx.theme`; the override layer folds into the active snapshot's tokens in registration order. Removing one never overwrites the last durable built-in preference. Third-party theme ids remain an in-process extension and do not cross the built-in settings schema. +A composition can register a third-party theme id with alias-token overrides through `ctx.theme`; the override layer folds into the active snapshot's tokens in registration order. A theme can also replace product icons: `ThemeDefinition.icons` maps ui-primitives `ICON_SYMBOLS` ids to complete SVG documents. The plugin keeps one hidden icon sprite at the start of `body` whose `` children follow the active theme's replacements and fall back to the defaults, so a replacement may be multi-color and may paint with `currentColor` or `--dsw-*` variables that follow the palette. Replacement markup is trusted like the registering plugin's code. `register` rejects unknown ids, documents without a well-formed SVG-namespace `` root and `viewBox`, `
{label} - {actions !== undefined && ( + {actions !== undefined && workspaceMenuItems.length > 0 && ( { setMenuOpen(false) }} @@ -297,17 +340,19 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog )} /> )} - - - + {access.create && ( + + + + )}
) @@ -472,17 +517,22 @@ function SessionHoverContent({ node, cwd, now, renderSlot, t }: { * @param props.currentId - selected session id. * @param props.onOpen - open the selected session. * @param props.onUnarchive - unarchive an archived result row. + * @param props.useSessionAccess - the browser root's Session access Hook; the restore control + * appears only while the snapshot allows `workspace/unarchiveSession` for that Session. * @param props.t - Workspace-browser translation seat. * @returns the result row. */ -export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }: { +export function SearchResultItem({ + result, currentId, onOpen, onUnarchive, useSessionAccess, t, +}: { result: SearchResultNode currentId: string | undefined onOpen: (id: SearchResultNode['id']) => void onUnarchive: (id: SearchResultNode['id']) => void t: RowTranslate -}) { +} & Pick) { const selected = result.id === currentId + const canUnarchive = useSessionAccess(result.id, 'workspace/unarchiveSession') const statuses = sessionStatuses(result, t) const primaryStatus = statuses[0] return ( @@ -503,7 +553,7 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }: )} {result.title || t('session.untitled')} - {result.archived && ( + {result.archived && canUnarchive && (
@@ -135,6 +135,7 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define when clock context is a poor fit. They are current package constraints. +- **State and replay cost** — timestamps and the current turn number have fixed size. Zone state grows with distinct reported strings in the latest entered turn, not all messages; each new distinct zone copies that list. Formatters remain discardable process caches. The standard AgentLoop requests its latest entered turn. If a typed pre-step caller requests another turn, the original full-prefix synchronous scan includes later entered messages. Retiring this fallback requires limiting pre-step requests to the latest entered turn and updating callers together. - **Prompt guidance only** — browser-zone context guides natural-language interpretation but does not silently supply another tool's required zone field. - **Mixed turns ask** — if one open turn contains prompts from different browser zones, the model is told to clarify rather than guess which one owns an unqualified time. - **Fallback is not user authority** — the configured or process zone formats the clock when current-turn browser zones are missing or mixed, but the model-facing policy still says to clarify. diff --git a/packages/context/time-context/README.zh.md b/packages/context/time-context/README.zh.md index f392be5216..09afe4cd03 100644 --- a/packages/context/time-context/README.zh.md +++ b/packages/context/time-context/README.zh.md @@ -78,7 +78,7 @@ kind: "package-reference" pre-step 监听器在等待投影注册前安装。激活期间到达的步骤会等待读取句柄;注册失败或所属插件释放会终止请求,等待期间取消不会产生读数。 -需要注入时,插件采样挂钟时间,从开放轮次的 `user-rpc` 消息派生浏览器时区策略,解析显示时区(请求本地或回退),并渲染三行读数。正数间隔调度会扫描原始持久会话事件,查找最新一条归因于插件的消息——包括被压缩(compaction)遮蔽的读数——因此调度无需进程本地缓存也能在恢复后存续。读数记录的是已进入的步骤,不是已完成或已传输的请求;后续准备失败时,该读数可能留在历史中。 +需要注入时,插件采样挂钟时间,确定浏览器时区策略,解析显示时区(请求本地或回退),并渲染三行读数。私有 `timeContext` Projection 保存消息及注入时间、最近进入的轮次,以及按出现顺序去重的浏览器时区值,也保留被压缩遮蔽消息的时区事实。时区校验仍在拒绝和刷新间隔检查之后执行;重放不调用格式化器或 hook。JSON 检查点加后缀重放与完整重放一致;旧版本缓存行通过完整日志重建。读数记录的是已进入的步骤,不是已完成或已传输的请求;后续准备失败时,该读数可能留在历史中。
@@ -135,6 +135,7 @@ Elapsed since the preceding step context: . 这些限制说明时钟上下文何时不合适。它们是当前包约束。 +- **状态与重放成本**:时间戳和当前轮次编号占固定空间。时区状态随最近进入轮次中不同的时区字符串数量增长,不随所有消息增长;每次增加新时区时复制该列表。格式化器仍是可丢弃的进程缓存。标准 AgentLoop 请求最近进入的轮次。若类型化 pre-step 调用方请求其他轮次,原同步完整前缀扫描会包含目标轮次之后已进入的消息。只有将 pre-step 请求明确限定为最近进入的轮次并同步更新调用方后,才可移除此回退。 - **仅限提示词来源信息**:浏览器时区上下文用于指导自然语言解释,但不会悄然填入另一工具所要求的时区字段。 - **混合轮次会询问**:如果同一个开放轮次包含来自不同浏览器时区的提示词,模型会收到要求澄清的指令,而不会猜测哪个时区拥有未限定的时间。 - **回退值不代表用户权威**:浏览器来源信息缺失或混杂时,配置或进程时区用于格式化时钟,但面向模型的策略仍要求澄清。 diff --git a/packages/context/time-context/locale/de.json b/packages/context/time-context/locale/de.json new file mode 100644 index 0000000000..d7df3e00ce --- /dev/null +++ b/packages/context/time-context/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Zeitbewusstsein", + "description": "Teilt dem Modell bei jedem Schritt die aktuelle Uhrzeit, die Zeitzone des Browsers und die Zeit seit der vorherigen Nachricht mit." + } +} diff --git a/packages/context/time-context/locale/es.json b/packages/context/time-context/locale/es.json new file mode 100644 index 0000000000..281275e919 --- /dev/null +++ b/packages/context/time-context/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conciencia del tiempo", + "description": "Le indica al modelo la hora actual, la zona horaria del navegador y la hora desde el mensaje anterior en cada paso." + } +} diff --git a/packages/context/time-context/locale/fr.json b/packages/context/time-context/locale/fr.json new file mode 100644 index 0000000000..26abbe6628 --- /dev/null +++ b/packages/context/time-context/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conscience du temps", + "description": "Indique au modèle l'heure actuelle, le fuseau horaire du navigateur et l'heure écoulée depuis le message précédent à chaque étape." + } +} diff --git a/packages/context/time-context/locale/pt.json b/packages/context/time-context/locale/pt.json new file mode 100644 index 0000000000..06d48256e2 --- /dev/null +++ b/packages/context/time-context/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Consciência do tempo", + "description": "Informa ao modelo a hora atual, o fuso horário do navegador e o tempo desde a mensagem anterior em cada etapa." + } +} diff --git a/packages/context/time-context/locale/ru.json b/packages/context/time-context/locale/ru.json new file mode 100644 index 0000000000..8247654351 --- /dev/null +++ b/packages/context/time-context/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Осведомленность о времени", + "description": "На каждом шаге сообщает модели текущее время, часовой пояс браузера и время с момента предыдущего сообщения." + } +} diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index b56d424e66..e3164339ca 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Durable per-step context with the current time and elapsed time", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index c1d959ecd2..160beb9e8a 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -23,6 +23,7 @@ import { Duration } from '@deepseek-ai/dsh-util-time' import type {} from '@deepseek-ai/dsh-session-projection' import { deriveBrowserTimeZoneContext, + reportedBrowserTimeZone, renderBrowserTimeZoneContext, } from './request-zone.ts' import type { BrowserTimeZoneContext } from './request-zone.ts' @@ -38,6 +39,10 @@ const timeContextStateSchema = zod.object({ lastInjectionTime: zod.number().nullable(), /** Latest injection time in the open turn, or null before that turn receives one. */ lastTurnInjectionTime: zod.number().nullable(), + /** Latest entered turn; null before any turn/start record. */ + browserTurn: zod.number().nullable(), + /** Distinct reported zones in encounter order; validation remains request-time. */ + browserTimeZones: zod.array(zod.string()), }) /** The agent registry that owns pre-step processing. */ @@ -76,7 +81,7 @@ function formatDuration(elapsedMs: number): string { return parts.join(' ') } -/** Collect already-entered and proposed user messages belonging to one open turn. */ +/** Preserve the original scan for typed pre-step callers requesting a nonlatest turn. */ function requestMessages(agent: Agent, turn: number, proposed: readonly UserMessage[]): UserMessage[] { const entered: UserMessage[] = [] for (let seq = agent.session.seq - 1; seq >= 0; seq -= 1) { @@ -152,18 +157,31 @@ export async function apply(ctx: Context, config: Config): Promise { const projectionReady = ctx.sessionProjections.register({ key: 'timeContext', - stateVersion: 2, + stateVersion: 3, stateSchema: timeContextStateSchema, - init: () => ({ lastMessageTime: null, lastInjectionTime: null, lastTurnInjectionTime: null }), + init: () => ({ + lastMessageTime: null, + lastInjectionTime: null, + lastTurnInjectionTime: null, + browserTurn: null, + browserTimeZones: [], + }), apply: (state, event) => { - if (event.type === 'turn/start' || event.type === 'turn/end') { + if (event.type === 'turn/start') { + return { ...state, lastTurnInjectionTime: null, browserTurn: event.data.turn, browserTimeZones: [] } + } + if (event.type === 'turn/end') { return state.lastTurnInjectionTime === null ? state : { ...state, lastTurnInjectionTime: null } } if (event.type === 'user/message') { const injected = event.data.source.kind === name - const withMessage = state.lastMessageTime === event.time + let withMessage = state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time } + const timeZone = reportedBrowserTimeZone(event.data) + if (state.browserTurn !== null && timeZone !== undefined && !state.browserTimeZones.includes(timeZone)) { + withMessage = { ...withMessage, browserTimeZones: [...state.browserTimeZones, timeZone] } + } if (!injected) return withMessage return { ...withMessage, @@ -194,12 +212,13 @@ export async function apply(ctx: Context, config: Config): Promise { && now >= lastInjection && now - lastInjection < refreshIntervalMs) return decision } - /* v8 ignore next 6 -- every later step follows a recorded injection in the same turn */ + /* v8 ignore next 3 -- every later step follows a recorded injection in the same turn */ const previous = step === 1 ? state.lastMessageTime ?? undefined : state.lastTurnInjectionTime ?? undefined - const messages = requestMessages(agent, turn, decision.messages) - const browser = deriveBrowserTimeZoneContext(messages) + const browser = state.browserTurn === turn + ? deriveBrowserTimeZoneContext(decision.messages, state.browserTimeZones) + : deriveBrowserTimeZoneContext(requestMessages(agent, turn, decision.messages)) const selectedTimeZone = browser.kind === 'resolved' ? browser.timeZone : fallbackTimeZone const text = renderText( now, diff --git a/packages/context/time-context/src/request-zone.ts b/packages/context/time-context/src/request-zone.ts index 800cd04124..b59072b7d5 100644 --- a/packages/context/time-context/src/request-zone.ts +++ b/packages/context/time-context/src/request-zone.ts @@ -1,32 +1,33 @@ -/** Browser-zone derivation and model-facing policy text for one open request turn. */ +/** Browser-zone facts from committed or proposed user-rpc messages and model-facing policy text. */ import { canonicalizeTimeZone } from '@deepseek-ai/dsh-util-time' import type { UserMessage } from '@deepseek-ai/dsh-llm' import { assertNever } from '@deepseek-ai/dsh-util-values' -/** Browser-zone facts derived from user-rpc messages in one open turn. */ +/** Browser-zone facts for the messages selected while preparing a request. */ export type BrowserTimeZoneContext = | { readonly kind: 'resolved'; readonly timeZone: string } | { readonly kind: 'mixed'; readonly timeZones: readonly string[] } | { readonly kind: 'missing' } /** - * Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. - * - * A request may only name a zone another process resolves back to the same - * identity, so the shared vocabulary's canonical answer must equal the reported - * value; an alias is refused here rather than silently rewritten. + * Read the raw zone reported by a user-rpc source; validation belongs to request preparation. + * @param message - user message; only user sources with a string rpcId and clientTimeZone contribute. + * @returns the reported zone, or undefined when no user-rpc zone was reported. */ -function browserTimeZone(message: UserMessage): string | undefined { +export function reportedBrowserTimeZone(message: UserMessage): string | undefined { const source = message.source - const value = source.kind === 'user' + return source.kind === 'user' && 'rpcId' in source && typeof source.rpcId === 'string' && 'clientTimeZone' in source && typeof source.clientTimeZone === 'string' ? source.clientTimeZone : undefined - if (value === undefined) return undefined +} + +/** Validate a reported zone at request preparation, preserving the platform refusal. */ +function browserTimeZone(value: string): string { const resolved = canonicalizeTimeZone(value) if (resolved.kind === 'accepted') { if (resolved.timeZone !== value) { @@ -51,18 +52,21 @@ function browserTimeZone(message: UserMessage): string | undefined { } /** - * Derive the unique, mixed, or missing browser zone for one open turn. - * @param messages - Entered and proposed user messages belonging to the turn. + * Derive the unique, mixed, or missing browser zone for the selected request messages. + * @param messages - proposed messages, or entered plus proposed messages selected by the legacy reader. + * @param enteredTimeZones - reported zones from committed messages, before the proposed messages. * @returns Sorted, duplicate-free browser-zone facts. * @throws TypeError when a user-rpc source carries an invalid or noncanonical zone. */ export function deriveBrowserTimeZoneContext( messages: readonly UserMessage[], + enteredTimeZones: readonly string[] = [], ): BrowserTimeZoneContext { - const timeZones = [...new Set(messages.flatMap((message) => { - const timeZone = browserTimeZone(message) + const reported = messages.flatMap((message) => { + const timeZone = reportedBrowserTimeZone(message) return timeZone === undefined ? [] : [timeZone] - }))].sort() + }) + const timeZones = [...new Set([...enteredTimeZones, ...reported].map(browserTimeZone))].sort() const [timeZone, ...remaining] = timeZones if (timeZone === undefined) return { kind: 'missing' } if (remaining.length === 0) return { kind: 'resolved', timeZone } @@ -71,7 +75,7 @@ export function deriveBrowserTimeZoneContext( /** * Render the model instruction for one browser-zone context. - * @param context - Browser-zone facts for the open turn. + * @param context - browser-zone facts selected for this request. * @returns One durable policy line. */ export function renderBrowserTimeZoneContext(context: BrowserTimeZoneContext): string { diff --git a/packages/context/time-context/tests/request-zone.spec.ts b/packages/context/time-context/tests/request-zone.spec.ts index 74eb828337..2ef4ecd919 100644 --- a/packages/context/time-context/tests/request-zone.spec.ts +++ b/packages/context/time-context/tests/request-zone.spec.ts @@ -53,6 +53,13 @@ describe('browser request-zone context', () => { ])).toThrow(/browser time zone must be canonical/) }) + it('combines committed reported zones with proposed messages before validation', () => { + expect(deriveBrowserTimeZoneContext([browserMessage('America/New_York')], ['Asia/Shanghai'])) + .toEqual({ kind: 'mixed', timeZones: ['America/New_York', 'Asia/Shanghai'] }) + expect(() => deriveBrowserTimeZoneContext([browserMessage('Not/A_Real_Zone')], ['+08:00'])) + .toThrow('browser time zone must be canonical UTC or IANA Area/Location: "+08:00"') + }) + it('keeps the exact refusal and the platform cause for every unusable zone', () => { const refusal = (timeZone: string): Error => { try { diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index ec0595d197..bd4883a13e 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' -import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' +import { Session, SessionId, SessionLogOffset, type SessionEvent } from '@deepseek-ai/dsh-session' import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -422,6 +422,115 @@ describe('configuration and lifecycle', () => { }) describe('time-context projection fold edges', () => { + it('restores browser zones from a JSON checkpoint and suffix without reading the prefix', async () => { + const { ctx } = await mount({ refreshIntervalMs: 0 }) + onTestFinished(() => ctx.fiber.dispose()) + const session = Session.create(SessionId('browser-checkpoint')) + openMessageTurn(session, 1, 'Asia/Shanghai') + const cut = session.seq + const rows = structuredClone(ctx.sessionProjections.checkpoint(session)) + session.append('user/message', createUserMessage({ + content: [], + source: { kind: 'user', rpcId: 'steer', clientTimeZone: 'America/New_York' } as never, + }), { surfaceOp: 'append' }) + const events = session.snapshotEvents() + const suffix = session.snapshotEvents(cut) + const jsonRows = JSON.parse(JSON.stringify(rows)) as typeof rows + const restored = ctx.sessionProjections.restore(jsonRows, suffix, cut, session.header, session.inheritedEventCount) + expect(restored.checkpoint.timeContext).toEqual(ctx.sessionProjections.checkpoint(session).timeContext) + + const cold = Session.create(SessionId('browser-cold'), events) + const lookup = vi.spyOn(cold, 'eventAt') + const snapshot = vi.spyOn(cold, 'snapshotEvents') + const own = vi.spyOn(cold, 'ownEvents') + ctx.sessionProjections.hydrate(cold, jsonRows, suffix, cut) + await fire(ctx, sessionAgent(cold), 1, 1) + // The supplied suffix is already hydrated; later lifecycle records may still be read. + expect(lookup.mock.calls.every(([seq]) => seq >= events.length)).toBe(true) + expect(snapshot).not.toHaveBeenCalled() + expect(own).not.toHaveBeenCalled() + lookup.mockRestore() + snapshot.mockRestore() + own.mockRestore() + expect(contextTexts(cold).at(-1)).toContain('mixed ["America/New_York","Asia/Shanghai"]') + }) + + it('rebuilds an old checkpoint from the full journal and removes the unit on unload', async () => { + const { ctx, fiber } = await mount() + onTestFinished(() => ctx.fiber.dispose()) + const session = Session.create(SessionId('browser-old-row')) + openMessageTurn(session, 1, 'Asia/Shanghai') + const rows = ctx.sessionProjections.checkpoint(session) + const oldRows = { + ...rows, + timeContext: { + ver: 2, + seq: rows.timeContext!.seq, + val: { lastMessageTime: BASE, lastInjectionTime: null, lastTurnInjectionTime: null }, + }, + } + const cold = Session.create(SessionId('browser-old-cold'), session.snapshotEvents()) + ctx.sessionProjections.hydrate(cold, oldRows, cold.snapshotEvents(), SessionLogOffset(0)) + expect(ctx.sessionProjections.checkpoint(cold).timeContext).toEqual({ + ver: 3, seq: cold.seq - 1, val: rows.timeContext!.val, + }) + await fiber.dispose() + expect(ctx.sessionProjections.checkpoint(cold).timeContext).toBeUndefined() + }) + + it('retains reported invalid zones without validating rejected or throttled requests', async () => { + const { ctx } = await mount({ refreshIntervalMs: 1000 }) + onTestFinished(() => ctx.fiber.dispose()) + const session = Session.create(SessionId('browser-refusal-timing')) + const agent = sessionAgent(session) + openMessageTurn(session, 1, 'UTC') + await fire(ctx, agent, 1, 1) + session.append('user/message', createUserMessage({ + content: [], + source: { kind: 'user', rpcId: 'invalid', clientTimeZone: '+08:00' } as never, + }), { surfaceOp: 'append' }) + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ + browserTimeZones: ['UTC', '+08:00'], + }) + vi.setSystemTime(BASE + 500) + await expect(fire(ctx, agent, 1, 2)).resolves.toBeUndefined() + vi.setSystemTime(BASE + 1000) + const reject = ctx.on('agent/pre-step', () => Promise.resolve({ kind: 'reject' as const })) + await expect(fire(ctx, agent, 1, 2)).resolves.toBeUndefined() + reject() + await expect(fire(ctx, agent, 1, 2)).rejects.toThrow( + 'browser time zone must be canonical UTC or IANA Area/Location: "+08:00"', + ) + expect(contextTexts(session)).toHaveLength(1) + }) + + it('keeps the historical requested-turn fallback and resets distinct zones on the next turn', async () => { + const { ctx } = await mount({ refreshIntervalMs: 0 }) + onTestFinished(() => ctx.fiber.dispose()) + const session = Session.create(SessionId('browser-historical-turn')) + session.append('user/message', createUserMessage({ + content: [], + source: { kind: 'user', rpcId: 'before-turn', clientTimeZone: 'Europe/London' } as never, + }), { surfaceOp: 'append' }) + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ + browserTurn: null, browserTimeZones: [], + }) + openMessageTurn(session, 1, 'Asia/Shanghai') + session.append('user/message', createUserMessage({ + content: [], + source: { kind: 'user', rpcId: 'duplicate', clientTimeZone: 'Asia/Shanghai' } as never, + }), { surfaceOp: 'append' }) + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ + browserTurn: 1, browserTimeZones: ['Asia/Shanghai'], + }) + openMessageTurn(session, 2, 'America/New_York') + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ + browserTurn: 2, browserTimeZones: ['America/New_York'], + }) + await fire(ctx, sessionAgent(session), 1, 1) + expect(contextTexts(session).at(-1)).toContain('mixed ["America/New_York","Asia/Shanghai"]') + }) + it('clears the open-turn injection time at the next turn start', async () => { const { ctx } = await mount() const session = Session.create(SessionId('same-turn')) diff --git a/packages/context/tmux-context/locale/de.json b/packages/context/tmux-context/locale/de.json new file mode 100644 index 0000000000..0d181e2692 --- /dev/null +++ b/packages/context/tmux-context/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "tmux-Kontext", + "description": "Speichern Sie bei Bedarf die tmux-Bereichs- und Fensterpositionen für jeden Schritt." + } +} diff --git a/packages/context/tmux-context/locale/es.json b/packages/context/tmux-context/locale/es.json new file mode 100644 index 0000000000..93664d1b33 --- /dev/null +++ b/packages/context/tmux-context/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "contexto tmux", + "description": "Guarde las ubicaciones de las ventanas y paneles tmux para cada paso cuando sea necesario." + } +} diff --git a/packages/context/tmux-context/locale/fr.json b/packages/context/tmux-context/locale/fr.json new file mode 100644 index 0000000000..7791998a86 --- /dev/null +++ b/packages/context/tmux-context/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "contexte tmux", + "description": "Enregistrez les emplacements du volet tmux et des fenêtres pour chaque étape si nécessaire." + } +} diff --git a/packages/context/tmux-context/locale/pt.json b/packages/context/tmux-context/locale/pt.json new file mode 100644 index 0000000000..5829153e8e --- /dev/null +++ b/packages/context/tmux-context/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "contexto tmux", + "description": "Salve os locais do painel e da janela do tmux para cada etapa, quando necessário." + } +} diff --git a/packages/context/tmux-context/locale/ru.json b/packages/context/tmux-context/locale/ru.json new file mode 100644 index 0000000000..8a161df7a6 --- /dev/null +++ b/packages/context/tmux-context/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "контекст tmux", + "description": "При необходимости сохраняйте расположение панели tmux и окон для каждого шага." + } +} diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index c9f4d6f312..cf40f9967d 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-default-model/locale/de.json b/packages/core/agent-default-model/locale/de.json new file mode 100644 index 0000000000..b79c700564 --- /dev/null +++ b/packages/core/agent-default-model/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Standardmodell", + "description": "Bietet eine Standardmodellauswahl für neue Agenten." + } +} diff --git a/packages/core/agent-default-model/locale/es.json b/packages/core/agent-default-model/locale/es.json new file mode 100644 index 0000000000..af4c8a86c0 --- /dev/null +++ b/packages/core/agent-default-model/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modelo predeterminado", + "description": "Proporciona selección de modelo predeterminado para nuevos agentes." + } +} diff --git a/packages/core/agent-default-model/locale/fr.json b/packages/core/agent-default-model/locale/fr.json new file mode 100644 index 0000000000..2159533905 --- /dev/null +++ b/packages/core/agent-default-model/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modèle par défaut", + "description": "Fournit une sélection de modèle par défaut pour les nouveaux agents." + } +} diff --git a/packages/core/agent-default-model/locale/pt.json b/packages/core/agent-default-model/locale/pt.json new file mode 100644 index 0000000000..12d140156d --- /dev/null +++ b/packages/core/agent-default-model/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modelo padrão", + "description": "Fornece seleção de modelo padrão para novos agentes." + } +} diff --git a/packages/core/agent-default-model/locale/ru.json b/packages/core/agent-default-model/locale/ru.json new file mode 100644 index 0000000000..cc24d5b144 --- /dev/null +++ b/packages/core/agent-default-model/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Модель по умолчанию", + "description": "Обеспечивает выбор модели по умолчанию для новых агентов." + } +} diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index cc8142a710..187829f104 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index b4c7aaffa1..33c53afffa 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -45,8 +45,8 @@ en: 3b48a480036bd5df zh: 28cea2036b0dccee /deepseek-ai-dsh-agent-loop/understand-the-implementation/turn-and-step-flow: - en: 4926502ada48be6b - zh: 7b3ab909f771d2a2 + en: ad92c751515520ff + zh: ca6b24db5ab4e471 /deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation: en: eea56903c4ff82f5 zh: c0d5abb5767780ed @@ -66,8 +66,8 @@ en: ad93d8d61968a25d zh: 268ff858b0fa96e8 /deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/kv-cache-effect: - en: c002d27c825d04e8 - zh: b0dbdd95d3b3b6b5 + en: 12a2fc9e3654ddbf + zh: 133b7ca59925c82a /deepseek-ai-dsh-agent-loop/model-experience/retained-message-history: en: 50d33d852dec1e28 zh: ec15695304f25f38 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 725688ce04..28b862ff9d 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -119,7 +119,7 @@ The loop is the production acquisition point for session write handles. When `ct The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. It also owns `modelSelection` and constructs an `AgentModel` for each Agent, with the public [Model API](../agent/README.md#model-selection). Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver assembles the prompt and tools and runs `agent/pre-step`; the fallback projects runtime context after listeners run. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts without reassembling the bound prompt or tools, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars) -Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. +Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route, or at a new request series whose effective text differs, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. A series start whose latest surviving node already holds the rendering produces no system event, so the request keeps the prefix it had. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or a new series whose effective text differs refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. ### Failure and cancellation @@ -161,7 +161,7 @@ System text and schemas are paid again on every step, and on an `in-history` rou #### KV Cache effect -Prefix reuse requires earlier messages and declarations to stay byte-identical under the same provider and model route. An unchanged rendered prompt keeps the cached prefix unless an incapable route or a new request series must consolidate retained in-history system nodes. A prompt change that replaces a system node in place makes the request differ from that node's first token — in full when the node is node 0 — so the provider prefix cache misses from there; when the prepared call declares `systemPromptUpdate: 'in-history'`, a non-empty prompt change inside a continuing request series is appended after the cached history, so the prefix through that history stays reusable. Changing a retained tool definition or rebuilding declarations invalidates reuse from the first altered request token. +Prefix reuse requires earlier messages and declarations to stay byte-identical under the same provider and model route. An unchanged rendered prompt keeps the cached prefix: a capable route leaves retained in-history system nodes in place even when a new series starts, and only an incapable route, or a series start whose effective text differs, consolidates them. A prompt change that replaces a system node in place makes the request differ from that node's first token — in full when the node is node 0 — so the provider prefix cache misses from there; when the prepared call declares `systemPromptUpdate: 'in-history'`, a non-empty prompt change inside a continuing request series is appended after the cached history, so the prefix through that history stays reusable. Changing a retained tool definition or rebuilding declarations invalidates reuse from the first altered request token. ### Retained message history diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 97835c11cd..f872d4289e 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -119,7 +119,7 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。它还拥有 `modelSelection`,并为每个 Agent 构建 `AgentModel`,实现公开的 [Model API](../agent/README.zh.md#model-selection)。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器组装提示词与工具,并运行 `agent/pre-step`;fallback 在监听器执行后投影运行时上下文。被拒绝的决定或空的首批输入不打开步骤。接纳后的首次尝试先记录 `step/start`,再运行 `agent/request` waterfall 与 `prepareCall()`;这两个异步阶段都看不到待提交的系统提示词与已接纳用户消息进入历史,在任一阶段取消都不会提交这两者。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实而不重新组装绑定的提示词或工具、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用同一份已渲染组装结果,不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。 -提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 +提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上,或有效文本不同的新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。新序列开始时若最新存活节点已经持有该渲染文本,则不产生系统事件,请求保留原有前缀。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或有效文本不同的新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 ### 失败与取消 @@ -161,7 +161,7 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 #### KV Cache 影响 -前缀复用要求在同一提供方与模型路由下,此前消息和声明保持逐字节一致。渲染后的提示词未变时,缓存前缀得以保留,除非不具备能力的路由或新请求序列必须归并保留的历史内系统节点。原地替换某个系统节点的提示词变更会使请求从该节点的第一个 token 起就不同——该节点是第 0 号节点时则整个请求都不同——因此提供方前缀缓存从那里开始未命中;当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,同一请求序列延续期间的非空提示词变更会追加到已缓存历史之后,因此直到该历史末尾的前缀仍可复用。更改保留工具的定义或重建声明,会从第一个改变的请求 token 起使复用失效。 +前缀复用要求在同一提供方与模型路由下,此前消息和声明保持逐字节一致。渲染后的提示词未变时,缓存前缀得以保留:具备能力的路由即使开始新序列也保留历史内系统节点,只有不具备能力的路由,或有效文本不同的新序列开始,才归并它们。原地替换某个系统节点的提示词变更会使请求从该节点的第一个 token 起就不同——该节点是第 0 号节点时则整个请求都不同——因此提供方前缀缓存从那里开始未命中;当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,同一请求序列延续期间的非空提示词变更会追加到已缓存历史之后,因此直到该历史末尾的前缀仍可复用。更改保留工具的定义或重建声明,会从第一个改变的请求 token 起使复用失效。 ### 保留的消息历史 diff --git a/packages/core/agent-loop/locale/de.json b/packages/core/agent-loop/locale/de.json new file mode 100644 index 0000000000..bb62da6911 --- /dev/null +++ b/packages/core/agent-loop/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Agenten-Ausführungsschleife", + "description": "Koordinieren Sie Modellanfragen, Toolaufrufe und Folgeschritte, um Agentenaufgaben voranzutreiben." + } +} diff --git a/packages/core/agent-loop/locale/es.json b/packages/core/agent-loop/locale/es.json new file mode 100644 index 0000000000..4433ae5c9f --- /dev/null +++ b/packages/core/agent-loop/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Bucle de ejecución del agente", + "description": "Coordine solicitudes de modelos, invocaciones de herramientas y pasos de seguimiento para avanzar en las tareas de los agentes." + } +} diff --git a/packages/core/agent-loop/locale/fr.json b/packages/core/agent-loop/locale/fr.json new file mode 100644 index 0000000000..2a2bd9cfe1 --- /dev/null +++ b/packages/core/agent-loop/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Boucle d'exécution de l'agent", + "description": "Coordonnez les demandes de modèles, les appels d’outils et les étapes de suivi pour faire avancer les tâches des agents." + } +} diff --git a/packages/core/agent-loop/locale/pt.json b/packages/core/agent-loop/locale/pt.json new file mode 100644 index 0000000000..94a4212d20 --- /dev/null +++ b/packages/core/agent-loop/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Loop de execução do agente", + "description": "Coordene solicitações de modelo, invocações de ferramentas e etapas de acompanhamento para avançar nas tarefas do agente." + } +} diff --git a/packages/core/agent-loop/locale/ru.json b/packages/core/agent-loop/locale/ru.json new file mode 100644 index 0000000000..3d9658aa58 --- /dev/null +++ b/packages/core/agent-loop/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Цикл выполнения агента", + "description": "Координируйте запросы моделей, вызовы инструментов и последующие шаги для выполнения задач агента." + } +} diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index a5d470f452..e5349bbec4 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/src/runtime-context.ts b/packages/core/agent-loop/src/runtime-context.ts index cb959b055a..1df199c9f8 100644 --- a/packages/core/agent-loop/src/runtime-context.ts +++ b/packages/core/agent-loop/src/runtime-context.ts @@ -41,9 +41,9 @@ export interface SystemPromptDecisionInput { /** Whether the prepared route for this attempt reads a later `system` message as the effective prompt. */ inHistory: boolean /** - * Whether prompt admission must consolidate: a pre-step listener declared a - * new series, the surface changed since the last request, or assembled tools - * changed on a route without tool-update support. + * Whether this step starts a new request series: a pre-step listener declared + * one, the surface changed since the last request, or assembled tools changed + * on a route without tool-update support. */ startsSeries: boolean } @@ -58,9 +58,12 @@ function eventsNewestFirst(session: Session): readonly SessionEvent[] { * Decides how a rendered system prompt reaches the surface without owning the * commit. The first prompt, even empty, reserves surface node 0. * A capable continuing series appends changed nonempty text after the - * cached history. An incapable route, broken series, or cleared prompt instead - * normalizes the first system node and empties later active nodes. Dormant empty - * tails do not supply effective text or require repeated replacements. + * cached history. An incapable route, a cleared prompt, or a new series whose + * effective text differs instead normalizes the first system node and empties + * later active nodes. A series start whose latest surviving node already holds + * the rendering emits nothing: moving the same text to node 0 would change the + * request prefix without changing the prompt. Dormant empty tails do not supply + * effective text or require repeated replacements. */ export class SystemPromptProjection { constructor(private readonly session: Session) {} @@ -81,6 +84,8 @@ export class SystemPromptProjection { /** * Reconcile effective text and retained nodes with the prepared route and series. + * A series start re-baselines node 0 only when the effective text differs; a + * capable continuing series keeps appended nodes in place to preserve the prefix. * @param rendered - the fully rendered system prompt; `''` when none is active. * @param input - the route capability and series facts for this step. * @returns ordered per-node updates; an empty list means no update is needed. @@ -92,7 +97,7 @@ export class SystemPromptProjection { return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }] } const latest = nodes.findLast(node => node.text !== '') ?? head - if (!input.inHistory || input.startsSeries || rendered.length === 0) { + if (!input.inHistory || rendered.length === 0 || (input.startsSeries && latest.text !== rendered)) { const updates = nodes.slice(1).filter(node => node.text !== '') .map(node => this.replace(node.seq, '')) if (head.text !== rendered) updates.push(this.replace(head.seq, rendered)) diff --git a/packages/core/agent-loop/tests/fixtures/serial-created.mjs b/packages/core/agent-loop/tests/fixtures/serial-created.mjs index c4ed2df17c..8cc624caae 100644 --- a/packages/core/agent-loop/tests/fixtures/serial-created.mjs +++ b/packages/core/agent-loop/tests/fixtures/serial-created.mjs @@ -3,17 +3,31 @@ import { randomUUID } from 'node:crypto' import { setImmediate } from 'node:timers/promises' export const name = 'serial-created-fixture' -export const inject = ['agents'] +export const inject = ['agents', 'sessions'] /** Install ordered creation listeners whose context must precede the first turn. */ export function apply(ctx) { const ready = new WeakSet() ctx.on('agent/created', async ({ agent }) => { await setImmediate() + let forkSummary = '' + if (agent.session.header.origin !== 'subagent') { + const fork = ctx.sessions.fork(agent.session) + const nested = ctx.sessions.fork(fork) + for (const independent of [fork, nested]) { + if (independent.header.parentSession !== undefined || independent.header.origin !== undefined + || !independent.header.isSeeded) throw new Error('ordinary fork retained a creator relationship') + } + ctx.sessions.create(undefined, { + seed: agent.session.snapshotEvents(), inheritedEventCount: agent.session.seq, + meta: { parentSession: agent.id, isSeeded: true }, + }) + forkSummary = ' Ordinary and nested forks are independent; historical fork metadata remains readable.' + } agent.inject({ id: randomUUID(), role: 'user', - content: [{ type: 'text', text: 'Serial agent creation completed.' }], + content: [{ type: 'text', text: 'Serial agent creation completed.' + forkSummary }], source: { kind: `plugin:${name}` }, }) ready.add(agent) diff --git a/packages/core/agent-loop/tests/system-prompt-admission.spec.ts b/packages/core/agent-loop/tests/system-prompt-admission.spec.ts index 641a51951b..206648bb9c 100644 --- a/packages/core/agent-loop/tests/system-prompt-admission.spec.ts +++ b/packages/core/agent-loop/tests/system-prompt-admission.spec.ts @@ -123,7 +123,7 @@ describe('prepared-route prompt admission', () => { expect(request.messages.filter(message => message.role === 'developer')).toEqual([]) }) - it.each(['explicit', 'replacement'] as const)('rebuilds prompt and declarations at a %s series start', async (reason) => { + it.each(['explicit', 'replacement'] as const)('rebuilds declarations without re-baselining an unchanged prompt at a %s series start', async (reason) => { const h = await harness() h.capable.toolUpdate = toolUpdate await send(h.agent, 'first') @@ -144,7 +144,13 @@ describe('prepared-route prompt admission', () => { await send(h.agent, 'add fetch') const request = h.capable.requests.at(-1)! - expectPlain(request, 'prompt two') + const previous = h.capable.requests.at(-2)! + // The rendering is unchanged: the surviving prompt node keeps its place and the new series + // only rebuilds declarations. Rewriting node 0 would invalidate the provider prefix. + expect(systemTexts(request)).toEqual([ + [{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }], + ]) + expect(request.messages[0]).toEqual(previous.messages[0]) expect(request.tools?.find(tool => tool.name === 'fetch')?.deferLoading).not.toBe(true) expect(request.messages.filter(message => message.role === 'developer')).toEqual([]) const header = h.agent.session.snapshotEvents().filter(event => event.type === 'request/header').at(-1) @@ -217,7 +223,7 @@ describe('prepared-route prompt admission', () => { expect(JSON.stringify(adapter.requests.at(-1)!)).not.toContain('prompt ') }) - it.each(['explicit', 'tools'] as const)('normalizes surviving prompt versions with unchanged text at a %s series start', async (reason) => { + it.each(['explicit', 'tools'] as const)('keeps surviving prompt versions with unchanged text at a %s series start', async (reason) => { const h = await harness() await send(h.agent, 'first') h.setPrompt('prompt two') @@ -234,10 +240,50 @@ describe('prepared-route prompt admission', () => { })) } await send(h.agent, 'third') - expectPlain(h.capable.requests[2]!, 'prompt two') + const previous = h.capable.requests[1]! + const request = h.capable.requests[2]! + // The rendering is unchanged, so the new series keeps the surviving prompt node: the whole + // previous request stays a wire prefix instead of moving the prompt into message 0. + expect(systemTexts(request)).toEqual([ + [{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }], + ]) + expect(request.messages.slice(0, previous.messages.length)).toEqual(previous.messages) const header = h.agent.session.snapshotEvents().filter(event => event.type === 'request/header').at(-1) expect(header?.data.reason).toBe(reason === 'explicit' ? 'series' : 'change') - if (reason === 'tools') expect(header?.data.startsSeries).toBe(true) + if (reason === 'tools') { + // A tool-schema change starts the prompt series without a system event. The header records + // the declaration change without `startsSeries`; the unsupported-route projection supplies + // the complete active set, while tool history keeps its incremental baseline. + expect(header?.data.startsSeries).toBeUndefined() + expect(request.tools?.map(tool => tool.name)).toEqual(['extra']) + } + }) + + it('keeps the cached prefix at a compaction series start when the prompt text is unchanged', async () => { + const h = await harness() + await send(h.agent, 'first') + h.setPrompt('prompt two') + await send(h.agent, 'second') + const previous = h.capable.requests[1]! + + // A prune/compaction replaces the last admitted user message: the surface changes, so the + // next step starts a new request series while the appended prompt node still holds the + // rendered text. + const seq = h.agent.session.surface.nodes.findLast(node => h.agent.session.eventAt(node)?.type === 'user/message')! + h.agent.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'compacted history' }], source: { kind: 'test-compaction' }, + }), { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] }) + await send(h.agent, 'third') + + // Folding the prompt back into node 0 would move the system segment to wire message 0 and + // miss the provider prefix cache from the first token, although no prompt text changed. + const request = h.capable.requests[2]! + expect(systemTexts(request)).toEqual([ + [{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }], + ]) + // The replacement shadows the previous request's last admitted message, so the shared wire + // prefix is everything before it. + expect(request.messages.slice(0, previous.messages.length - 1)).toEqual(previous.messages.slice(0, -1)) }) it.each([false, true])('reconciles compaction retries without replaying admission, older tail survives=%s', async (retainOlder) => { @@ -353,9 +399,15 @@ describe('prepared-route prompt admission', () => { return next() }) await send(resumed, 'resume') - expect(systemTexts(h.capable.requests[2]!)).toEqual(replace - ? [[{ type: 'text', text: 'prompt two' }]] - : [[{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }]]) + const previous = h.capable.requests[1]! + const request = h.capable.requests[2]! + // The first resumed pre-step replacement still starts a series, but an unchanged rendering + // keeps both prompt versions in place instead of re-baselining node 0. + expect(systemTexts(request)).toEqual([ + [{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }], + ]) + expect(request.messages[0]).toEqual(previous.messages[0]) + if (!replace) expect(request.messages.slice(0, previous.messages.length)).toEqual(previous.messages) expect(resumed.session.snapshotEvents().filter(event => event.type === 'request/header').map(event => event.data.reason)) .toEqual(['initial', 'resume']) }) diff --git a/packages/core/agent-loop/tests/system-prompt-projection.spec.ts b/packages/core/agent-loop/tests/system-prompt-projection.spec.ts index 65ec74c486..9b58a2e8d9 100644 --- a/packages/core/agent-loop/tests/system-prompt-projection.spec.ts +++ b/packages/core/agent-loop/tests/system-prompt-projection.spec.ts @@ -177,6 +177,23 @@ describe('SystemPromptProjection', () => { .toEqual([{ type: 'text', text: 'v1' }, { type: 'text', text: 'v2' }, { type: 'text', text: 'v3' }]) }) + it('keeps the surviving prompt node at a series start when its text already matches', async () => { + const ctx = await sessionStore() + const session = ctx.sessions.create(SessionId('system-prompt-series-start-current')) + const projection = new SystemPromptProjection(session) + const head = commit(session, 1, projection.project('v1', CONTINUING)[0]) + appendUser(session, 'hello') + const appended = commit(session, 2, projection.project('v2', CONTINUING)[0]) + + // The effective text is already current: re-baselining node 0 would move the same + // text into the request's first message and invalidate the provider prefix. + expect(projection.project('v2', NEW_SERIES)).toEqual([]) + expect(session.surface.nodes[0]).toBe(head.seq) + expect(session.surface.nodes).toContain(appended.seq) + expect(session.deriveMessages().flatMap(message => message.role === 'system' ? [message.content[0]] : [])) + .toEqual([{ type: 'text', text: 'v1' }, { type: 'text', text: 'v2' }]) + }) + it('re-baselines node 0 and empties later active nodes at a series start', async () => { const ctx = await sessionStore() const session = ctx.sessions.create(SessionId('system-prompt-series-start')) diff --git a/packages/core/agent-tool-presentation/locale/de.json b/packages/core/agent-tool-presentation/locale/de.json new file mode 100644 index 0000000000..72b3a37049 --- /dev/null +++ b/packages/core/agent-tool-presentation/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tool-Aufrufmethode", + "description": "Wählen Sie den Agent aus, um das Tool über native Tools oder PTC-Code aufzurufen." + } +} diff --git a/packages/core/agent-tool-presentation/locale/es.json b/packages/core/agent-tool-presentation/locale/es.json new file mode 100644 index 0000000000..0d32a6fe21 --- /dev/null +++ b/packages/core/agent-tool-presentation/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Método de llamada de herramienta", + "description": "Seleccione el agente para invocar la herramienta a través de herramientas nativas o código PTC." + } +} diff --git a/packages/core/agent-tool-presentation/locale/fr.json b/packages/core/agent-tool-presentation/locale/fr.json new file mode 100644 index 0000000000..545401ae5f --- /dev/null +++ b/packages/core/agent-tool-presentation/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Méthode d'appel d'outil", + "description": "Sélectionnez l'agent pour appeler l'outil via des outils natifs ou du code PTC." + } +} diff --git a/packages/core/agent-tool-presentation/locale/pt.json b/packages/core/agent-tool-presentation/locale/pt.json new file mode 100644 index 0000000000..effdf61daa --- /dev/null +++ b/packages/core/agent-tool-presentation/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Método de chamada de ferramenta", + "description": "Selecione o agente para invocar a ferramenta por meio de ferramentas nativas ou código PTC." + } +} diff --git a/packages/core/agent-tool-presentation/locale/ru.json b/packages/core/agent-tool-presentation/locale/ru.json new file mode 100644 index 0000000000..bb0345c60a --- /dev/null +++ b/packages/core/agent-tool-presentation/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Метод вызова инструмента", + "description": "Выберите агент для вызова инструмента с помощью собственных инструментов или кода PTC." + } +} diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index 00554b1d37..c1292429f7 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Deprecated tool presentation selector; use dsh-tool-executor-native or dsh-tool-executor-ptc directly", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/locale/de.json b/packages/core/agent/locale/de.json new file mode 100644 index 0000000000..ff00c84c39 --- /dev/null +++ b/packages/core/agent/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Agent", + "description": "Verwalten Sie Agenteninstanzen sowie deren laufenden Kontext und Ereignisse." + } +} diff --git a/packages/core/agent/locale/es.json b/packages/core/agent/locale/es.json new file mode 100644 index 0000000000..bf543e11aa --- /dev/null +++ b/packages/core/agent/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "agente", + "description": "Administre instancias de agentes y su contexto y eventos de ejecución." + } +} diff --git a/packages/core/agent/locale/fr.json b/packages/core/agent/locale/fr.json new file mode 100644 index 0000000000..a3dc7de26e --- /dev/null +++ b/packages/core/agent/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "agent", + "description": "Gérez les instances d'agent, leur contexte d'exécution et leurs événements." + } +} diff --git a/packages/core/agent/locale/pt.json b/packages/core/agent/locale/pt.json new file mode 100644 index 0000000000..c3eb79d40a --- /dev/null +++ b/packages/core/agent/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "agente", + "description": "Gerencie instâncias de agentes e seus contextos e eventos em execução." + } +} diff --git a/packages/core/agent/locale/ru.json b/packages/core/agent/locale/ru.json new file mode 100644 index 0000000000..48dfc8eed3 --- /dev/null +++ b/packages/core/agent/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "агент", + "description": "Управляйте экземплярами агентов, их рабочим контекстом и событиями." + } +} diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 4346e73f12..19406c7fe1 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 7c4400a695..ac35e6ca09 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/README.i18n.yaml b/packages/core/session/README.i18n.yaml index 55bf1f876b..7221e1de95 100644 --- a/packages/core/session/README.i18n.yaml +++ b/packages/core/session/README.i18n.yaml @@ -18,8 +18,8 @@ en: 0882d4056c45aa75 zh: fcee78e9845b78b3 /deepseek-ai-dsh-session/use-this-package/create-and-inspect-sessions: - en: 05dac2b797457bab - zh: 9dbbcfcb9ea7d2d2 + en: 971e583b3d6c1af8 + zh: c7da934ecb8f1e87 /deepseek-ai-dsh-session/use-this-package/append-and-derive: en: c9ad40c8a8f2cd0b zh: 0b76ae73f39bbb18 diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 9c3bd6e0d2..bcebe26075 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -29,7 +29,7 @@ Mount `dsh-session` wherever a session must exist. It creates and holds event-so ### Create and inspect sessions -`ctx.sessions.create()` builds a live session bound to the calling fiber; `get(id)` and `list()` find sessions, and `fork()` creates a child session from a stable prefix of a live one. +`ctx.sessions.create()` builds a live session bound to the calling fiber; `get(id)` and `list()` find sessions, and `fork()` creates an independent session from a stable prefix of a live one. It copies history, `cwd`, `isSeeded`, and the inherited event count without recording a source relationship. `parentSession` records a subagent’s direct creator; historical ordinary forks may retain their source there, so classification uses `origin`. Independent `Session.create()` and `Session.fromRestore()` instances accept events and plugin records without sending `session/event`. A `SessionStore` takes ownership through `prepare()` or `enter()`; writes then require store membership and a completed synchronous `session/created` announcement. Creation listeners are read-only and see all existing history; entering an independent instance does not replay that history as `session/event`. Failed announcement or removal leaves writes disabled. Removal emits `session/disposed` only if `session/created` began, including a failed announcement; an entered but never announced instance emits neither notification. diff --git a/packages/core/session/README.zh.md b/packages/core/session/README.zh.md index e134202d01..700289094e 100644 --- a/packages/core/session/README.zh.md +++ b/packages/core/session/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 创建与检查会话 -`ctx.sessions.create()` 构建绑定到调用方 fiber 的实时会话;`get(id)` 与 `list()` 查找会话,`fork()` 从实时会话的精确前缀创建子会话。 +`ctx.sessions.create()` 构建绑定到调用方 fiber 的实时会话;`get(id)` 与 `list()` 查找会话,`fork()` 从实时会话的精确前缀创建独立会话。它复制历史、`cwd`、`isSeeded` 和继承事件数,不记录来源关系。`parentSession` 记录子代理的直接创建者;历史普通 fork 可能仍在此字段保留来源,因此分类使用 `origin`。 独立的 `Session.create()` 和 `Session.fromRestore()` 实例接受事件和插件记录,但不发送 `session/event`。`SessionStore` 通过 `prepare()` 或 `enter()` 接管实例;此后只有实例仍在 Store 中且同步 `session/created` 宣告已完成时才能写入。创建监听器只能读取,并能看到全部已有历史;独立实例进入 Store 时不会把已有历史重放为 `session/event`。宣告失败或移除后仍禁止写入。只有开始过 `session/created` 宣告的实例在移除时发出 `session/disposed`,宣告失败也不例外;进入 Store 但从未宣告的实例不发出这两个通知。 diff --git a/packages/core/session/locale/de.json b/packages/core/session/locale/de.json new file mode 100644 index 0000000000..52c3407da6 --- /dev/null +++ b/packages/core/session/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sitzung", + "description": "Protokollieren Sie Nachrichten, Toolaufrufe und führen Sie Ereignisse in einer Sitzung aus." + } +} diff --git a/packages/core/session/locale/es.json b/packages/core/session/locale/es.json new file mode 100644 index 0000000000..e3f9174905 --- /dev/null +++ b/packages/core/session/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "sesión", + "description": "Registre mensajes, llamadas de herramientas y ejecute eventos en una sesión." + } +} diff --git a/packages/core/session/locale/fr.json b/packages/core/session/locale/fr.json new file mode 100644 index 0000000000..d2b0866dc3 --- /dev/null +++ b/packages/core/session/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "séance", + "description": "Enregistrez les messages, les appels d'outils et exécutez des événements dans une session." + } +} diff --git a/packages/core/session/locale/pt.json b/packages/core/session/locale/pt.json new file mode 100644 index 0000000000..6499804abc --- /dev/null +++ b/packages/core/session/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "sessão", + "description": "Registrar mensagens, chamadas de ferramentas e executar eventos em uma sessão." + } +} diff --git a/packages/core/session/locale/ru.json b/packages/core/session/locale/ru.json new file mode 100644 index 0000000000..ea1cc45b11 --- /dev/null +++ b/packages/core/session/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "сессия", + "description": "Регистрируйте сообщения, вызовы инструментов и запускайте события в сеансе." + } +} diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 4b51d4fc79..4be25b91f8 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index 9df05fb6f5..e9baee1b82 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -1385,7 +1385,7 @@ export class SessionStore extends Service { } /** - * Create a live child session from an exact prefix of a live source. + * Create an independent live session from an exact prefix of a live source. * `boundary` is an inclusive source event seq; omitted means the source's * current last event. An open tail receives synthetic tool results and * step/turn closers with the forked cause. Closed steps and turns remain @@ -1414,7 +1414,6 @@ export class SessionStore extends Service { inheritedEventCount: SessionLogOffset(resolved === undefined ? 0 : resolved + 1), meta: { ...liveSource.header.cwd !== undefined ? { cwd: liveSource.header.cwd } : {}, - parentSession: liveSource.id, isSeeded: true, }, }) diff --git a/packages/core/session/src/types.ts b/packages/core/session/src/types.ts index f88777ae0d..7787dd9bd0 100644 --- a/packages/core/session/src/types.ts +++ b/packages/core/session/src/types.ts @@ -104,7 +104,11 @@ export interface SessionHeader { readonly createdAt: number /** Absolute working directory the session was created in (if any). */ readonly cwd?: string - /** The session this one was forked from (seed lineage), if any. */ + /** + * Direct creator of a subagent Session. Ordinary forks omit this field; + * historical ordinary forks may retain their source id here. Presence alone + * does not classify a Session as a subagent; use `origin` for that distinction. + */ readonly parentSession?: SessionId /** * Whether this Session contains a fork-inherited event prefix. The exact prefix @@ -321,8 +325,10 @@ export interface SessionEventMap { * The rendered system prompt on the model-visible surface. The loop appends * the first one as surface node 0 before the step's first `user/message`. * A prepared in-history route can append nonempty changes in a continuing - * series. An incapable route or new series normalizes text to the first system - * node. Normalization empties nonempty later nodes, then rewrites the head if + * series. An incapable route, or a new series whose effective text differs, + * normalizes text to the first system node; a series start whose latest + * surviving node already holds the rendering leaves it in place. + * Normalization empties nonempty later nodes, then rewrites the head if * needed, through logged per-node replacements. An empty rendering always * clears all active system nodes, leaving no older instructions model-visible. * Empty later nodes are dormant and project to no message; an empty head with diff --git a/packages/core/session/tests/fork.spec.ts b/packages/core/session/tests/fork.spec.ts index 1fa38bc2e1..d5855b626d 100644 --- a/packages/core/session/tests/fork.spec.ts +++ b/packages/core/session/tests/fork.spec.ts @@ -59,17 +59,17 @@ function inherited(session: Session): readonly SessionEvent[] { } describe('SessionStore.fork', () => { - it('forks an empty live session as an empty child with lineage metadata', async () => { + it('forks an empty live session as an independent empty seeded session', async () => { const { ctx, sessions } = await setup() const source = ctx.sessions.create(SessionId('empty-parent'), { meta: { cwd: '/workspace' } }) const child = sessions.fork(source, undefined, SessionId('empty-child')) expect(inherited(child)).toEqual([]) + expect(child.header).not.toHaveProperty('parentSession') expect(child.header).toMatchObject({ id: SessionId('empty-child'), cwd: '/workspace', - parentSession: SessionId('empty-parent'), isSeeded: true, }) expect(child.inheritedEventCount).toBe(0) @@ -91,10 +91,10 @@ describe('SessionStore.fork', () => { }).toThrow(TypeError) expect(firstUserMessage(source.snapshotEvents()).data.content).toEqual([{ type: 'text', text: 'hello' }]) expect(firstUserMessage(child.snapshotEvents()).data.content).toEqual([{ type: 'text', text: 'hello' }]) + expect(child.header).not.toHaveProperty('parentSession') expect(child.header).toMatchObject({ id: SessionId('child'), cwd: '/workspace', - parentSession: SessionId('parent'), isSeeded: true, }) expect(child.inheritedEventCount).toBe(source.seq) diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index 757f00eec7..293a721008 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/README.i18n.yaml @@ -66,8 +66,8 @@ en: 29722e212137deaa zh: 92cd0f1a16b10315 /deepseek-ai-dsh-system-prompt/model-experience/system-prompt/kv-cache-effect: - en: ae980c28b57dc060 - zh: 331eea5b204fa9ac + en: 52fa5a522891ec6b + zh: 5c5c18359aaaf3db /deepseek-ai-dsh-system-prompt/model-experience/tool-schemas: en: 3039bc5e7a796b21 zh: 6b7a9eaf69c9ea53 diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 777f4202d2..48b43666ee 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -153,7 +153,7 @@ Identity is a fixed per-request cost when enabled. Persona prefixes, suffixes, a #### KV Cache effect -Prefix-stable while identity, persona, variables, section text, and order render identically: an unchanged rendering leaves the system nodes untouched unless an incapable route or a new request series must consolidate retained in-history prompts. Without `systemPromptUpdate`, non-empty prompt text is consolidated at the first system node through logged per-node replacements, so a head rewrite loses prefix reuse from its first changed token; when the prepared call declares `systemPromptUpdate: 'in-history'`, the agent loop appends a non-empty changed prompt after the cached history inside a continuing request series, so the prefix through that history stays reusable ([decision rule](../agent-loop/README.md#understand-the-implementation)). With the same model, persona prefix, tools, and preceding instructions, different source paths, local Web URLs, or persona suffix values leave the reusable first-party prefix unchanged. Persona prefix changes can alter the early prefix. Any change may invalidate reuse from the first changed token; provider cache sharing and measured hit rates are not guaranteed. +Prefix-stable while identity, persona, variables, section text, and order render identically: an unchanged rendering leaves the system nodes untouched, including at a new request series on a capable route; only an incapable route, or a series start whose effective text differs, consolidates retained in-history prompts. Without `systemPromptUpdate`, non-empty prompt text is consolidated at the first system node through logged per-node replacements, so a head rewrite loses prefix reuse from its first changed token; when the prepared call declares `systemPromptUpdate: 'in-history'`, the agent loop appends a non-empty changed prompt after the cached history inside a continuing request series, so the prefix through that history stays reusable ([decision rule](../agent-loop/README.md#understand-the-implementation)). With the same model, persona prefix, tools, and preceding instructions, different source paths, local Web URLs, or persona suffix values leave the reusable first-party prefix unchanged. Persona prefix changes can alter the early prefix. Any change may invalidate reuse from the first changed token; provider cache sharing and measured hit rates are not guaranteed. ### Tool schemas diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index 07c04be1a3..3964b4af17 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -153,7 +153,7 @@ You are an AI agent powered by DeepSeek Harness. #### KV Cache 影响 -只要身份、persona、变量、段文本与顺序的渲染完全相同,前缀就保持稳定:渲染未变时系统节点保持不动,除非不支持该能力的路由或新请求序列必须归并保留的历史内提示词。没有 `systemPromptUpdate` 时,非空提示词文本通过有日志记录的逐节点替换归并到首个系统节点,因此头节点重写会从首个变化的 token 起失去前缀复用;当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,agent loop(智能体循环)会在同一请求序列延续期间把变化后的非空提示词追加到已缓存历史之后,因此直到该历史末尾的前缀仍可复用([决策规则](../agent-loop/README.zh.md#understand-the-implementation))。模型、persona 前缀、工具与前置指令一致时,不同源码路径、本地 Web URL 或 persona 后缀值不会改变可复用的第一方前缀。persona 前缀变化可能改变靠前的前缀。任何变更都可能从第一个变化的 token 起使复用失效;不保证提供方共享缓存或实际命中率。 +只要身份、persona、变量、段文本与顺序的渲染完全相同,前缀就保持稳定:渲染未变时系统节点保持不动,具备能力路由的新请求序列也不例外;只有不支持该能力的路由,或有效文本不同的新序列开始,才归并保留的历史内提示词。没有 `systemPromptUpdate` 时,非空提示词文本通过有日志记录的逐节点替换归并到首个系统节点,因此头节点重写会从首个变化的 token 起失去前缀复用;当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,agent loop(智能体循环)会在同一请求序列延续期间把变化后的非空提示词追加到已缓存历史之后,因此直到该历史末尾的前缀仍可复用([决策规则](../agent-loop/README.zh.md#understand-the-implementation))。模型、persona 前缀、工具与前置指令一致时,不同源码路径、本地 Web URL 或 persona 后缀值不会改变可复用的第一方前缀。persona 前缀变化可能改变靠前的前缀。任何变更都可能从第一个变化的 token 起使复用失效;不保证提供方共享缓存或实际命中率。 ### 工具 schema diff --git a/packages/core/system-prompt/locale/de.json b/packages/core/system-prompt/locale/de.json new file mode 100644 index 0000000000..f0e712b2af --- /dev/null +++ b/packages/core/system-prompt/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Systemaufforderungswortzusammenstellung", + "description": "Fassen Sie den Wortinhalt der Systemaufforderung zusammen, der von jedem Plug-In bereitgestellt wird." + } +} diff --git a/packages/core/system-prompt/locale/es.json b/packages/core/system-prompt/locale/es.json new file mode 100644 index 0000000000..ae225def38 --- /dev/null +++ b/packages/core/system-prompt/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conjunto de palabras del mensaje del sistema", + "description": "Resuma el contenido de las palabras del mensaje del sistema proporcionado por cada complemento." + } +} diff --git a/packages/core/system-prompt/locale/fr.json b/packages/core/system-prompt/locale/fr.json new file mode 100644 index 0000000000..d00e251ab5 --- /dev/null +++ b/packages/core/system-prompt/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Assemblage de mots d’invite système", + "description": "Résumez le contenu des mots d’invite système fournis par chaque plug-in." + } +} diff --git a/packages/core/system-prompt/locale/pt.json b/packages/core/system-prompt/locale/pt.json new file mode 100644 index 0000000000..9b6b4cbed8 --- /dev/null +++ b/packages/core/system-prompt/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Montagem de palavras de prompt do sistema", + "description": "Resuma o conteúdo do prompt do sistema fornecido por cada plug-in." + } +} diff --git a/packages/core/system-prompt/locale/ru.json b/packages/core/system-prompt/locale/ru.json new file mode 100644 index 0000000000..fd6b109101 --- /dev/null +++ b/packages/core/system-prompt/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Сборка слов системной подсказки", + "description": "Обобщите содержание слов системной подсказки, предоставляемое каждым подключаемым модулем." + } +} diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 0998a2aa15..22d6ede357 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/tool-executor-native/locale/de.json b/packages/core/tool-executor-native/locale/de.json new file mode 100644 index 0000000000..1667c6d05a --- /dev/null +++ b/packages/core/tool-executor-native/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Native Tool-Ausführung", + "description": "Führt Modell-Tool-Aufrufe direkt aus." + } +} diff --git a/packages/core/tool-executor-native/locale/es.json b/packages/core/tool-executor-native/locale/es.json new file mode 100644 index 0000000000..88ee9cab99 --- /dev/null +++ b/packages/core/tool-executor-native/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ejecución de herramientas nativas", + "description": "Ejecuta llamadas a herramientas de modelo directamente." + } +} diff --git a/packages/core/tool-executor-native/locale/fr.json b/packages/core/tool-executor-native/locale/fr.json new file mode 100644 index 0000000000..57b5c6fabc --- /dev/null +++ b/packages/core/tool-executor-native/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exécution d'outils natifs", + "description": "Exécute directement les appels de l’outil de modèle." + } +} diff --git a/packages/core/tool-executor-native/locale/pt.json b/packages/core/tool-executor-native/locale/pt.json new file mode 100644 index 0000000000..7830a29c8c --- /dev/null +++ b/packages/core/tool-executor-native/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Execução de ferramenta nativa", + "description": "Executa chamadas de ferramenta de modelo diretamente." + } +} diff --git a/packages/core/tool-executor-native/locale/ru.json b/packages/core/tool-executor-native/locale/ru.json new file mode 100644 index 0000000000..c8d00a0149 --- /dev/null +++ b/packages/core/tool-executor-native/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Исполнение собственного инструмента", + "description": "Выполняет вызовы инструментов модели напрямую." + } +} diff --git a/packages/core/tool-executor-native/package.json b/packages/core/tool-executor-native/package.json index 2ce899c77a..848555ad8d 100644 --- a/packages/core/tool-executor-native/package.json +++ b/packages/core/tool-executor-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-executor-native", "description": "Native tool execution plugin", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/tool-executor-ptc/locale/de.json b/packages/core/tool-executor-ptc/locale/de.json new file mode 100644 index 0000000000..9e1a4e6a95 --- /dev/null +++ b/packages/core/tool-executor-ptc/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "PTC Werkzeugausführung", + "description": "Führt Tools über run_code-Programme aus und generiert SDK-Bindungen." + } +} diff --git a/packages/core/tool-executor-ptc/locale/es.json b/packages/core/tool-executor-ptc/locale/es.json new file mode 100644 index 0000000000..e21cbb9f8b --- /dev/null +++ b/packages/core/tool-executor-ptc/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ejecución de la herramienta PTC", + "description": "Ejecuta herramientas a través de programas run_code y genera enlaces SDK." + } +} diff --git a/packages/core/tool-executor-ptc/locale/fr.json b/packages/core/tool-executor-ptc/locale/fr.json new file mode 100644 index 0000000000..69d02cea40 --- /dev/null +++ b/packages/core/tool-executor-ptc/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exécution de l'outil PTC", + "description": "Exécute des outils via des programmes run_code et génère des liaisons SDK." + } +} diff --git a/packages/core/tool-executor-ptc/locale/pt.json b/packages/core/tool-executor-ptc/locale/pt.json new file mode 100644 index 0000000000..5ae360ee49 --- /dev/null +++ b/packages/core/tool-executor-ptc/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "PTC execução da ferramenta", + "description": "Executa ferramentas por meio de programas run_code e gera vinculações SDK." + } +} diff --git a/packages/core/tool-executor-ptc/locale/ru.json b/packages/core/tool-executor-ptc/locale/ru.json new file mode 100644 index 0000000000..29d0df4c58 --- /dev/null +++ b/packages/core/tool-executor-ptc/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "PTC выполнение инструмента", + "description": "Выполняет инструменты через программы run_code и генерирует привязки SDK." + } +} diff --git a/packages/core/tool-executor-ptc/package.json b/packages/core/tool-executor-ptc/package.json index c52063e814..c4e5a51d49 100644 --- a/packages/core/tool-executor-ptc/package.json +++ b/packages/core/tool-executor-ptc/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-executor-ptc", "description": "PTC program execution, tool bindings, and generated SDK prompts", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 771d7514e9..68b03222e1 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -18,8 +18,8 @@ en: d382227271bf92ae zh: 1f66b181418c91b9 /deepseek-ai-dsh-tools/use-this-package/register-a-tool: - en: 9d2e6caa39c53d62 - zh: 096a777f828c7565 + en: 1808e2a95ee21584 + zh: 40c40da7d1418407 /deepseek-ai-dsh-tools/use-this-package/conditional-tools: en: eb80334754c2493a zh: b431635ec1f4664f diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 22473d0c21..a0ce675371 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -63,6 +63,7 @@ ctx.tools.register(defineTool({ The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; `InferValue` preserves exact types through 16 container levels before widening to `JsonValue`. A raw JSON Schema (`JsonSchemaNode`) is the wire-level counterpart shared with subagents, workflows, and MCP. + ### Conditional tools `register(definition, { when })` evaluates a synchronous or asynchronous condition once for each model step. The condition receives `{ agent, modelSelection, signal }`; `modelSelection` is the provider, model, and reasoning effort that step preparation captured for every request attempt in the step, the same value as `AssembleContext.modelSelection`. False omits the tool from the executor's description, PTC bindings, and execution. A thrown condition fails step preparation as `TOOL_ACTIVATION_FAILED`; it is not treated as ordinary unavailability. Conditions must observe cancellation and must not change registrations or perform tool work. diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index 0f621304c7..55e9c50020 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -63,6 +63,7 @@ ctx.tools.register(defineTool({ 统一 schema DSL 支持 `string`、`number`、`integer`、`boolean`、`null`、`array`、`object`、仅供作者使用的 `json` 与恰好匹配一个分支的 `oneOf`;`InferValue` 在 16 层容器内保留精确类型,之后加宽为 `JsonValue`。原始 JSON Schema(`JsonSchemaNode`)是与 subagent、工作流和 MCP 共享的协议级对应类型。 + ### 条件工具 `register(definition, { when })` 为每个模型步骤计算一次同步或异步条件。条件接收 `{ agent, modelSelection, signal }`;`modelSelection` 是步骤准备时为该步骤所有请求尝试捕获的提供方、模型与推理强度,与 `AssembleContext.modelSelection` 相同。返回 false 会从执行器的描述、PTC 绑定和执行集合中排除工具。条件抛错会以 `TOOL_ACTIVATION_FAILED` 中止步骤准备,不会被当成正常不可用。条件必须响应取消,不得改变注册或执行工具操作。 diff --git a/packages/core/tools/locale/de.json b/packages/core/tools/locale/de.json new file mode 100644 index 0000000000..3af3606a63 --- /dev/null +++ b/packages/core/tools/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Werkzeuge", + "description": "Verwalten Sie verfügbare Tools und deren Aufrufausführungsprozesse." + } +} diff --git a/packages/core/tools/locale/es.json b/packages/core/tools/locale/es.json new file mode 100644 index 0000000000..59ba250932 --- /dev/null +++ b/packages/core/tools/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "herramientas", + "description": "Gestionar las herramientas disponibles y sus procesos de ejecución de invocaciones." + } +} diff --git a/packages/core/tools/locale/fr.json b/packages/core/tools/locale/fr.json new file mode 100644 index 0000000000..2c63804f7d --- /dev/null +++ b/packages/core/tools/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "outils", + "description": "Gérez les outils disponibles et leurs processus d’exécution d’appels." + } +} diff --git a/packages/core/tools/locale/pt.json b/packages/core/tools/locale/pt.json new file mode 100644 index 0000000000..3621e8e5da --- /dev/null +++ b/packages/core/tools/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ferramentas", + "description": "Gerencie as ferramentas disponíveis e seus processos de execução de invocação." + } +} diff --git a/packages/core/tools/locale/ru.json b/packages/core/tools/locale/ru.json new file mode 100644 index 0000000000..a337612e5e --- /dev/null +++ b/packages/core/tools/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "инструменты", + "description": "Управляйте доступными инструментами и процессами выполнения их вызовов." + } +} diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index f87e90cbee..633a872912 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index d571a19a80..4948fdb66f 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -31,7 +31,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'create_goal', 'create_worktree', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources', - 'list_subagent_models', 'load_workspace_dependencies', 'lsp', 'plugin_manager', 'plugin_market', 'present', 'pwsh', 'pwsh', 'ralph', + 'list_subagent_models', 'load_workspace_dependencies', 'lsp', 'plugin_manager', 'plugin_market', 'present', 'propose_action', 'pwsh', 'pwsh', 'ralph', 'read', 'read_image', 'read_mcp_resource', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update', 'send_message', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', diff --git a/packages/credentials/authorization/locale/de.json b/packages/credentials/authorization/locale/de.json new file mode 100644 index 0000000000..15d3e38ba4 --- /dev/null +++ b/packages/credentials/authorization/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Autorisierung", + "description": "Organisationen verlangen die Beteiligung des Benutzers am Anmelde- und Anmeldedaten-Autorisierungsprozess." + } +} diff --git a/packages/credentials/authorization/locale/es.json b/packages/credentials/authorization/locale/es.json new file mode 100644 index 0000000000..e0d2992b5e --- /dev/null +++ b/packages/credentials/authorization/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "autorización", + "description": "Las organizaciones requieren la participación del usuario en el proceso de inicio de sesión y autorización de credenciales." + } +} diff --git a/packages/credentials/authorization/locale/fr.json b/packages/credentials/authorization/locale/fr.json new file mode 100644 index 0000000000..2e4fc50080 --- /dev/null +++ b/packages/credentials/authorization/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "autorisation", + "description": "Les organisations exigent la participation des utilisateurs au processus de connexion et d’autorisation des informations d’identification." + } +} diff --git a/packages/credentials/authorization/locale/pt.json b/packages/credentials/authorization/locale/pt.json new file mode 100644 index 0000000000..c465157682 --- /dev/null +++ b/packages/credentials/authorization/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "autorização", + "description": "As organizações exigem a participação do usuário no processo de login e autorização de credenciais." + } +} diff --git a/packages/credentials/authorization/locale/ru.json b/packages/credentials/authorization/locale/ru.json new file mode 100644 index 0000000000..6442c5ed91 --- /dev/null +++ b/packages/credentials/authorization/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "авторизация", + "description": "Организации требуют участия пользователя в процессе входа в систему и авторизации учетных данных." + } +} diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index c2d5d895fa..944439105b 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/locale/de.json b/packages/credentials/credentials-local/locale/de.json new file mode 100644 index 0000000000..35c3fc917b --- /dev/null +++ b/packages/credentials/credentials-local/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lokaler Speicher für Anmeldeinformationen", + "description": "Speichern Sie Anmeldeinformationen wie den API-Schlüssel und lesen Sie Umgebungsvariablen und lokale Anmeldeinformationsdateien in der Reihenfolge ihrer Priorität." + } +} diff --git a/packages/credentials/credentials-local/locale/es.json b/packages/credentials/credentials-local/locale/es.json new file mode 100644 index 0000000000..4d7041d7d6 --- /dev/null +++ b/packages/credentials/credentials-local/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Almacenamiento de credenciales locales", + "description": "Guarde credenciales como la clave API y lea las variables de entorno y los archivos de credenciales locales en orden de prioridad." + } +} diff --git a/packages/credentials/credentials-local/locale/fr.json b/packages/credentials/credentials-local/locale/fr.json new file mode 100644 index 0000000000..19cd98d258 --- /dev/null +++ b/packages/credentials/credentials-local/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Stockage local des identifiants", + "description": "Enregistrez les informations d'identification telles que la clé API et lisez les variables d'environnement et les fichiers d'informations d'identification locales par ordre de priorité." + } +} diff --git a/packages/credentials/credentials-local/locale/pt.json b/packages/credentials/credentials-local/locale/pt.json new file mode 100644 index 0000000000..fcd1bd9a51 --- /dev/null +++ b/packages/credentials/credentials-local/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Armazenamento de credenciais locais", + "description": "Salve credenciais como a chave API e leia variáveis de ambiente e arquivos de credenciais locais em ordem de prioridade." + } +} diff --git a/packages/credentials/credentials-local/locale/ru.json b/packages/credentials/credentials-local/locale/ru.json new file mode 100644 index 0000000000..5f4102830a --- /dev/null +++ b/packages/credentials/credentials-local/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Локальное хранилище учетных данных", + "description": "Сохраните учетные данные, такие как ключ API, и прочитайте переменные среды и локальные файлы учетных данных в порядке приоритета." + } +} diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index f84235fef9..d36941dd48 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index 724cf68acf..0779e30160 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account-platform/locale/de.json b/packages/credentials/deepseek-account-platform/locale/de.json new file mode 100644 index 0000000000..edd924d206 --- /dev/null +++ b/packages/credentials/deepseek-account-platform/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Kontoanmeldung", + "description": "Schließen Sie die Anmeldeautorisierung für das DeepSeek-Konto über den Browser ab." + } +} diff --git a/packages/credentials/deepseek-account-platform/locale/es.json b/packages/credentials/deepseek-account-platform/locale/es.json new file mode 100644 index 0000000000..e5ce28e7cd --- /dev/null +++ b/packages/credentials/deepseek-account-platform/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek inicio de sesión de cuenta", + "description": "Complete la autorización de inicio de sesión de la cuenta DeepSeek a través del navegador." + } +} diff --git a/packages/credentials/deepseek-account-platform/locale/fr.json b/packages/credentials/deepseek-account-platform/locale/fr.json new file mode 100644 index 0000000000..e472dc67ec --- /dev/null +++ b/packages/credentials/deepseek-account-platform/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek connexion au compte", + "description": "Complétez l'autorisation de connexion au compte DeepSeek via le navigateur." + } +} diff --git a/packages/credentials/deepseek-account-platform/locale/pt.json b/packages/credentials/deepseek-account-platform/locale/pt.json new file mode 100644 index 0000000000..2653ad2013 --- /dev/null +++ b/packages/credentials/deepseek-account-platform/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek login da conta", + "description": "Conclua a autorização de login da conta DeepSeek através do navegador." + } +} diff --git a/packages/credentials/deepseek-account-platform/locale/ru.json b/packages/credentials/deepseek-account-platform/locale/ru.json new file mode 100644 index 0000000000..3d5fff9ff8 --- /dev/null +++ b/packages/credentials/deepseek-account-platform/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek вход в аккаунт", + "description": "Завершите авторизацию входа в учетную запись DeepSeek через браузер." + } +} diff --git a/packages/credentials/deepseek-account-platform/package.json b/packages/credentials/deepseek-account-platform/package.json index f033e54f58..62451d7d04 100644 --- a/packages/credentials/deepseek-account-platform/package.json +++ b/packages/credentials/deepseek-account-platform/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account-platform", "description": "Authorize DeepSeek accounts through browser PKCE", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account-platform/tests/account.spec.ts b/packages/credentials/deepseek-account-platform/tests/account.spec.ts index 359126b30d..590fb0b7be 100644 --- a/packages/credentials/deepseek-account-platform/tests/account.spec.ts +++ b/packages/credentials/deepseek-account-platform/tests/account.spec.ts @@ -1350,7 +1350,7 @@ it('rejects callback requests whose raw URL is absent or malformed', async () => await f.account.startSignIn(clientMetadata(), f.callbackOrigin, 'web') await f.wait('waiting-browser') const route = register.mock.calls.find(([route]) => route.path === '/oauth/callback')?.[0] - if (route === undefined) throw new Error('missing callback route') + if (route === undefined || !('handler' in route)) throw new Error('missing callback route') for (const url of [undefined, 'http://[']) { const socket = new Socket() const req = new IncomingMessage(socket) @@ -1372,7 +1372,7 @@ it('settles an attempt after its browser callback connection closes during excha const response = Promise.withResolvers() const serverClosed = Promise.withResolvers() const original = f.ctx.webServer.register.bind(f.ctx.webServer) - const register = vi.spyOn(f.ctx.webServer, 'register').mockImplementation(route => original({ + const register = vi.spyOn(f.ctx.webServer, 'register').mockImplementation(route => 'resource' in route ? original(route) : original({ ...route, handler: (req, res) => { response.resolve(res) res.once('error', () => undefined) diff --git a/packages/credentials/deepseek-account/package.json b/packages/credentials/deepseek-account/package.json index e3207d57d8..c7fb418d6d 100644 --- a/packages/credentials/deepseek-account/package.json +++ b/packages/credentials/deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account", "description": "Read account state and resolve official API credentials", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/deliverable-file-references/locale/de.json b/packages/deliverables/deliverable-file-references/locale/de.json new file mode 100644 index 0000000000..aac9344c51 --- /dev/null +++ b/packages/deliverables/deliverable-file-references/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lieferbare Dateireferenzen", + "description": "Stellen Sie gemeinsame Modellanleitungen für Dateiverknüpfungen und Ergebnisse in allen Anwendungsumgebungen bereit." + } +} diff --git a/packages/deliverables/deliverable-file-references/locale/es.json b/packages/deliverables/deliverable-file-references/locale/es.json new file mode 100644 index 0000000000..9b8bd45ba8 --- /dev/null +++ b/packages/deliverables/deliverable-file-references/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Referencias de archivos entregables", + "description": "Proporcione orientación de modelo compartido para enlaces de archivos y entregables en entornos de aplicaciones." + } +} diff --git a/packages/deliverables/deliverable-file-references/locale/fr.json b/packages/deliverables/deliverable-file-references/locale/fr.json new file mode 100644 index 0000000000..75ed3b038e --- /dev/null +++ b/packages/deliverables/deliverable-file-references/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Références des fichiers livrables", + "description": "Fournissez des conseils de modèle partagé pour les liens de fichiers et les livrables dans les environnements d’application." + } +} diff --git a/packages/deliverables/deliverable-file-references/locale/pt.json b/packages/deliverables/deliverable-file-references/locale/pt.json new file mode 100644 index 0000000000..73c6f06736 --- /dev/null +++ b/packages/deliverables/deliverable-file-references/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Referências de arquivos entregáveis", + "description": "Forneça orientação de modelo compartilhado para links de arquivos e resultados em ambientes de aplicativos." + } +} diff --git a/packages/deliverables/deliverable-file-references/locale/ru.json b/packages/deliverables/deliverable-file-references/locale/ru.json new file mode 100644 index 0000000000..39494e5868 --- /dev/null +++ b/packages/deliverables/deliverable-file-references/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ссылки на доставляемые файлы", + "description": "Предоставьте руководство по общей модели для ссылок на файлы и результатов в различных средах приложений." + } +} diff --git a/packages/deliverables/deliverable-file-references/package.json b/packages/deliverables/deliverable-file-references/package.json index 12ef353746..83e6910ff9 100644 --- a/packages/deliverables/deliverable-file-references/package.json +++ b/packages/deliverables/deliverable-file-references/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deliverable-file-references", "description": "Shared model guidance for file links and deliverables across DeepSeek Harness hosts", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/tool-present/locale/de.json b/packages/deliverables/tool-present/locale/de.json new file mode 100644 index 0000000000..9c4095a246 --- /dev/null +++ b/packages/deliverables/tool-present/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tool zur Dateiübermittlung", + "description": "Lassen Sie die KI in dieser Runde Arbeitsbereichsdateien deutlich als zu erbringende Leistungen kennzeichnen." + } +} diff --git a/packages/deliverables/tool-present/locale/es.json b/packages/deliverables/tool-present/locale/es.json new file mode 100644 index 0000000000..1b99d7083b --- /dev/null +++ b/packages/deliverables/tool-present/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "herramienta de entrega de archivos", + "description": "Dejemos que la IA marque claramente los archivos del espacio de trabajo como entregables en esta ronda." + } +} diff --git a/packages/deliverables/tool-present/locale/fr.json b/packages/deliverables/tool-present/locale/fr.json new file mode 100644 index 0000000000..59373241a1 --- /dev/null +++ b/packages/deliverables/tool-present/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Outil de livraison de fichiers", + "description": "Laissez l’IA marquer clairement les fichiers de l’espace de travail comme livrables ce cycle-ci." + } +} diff --git a/packages/deliverables/tool-present/locale/pt.json b/packages/deliverables/tool-present/locale/pt.json new file mode 100644 index 0000000000..bce97776d9 --- /dev/null +++ b/packages/deliverables/tool-present/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ferramenta de entrega de arquivos", + "description": "Deixe a IA marcar claramente os arquivos do espaço de trabalho como resultados nesta rodada." + } +} diff --git a/packages/deliverables/tool-present/locale/ru.json b/packages/deliverables/tool-present/locale/ru.json new file mode 100644 index 0000000000..d6da55fd7a --- /dev/null +++ b/packages/deliverables/tool-present/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Инструмент доставки файлов", + "description": "Пусть в этом раунде ИИ четко отметит файлы рабочей области как результаты." + } +} diff --git a/packages/deliverables/tool-present/package.json b/packages/deliverables/tool-present/package.json index ba38ec84c8..021b9fd26b 100644 --- a/packages/deliverables/tool-present/package.json +++ b/packages/deliverables/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/workspace-changes/locale/de.json b/packages/deliverables/workspace-changes/locale/de.json new file mode 100644 index 0000000000..53d0359650 --- /dev/null +++ b/packages/deliverables/workspace-changes/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Änderungsverlauf des Arbeitsbereichs", + "description": "Zeichnen Sie Dateiänderungen in jeder Aufgabenrunde auf, um Datei für Datei zu vergleichen." + } +} diff --git a/packages/deliverables/workspace-changes/locale/es.json b/packages/deliverables/workspace-changes/locale/es.json new file mode 100644 index 0000000000..d09428c212 --- /dev/null +++ b/packages/deliverables/workspace-changes/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Historial de cambios en el espacio de trabajo", + "description": "Registre los cambios de archivos en cada ronda de tareas para comparar archivo por archivo." + } +} diff --git a/packages/deliverables/workspace-changes/locale/fr.json b/packages/deliverables/workspace-changes/locale/fr.json new file mode 100644 index 0000000000..d44c231a7a --- /dev/null +++ b/packages/deliverables/workspace-changes/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Historique des modifications de l'espace de travail", + "description": "Enregistrez les modifications apportées aux fichiers à chaque série de tâches pour une comparaison fichier par fichier." + } +} diff --git a/packages/deliverables/workspace-changes/locale/pt.json b/packages/deliverables/workspace-changes/locale/pt.json new file mode 100644 index 0000000000..6d8e74a908 --- /dev/null +++ b/packages/deliverables/workspace-changes/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Histórico de alterações do espaço de trabalho", + "description": "Registre as alterações nos arquivos em cada rodada de tarefas para comparação arquivo por arquivo." + } +} diff --git a/packages/deliverables/workspace-changes/locale/ru.json b/packages/deliverables/workspace-changes/locale/ru.json new file mode 100644 index 0000000000..b12ec1a9b2 --- /dev/null +++ b/packages/deliverables/workspace-changes/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "История изменений рабочей области", + "description": "Записывайте изменения файлов в каждом раунде задач для сравнения файлов по файлам." + } +} diff --git a/packages/deliverables/workspace-changes/package.json b/packages/deliverables/workspace-changes/package.json index 7a5e83f25e..9339ce365e 100644 --- a/packages/deliverables/workspace-changes/package.json +++ b/packages/deliverables/workspace-changes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace-changes", "description": "Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the DeepSeek Harness", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/document/office-to-pdf/locale/de.json b/packages/document/office-to-pdf/locale/de.json new file mode 100644 index 0000000000..cbcfec7db1 --- /dev/null +++ b/packages/document/office-to-pdf/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Office zu PDF", + "description": "Konvertieren Sie Dokumente wie Word, Excel und PowerPoint zur Vorschau in PDF." + } +} diff --git a/packages/document/office-to-pdf/locale/es.json b/packages/document/office-to-pdf/locale/es.json new file mode 100644 index 0000000000..5490b4c9e1 --- /dev/null +++ b/packages/document/office-to-pdf/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Oficina a PDF", + "description": "Convierta documentos como Word, Excel y PowerPoint a PDF para obtener una vista previa." + } +} diff --git a/packages/document/office-to-pdf/locale/fr.json b/packages/document/office-to-pdf/locale/fr.json new file mode 100644 index 0000000000..b70e479d75 --- /dev/null +++ b/packages/document/office-to-pdf/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Bureau en PDF", + "description": "Convertissez des documents tels que Word, Excel et PowerPoint en PDF pour un aperçu." + } +} diff --git a/packages/document/office-to-pdf/locale/pt.json b/packages/document/office-to-pdf/locale/pt.json new file mode 100644 index 0000000000..ca55632ba7 --- /dev/null +++ b/packages/document/office-to-pdf/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Escritório para PDF", + "description": "Converta documentos como Word, Excel e PowerPoint em PDF para visualização." + } +} diff --git a/packages/document/office-to-pdf/locale/ru.json b/packages/document/office-to-pdf/locale/ru.json new file mode 100644 index 0000000000..2082ca887d --- /dev/null +++ b/packages/document/office-to-pdf/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Офис в PDF", + "description": "Преобразуйте такие документы, как Word, Excel и PowerPoint, в PDF для предварительного просмотра." + } +} diff --git a/packages/document/office-to-pdf/package.json b/packages/document/office-to-pdf/package.json index ca84e66374..c610e7c7f6 100644 --- a/packages/document/office-to-pdf/package.json +++ b/packages/document/office-to-pdf/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-office-to-pdf", "description": "Shared Office-to-PDF conversion with bounded queues and caching", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/README.i18n.yaml b/packages/experimental/README.i18n.yaml index 04807a8205..21f50eee7a 100644 --- a/packages/experimental/README.i18n.yaml +++ b/packages/experimental/README.i18n.yaml @@ -18,8 +18,8 @@ en: b55aeb927737b90d zh: 0b215fcb5c2c7908 /packages-experimental/packages: - en: b16e94185c9b8b24 - zh: 784faa79e027cd14 + en: 036d034ac12d686c + zh: c748a9230fefee86 /packages-experimental/related-documentation: en: 1cb9d3448d44e48d zh: cd326d70a746b896 diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 6240792eda..4d89dd0faf 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -36,6 +36,13 @@ Promotion requires a named product role, documented public behavior and limitati | Package | Role | ctx key | |---|---|---| +| [`action-cards`](action-cards/README.md) | Optional action cards below answers: the proposal tool, card state, user actions and status notices | `ctx.actionCards` | +| [`client-ui-action-cards`](client-ui-action-cards/README.md) | Card frame below completed answers, kind bodies and the proposal process row | — | +| [`creation-recommendations`](creation-recommendations/README.md) | Explicit-need creation of persistent removable interface plugins | — | +| [`client-ui-creation-recommendations`](client-ui-creation-recommendations/README.md) | Personalized page card body with location selection and undo | — | +| [`plugin-recommendations`](plugin-recommendations/README.md) | Explicit-need official plugin recommendations in conversations | `ctx.pluginRecommendations` | +| [`client-ui-plugin-recommendations`](client-ui-plugin-recommendations/README.md) | Official plugin card body with catalog text and enabling | — | +| [`personalized-recommendations`](personalized-recommendations/README.md) | Default-disabled bundle of personalized pages, plugin recommendations and their action cards | — | | [`tool-session-query`](tool-session-query/README.md) | Workspace-authorized model session search, trace, and event reads | — | | [`session-search`](session-search/README.md) | Optional experimental global session search tools | — | | [`ralph-bundle`](ralph-bundle/README.md) | Optional repeated delegation with an isolated workflow engine | — | @@ -58,7 +65,8 @@ Promotion requires a named product role, documented public behavior and limitati | [`speech-to-text-sensevoice`](speech-to-text-sensevoice/README.md) | Managed local SenseVoice inference | — | | [`api-speech-to-text`](api-speech-to-text/README.md) | Authenticated transient transcription Remote | `ctx.speechController` | | [`client-ui-voice-input`](client-ui-voice-input/README.md) | Microphone capture and guarded draft insertion | — | -| [`plugin-market-bundle`](plugin-market-bundle/README.md) | Default-disabled optional custom plugin markets: the market service and the Discover tab | — | +| [`plugin-market-bundle`](plugin-market-bundle/README.md) | Default-disabled optional custom plugin indexes: the index service and the Discover tab | — | +| [`liveshare-protocol`](liveshare-protocol/README.md) | Wire frames and the identity contract shared by the owner bundle and the Relay | — | | [`voice-input-bundle`](voice-input-bundle/README.md) | Default-disabled optional voice input composition | — | | [`agent-team-profile`](agent-team-profile/README.md) | Agent Teams collaboration, tools, and Web UI bundle | — | | [`agent-team`](agent-team/README.md) | Named teammates with direct messages and a durable shared task board | `ctx.agentTeams` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index e1fc68e084..d979466bd1 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -36,6 +36,13 @@ kind: "package-group" | 包 | 职责 | ctx 键 | |---|---|---| +| [`action-cards`](action-cards/README.zh.md) | 回答下方的可选操作卡片:提议工具、卡片状态、用户操作与状态通知 | `ctx.actionCards` | +| [`client-ui-action-cards`](client-ui-action-cards/README.zh.md) | 已完成回答下方的卡片框架、种类正文与提议过程行 | — | +| [`creation-recommendations`](creation-recommendations/README.zh.md) | 针对明确需求创建持续可用、可移除的界面插件 | — | +| [`client-ui-creation-recommendations`](client-ui-creation-recommendations/README.zh.md) | 个性化页面卡片正文、位置选择和撤销 | — | +| [`plugin-recommendations`](plugin-recommendations/README.zh.md) | 在对话中针对明确需求推荐官方插件 | `ctx.pluginRecommendations` | +| [`client-ui-plugin-recommendations`](client-ui-plugin-recommendations/README.zh.md) | 官方插件卡片正文:目录文本与启用 | — | +| [`personalized-recommendations`](personalized-recommendations/README.zh.md) | 默认禁用的个性化页面、插件推荐及其操作卡片组合 | — | | [`tool-session-query`](tool-session-query/README.zh.md) | 按工作区授权的模型会话搜索、追踪与事件读取 | — | | [`session-search`](session-search/README.zh.md) | 可选实验性全局会话搜索工具 | — | | [`ralph-bundle`](ralph-bundle/README.zh.md) | 带隔离 workflow 引擎的可选重复委派 | — | @@ -58,7 +65,8 @@ kind: "package-group" | [`speech-to-text-sensevoice`](speech-to-text-sensevoice/README.zh.md) | 托管本地 SenseVoice 推理 | — | | [`api-speech-to-text`](api-speech-to-text/README.zh.md) | 带认证的临时转写 Remote | `ctx.speechController` | | [`client-ui-voice-input`](client-ui-voice-input/README.zh.md) | 麦克风录音与版本检查后的草稿插入 | — | -| [`plugin-market-bundle`](plugin-market-bundle/README.zh.md) | 默认禁用的可选自定义插件市场:市场服务和「发现」标签页 | — | +| [`plugin-market-bundle`](plugin-market-bundle/README.zh.md) | 默认禁用的可选自定义插件索引:索引服务和「发现」标签页 | — | +| [`liveshare-protocol`](liveshare-protocol/README.zh.md) | owner bundle 与 Relay 共用的线路帧与身份契约 | — | | [`voice-input-bundle`](voice-input-bundle/README.zh.md) | 默认禁用的可选语音输入组合 | — | | [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 协作、工具与 Web UI 组合包 | — | | [`agent-team`](agent-team/README.zh.md) | 具名 teammate,成员之间直接消息与持久共享任务板 | `ctx.agentTeams` | diff --git a/packages/experimental/action-cards/README.i18n.yaml b/packages/experimental/action-cards/README.i18n.yaml new file mode 100644 index 0000000000..142162f890 --- /dev/null +++ b/packages/experimental/action-cards/README.i18n.yaml @@ -0,0 +1,67 @@ +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/experimental/action-cards/README.md +/: + en: 1a68a7a733a2676d + zh: 01f72eb61a0626b7 +/deepseek-ai-dsh-action-cards: + en: 63bf9077e164f6fe + zh: 8b696bb8c3d6fcd4 +/deepseek-ai-dsh-action-cards/summary: + en: 7621088773d073f4 + zh: 57674e6dcee15997 +/deepseek-ai-dsh-action-cards/table-of-contents: + en: d152484eb41ac6b4 + zh: b00dddf4c88f0a45 +/deepseek-ai-dsh-action-cards/use-this-package: + en: e239e255a6692f95 + zh: ae8e359e37e484b3 +/deepseek-ai-dsh-action-cards/use-this-package/register-a-card-kind: + en: 8432c02ef18eda11 + zh: 6d43a0a32edd7e30 +/deepseek-ai-dsh-action-cards/use-this-package/card-rules: + en: 2f5b3ccb91f3b30e + zh: 9bd877d9d92615da +/deepseek-ai-dsh-action-cards/understand-the-implementation: + en: d92243d71d2106c1 + zh: 47f0840fa723bb31 +/deepseek-ai-dsh-action-cards/further-exploration: + en: 1966ab8d7063c450 + zh: 033c2c0d258a257f +/deepseek-ai-dsh-action-cards/model-experience: + en: 215e7ba838619b7b + zh: a311da3843709f90 +/deepseek-ai-dsh-action-cards/model-experience/card-tool: + en: 4925bc5f7153825a + zh: 3a7bfb2210d9c6aa +/deepseek-ai-dsh-action-cards/model-experience/card-tool/what-the-model-sees: + en: 58521d3992ce42ec + zh: 45681f7b43e67613 +/deepseek-ai-dsh-action-cards/model-experience/card-tool/token-effect: + en: 7efaaad09be98a07 + zh: 86c7fa13d60c3588 +/deepseek-ai-dsh-action-cards/model-experience/card-tool/kv-cache-effect: + en: d4192e35d47b5fa3 + zh: c786828e123de657 +/deepseek-ai-dsh-action-cards/model-experience/card-status-notice: + en: 407ebca5eb94992e + zh: 779819ea83974103 +/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/what-the-model-sees: + en: a14013ca3ddb5ee3 + zh: 18cd7ffc62fd6590 +/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/what-the-model-sees/notice-preamble: + en: 04dd9469bfaff052 + zh: 35110b231b8e3928 +/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/token-effect: + en: 4e73a2cff60b5e88 + zh: 4887212815e7425d +/deepseek-ai-dsh-action-cards/model-experience/card-status-notice/kv-cache-effect: + en: 58785543e2428dba + zh: 4b082f77b3b2f482 +/deepseek-ai-dsh-action-cards/known-limitations-and-deferred-work: + en: 8e3eba4e9fda82cb + zh: afaf74aad9a01533 +/deepseek-ai-dsh-action-cards/known-limitations-and-deferred-work/dev-note: + en: e67f633dc6fe7773 + zh: bd8a8936229e035e diff --git a/packages/experimental/action-cards/README.md b/packages/experimental/action-cards/README.md new file mode 100644 index 0000000000..c6ae933f1e --- /dev/null +++ b/packages/experimental/action-cards/README.md @@ -0,0 +1,137 @@ +--- +description: "Shared Host service of optional action cards: the propose_action tool, card state, user actions and status notices that experiment packages register card kinds into." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-action-cards + +English | [中文](README.zh.md) + +## Summary + +An experiment can offer one optional action below an answer, such as creating a page, without adding a tool of its own. The model loads the experiment's skill, then calls `propose_action`; the card appears after the turn ends, and nothing runs until the user acts on it. Each choice is recorded and reported to the model on its next request. The tool costs nothing until a card skill loads in the conversation. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +The [Personalized recommendations](../personalized-recommendations/README.md) bundle inserts this service and its [browser half](../client-ui-action-cards/README.md). The service has one configuration field: + +| Field | Default | Meaning | +|---|---|---| +| `queuedMessageLimit` | `2` | Queued next-turn user messages at or above which `propose_action` refuses the call, so no card is offered. | + +The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-action-cards) is the exhaustive source for accepted fields. + +### Register a card kind + +A kind package injects `actionCards` and calls `ctx.actionCards.register(kind)` from its own plugin; unloading that plugin removes the kind, and actions on its cards are then refused as unavailable, except that Not now still dismisses an unstarted card. The kind names its action, the skill that proposes it, the closing invitation guidance that an accepted call's result gives the model, a zod schema for the card's `details`, the user actions it accepts and the shared checks each needs (`global` Remote access, `writes` refused in a read-only Session), the recorded phases that still offer dismissal, optional status guidance, the decision data fields the status notice reports, an optional `reconcile` that updates the card's recorded decision from state outside the Session before every action, and the handler for every action except `dismiss`. The handler records each new decision with `ctx.actionCards.record()`, which refuses an action no kind registered and the derived `replaced` phase, and returns `accepted`, `settled` with the decision in effect, `refused` or `unconfirmed`. + +### Card rules + +- The first accepted `propose_action` call of a turn becomes the card. The tool refuses a second call in the same turn, an unregistered action, a call whose kind skill was not loaded in the current turn, a blank title or brief, details that fail the kind schema, calls dispatched from code, calls from a subagent, and any call while `queuedMessageLimit` or more user messages wait for later turns. +- A newer card of any kind replaces every unstarted earlier card. Replacement is derived from the log and never recorded. +- Events a forked Session inherits create no cards. +- Before every action, including Not now, the kind's `reconcile` updates the card from state outside the Session. Not now is then recorded for an unstarted card and for the phases the kind lists; dismissed and replaced cards accept no further action. Without its registered kind, Not now still dismisses an unstarted card, and every other action is refused as unavailable. +- Concurrent actions on one card share one operation. Actions require the card's live root Agent. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +The package default-exports the `actionCards` Remote service. It registers `propose_action` once, globally, with a `when` condition: a live root Agent becomes eligible when its own direct `skill` call loads the skill of a registered kind, while loads by subagents or from code are ignored. The flag lives in memory until the Agent is disposed. The call's `execute` checks the live root, the queued messages the projection counts against `queuedMessageLimit` and the current turn's skill loads through the turn boundary, and keeps an in-memory mark of the turn's accepted call; the logged `tool/call` and its successful `tool/result` are the durable proposal. + +The `actionCards` Session projection (state version 2) pairs each `propose_action` call with its successful result, records decisions from `plugin:action-cards/decision`, counts queued next-turn messages from inbox splices for the tool's limit, and keeps unreported decisions until a status notice whose `through` covers them enters the log. Its Client view `{ proposals, decisions }` is excluded from Session summaries. + +The Remote `act(sessionId, callId, verb, payload?)` checks the owner, the live root and the card; Not now continues without a registered kind, while other verbs need the kind, the verb and Remote access. It then shares one operation per card. The kind's `reconcile` runs first and the live root is checked again; a dismissed or replaced card then settles unchanged, Not now is recorded, a writing verb is refused in a read-only Session, and every other verb goes to the kind handler under the Agent's initiator. `record()` also accepts an action whose kind has unregistered, because Cordis disposes a plugin's effects concurrently and an unloading kind still records the outcome of work it started. An `agent/pre-step` listener adds the status notice after later listeners prepared the step. + +| Source | Contents | +|---|---| +| [`src/index.ts`](src/index.ts) | Service, tool, kind registry, Remote action and status notice | +| [`src/projection.ts`](src/projection.ts) | Session projection | +| [`src/types.ts`](src/types.ts) | Proposal, decision, view and result types | +| [`tests/`](tests/) | Activation, validation, projection, action and notice tests | + +
+ +----- + + +## Further Exploration + +- [Action card display](../client-ui-action-cards/README.md) — the browser frame, kind bodies and the process row. +- [Personalized pages](../creation-recommendations/README.md) and [Plugin recommendations](../plugin-recommendations/README.md) — the shipped card kinds. +- [Tools](../../core/tools/README.md) — conditional registration and step admission. +- [Session projection](../../session/session-projection/README.md) — the card state the browser reads. + + +## Model Experience + +### Card tool + +#### What the model sees + +The [`propose_action` declaration](../../../docs/tool-catalog.md#deepseek-aidsh-action-cards) appears only to a live root Agent whose own direct call loaded the skill of a registered kind in this Session, from the next step on, for that Agent's lifetime. Subagents never see it, and neither do PTC presets, whose skill loads are dispatched from code, so they show no cards. A kind's skill names the action and the fields of `details`. An accepted call returns `Card shown below your answer. Do not restate it. `, where `` is the kind's one- or two-sentence guidance on inviting the user to the card in the user's language, so the model writes an invitation only after a card exists. A refused call carries no invitation and returns its reason, for example `Load skill first, then call propose_action.`, `One card per turn; the earlier card stands.`, `The user has queued more messages; show no card and answer briefly.`, `Cards are offered in the main conversation only.`, `Unknown action "". Registered actions: .`, where the list reads `none` once the last kind unregistered, or the failing `details` fields. + +#### Token effect + +Before a card skill loads, the tool adds no tokens. Afterwards each request of that Agent declares it: 574 characters of JSON, about 144 tokens at the token meter's four characters per token. A card costs one extra model round trip, the call before the final answer, and its tool result: a 49-character prefix plus the kind's invitation. The Personalized pages result is 305 characters, about 77 tokens at the same rate. + +#### KV Cache effect + +Activation is append-only on routes that declare tool updates, such as DeepSeek-V4.1-Flash: when the condition turns true the loop logs a header change and a `tool-addition` developer message, and those routes receive the definition as a deferred declaration referenced at that position, so the cached prefix is kept. A route without tool updates receives the tool in its declaration list from that step on, which changes the request prefix once. Disposing the Agent drops the activation; a reopened Session declares the tool again only after a card skill loads again, logging a removal and a later addition. + +### Card status notice + +#### What the model sees + +After a decision is recorded, the next request carries one `action-cards` notice: the preamble below, the guidance sentences of each involved kind, `Continue with the user’s current request.`, and a JSON array of the changed cards with `action`, `callId`, `title`, `phase` and the data fields their kind reports. + +##### Notice preamble + +```markdown +Latest status of action cards from user actions and background work. These are status facts, not a request to start or retry work. Use these receipts instead of the earlier proposal when describing progress. +``` + +#### Token effect + +One notice per request that follows new decisions, sized by the changed cards; unchanged cards add nothing, and several decisions of one card collapse to its latest. + +#### KV Cache effect + +The notice is appended after the request's other new context and never rewrites earlier messages. The notice neither starts a turn nor wakes an idle conversation. + +## Known Limitations and Deferred Work + + + +- Activation is per live Agent and kept in memory, so after a Session reopens the tool appears again only after a card skill loads again. +- PTC presets show no cards: skill loads and calls dispatched from code never activate or reach the tool, because the log records them as program dispatches rather than tool calls. +- Cards are offered in the main conversation only. A subagent's skill load, including a Creator child's, activates nothing and its calls are refused, because only a live root can act on a card. +- Only one card is offered per turn across all kinds. A later card of any kind replaces every unstarted earlier card, so a second kind's card replaces an unstarted card of the first. +- Refusal reasons of user actions are plain strings by design: the service defines `closing`, `not-live-root`, `unknown`, `unavailable`, `access-denied` and `read-only`, each kind adds its own, and a browser body names the reasons it knows and treats others as unavailable. +- Each kind validates `details` on both sides, with its Host schema when the model proposes and before each action, and with its browser body's own schema, so the two schemas change together. + + +### Dev Note + +
+Working context for maintainers — click to expand + +None. + +
diff --git a/packages/experimental/action-cards/README.zh.md b/packages/experimental/action-cards/README.zh.md new file mode 100644 index 0000000000..c2202d58d8 --- /dev/null +++ b/packages/experimental/action-cards/README.zh.md @@ -0,0 +1,137 @@ +--- +description: "可选操作卡片的共享 Host 服务:propose_action 工具、卡片状态、用户操作和状态通知,各实验包把自己的卡片种类注册进来。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-action-cards + +[English](README.md) | 中文 + +## 概述 + +实验可以在回答下方提供一个可选操作,例如创建页面,而不必新增自己的工具。模型先加载该实验的 Skill,再调用 `propose_action`;本轮结束后卡片出现在回答下方,用户操作之前不会执行任何事。用户的每次选择都会被记录,并在模型的下一次请求中告知模型。对话中加载卡片 Skill 之前,这个工具不占用任何 token。 + +## 目录 + +- [使用本包](#use-this-package) +- [实现原理](#understand-the-implementation) +- [延伸阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +[个性化推荐](../personalized-recommendations/README.zh.md) Bundle 会插入本服务及其[浏览器端](../client-ui-action-cards/README.zh.md)。本服务只有一个配置字段: + +| 字段 | 默认值 | 含义 | +|---|---|---| +| `queuedMessageLimit` | `2` | 排队到后续轮次的用户消息达到该数量时,`propose_action` 拒绝调用,因此不会展示卡片。 | + +生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-action-cards)是可接受字段的完整来源。 + +### 注册卡片种类 + +种类所在的包注入 `actionCards`,并在自己的插件中调用 `ctx.actionCards.register(kind)`;该插件卸载时种类随之移除,此后对其卡片的操作会以 unavailable 拒绝,只有“暂不需要”仍可忽略尚未开始的卡片。种类需要给出:动作名、提议该动作的 Skill、被接受的调用结果交给模型的结尾邀请指引、卡片 `details` 的 zod 校验、接受的用户操作及各自需要的共享检查(`global` 表示需要全局 Remote 权限,`writes` 表示只读会话中拒绝)、仍可忽略卡片的已记录阶段、可选的状态说明、状态通知要报告的决定数据字段、可选的 `reconcile`(每次操作前根据会话之外的状态更新卡片的已记录决定),以及处理 `dismiss` 以外所有操作的处理器。处理器用 `ctx.actionCards.record()` 记录每个新决定(该方法拒绝没有种类注册过的动作和推导得出的 `replaced` 阶段),并返回 `accepted`、带当前决定的 `settled`、`refused` 或 `unconfirmed`。 + +### 卡片规则 + +- 一轮中第一个被接受的 `propose_action` 调用成为卡片。工具会拒绝同一轮的第二次调用、未注册的动作、本轮没有加载对应种类 Skill 的调用、空白的标题或说明、未通过种类校验的 details、从代码中发起的调用、来自 subagent 的调用,以及排队到后续轮次的用户消息达到 `queuedMessageLimit` 条时的任何调用。 +- 任何种类的新卡片都会替换此前所有尚未开始的卡片。替换从日志推导,从不单独记录。 +- fork 出的会话从父会话继承的事件不会产生卡片。 +- 每次操作(包括“暂不需要”)之前,种类的 `reconcile` 先根据会话之外的状态更新卡片。之后,对尚未开始的卡片,以及处于种类列出阶段的卡片,“暂不需要”会被记录;已忽略、已替换的卡片不再接受任何操作。卡片的种类未注册时,“暂不需要”仍可忽略尚未开始的卡片,其他操作一律以 unavailable 拒绝。 +- 同一张卡片上的并发操作共用一次处理。操作要求卡片所在会话的根 Agent 仍然在线。 + +----- + + +## 实现原理 + +
+实现细节——点击展开 + +本包默认导出 `actionCards` Remote 服务。它在全局注册一次 `propose_action`,并附带 `when` 条件:在线的根 Agent 自己直接调用 `skill` 加载了已注册种类的 Skill 后,该 Agent 才满足条件,subagent 或代码中发起的加载一律忽略;这个标记保存在内存中,直到 Agent 被释放。调用的 `execute` 检查在线根 Agent、投影累计的排队消息是否达到 `queuedMessageLimit`,并通过轮次边界检查本轮加载过的 Skill,同时在内存中标记本轮已接受的调用;日志中的 `tool/call` 及其成功的 `tool/result` 就是持久的提议。 + +`actionCards` 会话投影(状态版本 2)把每个 `propose_action` 调用与其成功结果配对,从 `plugin:action-cards/decision` 读取决定,从收件箱拼接事件累计排队到下一轮的消息供工具上限使用,并保留尚未报告的决定,直到 `through` 覆盖它们的状态通知写入日志。投影的客户端视图 `{ proposals, decisions }` 不进入会话摘要。 + +Remote `act(sessionId, callId, verb, payload?)` 先检查服务所有者、在线根 Agent 和卡片;“暂不需要”在种类未注册时也继续处理,其他操作还需要种类、操作和 Remote 权限。然后同一张卡片共用一次处理。种类的 `reconcile` 先运行,之后再次检查在线根 Agent;已忽略或已替换的卡片原样返回,“暂不需要”被记录,写入类操作在只读会话中被拒绝,其余操作在该 Agent 的发起方作用域内交给种类处理器。`record()` 也接受种类已注销的动作,因为 Cordis 并发释放插件的各个 effect,卸载中的种类仍要记录已开始工作的结果。一个 `agent/pre-step` 监听器在后续监听器准备好本步之后追加状态通知。 + +| 源码 | 内容 | +|---|---| +| [`src/index.ts`](src/index.ts) | 服务、工具、种类注册表、Remote 操作与状态通知 | +| [`src/projection.ts`](src/projection.ts) | 会话投影 | +| [`src/types.ts`](src/types.ts) | 提议、决定、视图与结果类型 | +| [`tests/`](tests/) | 激活、校验、投影、操作与通知测试 | + +
+ +----- + + +## 延伸阅读 + +- [操作卡片显示](../client-ui-action-cards/README.zh.md) — 浏览器端卡片框架、种类正文与过程行。 +- [个性化页面](../creation-recommendations/README.zh.md)和[插件推荐](../plugin-recommendations/README.zh.md) — 随产品提供的卡片种类。 +- [工具](../../core/tools/README.zh.md) — 条件注册与每步准入。 +- [会话投影](../../session/session-projection/README.zh.md) — 浏览器读取的卡片状态。 + + +## 模型体验 + +### 卡片工具 + +#### 模型看到什么 + +[`propose_action` 声明](../../../docs/tool-catalog.zh.md#deepseek-aidsh-action-cards)只对在本会话中自己直接调用加载过已注册种类 Skill 的在线根 Agent 出现,从下一步开始,持续到该 Agent 被释放。subagent 看不到它;PTC 预设的 Skill 加载由代码发起,也看不到它,因此不会展示卡片。种类的 Skill 给出动作名和 `details` 的字段。被接受的调用返回 `Card shown below your answer. Do not restate it. `,其中 `` 是种类给出的一两句指引,说明如何用用户的语言邀请用户使用卡片,因此模型只会在卡片已经存在之后写邀请。被拒绝的调用不带邀请,只返回原因,例如 `Load skill first, then call propose_action.`、`One card per turn; the earlier card stands.`、`The user has queued more messages; show no card and answer briefly.`、`Cards are offered in the main conversation only.`、`Unknown action "". Registered actions: .`(最后一个种类注销后列表为 `none`),或未通过校验的 `details` 字段。 + +#### Token 影响 + +加载卡片 Skill 之前,工具不增加任何 token。加载之后,该 Agent 的每次请求都会声明它:574 个字符的 JSON,按 token 计量器每 4 个字符计 1 个 token 约 144 个 token。出一张卡片多一次模型往返(最终回答前的这次调用)以及一条工具结果:49 个字符的前缀加上种类的邀请。个性化页面的结果为 305 个字符,按同样的比例约 77 个 token。 + +#### KV Cache 影响 + +在声明支持工具更新的路由上(例如 DeepSeek-V4.1-Flash),激活只追加内容:条件变为真时,循环记录一次请求头变化和一条 `tool-addition` 开发者消息,这些路由以延迟加载声明的形式在该位置引用新定义,因此已缓存的前缀保持不变。不支持工具更新的路由从这一步起在声明列表中收到该工具,请求前缀因此改变一次。Agent 被释放后激活随之失效;重新打开的会话要再次加载卡片 Skill 才会再次声明该工具,并依次记录一次移除和一次添加。 + +### 卡片状态通知 + +#### 模型看到什么 + +记录决定之后,下一次请求会带一条 `action-cards` 通知:下方的开头段落、所涉及各种类的说明句、`Continue with the user’s current request.`,以及一个 JSON 数组,列出发生变化的卡片的 `action`、`callId`、`title`、`phase` 和种类要报告的数据字段。 + +##### 通知开头 + +```markdown +Latest status of action cards from user actions and background work. These are status facts, not a request to start or retry work. Use these receipts instead of the earlier proposal when describing progress. +``` + +#### Token 影响 + +有新决定之后的每次请求带一条通知,大小取决于发生变化的卡片;没有变化的卡片不增加内容,同一张卡片的多次决定只报告最新一次。 + +#### KV Cache 影响 + +通知追加在本次请求其他新上下文之后,从不改写之前的消息。通知既不会开始新轮次,也不会唤醒空闲的对话。 + +## 已知限制与后续工作 + + + +- 激活按在线 Agent 记在内存中,因此会话重新打开后,要再次加载卡片 Skill 工具才会再次出现。 +- PTC 预设不会展示卡片:从代码中发起的 Skill 加载和调用既不会激活工具,也到达不了工具,因为日志把它们记为程序派发而不是工具调用。 +- 卡片只在主对话中提供。subagent(包括 Creator 子 Agent)加载 Skill 不会激活任何东西,其调用也会被拒绝,因为只有在线的根 Agent 才能对卡片进行操作。 +- 所有种类合计每轮只展示一张卡片。任何种类的新卡片都会替换此前所有尚未开始的卡片,因此第二个种类的卡片会替换第一个种类尚未开始的卡片。 +- 用户操作的拒绝原因有意设计为普通字符串:服务定义 `closing`、`not-live-root`、`unknown`、`unavailable`、`access-denied` 和 `read-only`,各种类可以添加自己的原因,浏览器端正文为已知原因显示文案,其余按 unavailable 处理。 +- 每个种类在两端各自校验 `details`:Host 端的 schema 在模型提议时和每次操作前校验,浏览器端正文用自己的 schema 校验,因此两个 schema 要一起修改。 + + +### 开发备注 + +
+维护者工作背景——点击展开 + +无。 + +
diff --git a/packages/experimental/action-cards/locale/de.json b/packages/experimental/action-cards/locale/de.json new file mode 100644 index 0000000000..1ff66c5a5a --- /dev/null +++ b/packages/experimental/action-cards/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aktionskarten", + "description": "Zeigt für aktivierte Experimente optionale Aktionskarten unter Antworten an und meldet die Entscheidungen des Benutzers an das Gespräch zurück." + } +} diff --git a/packages/experimental/action-cards/locale/en.json b/packages/experimental/action-cards/locale/en.json new file mode 100644 index 0000000000..9b5b1fdc95 --- /dev/null +++ b/packages/experimental/action-cards/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Action cards", + "description": "Show optional action cards below answers for enabled experiments, and report the user's choices back to the conversation." + } +} diff --git a/packages/experimental/action-cards/locale/es.json b/packages/experimental/action-cards/locale/es.json new file mode 100644 index 0000000000..71c4adb3ad --- /dev/null +++ b/packages/experimental/action-cards/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tarjetas de acción", + "description": "Muestra tarjetas de acción opcionales debajo de las respuestas para los experimentos habilitados e informa a la conversación de las elecciones del usuario." + } +} diff --git a/packages/experimental/action-cards/locale/fr.json b/packages/experimental/action-cards/locale/fr.json new file mode 100644 index 0000000000..5b8fef7ac1 --- /dev/null +++ b/packages/experimental/action-cards/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cartes d'action", + "description": "Affiche des cartes d'action facultatives sous les réponses pour les expériences activées et transmet les choix de l'utilisateur à la conversation." + } +} diff --git a/packages/experimental/action-cards/locale/ja.json b/packages/experimental/action-cards/locale/ja.json new file mode 100644 index 0000000000..5882383b52 --- /dev/null +++ b/packages/experimental/action-cards/locale/ja.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "アクションカード", + "description": "有効な実験のために回答の下に任意のアクションカードを表示し、ユーザーの選択を会話に伝えます。" + } +} diff --git a/packages/experimental/action-cards/locale/pt.json b/packages/experimental/action-cards/locale/pt.json new file mode 100644 index 0000000000..384e7146fb --- /dev/null +++ b/packages/experimental/action-cards/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cartões de ação", + "description": "Mostra cartões de ação opcionais abaixo das respostas para experimentos ativados e informa as escolhas do usuário à conversa." + } +} diff --git a/packages/experimental/action-cards/locale/ru.json b/packages/experimental/action-cards/locale/ru.json new file mode 100644 index 0000000000..28d428e532 --- /dev/null +++ b/packages/experimental/action-cards/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Карточки действий", + "description": "Показывает необязательные карточки действий под ответами для включенных экспериментов и передает выбор пользователя в разговор." + } +} diff --git a/packages/experimental/action-cards/locale/zh.json b/packages/experimental/action-cards/locale/zh.json new file mode 100644 index 0000000000..2ddbdd8262 --- /dev/null +++ b/packages/experimental/action-cards/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "操作卡片", + "description": "为已启用的实验在回答下方显示可选操作卡片,并把用户的选择告知对话。" + } +} diff --git a/packages/experimental/action-cards/package.json b/packages/experimental/action-cards/package.json new file mode 100644 index 0000000000..363428560d --- /dev/null +++ b/packages/experimental/action-cards/package.json @@ -0,0 +1,75 @@ +{ + "name": "@deepseek-ai/dsh-action-cards", + "description": "Optional action cards below answers: one shared proposal tool, card state, user actions and status notices", + "version": "0.2.1-rc.3", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/experimental/action-cards" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json", + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + } + }, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-typert-protocol": "workspace:*", + "@deepseek-ai/dsh-llm": "workspace:*", + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-typert-protocol": "workspace:*", + "@deepseek-ai/dsh-llm": "workspace:*", + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-agent": "workspace:*", + "@deepseek-ai/dsh-agent-loop": "workspace:*", + "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*", + "@deepseek-ai/dsh-session-projection": "workspace:*", + "@deepseek-ai/dsh-api-remote-access": "workspace:*", + "@deepseek-ai/dsh-sandbox-policy": "workspace:*", + "@deepseek-ai/dsh-skill": "workspace:*", + "@deepseek-ai/dsh-tool-skill": "workspace:*", + "@deepseek-ai/dsh-tool-testkit": "workspace:*", + "@deepseek-ai/dsh-util-values": "workspace:*" + }, + "dependencies": { + "zod": "4.4.3" + }, + "files": [ + "locale/*.json", + "lib/index.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" + ] +} diff --git a/packages/experimental/action-cards/src/index.ts b/packages/experimental/action-cards/src/index.ts new file mode 100644 index 0000000000..1a5e75a4fc --- /dev/null +++ b/packages/experimental/action-cards/src/index.ts @@ -0,0 +1,451 @@ +/** Optional action cards below an answer: the `propose_action` tool, card state, user actions and status notices for registered kinds. */ +import { FiberState, Service, type Context } from '@deepseek-ai/cordis' +import { z, type ZodError, type ZodType } from 'zod' +import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import { createUserMessage, HarnessError, type ToolCallId } from '@deepseek-ai/dsh-llm' +import { defineTool, type ToolRunContext } from '@deepseek-ai/dsh-tools' +import { appendPluginRecord, SessionSeq, type Session } from '@deepseek-ai/dsh-session' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import type { JsonValue } from '@deepseek-ai/dsh-util-values' +import type {} from '@deepseek-ai/dsh-api-remote-access' +import type {} from '@deepseek-ai/dsh-sandbox-policy' +import { actionCardProjection, PROPOSE_ACTION, type ActionCardState } from './projection.ts' +import type { ActionCardDecision, ActionCardFields, ActionCardProposal, ActionCardResult, ActionCardView } from './types.ts' +export type * from './types.ts' + +/** Deployment-tunable limits for offering cards. */ +export const Config = z.object({ + /** Queued next-turn user messages at or above which `propose_action` refuses, so no card is offered. */ + queuedMessageLimit: z.number().int().min(1).default(2), +}).prefault({}) +/** Validated card limits after schema defaults. */ +export type Config = z.infer + +declare module '@deepseek-ai/cordis' { + interface Context { actionCards: ActionCards } +} + +/** The user action every card offers; the shared service records it. */ +const DISMISS = 'dismiss' +/** Phases the projection derives from the log; no decision may record them. */ +const DERIVED_PHASES: readonly string[] = ['replaced'] +/** Phases after which no user action changes the card. */ +const FINAL_PHASES: readonly string[] = ['dismissed', ...DERIVED_PHASES] +/** Tool whose successful loads activate `propose_action`; its `name` argument names the skill. */ +const SKILL_TOOL = 'skill' +const skillArguments = z.object({ name: z.string() }) + +const STATUS_PREAMBLE = 'Latest status of action cards from user actions and background work. ' + + 'These are status facts, not a request to start or retry work. Use these receipts instead of the earlier proposal when describing progress.' +const STATUS_CLOSING = 'Continue with the user’s current request.' + +/** The fiber fields the stopping check reads. */ +export interface PluginFiber { + readonly uid: number | null + readonly state: FiberState + readonly parent: { readonly fiber: PluginFiber } +} + +/** + * Whether a plugin must refuse new work: its fiber or an ancestor is disposed or unloading, since a parent's disposal + * begins before the plugin's own effects run, or the plugin's own drain has begun. + * @param owner - the plugin's fiber. + * @param draining - whether the plugin's drain effect has started. + * @returns whether the plugin is stopping. + */ +export function isPluginStopping(owner: PluginFiber, draining: boolean): boolean { + for (let fiber = owner; ; fiber = fiber.parent.fiber) { + if (fiber.uid === null || fiber.state === FiberState.UNLOADING) return true + if (fiber.parent.fiber === fiber) return draining + } +} + +/** A refused `propose_action` call; the message tells the model what to do instead. */ +export class ActionCardError extends HarnessError {} + +/** Shared checks one user action of a kind requires before the kind handles it. */ +export interface ActionCardVerb { + /** The connection must hold global Remote access, as for profile changes. */ + readonly global?: true + /** Refused while the Session sandbox mode is read-only. */ + readonly writes?: true +} + +/** A proposal whose details passed its kind's schema. */ +export type ActionCardProposalOf
= Omit & { readonly details: Details } + +/** Host half of one action kind: the skill that proposes it, its fields and its user actions. */ +export interface ActionCardKind
{ + /** Value of the `action` argument. */ + readonly action: string + /** Skill whose instructions propose this action; a load in the current turn admits the call. */ + readonly skill: string + /** + * Model-facing closing guidance that the result of an accepted call gives: one or two sentences on how to invite + * the user to the card, in the user's language. Refused calls never carry it, so the model invites the user only to a + * card that exists. + */ + readonly invitation: string + /** Validates `details` when the model proposes and before each user action. */ + readonly details: ZodType
+ /** User actions besides `dismiss`, with the shared checks each requires; other verbs are refused as unavailable. */ + readonly verbs: Readonly> + /** Recorded phases that still offer the action; dismissal is recorded for these and for an unstarted card. */ + readonly openPhases?: readonly string[] + /** Sentences the status notice adds when it reports this kind's decisions. */ + readonly statusGuidance?: string + /** Decision data fields the status notice reports; omitted reports every field. */ + readonly statusFields?: readonly string[] + /** + * Fields recorded with a dismissal. + * @param proposal - the dismissed card. + * @param payload - fields the user's client sent. + * @returns decision data, or undefined to record none. + */ + dismissal?(proposal: ActionCardProposalOf
, payload: ActionCardFields | undefined): ActionCardFields | undefined + /** + * Bring the card's recorded decision up to date with state outside the Session, such as an installation that + * finished while the Session was closed, recording each change with {@link ActionCards.record}. The service awaits it + * before every user action, including `dismiss`, then reads the card's decision again, so a card that is no longer + * open settles instead of being dismissed. + * @param agent - live root of the card's Session. + * @param proposal - the card with validated details. + * @throws when that state cannot be read; the action is then refused as unavailable. + */ + reconcile?(agent: Agent, proposal: ActionCardProposalOf
): Promise + /** + * Handle one user action other than `dismiss` after the shared checks passed. The handler records each new + * decision with {@link ActionCards.record}. + * @param agent - live root of the card's Session. + * @param proposal - the card with validated details. + * @param verb - one of {@link verbs}. + * @param payload - fields the user's client sent; the kind validates them. + * @returns the action's result. + */ + act(agent: Agent, proposal: ActionCardProposalOf
, verb: string, payload: ActionCardFields | undefined): Promise +} + +/** A registered kind with its details type erased behind validating closures. */ +interface RegisteredKind { + readonly skill: string + readonly invitation: string + readonly verbs: Readonly> + readonly openPhases: readonly string[] + readonly statusGuidance: string | undefined + readonly statusFields: readonly string[] | undefined + /** Describe why details fail the kind's schema; undefined when they pass. */ + readonly detailsIssues: (details: ActionCardFields) => string | undefined + readonly dismissal: (proposal: ActionCardProposal, payload: ActionCardFields | undefined) => ActionCardFields | undefined + /** Absent when the kind keeps no state outside the Session. */ + readonly reconcile: ((agent: Agent, proposal: ActionCardProposal) => Promise) | undefined + readonly act: ( + agent: Agent, proposal: ActionCardProposal, verb: string, payload: ActionCardFields | undefined, + ) => Promise +} + +/** Own entries only: browser-supplied ids such as `toString` never reach inherited object members. */ +function ownEntry(record: Readonly>, key: string): T | undefined { + return Object.hasOwn(record, key) ? record[key] : undefined +} + +function refused(reason: string): ActionCardResult { + return { status: 'refused', reason } +} + +function issuesOf(error: ZodError): string { + return 'Invalid details: ' + error.issues.map(issue => `${['details', ...issue.path.map(String)].join('.')}: ${issue.message}`).join('; ') +} + +/** Card tool, kind registry, Session projection, user actions and status notices. */ +export class ActionCards extends TypertRemoteService { + static inject = ['tools', 'agents', 'sessionProjections', 'remoteAccess'] + static Config = Config + private projection!: ProjectionHandle + private readonly kinds = new Map() + /** + * Actions whose kind unregistered. Cordis disposes a plugin's effects concurrently, so a kind's unload drain can + * still record the outcome of its in-flight work after its registration is gone. + */ + private readonly retired = new Set() + /** Skills of registered kinds each live agent has loaded; `propose_action` is declared while one is still registered. */ + private readonly loaded = new Map>() + /** Kind skills each agent loaded in its latest turn. */ + private readonly turnLoads = new Map }>() + /** The call accepted in each agent's latest turn, until its result fails. */ + private readonly accepted = new Map() + private readonly actions = new Map>() + private closing = false + private readonly owner: Context['fiber'] + + /** + * @param ctx - Owning plugin context. + * @param config - Validated card limits; direct composition without a config gets the schema defaults. + */ + constructor(ctx: Context, private readonly config: Config = Config.parse({})) { + super(ctx, 'actionCards') + this.owner = ctx.fiber + } + + protected async [Service.init](): Promise { + this.projection = await this.ctx.sessionProjections.register(actionCardProjection) + this.ctx.tools.register(defineTool({ + name: PROPOSE_ACTION, + description: 'Show the user one optional action card under this answer. Call only as the skill that names the action instructs.', + parameters: { + action: { type: 'string', required: true, description: 'Action name from the skill.' }, + title: { type: 'string', required: true, description: 'Card title.' }, + brief: { type: 'string', required: true, description: 'What the action does, in the user\'s language.' }, + details: { type: 'object', additionalProperties: true, required: true, description: 'Fields the skill lists for this action.' }, + }, + output: { + schema: { + type: 'object', additionalProperties: false, + properties: { + action: { type: 'string', required: true }, title: { type: 'string', required: true }, invitation: { type: 'string', required: true }, + }, + }, + render: (_args, value) => [{ type: 'text', text: `Card shown below your answer. Do not restate it. ${value.invitation}` }], + }, + execute: (args, exec) => Promise.resolve(this.propose(args, exec)), + }), { when: ({ agent }) => this.declares(agent) }) + this.ctx.on('tools/result', (exec, result) => { + const agent = exec.agent + if (agent === undefined) return + if (exec.name === PROPOSE_ACTION) { + if (result.isError && this.accepted.get(agent)?.callId === exec.callId) this.accepted.delete(agent) + return + } + // Loads by subagents or from code activate nothing: `propose_action` refuses both callers. + if (exec.parent !== undefined || !this.ctx.agents.roots().includes(agent)) return + const skill = exec.name === SKILL_TOOL && !result.isError ? skillArguments.safeParse(exec.arguments) : undefined + if (skill?.success !== true || ![...this.kinds.values()].some(kind => kind.skill === skill.data.name)) return + const turn = this.ctx.agents.turnBoundaryOf(agent.session).lastTurn + const loads = this.turnLoads.get(agent) + if (loads?.turn === turn) loads.skills.add(skill.data.name) + else this.turnLoads.set(agent, { turn, skills: new Set([skill.data.name]) }) + this.loaded.set(agent, new Set([...this.loaded.get(agent) ?? [], skill.data.name])) + }) + this.ctx.on('agent/disposed', ({ agent }) => { + this.loaded.delete(agent) + this.turnLoads.delete(agent) + this.accepted.delete(agent) + }) + this.ctx.on('agent/pre-step', async ({ agent, signal }, next) => { + const decision = await next() + if (decision.kind === 'reject' || signal.aborted || this.isStopping()) return decision + const state = this.projection.stateOf(agent.session) + const pending = Object.values(state.pendingStatus) + if (pending.length === 0) return decision + const receipts = Object.values(state.view.decisions).filter(receipt => Object.hasOwn(state.pendingStatus, receipt.callId)) + const guidance = new Set(receipts.flatMap(receipt => this.kinds.get(receipt.action)?.statusGuidance ?? [])) + const updates = receipts.map((receipt) => { + const fields = this.kinds.get(receipt.action)?.statusFields + const data = receipt.data === undefined || fields === undefined ? receipt.data + : Object.fromEntries(Object.entries(receipt.data).filter(([key]) => fields.includes(key))) + return { action: receipt.action, callId: receipt.callId, title: ownEntry(state.view.proposals, receipt.callId)?.title, + phase: receipt.phase, ...data === undefined ? {} : { data } } + }) + return { ...decision, messages: [...decision.messages, createUserMessage({ + source: { kind: 'action-cards', through: SessionSeq(Math.max(...pending)), form: 'notice', summary: 'Action card status updated' }, + content: [{ type: 'text', text: [STATUS_PREAMBLE, ...guidance, STATUS_CLOSING].join(' ') + '\n' + JSON.stringify(updates) }], + })] } + }, { prepend: true }) + this.ctx.effect(() => async () => { + this.closing = true + await Promise.allSettled([...this.actions.values()]) + }, 'action-cards: drain user actions') + } + + /** + * Register one action kind for the caller's lifetime. + * @param kind - the kind's skill, details schema, verbs and handler. + * @returns disposer removing the kind; actions on its cards are then unavailable. + * @throws when another kind already registered the same action. + */ + register
(kind: ActionCardKind
): () => void { + const typed = (proposal: ActionCardProposal): ActionCardProposalOf
| undefined => { + const details = kind.details.safeParse(proposal.details) + return details.success ? { ...proposal, details: details.data } : undefined + } + const reconcile = kind.reconcile?.bind(kind) + const entry: RegisteredKind = { + skill: kind.skill, invitation: kind.invitation, verbs: kind.verbs, openPhases: kind.openPhases ?? [], + statusGuidance: kind.statusGuidance, statusFields: kind.statusFields, + detailsIssues: (details) => { + const parsed = kind.details.safeParse(details) + return parsed.success ? undefined : issuesOf(parsed.error) + }, + dismissal: (proposal, payload) => { + const card = typed(proposal) + return card === undefined ? undefined : kind.dismissal?.(card, payload) + }, + // A card whose details no longer validate never reached the kind, so the kind has nothing to reconcile. + reconcile: reconcile === undefined ? undefined : async (agent, proposal) => { + const card = typed(proposal) + if (card !== undefined) await reconcile(agent, card) + }, + act: (agent, proposal, verb, payload) => { + const card = typed(proposal) + return card === undefined ? Promise.resolve(refused('unavailable')) : kind.act(agent, card, verb, payload) + }, + } + const dispose = this.ctx.effect(() => { + if (this.kinds.has(kind.action)) throw new Error(`action-cards: action "${kind.action}" is already registered`) + this.kinds.set(kind.action, entry) + return () => { + this.kinds.delete(kind.action) + this.retired.add(kind.action) + } + }, `action-cards: kind ${kind.action}`) + return () => { void dispose() } + } + + /** + * Record a card decision in the Session log; the next model request reports it. + * @param agent - agent whose Session owns the card. + * @param decision - the card's new state. + * @throws when no kind ever registered the decision's action with this service, or its phase is one the projection derives. + */ + record(agent: Agent, decision: ActionCardDecision): void { + if (!this.kinds.has(decision.action) && !this.retired.has(decision.action)) { + throw new Error(`action-cards: cannot record a decision for unregistered action "${decision.action}"`) + } + if (DERIVED_PHASES.includes(decision.phase)) throw new Error(`action-cards: phase "${decision.phase}" is derived from the log and cannot be recorded`) + this.append(agent, decision) + } + + /** + * Read the cards of one Session. + * @param session - a Session whose projections are prepared. + * @returns proposals and decisions by call id. + */ + view(session: Session): ActionCardView { + return this.projection.stateOf(session).view + } + + /** + * Apply a user action to a logged card. Shared checks run first, then the kind reconciles the card; `dismiss` is + * recorded here and every other verb goes to the card's kind. `dismiss` also works on an unstarted card whose kind + * is no longer registered. Concurrent actions on one card share one operation. + * @param agent - live root resolved from the displayed Session. + * @param callId - the card's `propose_action` call id. + * @param verb - `dismiss` or one of the kind's verbs. + * @param payload - kind-defined fields such as a chosen location. + * @returns acceptance of background work, the decision in effect, or a refusal. + */ + @Remote + act(agent: Agent, callId: ToolCallId, verb: string, payload?: Record): Promise { + if (this.isStopping()) return Promise.resolve(refused('closing')) + if (!this.ctx.agents.roots().includes(agent)) return Promise.resolve(refused('not-live-root')) + const proposal = ownEntry(this.projection.stateOf(agent.session).view.proposals, callId) + if (proposal === undefined) return Promise.resolve(refused('unknown')) + const kind = this.kinds.get(proposal.action) + if (verb === DISMISS) return this.coalesce(agent, callId, () => this.dismiss(agent, kind, proposal, payload)) + const policy = kind === undefined ? undefined : ownEntry(kind.verbs, verb) + if (kind === undefined || policy === undefined) return Promise.resolve(refused('unavailable')) + if (policy.global === true && !this.ctx.remoteAccess.allows({ kind: 'global' })) return Promise.resolve(refused('access-denied')) + return this.coalesce(agent, callId, () => this.perform(agent, kind, proposal, verb, policy, payload)) + } + + /** Share one operation per card, run under the Agent's initiator, until it settles. */ + private coalesce(agent: Agent, callId: ToolCallId, run: () => Promise): Promise { + const key = JSON.stringify([agent.id, callId]) + const existing = this.actions.get(key) + if (existing !== undefined) return existing + const operation = this.ctx.agents.withInitiator(agent, run).finally(() => { this.actions.delete(key) }) + this.actions.set(key, operation) + return operation + } + + /** + * Await the kind's reconciliation. A kind that unloads meanwhile still handles the action and refuses it itself. + * @returns a refusal when reconciliation failed or the root closed meanwhile; undefined to continue. + */ + private async reconciled( + agent: Agent, kind: RegisteredKind, proposal: ActionCardProposal, verb: string, + ): Promise { + if (kind.reconcile === undefined) return undefined + try { await kind.reconcile(agent, proposal) } + catch (error) { + this.ctx.logger.warn('Cannot reconcile action card %s before %s: %s', proposal.callId, verb, String(error)) + return refused('unavailable') + } + return this.ctx.agents.roots().includes(agent) ? undefined : refused('not-live-root') + } + + private async dismiss( + agent: Agent, kind: RegisteredKind | undefined, proposal: ActionCardProposal, payload: ActionCardFields | undefined, + ): Promise { + const refusal = kind === undefined ? undefined : await this.reconciled(agent, kind, proposal, DISMISS) + if (refusal !== undefined) return refusal + const decision = ownEntry(this.projection.stateOf(agent.session).view.decisions, proposal.callId) + // Without its kind no recorded phase is known to be open, and the kind may return to act on it. + if (decision !== undefined && (FINAL_PHASES.includes(decision.phase) || kind?.openPhases.includes(decision.phase) !== true)) { + return { status: 'settled', decision } + } + const data = kind?.dismissal(proposal, payload) + const dismissed: ActionCardDecision = { callId: proposal.callId, action: proposal.action, phase: 'dismissed', ...data === undefined ? {} : { data } } + this.append(agent, dismissed) + return { status: 'settled', decision: dismissed } + } + + private async perform( + agent: Agent, kind: RegisteredKind, proposal: ActionCardProposal, verb: string, policy: ActionCardVerb, + payload: ActionCardFields | undefined, + ): Promise { + const refusal = await this.reconciled(agent, kind, proposal, verb) + if (refusal !== undefined) return refusal + const decision = ownEntry(this.projection.stateOf(agent.session).view.decisions, proposal.callId) + if (decision !== undefined && FINAL_PHASES.includes(decision.phase)) return { status: 'settled', decision } + if (policy.writes === true && this.ctx.get('sandboxPolicy')?.resolve({ session: agent.session }).mode === 'read-only') return refused('read-only') + return await kind.act(agent, proposal, verb, payload) + } + + private append(agent: Agent, decision: ActionCardDecision): void { + appendPluginRecord(agent.session, 'plugin:action-cards/decision', decision) + } + + /** Whether `propose_action` is declared for this agent's next step. */ + private declares(agent: Agent): boolean { + const skills = this.loaded.get(agent) + return skills !== undefined && [...this.kinds.values()].some(kind => skills.has(kind.skill)) + } + + private propose( + args: { readonly action: string; readonly title: string; readonly brief: string; readonly details: ActionCardFields }, + exec: ToolRunContext, + ): { action: string; title: string; invitation: string } { + const agent = exec.agent + // Step admission declares the tool only to an agent; calls dispatched from code are not logged as tool calls, + // so no card could follow them. + if (agent === undefined || exec.parent !== undefined) throw new ActionCardError('Call propose_action directly, not from code.', 'ACTION_CARD_NESTED') + // Only a live root can act on a card, so a subagent's card could never be used. + if (!this.ctx.agents.roots().includes(agent)) throw new ActionCardError('Cards are offered in the main conversation only.', 'ACTION_CARD_ROOT') + if (this.projection.stateOf(agent.session).queued >= this.config.queuedMessageLimit) { + throw new ActionCardError('The user has queued more messages; show no card and answer briefly.', 'ACTION_CARD_QUEUED') + } + const kind = this.kinds.get(args.action) + // Admission is decided when the step starts, so the last kind can unregister before this call runs. + if (kind === undefined) throw new ActionCardError(`Unknown action "${args.action}". Registered actions: ${[...this.kinds.keys()].join(', ') || 'none'}.`, 'ACTION_CARD_UNKNOWN') + const turn = this.ctx.agents.turnBoundaryOf(agent.session).lastTurn + const loads = this.turnLoads.get(agent) + if (loads?.turn !== turn || !loads.skills.has(kind.skill)) { + throw new ActionCardError(`Load skill ${kind.skill} first, then call propose_action.`, 'ACTION_CARD_SKILL') + } + if (args.title.trim() === '' || args.brief.trim() === '') throw new ActionCardError('Write a title and a brief.', 'ACTION_CARD_BLANK') + const issues = kind.detailsIssues(args.details) + if (issues !== undefined) throw new ActionCardError(issues, 'ACTION_CARD_DETAILS') + if (this.accepted.get(agent)?.turn === turn || Object.values(this.view(agent.session).proposals).some(card => card.turn === turn)) { + throw new ActionCardError('One card per turn; the earlier card stands.', 'ACTION_CARD_LIMIT') + } + this.accepted.set(agent, { turn, callId: exec.callId }) + return { action: args.action, title: args.title.trim(), invitation: kind.invitation } + } + + private isStopping(): boolean { + return isPluginStopping(this.owner, this.closing) + } +} + +export default ActionCards diff --git a/packages/experimental/action-cards/src/projection.ts b/packages/experimental/action-cards/src/projection.ts new file mode 100644 index 0000000000..3d1be18a97 --- /dev/null +++ b/packages/experimental/action-cards/src/projection.ts @@ -0,0 +1,124 @@ +/** Recover card proposals from logged `propose_action` calls and decisions from plugin records. */ +import { z } from 'zod' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { pluginRecordOf, SessionLogOffset } from '@deepseek-ai/dsh-session' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' + +/** Model-facing name of the card tool. */ +export const PROPOSE_ACTION = 'propose_action' + +/** Kind-defined JSON object of a proposal or decision. */ +export const fieldsSchema = z.record(z.string(), z.json()) + +/** Arguments of a `propose_action` call; blank titles and briefs create no card. */ +export const argumentsSchema = z.object({ + action: z.string().min(1), + title: z.string().trim().min(1), + brief: z.string().trim().min(1), + details: fieldsSchema, +}) + +const proposalSchema = z.object({ + callId: z.string().min(1).transform(ToolCallId), + turn: z.number().int().nonnegative(), + action: z.string().min(1), + title: z.string().min(1), + brief: z.string().min(1), + details: fieldsSchema, +}).strict() + +/** Recorded decision of one card. */ +export const decisionSchema = z.object({ + callId: z.string().min(1).transform(ToolCallId), + action: z.string().min(1), + phase: z.string().min(1), + data: fieldsSchema.optional(), +}).strict() + +const viewSchema = z.object({ + proposals: z.record(z.string(), proposalSchema), + decisions: z.record(z.string(), decisionSchema), +}) + +const stateSchema = z.object({ + /** Fork-inherited prefix length; inherited events create no proposals or decisions. */ + inheritedEventCount: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionLogOffset), + view: viewSchema, + /** `propose_action` calls of the open turn still waiting for their result. */ + calls: z.record(z.string(), proposalSchema.omit({ callId: true })), + /** Turn of the latest admitted proposal; later calls of that turn create no card. */ + admittedTurn: z.number().int().nullable(), + /** Decisions not yet reported to the model, by call id, with the seq of their record. */ + pendingStatus: z.record(z.string(), z.number()), + /** User messages still queued for later turns, folded from inbox splices; the card tool compares it with its limit. */ + queued: z.number().int().nonnegative(), +}) + +/** Fold state; `view` is also the Client wire value. */ +export type ActionCardState = z.infer + +/** Type of {@link actionCardProjection}: the fold plus its Client view wire. */ +export type ActionCardProjectionDefinition = Omit, 'wire'> & { + readonly wire: NonNullable['wire']> +} + +/** + * Full-Session fold. A successful `propose_action` result admits the call as a card, at most one per turn; + * the first admitted call of a turn wins. A newer card of any kind replaces every unstarted earlier card. + * Events inherited by a forked Session create no proposals or decisions. + */ +export const actionCardProjection: ActionCardProjectionDefinition = { + key: 'actionCards', stateVersion: 2, stateSchema, + init: (_header, inheritedEventCount) => ({ + inheritedEventCount, view: { proposals: {}, decisions: {} }, calls: {}, admittedTurn: null, pendingStatus: {}, queued: 0, + }), + apply: (state, event) => { + if (event.seq < state.inheritedEventCount) return state + switch (event.type) { + case 'turn/start': + // A turn's calls settle inside it; a call left without a result never becomes a card. + return Object.keys(state.calls).length === 0 ? state : { ...state, calls: {} } + case 'agent/inbox/spliced': + if (event.data.target !== 'next-turn') return state + return { ...state, queued: Math.max(0, state.queued - (event.data.removedCount ?? 0) + event.data.inserted.length) } + case 'user/message': { + if (event.data.source.kind !== 'action-cards') return state + const through = event.data.source.through + return { ...state, pendingStatus: Object.fromEntries(Object.entries(state.pendingStatus).filter(([, seq]) => seq > through)) } + } + case 'tool/call': { + if (event.data.name !== PROPOSE_ACTION) return state + let raw: unknown + try { raw = JSON.parse(event.data.arguments) } + catch { return state } // Malformed model JSON failed in the tool too and creates no card. + const parsed = argumentsSchema.safeParse(raw) + if (!parsed.success) return state + return { ...state, calls: { ...state.calls, [event.data.callId]: { turn: event.data.turn, ...parsed.data } } } + } + case 'tool/result': { + const callId = event.data.message.toolCallId + const call = Object.hasOwn(state.calls, callId) ? state.calls[callId] : undefined + if (call === undefined) return state + const calls = Object.fromEntries(Object.entries(state.calls).filter(([id]) => id !== callId)) + if (event.data.message.isError === true || state.admittedTurn === call.turn) return { ...state, calls } + const decisions = { ...state.view.decisions } + // A newer card supersedes every unstarted card; the model made that choice, so no status is reported. + for (const earlier of Object.values(state.view.proposals)) { + if (decisions[earlier.callId] === undefined) decisions[earlier.callId] = { callId: earlier.callId, action: earlier.action, phase: 'replaced' } + } + return { ...state, calls, admittedTurn: call.turn, + view: { proposals: { ...state.view.proposals, [callId]: { callId, ...call } }, decisions } } + } + default: { + const record = pluginRecordOf(event) + if (record?.type !== 'plugin:action-cards/decision') return state + // A malformed or newer record is skipped; throwing here would fail every later read of this Session. + const decision = decisionSchema.safeParse(record.data) + if (!decision.success) return state + return { ...state, view: { ...state.view, decisions: { ...state.view.decisions, [decision.data.callId]: decision.data } }, + pendingStatus: { ...state.pendingStatus, [decision.data.callId]: event.seq } } + } + } + }, + wire: { summary: false, viewSchema, view: (state: ActionCardState) => state.view }, +} diff --git a/packages/experimental/action-cards/src/types.ts b/packages/experimental/action-cards/src/types.ts new file mode 100644 index 0000000000..d240f47126 --- /dev/null +++ b/packages/experimental/action-cards/src/types.ts @@ -0,0 +1,79 @@ +/** Card proposals, recorded decisions and user-action results shared by every action kind. */ +import type { ContextFormed } from '@deepseek-ai/dsh-llm' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { SessionProjectionSummaryExcluded } from '@deepseek-ai/dsh-session-projection/types' +import type { JsonValue } from '@deepseek-ai/dsh-util-values' + +/** Kind-defined JSON fields of a proposal or decision; each kind validates its own fields. */ +export type ActionCardFields = Readonly> + +/** One card admitted from a successful `propose_action` call. */ +export interface ActionCardProposal { + /** Id of the logged tool call. */ + readonly callId: ToolCallId + /** Turn whose answer the card follows. */ + readonly turn: number + /** Registered action kind named by the call. */ + readonly action: string + readonly title: string + /** What the action does, in the user's language. */ + readonly brief: string + /** Fields the kind's skill asks for. */ + readonly details: ActionCardFields +} + +/** + * Latest state of one card. Kinds define their own phases; `dismissed` is shared. + * `replaced` (a later proposal superseded an unstarted card) is derived from the + * log and never recorded. + */ +export interface ActionCardDecision { + readonly callId: ToolCallId + readonly action: string + readonly phase: string + /** Kind-defined fields such as a chosen location or a created package. */ + readonly data?: ActionCardFields | undefined +} + +/** Open-Session proposals and decisions by call id, excluded from Session summaries. */ +export interface ActionCardView { + readonly proposals: Readonly> + readonly decisions: Readonly> +} + +/** + * Result of a user action. `accepted` starts background work that records its + * own progress; `settled` returns the decision now in effect; `refused` changed + * nothing; `unconfirmed` lost its outcome to an unloading owner, which the + * kind reconciles when the Session reopens. + */ +export type ActionCardResult = + | { readonly status: 'accepted' } + | { readonly status: 'settled'; readonly decision: ActionCardDecision } + | { readonly status: 'refused'; readonly reason: string } + | { readonly status: 'unconfirmed'; readonly reason: string } + +declare module '@deepseek-ai/dsh-llm' { + interface MessageSourceMap { + /** Card status notice; other readers preserve the recorded text without this plugin. + * The producer uses through only to acknowledge delivered decisions. + * @persistenceAttribution + */ + 'action-cards': { kind: 'action-cards'; through: SessionSeq } & ContextFormed + } +} + +declare module '@deepseek-ai/dsh-session/types' { + interface PluginRecordMap { + /** Dismissals and kind-defined progress of action cards. */ + 'plugin:action-cards/decision': ActionCardDecision + } +} + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionMap { + /** Action card proposals and decisions for the currently open Session. */ + actionCards: SessionProjectionSummaryExcluded + } +} diff --git a/packages/experimental/action-cards/tests/__snapshots__/status.spec.ts.snap b/packages/experimental/action-cards/tests/__snapshots__/status.spec.ts.snap new file mode 100644 index 0000000000..4b6482d24f --- /dev/null +++ b/packages/experimental/action-cards/tests/__snapshots__/status.spec.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`reports recorded decisions once on the next request, with each kind guidance once, without waking the conversation > status notice 1`] = ` +[ + { + "text": "Latest status of action cards from user actions and background work. These are status facts, not a request to start or retry work. Use these receipts instead of the earlier proposal when describing progress. Failed runs can be retried from their card. Continue with the user’s current request. +[{"action":"test_action","callId":"call-card-1","title":"Counter","phase":"failed","data":{"reason":"timeout"}},{"action":"test_action","callId":"call-card-2","title":"Second","phase":"dismissed"},{"action":"retired_action","callId":"call-orphan","phase":"done"}]", + "type": "text", + }, +] +`; diff --git a/packages/experimental/action-cards/tests/__snapshots__/tool.spec.ts.snap b/packages/experimental/action-cards/tests/__snapshots__/tool.spec.ts.snap new file mode 100644 index 0000000000..2eec86e6ec --- /dev/null +++ b/packages/experimental/action-cards/tests/__snapshots__/tool.spec.ts.snap @@ -0,0 +1,36 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`declares propose_action from the step after a card skill loads, and logs the addition > tool declaration 1`] = ` +{ + "description": "Show the user one optional action card under this answer. Call only as the skill that names the action instructs.", + "name": "propose_action", + "parameters": { + "properties": { + "action": { + "description": "Action name from the skill.", + "type": "string", + }, + "brief": { + "description": "What the action does, in the user's language.", + "type": "string", + }, + "details": { + "additionalProperties": true, + "description": "Fields the skill lists for this action.", + "type": "object", + }, + "title": { + "description": "Card title.", + "type": "string", + }, + }, + "required": [ + "action", + "title", + "brief", + "details", + ], + "type": "object", + }, +} +`; diff --git a/packages/experimental/action-cards/tests/act.spec.ts b/packages/experimental/action-cards/tests/act.spec.ts new file mode 100644 index 0000000000..0b6e580943 --- /dev/null +++ b/packages/experimental/action-cards/tests/act.spec.ts @@ -0,0 +1,167 @@ +import { expect, it, onTestFinished, vi } from 'vitest' +import { z } from 'zod' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { ActionCardProposalOf, ActionCardResult } from '../src/index.ts' +import { ACTION, callId, fixture, proposal, type TestDetails } from './fixture.ts' + +const dismissed = (data?: object) => ({ status: 'settled', decision: { callId, action: ACTION, phase: 'dismissed', ...data === undefined ? {} : { data } } }) + +it('refuses unknown cards, closed owners, verbs of removed kinds and undeclared verbs before any handler runs', async () => { + const f = await fixture() + expect(await f.ctx.actionCards.act(f.agent, callId, 'start')).toEqual({ status: 'refused', reason: 'unknown' }) + f.propose() + for (const id of ['toString', 'constructor']) { + expect(await f.ctx.actionCards.act(f.agent, ToolCallId(id), 'dismiss')).toEqual({ status: 'refused', reason: 'unknown' }) + } + for (const verb of ['toString', 'missing']) { + expect(await f.ctx.actionCards.act(f.agent, callId, verb)).toEqual({ status: 'refused', reason: 'unavailable' }) + } + const roots = vi.spyOn(f.ctx.agents, 'roots').mockReturnValue([]) + expect(await f.ctx.actionCards.act(f.agent, callId, 'start')).toEqual({ status: 'refused', reason: 'not-live-root' }) + roots.mockRestore() + await f.kindFiber.dispose() + expect(await f.ctx.actionCards.act(f.agent, callId, 'start')).toEqual({ status: 'refused', reason: 'unavailable' }) + const service = f.ctx.actionCards + await f.cards.dispose() + expect(await service.act(f.agent, callId, 'dismiss')).toEqual({ status: 'refused', reason: 'closing' }) + expect(f.act).not.toHaveBeenCalled() +}) + +it('applies the shared access and read-only checks the kind declares, then hands typed details to the kind', async () => { + const f = await fixture() + f.propose() + const access = vi.spyOn(f.ctx.remoteAccess, 'allows').mockReturnValue(false) + expect(await f.ctx.actionCards.act(f.agent, callId, 'publish')).toEqual({ status: 'refused', reason: 'access-denied' }) + expect(await f.ctx.actionCards.act(f.agent, callId, 'start', { note: 'x' })).toEqual({ status: 'settled', decision: { callId, action: ACTION, phase: 'start' } }) + access.mockRestore() + expect(f.act).toHaveBeenCalledWith(f.agent, { callId, turn: 1, ...proposal }, 'start', { note: 'x' }) + const resolve = vi.fn(() => ({ mode: 'read-only', workspaceRoot: '/' })) + f.ctx.provide('sandboxPolicy', { resolve }) + expect(await f.ctx.actionCards.act(f.agent, callId, 'write')).toEqual({ status: 'refused', reason: 'read-only' }) + expect(resolve).toHaveBeenCalledWith({ session: f.agent.session }) + expect((await f.ctx.actionCards.act(f.agent, callId, 'publish')).status).toBe('settled') + resolve.mockReturnValue({ mode: 'workspace-write', workspaceRoot: '/' }) + expect((await f.ctx.actionCards.act(f.agent, callId, 'write')).status).toBe('settled') + f.propose(ToolCallId('call-invalid'), { ...proposal, details: { value: 3 } }, f.agent, 2) + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-invalid'), 'start')).toEqual({ status: 'refused', reason: 'unavailable' }) + expect(f.act).toHaveBeenCalledTimes(3) +}) + +it('records dismissal of an unstarted or open card with the kind data, and settles started or final cards', async () => { + const f = await fixture({ + openPhases: ['failed'], + dismissal: (card, payload) => ({ value: card.details.value, ...z.object({ choice: z.string() }).safeParse(payload).data }), + }) + f.propose() + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss', { choice: 'later' })).toEqual(dismissed({ value: 'a', choice: 'later' })) + expect(f.view().decisions[callId]?.phase).toBe('dismissed') + for (const verb of ['dismiss', 'start']) expect(await f.ctx.actionCards.act(f.agent, callId, verb)).toEqual(dismissed({ value: 'a', choice: 'later' })) + f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'failed' }) + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toEqual(dismissed({ value: 'a' })) + f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'running', data: { step: 1 } }) + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toEqual({ status: 'settled', + decision: { callId, action: ACTION, phase: 'running', data: { step: 1 } } }) + f.propose(ToolCallId('call-invalid'), { ...proposal, details: { value: 3 } }, f.agent, 2) + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-invalid'), 'dismiss')).toEqual({ status: 'settled', + decision: { callId: 'call-invalid', action: ACTION, phase: 'dismissed' } }) + expect(f.act).not.toHaveBeenCalled() +}) + +it('dismisses without data when the kind records none', async () => { + const f = await fixture() + f.propose() + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss', { choice: 'later' })).toEqual(dismissed()) +}) + +it('shares one operation between concurrent actions on a card and drains it on unload', async () => { + const pending = Promise.withResolvers() + onTestFinished(() => { pending.resolve({ status: 'accepted' }) }) + const f = await fixture({ act: vi.fn(() => pending.promise) }) + f.propose() + const first = f.ctx.actionCards.act(f.agent, callId, 'start') + expect(f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toBe(first) + const disposing = f.cards.dispose() + pending.resolve({ status: 'accepted' }) + expect(await first).toEqual({ status: 'accepted' }) + await disposing +}) + +it('rejects a second kind for the same action and releases a kind through its disposer', async () => { + const f = await fixture() + const kind = (action: string) => ({ action, skill: 'other-skill', invitation: 'Invite the user.', details: z.object({}), verbs: { start: {} }, + act: () => Promise.resolve({ status: 'accepted' }) }) + expect(() => f.ctx.actionCards.register(kind(ACTION))).toThrow('action-cards: action "test_action" is already registered') + const unregister = f.ctx.actionCards.register(kind('other_action')) + f.propose(ToolCallId('call-other'), { action: 'other_action', title: 'Other', brief: 'Other card.', details: {} }) + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-other'), 'start')).toEqual({ status: 'accepted' }) + unregister() + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-other'), 'start')).toEqual({ status: 'refused', reason: 'unavailable' }) +}) + +it('lets the kind reconcile before every action, so a card that is no longer open settles instead of being dismissed', async () => { + let finished = false + const reconcile = vi.fn(async (agent: Agent, card: ActionCardProposalOf) => { + if (finished) f.ctx.actionCards.record(agent, { callId: card.callId, action: ACTION, phase: 'installed' }) + }) + const f = await fixture({ openPhases: ['unconfirmed'], reconcile }) + f.propose() + f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'unconfirmed' }) + finished = true + const installed = { status: 'settled', decision: { callId, action: ACTION, phase: 'installed' } } + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toEqual(installed) + expect(reconcile).toHaveBeenCalledWith(f.agent, { callId, turn: 1, ...proposal }) + expect(f.view().decisions[callId]?.phase).toBe('installed') + expect(await f.ctx.actionCards.act(f.agent, callId, 'start')).toEqual({ status: 'settled', decision: { callId, action: ACTION, phase: 'start' } }) + expect(reconcile).toHaveBeenCalledTimes(2) + f.propose(ToolCallId('call-invalid'), { ...proposal, details: { value: 3 } }, f.agent, 2) + expect((await f.ctx.actionCards.act(f.agent, ToolCallId('call-invalid'), 'dismiss')).status).toBe('settled') + expect(reconcile).toHaveBeenCalledTimes(2) +}) + +it('refuses the action when reconciliation fails or the root closes meanwhile', async () => { + const reconcile = vi.fn<(agent: Agent) => Promise>(async () => { throw new Error('profile read failed') }) + const f = await fixture({ reconcile }) + f.propose() + const warn = vi.spyOn(f.ctx.logger, 'warn').mockImplementation(() => {}) + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toEqual({ status: 'refused', reason: 'unavailable' }) + expect(warn).toHaveBeenCalledWith('Cannot reconcile action card %s before %s: %s', callId, 'dismiss', 'Error: profile read failed') + expect(await f.ctx.actionCards.act(f.agent, callId, 'start')).toEqual({ status: 'refused', reason: 'unavailable' }) + expect(warn).toHaveBeenLastCalledWith('Cannot reconcile action card %s before %s: %s', callId, 'start', 'Error: profile read failed') + expect(f.view().decisions).toEqual({}) + reconcile.mockImplementationOnce(async () => { await f.agentHandle.dispose() }) + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss')).toEqual({ status: 'refused', reason: 'not-live-root' }) + expect(f.act).not.toHaveBeenCalled() +}) + +it('refuses to record decisions of actions no kind registered or of phases the projection derives', async () => { + const f = await fixture() + f.propose() + expect(() => { f.ctx.actionCards.record(f.agent, { callId, action: 'other_action', phase: 'done' }) }) + .toThrow('action-cards: cannot record a decision for unregistered action "other_action"') + expect(() => { f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'replaced' }) }) + .toThrow('action-cards: phase "replaced" is derived from the log and cannot be recorded') + expect(f.view().decisions).toEqual({}) + // An unloading kind still records the outcome of work it had started. + await f.kindFiber.dispose() + f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'interrupted' }) + expect(f.view().decisions[callId]?.phase).toBe('interrupted') +}) + +it('dismisses an unstarted card whose kind is no longer registered, without data, and settles recorded ones', async () => { + const f = await fixture({ openPhases: ['failed'], dismissal: () => ({ value: 'kind data' }) }) + f.propose(ToolCallId('call-failed'), proposal, f.agent, 1) + f.ctx.actionCards.record(f.agent, { callId: ToolCallId('call-failed'), action: ACTION, phase: 'failed' }) + f.propose(ToolCallId('call-done'), proposal, f.agent, 2) + f.ctx.actionCards.record(f.agent, { callId: ToolCallId('call-done'), action: ACTION, phase: 'dismissed' }) + f.propose(callId, proposal, f.agent, 3) + await f.kindFiber.dispose() + expect(await f.ctx.actionCards.act(f.agent, callId, 'dismiss', { choice: 'later' })).toEqual(dismissed()) + expect(f.view().decisions[callId]).toEqual({ callId, action: ACTION, phase: 'dismissed' }) + // The kind may return and act on its recorded phases, so they stay as they are. + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-failed'), 'dismiss')).toEqual({ status: 'settled', + decision: { callId: 'call-failed', action: ACTION, phase: 'failed' } }) + expect(await f.ctx.actionCards.act(f.agent, ToolCallId('call-done'), 'dismiss')).toEqual({ status: 'settled', + decision: { callId: 'call-done', action: ACTION, phase: 'dismissed' } }) + expect(f.act).not.toHaveBeenCalled() +}) diff --git a/packages/experimental/action-cards/tests/fixture.ts b/packages/experimental/action-cards/tests/fixture.ts new file mode 100644 index 0000000000..5110f34676 --- /dev/null +++ b/packages/experimental/action-cards/tests/fixture.ts @@ -0,0 +1,73 @@ +/** Real Agent, Tools, skills and projections with the card service and one scripted test kind. */ +import { Context } from '@deepseek-ai/cordis' +import { onTestFinished, vi } from 'vitest' +import { z } from 'zod' +import { createToolResultMessage, ToolCallId } from '@deepseek-ai/dsh-llm' +import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' +import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit' +import Skills, { BUNDLED_SKILL_RANK } from '@deepseek-ai/dsh-skill' +import * as SkillTool from '@deepseek-ai/dsh-tool-skill' +import RemoteAccess from '@deepseek-ai/dsh-api-remote-access' +import type { Agent } from '@deepseek-ai/dsh-agent' +import ActionCards from '../src/index.ts' +import type { ActionCardFields, ActionCardKind, ActionCardProposalOf, ActionCardResult } from '../src/index.ts' + +export const ACTION = 'test_action' +export const SKILL = 'test-skill' +export const INVITATION = 'Invite the user to try the card in one sentence.' +export const callId = ToolCallId('call-card-1') +export const proposal = { action: ACTION, title: 'Counter', brief: 'Keep a counter visible.', details: { value: 'a' } } + +/** Details of the test kind. */ +export interface TestDetails { readonly value: string } + +/** Provide one model-invocable skill per name. */ +export function provideSkills(ctx: Context, ...names: string[]): void { + ctx.skills.registerProvider(() => ({ + name: 'card-skills', + list: () => Promise.resolve(names.map(name => ({ + name, description: `Use ${name}.`, provider: 'card-skills', source: 'bundled' as const, rank: BUNDLED_SKILL_RANK, locator: null, + invocation: { modelInvocable: true, userInvocable: false }, + }))), + get: candidate => Promise.resolve({ name: candidate.name, description: candidate.description, provider: 'card-skills', source: 'bundled' as const, + invocation: candidate.invocation, content: `Call propose_action for ${candidate.name}.` }), + })) +} + +/** Mount the card service with one registered kind whose handler is a spy. */ +export async function fixture(kind: Partial> = {}) { + const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose() }) + await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) + const harness = await mountAgentLoopTestHarness(ctx) + await ctx.plugin(RemoteAccess) + await ctx.plugin(Skills) + await ctx.plugin(SkillTool) + provideSkills(ctx, SKILL, 'other-skill') + const cards = await ctx.plugin(ActionCards) + type Handler = ( + agent: Agent, card: ActionCardProposalOf, verb: string, payload: ActionCardFields | undefined, + ) => Promise + const act = vi.fn(async (_agent, card, verb) => + ({ status: 'settled', decision: { callId: card.callId, action: ACTION, phase: verb } })) + const kindFiber = await ctx.plugin({ name: 'test-kind', inject: ['actionCards'], apply: (inner: Context) => { + inner.actionCards.register({ + action: ACTION, skill: SKILL, invitation: INVITATION, details: z.object({ value: z.string() }), + verbs: { start: {}, publish: { global: true }, write: { writes: true } }, + act, ...kind, + }) + } }) + const agentHandle = await ctx.agents.create({ sessionId: SessionId('cards-root') }) + const agent = agentHandle.agent + const events: SessionEvent[] = [] + ctx.on('session/event', (session, event) => { if (session === agent.session) events.push(event) }) + let step = 0 + /** Log a successful `propose_action` call, as the loop does after the tool accepts it. */ + const propose = (id: ToolCallId = callId, args: object = proposal, caller = agent, turn = 1) => { + caller.session.append('tool/call', { turn, step: ++step, callId: id, name: 'propose_action', arguments: JSON.stringify(args) }) + caller.session.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id, content: [], isError: false }) }, { surfaceOp: 'append' }) + return id + } + const view = (caller = agent) => ctx.actionCards.view(caller.session) + return { ctx, harness, cards, kindFiber, agent, agentHandle, events, act, propose, view } +} diff --git a/packages/experimental/action-cards/tests/projection.spec.ts b/packages/experimental/action-cards/tests/projection.spec.ts new file mode 100644 index 0000000000..b734b1bfdd --- /dev/null +++ b/packages/experimental/action-cards/tests/projection.spec.ts @@ -0,0 +1,130 @@ +import { expect, it } from 'vitest' +import { createToolResultMessage, createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' +import { + appendPluginRecord, Session, SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq, type SessionEvent, type SessionHeader, +} from '@deepseek-ai/dsh-session' +import { actionCardProjection, type ActionCardState } from '../src/projection.ts' +import type { ActionCardDecision } from '../src/types.ts' + +const projection = actionCardProjection +const args = { action: 'test_action', title: 'Counter', brief: 'Keep count visible', details: { value: 'a' } } +let seq = 0 + +function init(inherited = 0): ActionCardState { + const header: SessionHeader = { version: SESSION_FORMAT_VERSION, id: SessionId('cards'), createdAt: 0, isSeeded: inherited > 0 } + return projection.init(header, SessionLogOffset(inherited)) +} + +function turn(value: number): SessionEvent<'turn/start'> { + return { type: 'turn/start', seq: SessionSeq(seq++), time: 0, data: { turn: value } } +} + +function call(callId: string, value: object | string = args, turnNumber = 1, name = 'propose_action'): SessionEvent<'tool/call'> { + return { type: 'tool/call', seq: SessionSeq(seq++), time: 0, + data: { turn: turnNumber, step: 1, callId: ToolCallId(callId), name, arguments: typeof value === 'string' ? value : JSON.stringify(value) } } +} + +function result(callId: string, isError = false, turnNumber = 1): SessionEvent<'tool/result'> { + return { type: 'tool/result', seq: SessionSeq(seq++), time: 0, surfaceOp: 'append', + data: { turn: turnNumber, step: 1, message: createToolResultMessage({ callId: ToolCallId(callId), content: [], isError }) } } +} + +function spliced(inserted: number, removedCount?: number, target: 'next-turn' | 'next-step' = 'next-turn'): SessionEvent<'agent/inbox/spliced'> { + return { type: 'agent/inbox/spliced', seq: SessionSeq(seq++), time: 0, data: { target, start: 0, ...removedCount === undefined ? {} : { removedCount }, + inserted: Array.from({ length: inserted }, () => createUserMessage({ content: [{ type: 'text', text: 'next' }], source: { kind: 'user' } })) } } +} + +function records(...data: ActionCardDecision[]): readonly SessionEvent[] { + const session = Session.create(SessionId('records')) + for (const value of data) appendPluginRecord(session, 'plugin:action-cards/decision', value) + return session.snapshotEvents() +} + +const fold = (events: readonly SessionEvent[], state = init()) => events.reduce(projection.apply, state) + +it('admits a successful propose_action call as a card in its turn and ignores failed, malformed and other calls', () => { + const state = fold([turn(1), call('c1'), result('c1')]) + expect(state.view.proposals).toEqual({ c1: { callId: 'c1', turn: 1, ...args } }) + expect(state.calls).toEqual({}) + for (const events of [ + [call('c2'), result('c2', true)], + [call('c3', '{broken')], + [call('c4', { ...args, title: ' ' })], + [call('c5', { ...args, details: 'text' })], + [call('c6', args, 1, 'other_tool'), result('c6')], + [result('unrelated')], + ]) { + expect(fold(events).view.proposals).toEqual({}) + } + const trimmed = fold([call('c7', { ...args, title: ' Counter ' }), result('c7')]) + expect(trimmed.view.proposals.c7?.title).toBe('Counter') + const pending = fold([call('c8')]) + expect(Object.keys(pending.calls)).toEqual(['c8']) + expect(projection.apply(pending, turn(2)).calls).toEqual({}) + const idle = fold([turn(3)]) + expect(projection.apply(idle, turn(4))).toBe(idle) +}) + +it('keeps the first admitted card of a turn and replaces only unstarted earlier cards', () => { + const first = fold([turn(1), call('a'), result('a')]) + const late = fold([call('b'), result('b')], first) + expect(Object.keys(late.view.proposals)).toEqual(['a']) + const next = fold([turn(2), call('c', args, 2), result('c', false, 2)], first) + expect(next.view.decisions).toEqual({ a: { callId: 'a', action: 'test_action', phase: 'replaced' } }) + const started = fold(records({ callId: ToolCallId('a'), action: 'test_action', phase: 'creating' }), first) + const afterStart = fold([turn(2), call('c', args, 2), result('c', false, 2)], started) + expect(afterStart.view.decisions.a?.phase).toBe('creating') + expect(afterStart.view.decisions.c).toBeUndefined() + // Replacement crosses kinds: a card of another action replaces the unstarted one too. + const other = fold([turn(2), call('d', { ...args, action: 'other_action' }, 2), result('d', false, 2)], first) + expect(other.view.decisions).toEqual({ a: { callId: 'a', action: 'test_action', phase: 'replaced' } }) +}) + +it('counts user messages queued for later turns for the card tool and leaves cards to it', () => { + const queued = fold([turn(1), spliced(1, undefined, 'next-step'), spliced(2), call('a'), result('a')]) + expect(queued.queued).toBe(2) + expect(Object.keys(queued.view.proposals)).toEqual(['a']) + expect(queued.view.decisions).toEqual({}) + expect(fold([spliced(0, 2)], queued).queued).toBe(0) + expect(fold([spliced(0, 5)]).queued).toBe(0) +}) + +it('ignores the events a forked Session inherits from its parent', () => { + const parent = [turn(1), call('a'), result('a')] + // Every event created so far precedes the fork point. + const inherited = init(seq) + expect(fold(parent, inherited)).toBe(inherited) + const own = fold([turn(2), call('b', args, 2), result('b', false, 2)], inherited) + expect(Object.keys(own.view.proposals)).toEqual(['b']) + expect(own.view.decisions).toEqual({}) +}) + +it('records valid decisions as pending status until a status notice acknowledges them', () => { + const [created, malformed, dismissed] = records( + { callId: ToolCallId('a'), action: 'test_action', phase: 'creating', data: { value: 1 } }, + { callId: ToolCallId('a'), action: 'test_action', phase: '' }, + { callId: ToolCallId('b'), action: 'test_action', phase: 'dismissed' }, + ) + const state = fold([created!, malformed!, dismissed!]) + expect(state.view.decisions).toEqual({ + a: { callId: 'a', action: 'test_action', phase: 'creating', data: { value: 1 } }, + b: { callId: 'b', action: 'test_action', phase: 'dismissed' }, + }) + expect(state.pendingStatus).toEqual({ a: created!.seq, b: dismissed!.seq }) + const notice = (through: number): SessionEvent<'user/message'> => ({ type: 'user/message', seq: SessionSeq(99), time: 0, surfaceOp: 'append', + data: createUserMessage({ content: [{ type: 'text', text: 'status' }], + source: { kind: 'action-cards', through: SessionSeq(through), form: 'notice', summary: 'status' } }) }) + expect(projection.apply(state, notice(created!.seq)).pendingStatus).toEqual({ b: dismissed!.seq }) + const user: SessionEvent<'user/message'> = { type: 'user/message', seq: SessionSeq(100), time: 0, surfaceOp: 'append', + data: createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) } + expect(projection.apply(state, user)).toBe(state) +}) + +it('publishes proposals and decisions as one view reused across internal state changes', () => { + const state = fold([turn(1), call('a'), result('a')]) + const view = projection.wire.view(state) + expect(view).toBe(state.view) + expect(projection.wire.viewSchema.parse(view)).toEqual(view) + expect(projection.wire.view(fold([spliced(1), call('pending')], state))).toBe(view) + expect(projection.stateSchema.parse(state)).toEqual(state) +}) diff --git a/packages/experimental/action-cards/tests/status.spec.ts b/packages/experimental/action-cards/tests/status.spec.ts new file mode 100644 index 0000000000..76a6727a95 --- /dev/null +++ b/packages/experimental/action-cards/tests/status.spec.ts @@ -0,0 +1,71 @@ +import { expect, it, vi } from 'vitest' +import { agentEvents, type Agent, type PreStepDecision } from '@deepseek-ai/dsh-agent' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { appendPluginRecord } from '@deepseek-ai/dsh-session' +import ActionCards from '../src/index.ts' +import { ACTION, callId, fixture } from './fixture.ts' + +type Fixture = Awaited> + +async function prepare(f: Fixture, agent: Agent = f.agent, signal = new AbortController().signal, admitted?: () => void | Promise) { + const decision = await agentEvents(f.ctx, agent).waterfall('agent/pre-step', { messages: [], turn: 2, step: 1, signal, modelSelection: { provider: 'mock', model: 'mock' } }, + async (): Promise => { await admitted?.(); return { kind: 'enter', messages: [] } }) + return decision.kind === 'reject' ? decision : { ...decision, messages: decision.messages.filter(message => message.source.kind === 'action-cards') } +} + +function enter(agent: Agent, decision: PreStepDecision): void { + if (decision.kind === 'enter') for (const message of decision.messages) agent.session.append('user/message', message, { surfaceOp: 'append' }) +} + +it('reports recorded decisions once on the next request, with each kind guidance once, without waking the conversation', async () => { + const f = await fixture({ statusGuidance: 'Failed runs can be retried from their card.', statusFields: ['reason'] }) + const followup = vi.spyOn(f.agent, 'followup') + expect(await prepare(f)).toEqual({ kind: 'enter', messages: [] }) + f.propose() + f.propose(ToolCallId('call-card-2'), { action: ACTION, title: 'Second', brief: 'Another card.', details: { value: 'b' } }, f.agent, 2) + expect(await prepare(f)).toEqual({ kind: 'enter', messages: [] }) + f.ctx.actionCards.record(f.agent, { callId, action: ACTION, phase: 'failed', data: { reason: 'timeout', internal: 'x' } }) + f.ctx.actionCards.record(f.agent, { callId: ToolCallId('call-card-2'), action: ACTION, phase: 'dismissed' }) + // A decision logged by a kind that is no longer registered is still reported. + appendPluginRecord(f.agent.session, 'plugin:action-cards/decision', { callId: ToolCallId('call-orphan'), action: 'retired_action', phase: 'done' }) + const decision = await prepare(f) + if (decision.kind !== 'enter') throw new Error('request was rejected') + expect(decision.messages).toHaveLength(1) + expect(decision.messages[0]!.source).toMatchObject({ kind: 'action-cards', form: 'notice', summary: 'Action card status updated' }) + expect(decision.messages[0]!.content).toMatchSnapshot('status notice') + enter(f.agent, decision) + expect(await prepare(f)).toEqual({ kind: 'enter', messages: [] }) + expect(followup).not.toHaveBeenCalled() +}) + +it('keeps undelivered status through rejection, cancellation and plugin reload', async () => { + const f = await fixture() + f.propose() + await f.ctx.actionCards.act(f.agent, callId, 'dismiss') + const cancelled = new AbortController() + await expect(prepare(f, f.agent, cancelled.signal, () => { cancelled.abort() })).rejects.toThrow() + const rejected = f.ctx.on('agent/pre-step', async (): Promise => ({ kind: 'reject' })) + expect((await prepare(f)).kind).toBe('reject') + rejected() + expect(await prepare(f, f.agent, undefined, async () => { await f.cards.dispose() })).toEqual({ kind: 'enter', messages: [] }) + await f.ctx.plugin(ActionCards, { queuedMessageLimit: 3 }) + const decision = await prepare(f) + if (decision.kind !== 'enter') throw new Error('request was rejected') + expect(JSON.stringify(decision.messages[0]!.content)).toContain('dismissed') +}) + +it('adds nothing when cancellation arrives after downstream preparation', async () => { + const f = await fixture() + f.propose() + await f.ctx.actionCards.act(f.agent, callId, 'dismiss') + await f.cards.dispose() + const controller = new AbortController() + // Prepended before the card service returns, so it runs right after the service's own listener. + f.ctx.on('agent/pre-step', async (_event, next) => { + const decision = await next() + controller.abort() + return decision + }, { prepend: true }) + await f.ctx.plugin(ActionCards) + expect(await prepare(f, f.agent, controller.signal)).toEqual({ kind: 'enter', messages: [] }) +}) diff --git a/packages/experimental/action-cards/tests/tool.spec.ts b/packages/experimental/action-cards/tests/tool.spec.ts new file mode 100644 index 0000000000..2bf978cd69 --- /dev/null +++ b/packages/experimental/action-cards/tests/tool.spec.ts @@ -0,0 +1,198 @@ +import { expect, it, vi } from 'vitest' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import type { ToolExecutionToken } from '@deepseek-ai/dsh-tools' +import { executeModelToolFixture, executeToolFixture } from '@deepseek-ai/dsh-tool-testkit' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +import { z } from 'zod' +import type { ActionCardKind, ActionCardResult } from '../src/index.ts' +import { ACTION, fixture, INVITATION, proposal, SKILL, type TestDetails } from './fixture.ts' + +type Fixture = Awaited> +const signal = new AbortController().signal +const modelSelection = { provider: 'mock', model: 'mock' } + +/** Run one call through the executor with the step admission the loop would resolve. */ +async function call(f: Fixture, name: string, args: object, id: string, agent: Agent = f.agent, parent?: ToolExecutionToken) { + const toolSet = await f.ctx.tools.resolve({ agent, modelSelection, signal }) + try { + return parent === undefined + ? await executeModelToolFixture(f.ctx, { callId: ToolCallId(id), name, arguments: args, agent, signal, toolSet }) + : await executeToolFixture(f.ctx, { callId: ToolCallId(id), name, arguments: args, agent, signal, toolSet, parent }) + } finally { toolSet.dispose() } +} + +async function declared(f: Fixture, agent: Agent = f.agent): Promise { + const toolSet = await f.ctx.tools.resolve({ agent, modelSelection, signal }) + const visible = toolSet.view(agent).has('propose_action') + toolSet.dispose() + return visible +} + +function text(result: { content: readonly { type: string; text?: string }[] }): string { + return result.content.map(block => block.text ?? '').join('') +} + +it('declares propose_action from the step after a card skill loads, and logs the addition', async () => { + const f = await fixture() + const adapter = new MockAdapter([ + toolCallResponse('call-skill', 'skill', { name: SKILL }), + toolCallResponse('call-card-1', 'propose_action', proposal), + textResponse('The card is below.'), + ]) + f.ctx.llm.registerAdapter(['mock'], adapter) + const agent = await f.harness.create(SessionId('loop'), modelSelection) + expect(await declared(f, agent)).toBe(false) + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'I want a counter.' }], source: { kind: 'user' } })) + await agent.whenIdle() + expect(adapter.requests.map(request => (request.tools ?? []).some(tool => tool.name === 'propose_action'))).toEqual([false, true, true]) + const tool = adapter.requests[1]!.tools!.find(candidate => candidate.name === 'propose_action') + expect(tool).toMatchSnapshot('tool declaration') + expect(agent.session.snapshotEvents().flatMap(event => event.type === 'developer/message' ? event.data.message.content : [])) + .toEqual([{ type: 'tool-addition', toolName: 'propose_action' }]) + expect(await declared(f, agent)).toBe(true) + const result = agent.session.snapshotEvents().find(event => event.type === 'tool/result' && event.data.message.toolCallId === 'call-card-1') + expect(result?.type === 'tool/result' && result.data.message.content).toEqual([ + { type: 'text', text: `Card shown below your answer. Do not restate it. ${INVITATION}` }, + ]) + expect(f.view(agent).proposals['call-card-1']).toEqual({ callId: 'call-card-1', turn: 1, ...proposal }) +}) + +it('declares nothing for other skills and stops declaring when the kind unregisters', async () => { + const f = await fixture() + f.agent.session.append('turn/start', { turn: 1 }) + expect((await call(f, 'skill', { name: 'other-skill' }, 'other')).isError).toBe(false) + expect((await call(f, 'skill', { name: 'missing-skill' }, 'missing')).isError).toBe(true) + expect(await declared(f)).toBe(false) + expect((await call(f, 'propose_action', proposal, 'early')).isError).toBe(true) + expect((await call(f, 'skill', { name: SKILL }, 'load')).isError).toBe(false) + expect((await call(f, 'skill', { name: SKILL }, 'reload')).isError).toBe(false) + expect((await executeToolFixture(f.ctx, { callId: ToolCallId('detached'), name: 'skill', arguments: { name: SKILL }, signal })).isError).toBe(false) + expect(await declared(f)).toBe(true) + await f.kindFiber.dispose() + expect(await declared(f)).toBe(false) + await f.cards.dispose() + expect(f.ctx.tools.get('propose_action')).toBeUndefined() +}) + +it('refuses unknown actions, a skill not loaded in this turn, blank fields, invalid details and nested calls', async () => { + const f = await fixture() + f.agent.session.append('turn/start', { turn: 1 }) + await call(f, 'skill', { name: SKILL }, 'load') + expect(text(await call(f, 'propose_action', { ...proposal, action: 'nope' }, 'unknown'))).toBe('Error: Unknown action "nope". Registered actions: test_action.') + expect(text(await call(f, 'propose_action', { ...proposal, title: ' ' }, 'blank'))).toBe('Error: Write a title and a brief.') + expect(text(await call(f, 'propose_action', { ...proposal, brief: '' }, 'blank-brief'))).toBe('Error: Write a title and a brief.') + expect(text(await call(f, 'propose_action', { ...proposal, details: { value: 1 } }, 'invalid'))) + .toBe('Error: Invalid details: details.value: Invalid input: expected string, received number') + expect(text(await call(f, 'propose_action', proposal, 'nested', f.agent, Symbol('outer') as ToolExecutionToken))) + .toBe('Error: Call propose_action directly, not from code.') + f.agent.session.append('turn/start', { turn: 2 }) + expect(text(await call(f, 'propose_action', proposal, 'stale'))).toBe('Error: Load skill test-skill first, then call propose_action.') + await f.kindFiber.dispose() + await call(f, 'skill', { name: SKILL }, 'load-again') + expect(await declared(f)).toBe(false) + expect(Object.keys(f.view().proposals)).toEqual([]) +}) + +it('accepts one card per turn, admits a retry after a failed result and names the registered actions', async () => { + const f = await fixture() + f.agent.session.append('turn/start', { turn: 1 }) + await call(f, 'skill', { name: SKILL }, 'load') + const blocked = f.ctx.on('tools/post-execute', async (exec, _result, next) => exec.callId === 'blocked' + ? { kind: 'block', feedback: [{ type: 'text', text: 'blocked' }] } : await next()) + expect((await call(f, 'propose_action', proposal, 'blocked')).isError).toBe(true) + blocked() + const accepted = await call(f, 'propose_action', { ...proposal, title: ' Counter ' }, 'accepted') + expect(accepted).toMatchObject({ isError: false, value: { action: ACTION, title: 'Counter', invitation: INVITATION } }) + expect(text(accepted)).toBe(`Card shown below your answer. Do not restate it. ${INVITATION}`) + const second = text(await call(f, 'propose_action', proposal, 'second')) + // Only an accepted call tells the model how to invite the user to its card. + expect(second).toBe('Error: One card per turn; the earlier card stands.') + expect(second).not.toContain(INVITATION) + f.agent.session.append('turn/start', { turn: 2 }) + await call(f, 'skill', { name: SKILL }, 'load-2') + f.propose(ToolCallId('logged'), proposal, f.agent, 2) + expect(text(await call(f, 'propose_action', proposal, 'after-log'))).toBe('Error: One card per turn; the earlier card stands.') + // Disposing the agent releases its activation. + expect(await declared(f)).toBe(true) + await f.agentHandle.dispose() + expect(await declared(f)).toBe(false) +}) + +it('activates nothing for a skill load dispatched from code, so a PTC preset never declares the tool', async () => { + const f = await fixture() + f.agent.session.append('turn/start', { turn: 1 }) + const outer = Symbol('outer') as ToolExecutionToken + expect((await call(f, 'skill', { name: SKILL }, 'nested-load', f.agent, outer)).isError).toBe(false) + expect(await declared(f)).toBe(false) + expect((await call(f, 'propose_action', proposal, 'unadmitted')).isError).toBe(true) + expect((await call(f, 'skill', { name: SKILL }, 'direct-load')).isError).toBe(false) + expect(await declared(f)).toBe(true) +}) + +it('offers cards in the main conversation only: subagent skill loads activate nothing and non-root calls are refused', async () => { + const f = await fixture() + const child = (await f.ctx.agents.create({ sessionId: SessionId('cards-child'), parentAgent: f.agent, + meta: { parentSession: f.agent.id, origin: 'subagent' } })).agent + child.session.append('turn/start', { turn: 1 }) + expect((await call(f, 'skill', { name: SKILL }, 'child-load', child)).isError).toBe(false) + expect(await declared(f, child)).toBe(false) + expect((await call(f, 'propose_action', proposal, 'child-card', child)).isError).toBe(true) + f.agent.session.append('turn/start', { turn: 1 }) + await call(f, 'skill', { name: SKILL }, 'load') + // Admission is fixed when the step starts; the call still checks that its agent is a live root. + const toolSet = await f.ctx.tools.resolve({ agent: f.agent, modelSelection, signal }) + vi.spyOn(f.ctx.agents, 'roots').mockReturnValue([]) + const refused = await executeModelToolFixture(f.ctx, { callId: ToolCallId('detached-root'), name: 'propose_action', arguments: proposal, + agent: f.agent, signal, toolSet }) + toolSet.dispose() + expect(text(refused)).toBe('Error: Cards are offered in the main conversation only.') +}) + +it('names no actions when the last kind unregisters after the step admitted the tool', async () => { + const f = await fixture() + f.agent.session.append('turn/start', { turn: 1 }) + await call(f, 'skill', { name: SKILL }, 'load') + const toolSet = await f.ctx.tools.resolve({ agent: f.agent, modelSelection, signal }) + await f.kindFiber.dispose() + const result = await executeModelToolFixture(f.ctx, { callId: ToolCallId('late'), name: 'propose_action', arguments: proposal, + agent: f.agent, signal, toolSet }) + toolSet.dispose() + expect(text(result)).toBe('Error: Unknown action "test_action". Registered actions: none.') +}) + +it('refuses a card while the user has queued messages for later turns, so the log holds no card', async () => { + const f = await fixture() + const queued = () => createUserMessage({ content: [{ type: 'text', text: 'Next question.' }], source: { kind: 'user' } }) + const live: { agent?: Agent } = {} + const adapter = new MockAdapter([ + toolCallResponse('call-skill', 'skill', { name: SKILL }), + () => { + // The user queues two more messages while the model writes its answer. + live.agent!.inbox.append('next-turn', queued()) + live.agent!.inbox.append('next-turn', queued()) + return toolCallResponse('call-card-1', 'propose_action', proposal) + }, + textResponse('Short answer.'), + textResponse('Next answer.'), + textResponse('Last answer.'), + ]) + f.ctx.llm.registerAdapter(['mock'], adapter) + const agent = await f.harness.create(SessionId('queued'), modelSelection) + live.agent = agent + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'I want a counter.' }], source: { kind: 'user' } })) + await agent.whenIdle() + const result = agent.session.snapshotEvents().find(event => event.type === 'tool/result' && event.data.message.toolCallId === 'call-card-1') + expect(result?.type === 'tool/result' && result.data.message).toMatchObject({ isError: true, + content: [{ type: 'text', text: 'Error: The user has queued more messages; show no card and answer briefly.' }] }) + expect(f.view(agent)).toEqual({ proposals: {}, decisions: {} }) +}) + +it('requires every kind to give the invitation its accepted calls return', () => { + const kind = { action: 'other_action', skill: 'other-skill', details: z.object({ value: z.string() }), verbs: {}, + act: () => Promise.resolve({ status: 'accepted' }) } + // @ts-expect-error: a kind without its invitation does not compile. + const incomplete: ActionCardKind = kind + expect(incomplete).toBe(kind) +}) diff --git a/packages/experimental/action-cards/tsconfig.json b/packages/experimental/action-cards/tsconfig.json new file mode 100644 index 0000000000..c5e45d6303 --- /dev/null +++ b/packages/experimental/action-cards/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [ + { "path": "../../../vendor/cordis" }, + { "path": "../../typert/protocol" }, + { "path": "../../llm/llm" }, + { "path": "../../core/tools" }, + { "path": "../../core/agent" }, + { "path": "../../core/session" }, + { "path": "../../session/session-projection" }, + { "path": "../../api/remote-access/tsconfig.host.json" }, + { "path": "../../sandbox/sandbox-policy" }, + { "path": "../../util/values" } + ], + "files": [ + "src/index.ts", + "src/types.ts", + "src/projection.ts" + ] +} diff --git a/packages/experimental/action-cards/tsdown.config.ts b/packages/experimental/action-cards/tsdown.config.ts new file mode 100644 index 0000000000..46d896c05a --- /dev/null +++ b/packages/experimental/action-cards/tsdown.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsdown' + +/** Build the action card plugin. */ +export default defineConfig([ + { + entry: ['lib/types/index.js'], + outDir: 'lib', + format: ['esm'], + platform: 'node', + target: 'es2024', + fixedExtension: false, + dts: false, + clean: false, + }, +]) diff --git a/packages/experimental/agent-team-profile/locale/de.json b/packages/experimental/agent-team-profile/locale/de.json new file mode 100644 index 0000000000..07058805df --- /dev/null +++ b/packages/experimental/agent-team-profile/locale/de.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Agententeams", + "description": "Aktivieren Sie Teamzusammenarbeit, Teamtools, die Mitgliederliste und das gemeinsame Aufgabenboard.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/agent-team-profile/locale/es.json b/packages/experimental/agent-team-profile/locale/es.json new file mode 100644 index 0000000000..b094b4371b --- /dev/null +++ b/packages/experimental/agent-team-profile/locale/es.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Equipos de agentes", + "description": "Habilite la colaboración en equipo, las herramientas del equipo, la lista de miembros y el tablero de tareas compartido.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/agent-team-profile/locale/fr.json b/packages/experimental/agent-team-profile/locale/fr.json new file mode 100644 index 0000000000..a37e31344d --- /dev/null +++ b/packages/experimental/agent-team-profile/locale/fr.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Équipes d'agents", + "description": "Activez la collaboration en équipe, les outils d’équipe, la liste des membres et le tableau des tâches partagé.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/agent-team-profile/locale/pt.json b/packages/experimental/agent-team-profile/locale/pt.json new file mode 100644 index 0000000000..12b0894600 --- /dev/null +++ b/packages/experimental/agent-team-profile/locale/pt.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Equipes de agentes", + "description": "Habilite a colaboração em equipe, as ferramentas da equipe, a lista de membros e o quadro de tarefas compartilhado.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/agent-team-profile/locale/ru.json b/packages/experimental/agent-team-profile/locale/ru.json new file mode 100644 index 0000000000..828f0f7b3c --- /dev/null +++ b/packages/experimental/agent-team-profile/locale/ru.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Команды агентов", + "description": "Включите совместную работу в команде, инструменты группы, список участников и общую доску задач.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index cb7b06d7bd..5f6ff6988f 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/agent-team/locale/de.json b/packages/experimental/agent-team/locale/de.json new file mode 100644 index 0000000000..1822ee3f21 --- /dev/null +++ b/packages/experimental/agent-team/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Teamzusammenarbeit", + "description": "Verwalten Sie Teammitglieder, Mitgliedernachrichten und gemeinsame Aufgaben." + } +} diff --git a/packages/experimental/agent-team/locale/es.json b/packages/experimental/agent-team/locale/es.json new file mode 100644 index 0000000000..7bed5c6282 --- /dev/null +++ b/packages/experimental/agent-team/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Colaboración en equipo", + "description": "Administre los miembros del equipo, los mensajes de los miembros y las tareas compartidas." + } +} diff --git a/packages/experimental/agent-team/locale/fr.json b/packages/experimental/agent-team/locale/fr.json new file mode 100644 index 0000000000..df95c9af80 --- /dev/null +++ b/packages/experimental/agent-team/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Collaboration en équipe", + "description": "Gérez les membres de l'équipe, les messages des membres et les tâches partagées." + } +} diff --git a/packages/experimental/agent-team/locale/pt.json b/packages/experimental/agent-team/locale/pt.json new file mode 100644 index 0000000000..5ee714778a --- /dev/null +++ b/packages/experimental/agent-team/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Colaboração em equipe", + "description": "Gerencie membros da equipe, mensagens de membros e tarefas compartilhadas." + } +} diff --git a/packages/experimental/agent-team/locale/ru.json b/packages/experimental/agent-team/locale/ru.json new file mode 100644 index 0000000000..cea628fa82 --- /dev/null +++ b/packages/experimental/agent-team/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Командное сотрудничество", + "description": "Управляйте членами команды, сообщениями участников и общими задачами." + } +} diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index af9f829179..3e63072332 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, direct peer messaging, and shared task DAG", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/api-speech-to-text/locale/de.json b/packages/experimental/api-speech-to-text/locale/de.json new file mode 100644 index 0000000000..5b3c8be0ab --- /dev/null +++ b/packages/experimental/api-speech-to-text/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sprachtranskription API", + "description": "Bereitstellung von Sprachtranskriptionen für Browser-Clients." + } +} diff --git a/packages/experimental/api-speech-to-text/locale/es.json b/packages/experimental/api-speech-to-text/locale/es.json new file mode 100644 index 0000000000..63f8474505 --- /dev/null +++ b/packages/experimental/api-speech-to-text/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Transcripción de voz API", + "description": "Proporcionar transcripción de voz para clientes de navegador." + } +} diff --git a/packages/experimental/api-speech-to-text/locale/fr.json b/packages/experimental/api-speech-to-text/locale/fr.json new file mode 100644 index 0000000000..757cdc6d2b --- /dev/null +++ b/packages/experimental/api-speech-to-text/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Transcription vocale API", + "description": "Fournir une transcription vocale pour les clients du navigateur." + } +} diff --git a/packages/experimental/api-speech-to-text/locale/pt.json b/packages/experimental/api-speech-to-text/locale/pt.json new file mode 100644 index 0000000000..b2b8172215 --- /dev/null +++ b/packages/experimental/api-speech-to-text/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Transcrição de fala API", + "description": "Fornece transcrição de fala para clientes de navegador." + } +} diff --git a/packages/experimental/api-speech-to-text/locale/ru.json b/packages/experimental/api-speech-to-text/locale/ru.json new file mode 100644 index 0000000000..92f969dc64 --- /dev/null +++ b/packages/experimental/api-speech-to-text/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Транскрипция речи API", + "description": "Обеспечьте транскрипцию речи для клиентов браузера." + } +} diff --git a/packages/experimental/api-speech-to-text/package.json b/packages/experimental/api-speech-to-text/package.json index 5b34282933..7fd6730f0a 100644 --- a/packages/experimental/api-speech-to-text/package.json +++ b/packages/experimental/api-speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-api-speech-to-text", "description": "Authenticated experimental speech transcription for browser clients", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/auto-review/locale/de.json b/packages/experimental/auto-review/locale/de.json new file mode 100644 index 0000000000..457bddd8e4 --- /dev/null +++ b/packages/experimental/auto-review/locale/de.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Automatische Autorisierungsüberprüfung", + "description": "Fügen Sie einen Auto review-Berechtigungsmodus hinzu, der das Modell verwendet, um die Autorisierung vor jedem Toolaufruf zu bewerten.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/auto-review/locale/es.json b/packages/experimental/auto-review/locale/es.json new file mode 100644 index 0000000000..0ff73b015d --- /dev/null +++ b/packages/experimental/auto-review/locale/es.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Revisión de autorización automática", + "description": "Agregue un modo de permiso Auto review que utilice el modelo para evaluar la autorización antes de cada llamada a la herramienta.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/auto-review/locale/fr.json b/packages/experimental/auto-review/locale/fr.json new file mode 100644 index 0000000000..533cd20bf0 --- /dev/null +++ b/packages/experimental/auto-review/locale/fr.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Examen des autorisations automatiques", + "description": "Ajoutez un mode d'autorisation Auto review qui utilise le modèle pour évaluer l'autorisation avant chaque appel d'outil.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/auto-review/locale/pt.json b/packages/experimental/auto-review/locale/pt.json new file mode 100644 index 0000000000..6dd7e9ee7d --- /dev/null +++ b/packages/experimental/auto-review/locale/pt.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Revisão de autorização automática", + "description": "Adicione um modo de permissão Auto review que usa o modelo para avaliar a autorização antes de cada chamada de ferramenta.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/auto-review/locale/ru.json b/packages/experimental/auto-review/locale/ru.json new file mode 100644 index 0000000000..ddb103d129 --- /dev/null +++ b/packages/experimental/auto-review/locale/ru.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Проверка автоматической авторизации", + "description": "Добавьте режим разрешений Auto review, который использует модель для оценки авторизации перед каждым вызовом инструмента.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/auto-review/package.json b/packages/experimental/auto-review/package.json index f24edf48cb..72d462a7ad 100644 --- a/packages/experimental/auto-review/package.json +++ b/packages/experimental/auto-review/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-auto-review", "description": "Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "icon": "./icon.svg", "repository": { "type": "git", diff --git a/packages/experimental/auto-review/src/locales.ts b/packages/experimental/auto-review/src/locales.ts index 4a4dc5deff..df43559f1e 100644 --- a/packages/experimental/auto-review/src/locales.ts +++ b/packages/experimental/auto-review/src/locales.ts @@ -18,13 +18,52 @@ export const ja = { deniedWithReason: (reason: string): string => `自動認可レビューがこの呼び出しを拒否しました:${reason}`, } satisfies typeof en +/** Spanish dictionary, checked against the English keys. */ +export const es = { + denied: 'Auto review ha denegado esta llamada.', + deniedWithReason: (reason: string): string => `Auto review ha denegado esta llamada: ${reason}`, +} satisfies typeof en + +/** French dictionary, checked against the English keys. */ +export const fr = { + denied: 'Auto review a refusé cet appel.', + deniedWithReason: (reason: string): string => `Auto review a refusé cet appel : ${reason}`, +} satisfies typeof en + +/** German dictionary, checked against the English keys. */ +export const de = { + denied: 'Auto review hat diesen Aufruf abgelehnt.', + deniedWithReason: (reason: string): string => `Auto review hat diesen Aufruf abgelehnt: ${reason}`, +} satisfies typeof en + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + denied: 'Auto review отклонила этот вызов.', + deniedWithReason: (reason: string): string => `Auto review отклонила этот вызов: ${reason}`, +} satisfies typeof en + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + denied: 'Auto review negou esta chamada.', + deniedWithReason: (reason: string): string => `Auto review negou esta chamada: ${reason}`, +} satisfies typeof en + /** * Build all display translations without translating the reviewer's raw reason. * @param reason - optional reviewer explanation. * @returns text for the Client's localized-text resolver. */ -export function deniedDisplayReason(reason?: string): Record<'en' | 'zh' | 'ja', string> { +export function deniedDisplayReason(reason?: string): Record<'en' | 'zh' | 'ja' | 'es' | 'fr' | 'de' | 'ru' | 'pt', string> { return reason === undefined - ? { en: en.denied, zh: zh.denied, ja: ja.denied } - : { en: en.deniedWithReason(reason), zh: zh.deniedWithReason(reason), ja: ja.deniedWithReason(reason) } + ? { en: en.denied, zh: zh.denied, ja: ja.denied, es: es.denied, fr: fr.denied, de: de.denied, ru: ru.denied, pt: pt.denied } + : { + en: en.deniedWithReason(reason), + zh: zh.deniedWithReason(reason), + ja: ja.deniedWithReason(reason), + es: es.deniedWithReason(reason), + fr: fr.deniedWithReason(reason), + de: de.deniedWithReason(reason), + ru: ru.deniedWithReason(reason), + pt: pt.deniedWithReason(reason), + } } diff --git a/packages/experimental/auto-review/tests/auto-review.spec.ts b/packages/experimental/auto-review/tests/auto-review.spec.ts index 035ae3c923..20ab50c4c4 100644 --- a/packages/experimental/auto-review/tests/auto-review.spec.ts +++ b/packages/experimental/auto-review/tests/auto-review.spec.ts @@ -966,9 +966,16 @@ describe('native review request', () => { ? 'Auto review denied tool "probe"' : `Auto review denied tool "probe": ${item.expectedReason}`) expect(displayReasons.at(-1)).toEqual(item.expectedReason === undefined - ? { en: 'Auto review denied this call.', zh: 'Auto review 拒绝了此调用。', ja: '自動認可レビューがこの呼び出しを拒否しました。' } + ? { en: 'Auto review denied this call.', zh: 'Auto review 拒绝了此调用。', ja: '自動認可レビューがこの呼び出しを拒否しました。', + es: 'Auto review ha denegado esta llamada.', fr: 'Auto review a refusé cet appel.', + de: 'Auto review hat diesen Aufruf abgelehnt.', ru: 'Auto review отклонила этот вызов.', pt: 'Auto review negou esta chamada.' } : { en: `Auto review denied this call: ${item.expectedReason}`, zh: `Auto review 拒绝了此调用:${item.expectedReason}`, - ja: `自動認可レビューがこの呼び出しを拒否しました:${item.expectedReason}` }) + ja: `自動認可レビューがこの呼び出しを拒否しました:${item.expectedReason}`, + es: `Auto review ha denegado esta llamada: ${item.expectedReason}`, + fr: `Auto review a refusé cet appel : ${item.expectedReason}`, + de: `Auto review hat diesen Aufruf abgelehnt: ${item.expectedReason}`, + ru: `Auto review отклонила этот вызов: ${item.expectedReason}`, + pt: `Auto review negou esta chamada: ${item.expectedReason}` }) } expect(probe.runs()).toBe(2) diff --git a/packages/experimental/badge-skill-bundle/locale/de.json b/packages/experimental/badge-skill-bundle/locale/de.json new file mode 100644 index 0000000000..57c7bc3d08 --- /dev/null +++ b/packages/experimental/badge-skill-bundle/locale/de.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "DSH-Abzeichen-Fertigkeit", + "description": "Lassen Sie Agenten Dokumente, Pull-Requests und andere Ausgaben mit einem „powered by dsh“-Badge versehen.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/badge-skill-bundle/locale/es.json b/packages/experimental/badge-skill-bundle/locale/es.json new file mode 100644 index 0000000000..406e9f43c2 --- /dev/null +++ b/packages/experimental/badge-skill-bundle/locale/es.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Habilidad de insignia DSH", + "description": "Permita que los agentes agreguen una insignia \"con tecnología de dsh\" a documentos, solicitudes de extracción y otros resultados.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/badge-skill-bundle/locale/fr.json b/packages/experimental/badge-skill-bundle/locale/fr.json new file mode 100644 index 0000000000..2b173eea29 --- /dev/null +++ b/packages/experimental/badge-skill-bundle/locale/fr.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "Compétence du badge DSH", + "description": "Laissez les agents ajouter un badge « propulsé par dsh » aux documents, demandes d'extraction et autres sorties.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/badge-skill-bundle/locale/pt.json b/packages/experimental/badge-skill-bundle/locale/pt.json new file mode 100644 index 0000000000..bc1d6fab6f --- /dev/null +++ b/packages/experimental/badge-skill-bundle/locale/pt.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "DSH habilidade de distintivo", + "description": "Permita que os agentes adicionem um selo “powered by dsh” a documentos, pull requests e outros resultados.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/badge-skill-bundle/locale/ru.json b/packages/experimental/badge-skill-bundle/locale/ru.json new file mode 100644 index 0000000000..14770f9816 --- /dev/null +++ b/packages/experimental/badge-skill-bundle/locale/ru.json @@ -0,0 +1,7 @@ +{ + "meta": { + "title": "DSH значок навыка", + "description": "Позвольте агентам добавлять значок «Powered by dsh» к документам, запросам на включение и другим результатам.", + "detail": "docs/DETAIL.md" + } +} diff --git a/packages/experimental/badge-skill-bundle/package.json b/packages/experimental/badge-skill-bundle/package.json index 960895540d..4de3052489 100644 --- a/packages/experimental/badge-skill-bundle/package.json +++ b/packages/experimental/badge-skill-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-badge-skill-bundle", "description": "Let agents add a “powered by dsh” badge to documents, pull requests, and other outputs.", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/locale/de.json b/packages/experimental/browser-use-chrome-devtools-mcp/locale/de.json new file mode 100644 index 0000000000..a81cfdba45 --- /dev/null +++ b/packages/experimental/browser-use-chrome-devtools-mcp/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Browsernutzung (Chrome DevTools)", + "description": "Lassen Sie das Modell Webseiten über Chrome DevTools MCP in einem gestarteten Chrome überprüfen und betreiben." + } +} diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/locale/es.json b/packages/experimental/browser-use-chrome-devtools-mcp/locale/es.json new file mode 100644 index 0000000000..bf5a4aab96 --- /dev/null +++ b/packages/experimental/browser-use-chrome-devtools-mcp/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Uso del navegador (Chrome DevTools)", + "description": "Deje que el modelo inspeccione y opere páginas web a través de Chrome DevTools MCP en un Chrome iniciado." + } +} diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/locale/fr.json b/packages/experimental/browser-use-chrome-devtools-mcp/locale/fr.json new file mode 100644 index 0000000000..9160ad625c --- /dev/null +++ b/packages/experimental/browser-use-chrome-devtools-mcp/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Utilisation du navigateur (Chrome DevTools)", + "description": "Laissez le modèle inspecter et exploiter les pages Web via Chrome DevTools MCP dans un Chrome lancé." + } +} diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/locale/pt.json b/packages/experimental/browser-use-chrome-devtools-mcp/locale/pt.json new file mode 100644 index 0000000000..6670caf350 --- /dev/null +++ b/packages/experimental/browser-use-chrome-devtools-mcp/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Uso do navegador (Chrome DevTools)", + "description": "Deixe o modelo inspecionar e operar páginas da Web por meio do Chrome DevTools MCP em um Chrome iniciado." + } +} diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/locale/ru.json b/packages/experimental/browser-use-chrome-devtools-mcp/locale/ru.json new file mode 100644 index 0000000000..4a3826074c --- /dev/null +++ b/packages/experimental/browser-use-chrome-devtools-mcp/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Использование браузера (Chrome DevTools)", + "description": "Позвольте модели проверять и управлять веб-страницами с помощью Chrome DevTools MCP в запущенном Chrome." + } +} diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/package.json b/packages/experimental/browser-use-chrome-devtools-mcp/package.json index 5d617f5385..96f5e8b344 100644 --- a/packages/experimental/browser-use-chrome-devtools-mcp/package.json +++ b/packages/experimental/browser-use-chrome-devtools-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp", "description": "Experimental per-Session Chromium browser tools through chrome-devtools-mcp", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-playwright-mcp/locale/de.json b/packages/experimental/browser-use-playwright-mcp/locale/de.json new file mode 100644 index 0000000000..2d22818be0 --- /dev/null +++ b/packages/experimental/browser-use-playwright-mcp/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Browsernutzung (Dramatiker)", + "description": "Lassen Sie das Modell über Playwright MCP in einem gestarteten Chromium Webseiten durchsuchen und bedienen." + } +} diff --git a/packages/experimental/browser-use-playwright-mcp/locale/es.json b/packages/experimental/browser-use-playwright-mcp/locale/es.json new file mode 100644 index 0000000000..6230f676bd --- /dev/null +++ b/packages/experimental/browser-use-playwright-mcp/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Uso del navegador (Dramaturgo)", + "description": "Deje que el modelo navegue y opere páginas web a través de Playwright MCP en un Chromium iniciado." + } +} diff --git a/packages/experimental/browser-use-playwright-mcp/locale/fr.json b/packages/experimental/browser-use-playwright-mcp/locale/fr.json new file mode 100644 index 0000000000..f6371998c4 --- /dev/null +++ b/packages/experimental/browser-use-playwright-mcp/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Utilisation du navigateur (dramaturge)", + "description": "Laissez le modèle parcourir et exploiter des pages Web via Playwright MCP dans un Chromium lancé." + } +} diff --git a/packages/experimental/browser-use-playwright-mcp/locale/pt.json b/packages/experimental/browser-use-playwright-mcp/locale/pt.json new file mode 100644 index 0000000000..5041d561af --- /dev/null +++ b/packages/experimental/browser-use-playwright-mcp/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Uso do navegador (dramaturgo)", + "description": "Deixe o modelo navegar e operar páginas da web por meio do Playwright MCP em um Chromium iniciado." + } +} diff --git a/packages/experimental/browser-use-playwright-mcp/locale/ru.json b/packages/experimental/browser-use-playwright-mcp/locale/ru.json new file mode 100644 index 0000000000..2f641f4131 --- /dev/null +++ b/packages/experimental/browser-use-playwright-mcp/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Использование браузера (Драматург)", + "description": "Позвольте модели просматривать и управлять веб-страницами через Playwright MCP в запущенном Chromium." + } +} diff --git a/packages/experimental/browser-use-playwright-mcp/package.json b/packages/experimental/browser-use-playwright-mcp/package.json index 727a8bccd3..45b94b4b8a 100644 --- a/packages/experimental/browser-use-playwright-mcp/package.json +++ b/packages/experimental/browser-use-playwright-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp", "description": "Experimental per-Session Chromium browser tools through @playwright/mcp", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-runtime/package.json b/packages/experimental/browser-use-runtime/package.json index 634de5372f..64ff3470c2 100644 --- a/packages/experimental/browser-use-runtime/package.json +++ b/packages/experimental/browser-use-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-runtime", "description": "Session-owned browser resource lifecycles and MCP integration for experimental providers", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-stagehand-native/locale/de.json b/packages/experimental/browser-use-stagehand-native/locale/de.json new file mode 100644 index 0000000000..640f98244e --- /dev/null +++ b/packages/experimental/browser-use-stagehand-native/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Stagehand-Browsertools", + "description": "Stellen Sie experimentelle Stagehand-Browsertools mithilfe eines separat konfigurierten nativen Modells bereit." + } +} diff --git a/packages/experimental/browser-use-stagehand-native/locale/es.json b/packages/experimental/browser-use-stagehand-native/locale/es.json new file mode 100644 index 0000000000..d62a23cabd --- /dev/null +++ b/packages/experimental/browser-use-stagehand-native/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Herramientas de navegador Stagehand", + "description": "Proporcione herramientas experimentales de navegador Stagehand utilizando un modelo nativo configurado por separado." + } +} diff --git a/packages/experimental/browser-use-stagehand-native/locale/fr.json b/packages/experimental/browser-use-stagehand-native/locale/fr.json new file mode 100644 index 0000000000..1818988034 --- /dev/null +++ b/packages/experimental/browser-use-stagehand-native/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Outils de navigation Stagehand", + "description": "Fournissez des outils de navigation expérimentaux Stagehand à l’aide d’un modèle natif configuré séparément." + } +} diff --git a/packages/experimental/browser-use-stagehand-native/locale/pt.json b/packages/experimental/browser-use-stagehand-native/locale/pt.json new file mode 100644 index 0000000000..2094cf3d15 --- /dev/null +++ b/packages/experimental/browser-use-stagehand-native/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ferramentas de navegador para ajudantes de palco", + "description": "Forneça ferramentas experimentais de navegador Stagehand usando um modelo nativo configurado separadamente." + } +} diff --git a/packages/experimental/browser-use-stagehand-native/locale/ru.json b/packages/experimental/browser-use-stagehand-native/locale/ru.json new file mode 100644 index 0000000000..8ac48a94c7 --- /dev/null +++ b/packages/experimental/browser-use-stagehand-native/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Инструменты браузера Stagehand", + "description": "Предоставьте экспериментальные инструменты браузера Stagehand, используя отдельно настроенную собственную модель." + } +} diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json index 4805d55e82..ecdf47ff23 100644 --- a/packages/experimental/browser-use-stagehand-native/package.json +++ b/packages/experimental/browser-use-stagehand-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-stagehand-native", "description": "Experimental Stagehand browser tools with separately configured native models", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/claude-code-mods/locale/de.json b/packages/experimental/claude-code-mods/locale/de.json new file mode 100644 index 0000000000..ee27e0225b --- /dev/null +++ b/packages/experimental/claude-code-mods/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code-Mods", + "description": "Laden Sie Claude Code-Verlängerungen und führen Sie ihre Haken durch die Harness-Verlängerungspunkte." + } +} diff --git a/packages/experimental/claude-code-mods/locale/es.json b/packages/experimental/claude-code-mods/locale/es.json new file mode 100644 index 0000000000..b2fa75ad23 --- /dev/null +++ b/packages/experimental/claude-code-mods/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modificaciones Claude Code", + "description": "Cargue las extensiones Claude Code y pase sus ganchos a través de los puntos de extensión del arnés." + } +} diff --git a/packages/experimental/claude-code-mods/locale/fr.json b/packages/experimental/claude-code-mods/locale/fr.json new file mode 100644 index 0000000000..bec4fc8ea4 --- /dev/null +++ b/packages/experimental/claude-code-mods/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Mods Claude Code", + "description": "Chargez les extensions Claude Code et faites passer leurs crochets à travers les points d'extension du harnais." + } +} diff --git a/packages/experimental/claude-code-mods/locale/pt.json b/packages/experimental/claude-code-mods/locale/pt.json new file mode 100644 index 0000000000..d1fb2d9b07 --- /dev/null +++ b/packages/experimental/claude-code-mods/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code modificações", + "description": "Carregue Claude Code extensões e passe seus ganchos pelos pontos de extensão do chicote." + } +} diff --git a/packages/experimental/claude-code-mods/locale/ru.json b/packages/experimental/claude-code-mods/locale/ru.json new file mode 100644 index 0000000000..5d347cee67 --- /dev/null +++ b/packages/experimental/claude-code-mods/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code моды", + "description": "Загрузите расширения Claude Code и пропустите их перехваты через точки расширения Harness." + } +} diff --git a/packages/experimental/claude-code-mods/package.json b/packages/experimental/claude-code-mods/package.json index e38a0ea770..2580f8603a 100644 --- a/packages/experimental/claude-code-mods/package.json +++ b/packages/experimental/claude-code-mods/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-claude-code-mods", "description": "Experimental bridge: load Claude Code mods (hooks modules) and run their hook chains on DeepSeek Harness extension points", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts b/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts index a0a2e6cce0..f54c31bfcf 100644 --- a/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts +++ b/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts @@ -5,7 +5,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' -import { afterEach, expect, it } from 'vitest' +import { afterEach, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' @@ -23,6 +23,9 @@ import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit' import * as ClaudeCodeMods from '../src/index.ts' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +// Keep workspace imports in Vitest's source graph even when the config has a package scope. +vi.mock('@deepseek-ai/cordis-plugin-loader/src/resolve.ts', () => ({ createResolve: () => undefined })) + const FIXTURES = resolve(import.meta.dirname, 'fixtures') let ctx: Context | undefined @@ -37,6 +40,7 @@ afterEach(async () => { it('loads from cordis.yml, counts the model\'s tool calls, and answers /tally through the composed command registry', async () => { root = await mkdtemp(join(tmpdir(), 'dsh-cc-mods-composition-')) + await writeFile(join(root, 'package.json'), JSON.stringify({ type: 'module' })) const configPath = join(root, 'cordis.yml') const modules = new Map([ ['@deepseek-ai/dsh-llm', LlmRuntime], @@ -66,8 +70,6 @@ it('loads from cordis.yml, counts the model\'s tool calls, and answers /tally th context.baseUrl = pathToFileURL(root).href + '/' await context.plugin(Loader) context.loader.builtins.include = Include - // Without a Node internal loader the Loader imports bare names through this - // test runner's module graph, which resolves workspace packages to `src`. context.loader.internal = undefined await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } }) await context.loader.await() diff --git a/packages/experimental/client-ui-action-cards/README.i18n.yaml b/packages/experimental/client-ui-action-cards/README.i18n.yaml new file mode 100644 index 0000000000..d750da7f5b --- /dev/null +++ b/packages/experimental/client-ui-action-cards/README.i18n.yaml @@ -0,0 +1,40 @@ +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/experimental/client-ui-action-cards/README.md +/: + en: eec43de9002f4ab1 + zh: 0d2fddca5a1f993a +/deepseek-ai-dsh-client-ui-action-cards: + en: c36a554629f96f79 + zh: 0ea837da87938e52 +/deepseek-ai-dsh-client-ui-action-cards/summary: + en: 89ea57734c42a481 + zh: c7a4f5f0b59b78ad +/deepseek-ai-dsh-client-ui-action-cards/table-of-contents: + en: d152484eb41ac6b4 + zh: b00dddf4c88f0a45 +/deepseek-ai-dsh-client-ui-action-cards/use-this-package: + en: 717a53d2170d253b + zh: 4e52852690601371 +/deepseek-ai-dsh-client-ui-action-cards/use-this-package/register-a-card-body: + en: 9ec9f9f8f3165d3e + zh: 2d8154ec2fe0c9ab +/deepseek-ai-dsh-client-ui-action-cards/understand-the-implementation: + en: 168ae39a83d6f4eb + zh: 312cc12876bca834 +/deepseek-ai-dsh-client-ui-action-cards/further-exploration: + en: 55e4d74a33cdeeb3 + zh: f601d5a4dcf63ec5 +/deepseek-ai-dsh-client-ui-action-cards/model-experience: + en: f80a4b656c108f22 + zh: a3e30f46fa7136ac +/deepseek-ai-dsh-client-ui-action-cards/model-experience/kv-cache-effect: + en: 9cb7131bde146341 + zh: 1c7d1af00f58004d +/deepseek-ai-dsh-client-ui-action-cards/known-limitations-and-deferred-work: + en: df94821940e4e2b8 + zh: 490441ab06b153c9 +/deepseek-ai-dsh-client-ui-action-cards/known-limitations-and-deferred-work/dev-note: + en: e67f633dc6fe7773 + zh: bd8a8936229e035e diff --git a/packages/experimental/client-ui-action-cards/README.md b/packages/experimental/client-ui-action-cards/README.md new file mode 100644 index 0000000000..930c5e0fcb --- /dev/null +++ b/packages/experimental/client-ui-action-cards/README.md @@ -0,0 +1,89 @@ +--- +description: "Browser half of optional action cards: the card frame below completed answers, the body registry that experiment packages fill, and the one-line process row of propose_action calls." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-action-cards + +English | [中文](README.zh.md) + +## Summary + +Users see an optional action card below a completed answer: its title, a brief that folds when long, and the controls of the experiment that proposed it. In the process area, the proposal shows as one row with the card title and whether the call was accepted; a refused call expands to its reason. A card whose experiment provides no controls in this browser offers only Not now. Showing, expanding and folding cards make no model request. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Enable the [Personalized recommendations](../personalized-recommendations/README.md) bundle, which inserts this package beside its [Host service](../action-cards/README.md). This browser contribution has no configuration fields. + +### Register a card body + +A kind's browser plugin injects `actionCards` and registers its body inside its own `ctx.effect`: `ctx.actionCards.register({ action, locale, Body })` returns the disposer. The frame renders `Body` for cards of that action with the card, its recorded or derived decision, the Session id, the frame's title and folded brief as `summary`, the frame's fold component as `Folded`, an `act(verb, payload?)` bound to the card, and a translator `t` for the body's own locale namespace. The body places `summary` in its own layout and owns its header, controls, status and footer. A second body for the same action is a registration error. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +One `conversation.chat.turnTail` entry reads the `actionCards` Session projection and shows the card admitted in its Turn after that Turn has ended with a final answer; a Turn that ends without answer text shows no card, and should several cards share a Turn, the lowest call id is shown. The frame sets `data-action-card` to the call id and names its region by the card title. Folding counts a wide grapheme, such as a Chinese character, as two Latin-letter widths and folds after about 260; it never measures layout or alters the full text. + +A `tool.call.toolview` entry keyed `propose_action` renders the process row from the recorded call arguments and result. The plugin provides the body registry at its top level, then mounts the generated `actionCards` Remote and registers its dictionaries and slot entries for the same lifetime. + +| Source | Contents | +|---|---| +| [`src/client/mount.ts`](src/client/mount.ts) | Registry, Remote, dictionaries and slot entries | +| [`src/client/Card.tsx`](src/client/Card.tsx) | Turn tail, frame, fold and fallback body | +| [`src/client/ProposeRow.tsx`](src/client/ProposeRow.tsx) | Process row | +| [`src/client/registry.ts`](src/client/registry.ts) | Body registry and body props | +| [`tests/`](tests/) | Placement, frame, fallback, row and registration tests | + +
+ +----- + + +## Further Exploration + +- [Action cards](../action-cards/README.md) — the tool, card state and user actions behind the frame. +- [Personalized page cards](../client-ui-creation-recommendations/README.md) and [recommendation cards](../client-ui-plugin-recommendations/README.md) — the shipped card bodies. +- [Slots](../../client/ui-slots/README.md) — Turn tail and tool row registration. + + +## Model Experience + +Indirectly, through the Host card service: card actions reach it as Remote calls, and it owns the logged tool, decisions and status notice. + +#### KV Cache effect + +Rendering cards and process rows does not change any model request. + +## Known Limitations and Deferred Work + + + +- A card whose action has no registered body offers only Not now, so its experiment's other actions need that experiment's browser plugin. Not now still works after the experiment's Host plugin is turned off. +- The Turn tail shows one card per Turn, matching the Host rule of one card per turn. + + +### Dev Note + +
+Working context for maintainers — click to expand + +None. + +
diff --git a/packages/experimental/client-ui-action-cards/README.zh.md b/packages/experimental/client-ui-action-cards/README.zh.md new file mode 100644 index 0000000000..70989022d6 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/README.zh.md @@ -0,0 +1,89 @@ +--- +description: "可选操作卡片的浏览器端:已完成回答下方的卡片框架、供各实验包填入的正文注册表,以及 propose_action 调用的单行过程行。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-action-cards + +[English](README.md) | 中文 + +## 概述 + +用户在已完成的回答下方看到一张可选操作卡片:标题、过长时会折叠的说明,以及提议该卡片的实验所提供的控件。在过程区中,提议显示为一行,带卡片标题和调用是否被接受;被拒绝的调用可以展开查看原因。如果实验在当前浏览器中没有提供控件,卡片只提供“暂不需要”。展示、展开和折叠卡片都不会发起模型请求。 + +## 目录 + +- [使用本包](#use-this-package) +- [实现原理](#understand-the-implementation) +- [延伸阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +启用[个性化推荐](../personalized-recommendations/README.zh.md) Bundle,它会在本包旁插入对应的 [Host 服务](../action-cards/README.zh.md)。本浏览器组件没有配置字段。 + +### 注册卡片正文 + +种类的浏览器插件注入 `actionCards`,并在自己的 `ctx.effect` 中注册正文:`ctx.actionCards.register({ action, locale, Body })` 返回注销函数。框架为该动作的卡片渲染 `Body`,并传入:卡片、已记录或推导出的决定、会话 id、框架渲染的标题与折叠说明 `summary`、框架的折叠组件 `Folded`、绑定到该卡片的 `act(verb, payload?)`,以及正文自己语言命名空间的翻译函数 `t`。正文把 `summary` 放进自己的布局,并负责自己的标题栏、控件、状态和底部操作。为同一动作注册第二个正文属于注册错误。 + +----- + + +## 实现原理 + +
+实现细节——点击展开 + +一个 `conversation.chat.turnTail` 条目读取 `actionCards` 会话投影,在某一轮以最终回答结束后展示该轮接受的卡片;没有回答文本就结束的轮次不显示卡片,若同一轮有多张卡片,显示调用 id 最小的一张。框架把 `data-action-card` 设为调用 id,并以卡片标题命名所在区域。折叠时,宽字素(如汉字)按两个拉丁字母宽度计算,约 260 个单位后折叠;折叠不测量布局,也不改动完整文本。 + +一个以 `propose_action` 为键的 `tool.call.toolview` 条目,根据记录的调用参数和结果渲染过程行。插件在顶层提供正文注册表,然后挂载生成的 `actionCards` Remote,并在同一生命周期内注册词典和插槽条目。 + +| 源码 | 内容 | +|---|---| +| [`src/client/mount.ts`](src/client/mount.ts) | 注册表、Remote、词典与插槽条目 | +| [`src/client/Card.tsx`](src/client/Card.tsx) | 轮次尾部、框架、折叠与兜底正文 | +| [`src/client/ProposeRow.tsx`](src/client/ProposeRow.tsx) | 过程行 | +| [`src/client/registry.ts`](src/client/registry.ts) | 正文注册表与正文属性 | +| [`tests/`](tests/) | 位置、框架、兜底、过程行与注册测试 | + +
+ +----- + + +## 延伸阅读 + +- [操作卡片](../action-cards/README.zh.md) — 框架背后的工具、卡片状态与用户操作。 +- [个性化页面卡片](../client-ui-creation-recommendations/README.zh.md)和[推荐卡片](../client-ui-plugin-recommendations/README.zh.md) — 随产品提供的卡片正文。 +- [插槽](../../client/ui-slots/README.zh.md) — 轮次尾部与工具行的注册。 + + +## 模型体验 + +间接影响来自 Host 卡片服务:卡片操作以 Remote 调用到达该服务,由它负责记录在日志中的工具、决定和状态通知。 + +#### KV Cache 影响 + +渲染卡片和过程行不会改变任何模型请求。 + +## 已知限制与后续工作 + + + +- 动作没有注册正文的卡片只提供“暂不需要”,该实验的其他操作需要它的浏览器插件。即使该实验的 Host 插件已关闭,“暂不需要”仍然可用。 +- 轮次尾部每轮只显示一张卡片,与 Host 每轮一张卡片的规则一致。 + + +### 开发备注 + +
+维护者工作背景——点击展开 + +无。 + +
diff --git a/packages/experimental/client-ui-action-cards/locale/de.json b/packages/experimental/client-ui-action-cards/locale/de.json new file mode 100644 index 0000000000..5a480a2067 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anzeige von Aktionskarten", + "description": "Zeigt optionale Aktionskarten unter Antworten mit den eigenen Bedienelementen jedes Experiments an." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/en.json b/packages/experimental/client-ui-action-cards/locale/en.json new file mode 100644 index 0000000000..d11a646578 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Action card display", + "description": "Show optional action cards below answers, with each experiment's own controls." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/es.json b/packages/experimental/client-ui-action-cards/locale/es.json new file mode 100644 index 0000000000..7b5f75daa3 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización de tarjetas de acción", + "description": "Muestra tarjetas de acción opcionales debajo de las respuestas, con los controles propios de cada experimento." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/fr.json b/packages/experimental/client-ui-action-cards/locale/fr.json new file mode 100644 index 0000000000..283dd32f1d --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage des cartes d'action", + "description": "Affiche des cartes d'action facultatives sous les réponses, avec les commandes propres à chaque expérience." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/ja.json b/packages/experimental/client-ui-action-cards/locale/ja.json new file mode 100644 index 0000000000..01484f8e00 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/ja.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "アクションカード表示", + "description": "回答の下に任意のアクションカードを表示します。カードの内容は各実験が提供します。" + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/pt.json b/packages/experimental/client-ui-action-cards/locale/pt.json new file mode 100644 index 0000000000..22534af903 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de cartões de ação", + "description": "Mostra cartões de ação opcionais abaixo das respostas, com os controles próprios de cada experimento." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/ru.json b/packages/experimental/client-ui-action-cards/locale/ru.json new file mode 100644 index 0000000000..76cac2e24b --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение карточек действий", + "description": "Показывает необязательные карточки действий под ответами с элементами управления каждого эксперимента." + } +} diff --git a/packages/experimental/client-ui-action-cards/locale/zh.json b/packages/experimental/client-ui-action-cards/locale/zh.json new file mode 100644 index 0000000000..1792abcaf9 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "操作卡片显示", + "description": "在回答下方显示可选操作卡片,卡片内容由各实验提供。" + } +} diff --git a/packages/experimental/client-ui-action-cards/package.json b/packages/experimental/client-ui-action-cards/package.json new file mode 100644 index 0000000000..17ee00e968 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/package.json @@ -0,0 +1,71 @@ +{ + "version": "0.2.1-rc.3", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "license": "MIT", + "name": "@deepseek-ai/dsh-client-ui-action-cards", + "description": "Action cards below completed answers: the shared card frame, kind bodies and the compact tool row", + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/experimental/client-ui-action-cards" + }, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json", + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./locale/*.json": "./locale/*.json" + }, + "dsh": { + "client": { + "platform": "web", + "inject": [ + "@deepseek-ai/dsh-api-remotes", + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-chat", + "@deepseek-ai/dsh-client-ui-session", + "@deepseek-ai/dsh-client-ui-tool" + ] + } + }, + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-llm": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*", + "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", + "@deepseek-ai/dsh-api-remotes": "workspace:*", + "@deepseek-ai/dsh-client-locale": "workspace:*", + "@deepseek-ai/dsh-client-ui-conversation": "workspace:*", + "@deepseek-ai/dsh-client-ui-chat": "workspace:*", + "@deepseek-ai/dsh-client-ui-session": "workspace:*", + "@deepseek-ai/dsh-client-ui-slots": "workspace:*", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", + "@deepseek-ai/dsh-client-ui-tool": "workspace:*", + "react": "^18.2.0", + "@deepseek-ai/dsh-action-cards": "workspace:*", + "@deepseek-ai/dsh-typert-protocol": "workspace:*", + "@deepseek-ai/dsh-util-values": "workspace:*", + "@deepseek-ai/dsh-client-test-runtime": "workspace:*" + }, + "files": [ + "locale/*.json", + "lib/index.js", + "lib/client.js", + "lib/types/**/*.d.ts" + ] +} diff --git a/packages/experimental/client-ui-action-cards/src/client/Card.module.css b/packages/experimental/client-ui-action-cards/src/client/Card.module.css new file mode 100644 index 0000000000..cb8fd63989 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/Card.module.css @@ -0,0 +1,7 @@ +.card { margin: 16px 0; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-lg); overflow: hidden; background: var(--dsw-alias-bg-layer-1); font-family: var(--dsw-font-family); color: var(--dsw-alias-label-primary); } +.body { padding: 12px 20px 20px; } +.title { display: block; font-size: 16px; font-weight: 500; line-height: 1.5; overflow-wrap: anywhere; } +.text { margin: 8px 0 0; font-size: 13px; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--dsw-alias-label-secondary); } +.toggle { margin: 8px 0 0; padding-inline: 0; height: auto; min-height: 24px; font-size: 12px; } +.status { margin: 12px 0 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); } +.footer { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 12px; } diff --git a/packages/experimental/client-ui-action-cards/src/client/Card.tsx b/packages/experimental/client-ui-action-cards/src/client/Card.tsx new file mode 100644 index 0000000000..20e6f75700 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/Card.tsx @@ -0,0 +1,150 @@ +/** The shared card frame below a completed answer: title, folded brief, and the kind's body or a dismiss-only fallback. */ +import { useCallback, useId, useMemo, useState, type ReactNode } from 'react' +import { Button } from '@deepseek-ai/dsh-client-ui-primitives' +import type { HostObservable, InjectFace, PropsLocale, PropsRuntime, Translate } from '@deepseek-ai/dsh-client-ui-slots' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' +import type { + ActionCardDecision, ActionCardFields, ActionCardProposal, ActionCardResult, ActionCardView, +} from '@deepseek-ai/dsh-action-cards/types' +import type {} from '@deepseek-ai/dsh-client-ui-chat/client' +import type { ActionCardKey } from './locales.ts' +import type { FoldedTextProps, RegisteredBody } from './registry.ts' +import css from './Card.module.css' + +/** Session-bound actions and registered bodies, without a service in the component. */ +export interface TailInjected { + /** + * Send one user action for a card of the displayed Session. + * @param callId - the card's call id. + * @param verb - `dismiss` or one of the kind's verbs. + * @param payload - kind-defined fields. + * @returns the Host result, or undefined when the reply was lost. + */ + act: (callId: ToolCallId, verb: string, payload?: ActionCardFields) => Promise + /** + * Bind a body's own locale namespace. + * @param locale - namespace the body declared. + * @returns its translator. + */ + bind: (locale: string) => Translate + hooks: { bodies: HostObservable> } + sessionId: SessionId +} + +type TailProps = Pick, 'turn' | 'useProjection'> + & PropsLocale<'actionCards'> & InjectFace + +/** Graphemes drawn about twice as wide as a Latin letter: CJK scripts, CJK punctuation and fullwidth forms. */ +const WIDE_GRAPHEME = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul} -〿＀-⦆¢-₩]/u +/** Preview budget in Latin-letter widths, about two card lines at desktop width. */ +const FOLD_WIDTH = 260 + +/** + * Fold omitted text without measuring layout; a wide grapheme counts twice so Chinese and English fold near the + * same line. The full text is never altered. + * @param props - text and its localized toggle labels. + * @returns the paragraph and, when text was omitted, its toggle. + */ +export function FoldedText({ text, expandLabel, collapseLabel }: FoldedTextProps) { + const id = useId() + const [expanded, setExpanded] = useState(false) + const preview = useMemo(() => { + const compact = text.replace(/\s+/g, ' ').trim() + const characters = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(compact), part => part.segment) + let width = 0 + for (const [index, character] of characters.entries()) { + width += WIDE_GRAPHEME.test(character) ? 2 : 1 + if (width > FOLD_WIDTH) return characters.slice(0, index).join('').trimEnd() + '…' + } + return undefined + }, [text]) + return <> +

{expanded || preview === undefined ? text : preview}

+ {preview !== undefined && } + +} + +/** The projection admits at most one card per turn; should several exist, the lowest call id is shown. */ +function proposalOf(view: ActionCardView | undefined, turn: number): ActionCardProposal | undefined { + let found: ActionCardProposal | undefined + for (const proposal of Object.values(view?.proposals ?? {})) { + if (proposal.turn === turn && (found === undefined || proposal.callId < found.callId)) found = proposal + } + return found +} + +function decisionOf(view: ActionCardView | undefined, callId: string): ActionCardDecision | undefined { + return view !== undefined && Object.hasOwn(view.decisions, callId) ? view.decisions[callId] : undefined +} + +/** + * Place the card admitted in a turn after that turn's final answer, once the turn has ended with one. + * @param props - Turn location, projection reader, registered bodies and Session-bound actions. + * @returns the card, or null while the turn is open or when it ended without answer text. + */ +export function ActionCardTail({ turn, useProjection, ...props }: TailProps) { + const view = useProjection('actionCards') + const proposal = proposalOf(view, turn.turn) + return proposal === undefined || turn.end === undefined || turn.data.get('turn-tail')?.closing == null + ? null : +} + +/** + * Render the frame around one card. + * @param props - the card, its state, registered bodies and actions. + * @returns the card section. + */ +export function ActionCard({ proposal, decision, sessionId, act, bind, useBodies, t }: { + proposal: ActionCardProposal + decision: ActionCardDecision | undefined +} & InjectFace & PropsLocale<'actionCards'>) { + const body = useBodies(bodies => bodies.get(proposal.action)) + const send = useCallback((verb: string, payload?: ActionCardFields) => act(proposal.callId, verb, payload), [act, proposal.callId]) + const summary = <> + {proposal.title} + + + // The frame re-renders on a locale change and hands the body a new summary, so the body re-renders with it. + const content = body === undefined + ? + : body.render({ proposal, decision, sessionId, summary, Folded: FoldedText, act: send }, bind(body.locale)) + return
+ {content} +
+} + +/** Phases the fallback can name; other kind phases leave only the summary. */ +const NAMED_PHASES: Readonly> = { dismissed: 'dismissed', replaced: 'replaced' } + +/** Title, brief and Not now for a card whose kind registered no body in this browser. */ +function FallbackBody({ summary, decision, act, t }: { + summary: ReactNode + decision: ActionCardDecision | undefined + act: (verb: string) => Promise +} & PropsLocale<'actionCards'>) { + const [pending, setPending] = useState(false) + const [error, setError] = useState() + const phase = decision === undefined || !Object.hasOwn(NAMED_PHASES, decision.phase) ? undefined : NAMED_PHASES[decision.phase] + const dismiss = async () => { + setPending(true) + setError(undefined) + try { + const result = await act('dismiss') + if (result === undefined) setError('transport') + else if (result.status === 'refused' || result.status === 'unconfirmed') setError('unavailable') + } catch { + // A lost reply does not prove failure; replay supplies the durable decision. + setError('transport') + } finally { setPending(false) } + } + return
+ {summary} + {phase !== undefined &&

{t(phase)}

} + {error !== undefined &&

{t(error)}

} + {decision === undefined &&
+ +
} +
+} diff --git a/packages/experimental/client-ui-action-cards/src/client/ProposeRow.module.css b/packages/experimental/client-ui-action-cards/src/client/ProposeRow.module.css new file mode 100644 index 0000000000..22915510e1 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/ProposeRow.module.css @@ -0,0 +1,6 @@ +/** The card title stays readable beside the call state. */ +.summary { margin-left: 8px; display: flex; align-items: center; gap: 8px; min-width: 0; color: inherit; font-size: 12px; } +.summary > :first-child { flex-shrink: 0; } +.title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.reason { margin: 8px 0; padding: 12px; border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; } +.inspect { align-self: flex-start; border: none; padding: 4px 0; background: transparent; color: var(--dsw-alias-link); font: inherit; font-size: 12px; cursor: pointer; } diff --git a/packages/experimental/client-ui-action-cards/src/client/ProposeRow.tsx b/packages/experimental/client-ui-action-cards/src/client/ProposeRow.tsx new file mode 100644 index 0000000000..acd3b1b972 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/ProposeRow.tsx @@ -0,0 +1,43 @@ +/** One-line process row for a `propose_action` call: the card title and whether the card was shown. */ +import { useState } from 'react' +import { DisclosureRow, IconAppsAddOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' +import css from './ProposeRow.module.css' + +type ProposeRowProps = ToolCallViewProps & PropsLocale<'actionCards'> + +/** Call states with row copy; a preparing call has no complete arguments yet. */ +type RowState = 'preparing' | 'running' | 'ok' | 'error' | 'stopped' + +function stateOf(props: ProposeRowProps): RowState { + if (props.phase === 'preparing') return 'preparing' + if (props.phase === 'start') return 'running' + if (props.block.error?.code === 'interrupted') return 'stopped' + return props.block.isError ? 'error' : 'ok' +} + +/** + * Render a `propose_action` call from its recorded arguments and result. Only a refused call expands, to show + * why no card followed the answer. + * @param props - tool call and localized status copy. + * @returns the status row. + */ +export function ProposeRow(props: ProposeRowProps) { + const { block, inspect, t } = props + const [expanded, setExpanded] = useState(false) + const state = stateOf(props) + const title = block.args.text('title') ?? '' + const reason = props.phase === 'result' && props.block.isError + ? props.block.content.flatMap(item => item.type === 'text' ? [item.text] : []).join('\n') + : '' + return
+ } running={state === 'preparing' || state === 'running'} + open={expanded && reason !== ''} expandable={reason !== ''} expandOnRowClick keepContentWhenOpen + onToggle={() => { setExpanded(value => !value) }} + collapsedContent={{t(`row.${state}`)}{title}}> +
{reason}
+ {inspect && } +
+
+} diff --git a/packages/experimental/client-ui-action-cards/src/client/index.ts b/packages/experimental/client-ui-action-cards/src/client/index.ts new file mode 100644 index 0000000000..d8c3a96173 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/index.ts @@ -0,0 +1,15 @@ +/** Bind generated Remote descriptors to the source-tested browser lifecycle. */ +import type { Context } from '@deepseek-ai/cordis' +import remote from '@deepseek-ai/dsh-action-cards/remote' +import { mountActionCards } from './mount.ts' +export { inject } from './mount.ts' +export type { ActionCardBodyProps, ActionCardDescriptor, ActionCardRegistry, FoldedTextProps } from './registry.ts' + +/** + * Mount action cards and their Remote. + * @param ctx - Client plugin context. + * @returns disposer for every contribution. + */ +export async function apply(ctx: Context): Promise<() => Promise> { + return await mountActionCards(ctx, remote) +} diff --git a/packages/experimental/client-ui-action-cards/src/client/locales.ts b/packages/experimental/client-ui-action-cards/src/client/locales.ts new file mode 100644 index 0000000000..0c98b67158 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/locales.ts @@ -0,0 +1,82 @@ +/** English card frame, fallback and tool-row copy. */ +export const en = { + expandBrief: 'Show more', collapseBrief: 'Show less', + dismiss: 'Not now', dismissed: 'Dismissed', replaced: 'Replaced', + unavailable: 'This action is unavailable. Check the plugin in Plugins.', + transport: 'Could not confirm the result. Reconnect and check the card before retrying.', + 'row.title': 'Action card', 'row.preparing': 'Preparing a card', 'row.running': 'Proposing', + 'row.ok': 'Proposed', 'row.error': 'Not shown', 'row.stopped': 'Interrupted', 'row.inspect': 'Inspect call', +} + +/** Keys shared by every dictionary. */ +export type ActionCardKey = keyof typeof en + +/** Chinese card frame, fallback and tool-row copy. */ +export const zh: Record = { + expandBrief: '展开', collapseBrief: '收起', + dismiss: '暂不需要', dismissed: '已忽略', replaced: '已替换', + unavailable: '暂时无法执行,请到插件页面查看', + transport: '暂时无法确认结果,请重新连接并查看卡片状态后再重试', + 'row.title': '操作卡片', 'row.preparing': '正在准备卡片', 'row.running': '正在提议', + 'row.ok': '已提议', 'row.error': '未显示', 'row.stopped': '已中断', 'row.inspect': '查看调用', +} + +/** Japanese card frame, fallback and tool-row copy. */ +export const ja: Record = { + expandBrief: 'すべて表示', collapseBrief: '折りたたむ', + dismiss: '今はしない', dismissed: '却下済み', replaced: '置き換え済み', + unavailable: 'この操作は利用できません。プラグイン画面でプラグインを確認してください。', + transport: '結果を確認できませんでした。再接続してカードを確認してから再試行してください。', + 'row.title': 'アクションカード', 'row.preparing': 'カードを準備中', 'row.running': '提案中', + 'row.ok': '提案済み', 'row.error': '表示されませんでした', 'row.stopped': '中断されました', 'row.inspect': '呼び出しを確認', +} + +/** Spanish card frame, fallback and tool-row copy. */ +export const es: Record = { + expandBrief: 'Mostrar más', collapseBrief: 'Mostrar menos', + dismiss: 'Ahora no', dismissed: 'Descartado', replaced: 'Reemplazado', + unavailable: 'Esta acción no está disponible. Revisa el complemento en Complementos.', + transport: 'No se pudo confirmar el resultado. Vuelve a conectarte y revisa la tarjeta antes de reintentar.', + 'row.title': 'Tarjeta de acción', 'row.preparing': 'Preparando una tarjeta', 'row.running': 'Proponiendo', + 'row.ok': 'Propuesto', 'row.error': 'No se mostró', 'row.stopped': 'Interrumpido', 'row.inspect': 'Inspeccionar llamada', +} + +/** French card frame, fallback and tool-row copy. */ +export const fr: Record = { + expandBrief: 'Afficher plus', collapseBrief: 'Afficher moins', + dismiss: 'Pas maintenant', dismissed: 'Ignoré', replaced: 'Remplacé', + unavailable: "Cette action n'est pas disponible. Vérifiez le plugin dans Plugins.", + transport: 'Impossible de confirmer le résultat. Reconnectez-vous et vérifiez la carte avant de réessayer.', + 'row.title': "Carte d'action", 'row.preparing': "Préparation d'une carte", 'row.running': 'Proposition en cours', + 'row.ok': 'Proposé', 'row.error': 'Non affiché', 'row.stopped': 'Interrompu', 'row.inspect': "Inspecter l'appel", +} + +/** German card frame, fallback and tool-row copy. */ +export const de: Record = { + expandBrief: 'Mehr anzeigen', collapseBrief: 'Weniger anzeigen', + dismiss: 'Nicht jetzt', dismissed: 'Verworfen', replaced: 'Ersetzt', + unavailable: 'Diese Aktion ist nicht verfügbar. Prüfen Sie das Plugin unter Plugins.', + transport: 'Das Ergebnis konnte nicht bestätigt werden. Verbinden Sie sich erneut und prüfen Sie die Karte, bevor Sie es erneut versuchen.', + 'row.title': 'Aktionskarte', 'row.preparing': 'Karte wird vorbereitet', 'row.running': 'Wird vorgeschlagen', + 'row.ok': 'Vorgeschlagen', 'row.error': 'Nicht angezeigt', 'row.stopped': 'Unterbrochen', 'row.inspect': 'Aufruf prüfen', +} + +/** Russian card frame, fallback and tool-row copy. */ +export const ru: Record = { + expandBrief: 'Показать больше', collapseBrief: 'Показать меньше', + dismiss: 'Не сейчас', dismissed: 'Отклонено', replaced: 'Заменено', + unavailable: 'Это действие недоступно. Проверьте плагин в разделе «Плагины».', + transport: 'Не удалось подтвердить результат. Переподключитесь и проверьте карточку перед повторной попыткой.', + 'row.title': 'Карточка действия', 'row.preparing': 'Подготовка карточки', 'row.running': 'Предлагается', + 'row.ok': 'Предложено', 'row.error': 'Не показано', 'row.stopped': 'Прервано', 'row.inspect': 'Проверить вызов', +} + +/** Portuguese card frame, fallback and tool-row copy. */ +export const pt: Record = { + expandBrief: 'Mostrar mais', collapseBrief: 'Mostrar menos', + dismiss: 'Agora não', dismissed: 'Dispensado', replaced: 'Substituído', + unavailable: 'Esta ação não está disponível. Verifique o plug-in em Plug-ins.', + transport: 'Não foi possível confirmar o resultado. Reconecte-se e verifique o cartão antes de tentar novamente.', + 'row.title': 'Cartão de ação', 'row.preparing': 'Preparando um cartão', 'row.running': 'Propondo', + 'row.ok': 'Proposto', 'row.error': 'Não exibido', 'row.stopped': 'Interrompido', 'row.inspect': 'Inspecionar chamada', +} diff --git a/packages/experimental/client-ui-action-cards/src/client/mount.ts b/packages/experimental/client-ui-action-cards/src/client/mount.ts new file mode 100644 index 0000000000..9ffc9b4b17 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/mount.ts @@ -0,0 +1,63 @@ +/** Register the card frame, the tool row, their copy and the body registry. */ +import type { Context } from '@deepseek-ai/cordis' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type {} from '@deepseek-ai/dsh-api-remotes/client' +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type {} from '@deepseek-ai/dsh-client-ui-session/client' +import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' +import type {} from '@deepseek-ai/dsh-client-ui-tool/client' +import type {} from '@deepseek-ai/dsh-action-cards/remote' +import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' +import { ActionCardTail, type TailInjected } from './Card.tsx' +import { ProposeRow } from './ProposeRow.tsx' +import { ActionCardRegistry } from './registry.ts' +import { de, en, es, fr, ja, pt, ru, zh, type ActionCardKey } from './locales.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { actionCards: ActionCardKey } +} + +declare module '@deepseek-ai/cordis' { + interface Context { + /** Card bodies by action kind; kinds register theirs inside their own `ctx.effect`. */ + actionCards: ActionCardRegistry + } +} + +/** Registries available before this plugin mounts its Remote. */ +export const inject = ['slots', 'locale', 'remote'] + +/** + * Provide the body registry, then mount the Remote and the UI for the same plugin lifetime. + * @param ctx - Client plugin context. + * @param contribution - Generated Remote descriptors supplied by the built entry. + * @returns disposer removing the UI before the Remote contribution. + */ +export async function mountActionCards(ctx: Context, contribution: TypertRemoteContribution): Promise<() => Promise> { + // Built at the plugin's top level: kinds register into it from their own apply. + const registry = new ActionCardRegistry(ctx) + ctx.reflect.provide('actionCards', registry) + const unmount = await ctx.remote.$mount(contribution) + const ui = ctx.inject(['remote.actionCards'], (inner) => { registerUi(inner, registry) }) + try { await ui } catch (error) { await ui.dispose(); await unmount(); throw error } + return async () => { await ui.dispose(); await unmount() } +} + +function registerUi(ctx: Context, registry: ActionCardRegistry): void { + ctx.effect(() => ctx.locale.register('actionCards', { en, zh, ja, es, fr, de, ru, pt }), 'action-cards: locale') + ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({ + name: 'conversation.chat.turnTail', id: '@deepseek-ai/dsh-client-ui-action-cards', locale: 'actionCards', + inject: (sessionId: SessionId): TailInjected => ({ + sessionId, + hooks: { bodies: registry.source }, + bind: locale => ctx.locale.bind(locale), + act: async (callId, verb, payload) => { + const result = await ctx.remote.actionCards.act(sessionId, callId, verb, payload) + return result.ok ? result.value : undefined + }, + }), + }, ActionCardTail)) + ctx.slots.inject('tool.call.toolview', () => ctx.slots.register( + { name: 'tool.call.toolview', key: 'propose_action', locale: 'actionCards' }, ProposeRow, + )) +} diff --git a/packages/experimental/client-ui-action-cards/src/client/registry.ts b/packages/experimental/client-ui-action-cards/src/client/registry.ts new file mode 100644 index 0000000000..4933ee8dbb --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/client/registry.ts @@ -0,0 +1,106 @@ +/** Card bodies registered by action kind for the shared card frame. */ +import { createElement, type ComponentType, type ReactNode } from 'react' +import type { Context } from '@deepseek-ai/cordis' +import type { HostObservable, LocaleNamespaceMap, Translate, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { + ActionCardDecision, ActionCardFields, ActionCardProposal, ActionCardResult, +} from '@deepseek-ai/dsh-action-cards/types' + +/** Props of the frame's folded text, which bodies reuse for their own long fields. */ +export interface FoldedTextProps { + readonly text: string + readonly expandLabel: string + readonly collapseLabel: string +} + +/** Everything a kind's body receives from the shared frame. */ +export interface ActionCardBodyProps> { + /** The card as admitted; the body validates `details` with its own schema. */ + readonly proposal: ActionCardProposal + /** Recorded or derived card state; undefined while the card is unstarted. */ + readonly decision: ActionCardDecision | undefined + readonly sessionId: SessionId + /** Title and folded brief rendered by the frame; the body places them in its layout. */ + readonly summary: ReactNode + /** The frame's width-weighted text fold. */ + readonly Folded: ComponentType + /** + * Send one user action for this card. + * @param verb - `dismiss` or one of the kind's Host verbs. + * @param payload - fields the kind's Host handler validates. + * @returns the Host result, or undefined when the reply was lost. + */ + readonly act: (verb: string, payload?: ActionCardFields) => Promise + /** Translate the body's own locale namespace. */ + readonly t: TranslateNS +} + +/** Browser half of one action kind. */ +export interface ActionCardDescriptor> { + /** Value of the card's `action`. */ + readonly action: string + /** Locale namespace the body's copy belongs to. */ + readonly locale: N + /** Kind-owned header, controls, status and footer. */ + readonly Body: ComponentType> +} + +/** Frame-supplied body props before the kind's translator is bound. */ +export type ActionCardFrameProps = Omit, 't'> + +/** A registered body with its namespace type erased. */ +export interface RegisteredBody { + readonly locale: string + /** + * Render the body. + * @param props - frame-supplied props. + * @param t - translator bound to {@link locale}. + * @returns the body element. + */ + readonly render: (props: ActionCardFrameProps, t: Translate) => ReactNode +} + +/** Card bodies by action; the frame renders a dismiss-only fallback for an action without one. */ +export class ActionCardRegistry { + private bodies: ReadonlyMap = new Map() + private readonly listeners = new Set<() => void>() + /** Current bodies by action; the snapshot changes on every registration change. */ + readonly source: HostObservable> = { + getSnapshot: () => this.bodies, + subscribe: (listener) => { + this.listeners.add(listener) + return () => { this.listeners.delete(listener) } + }, + } + + /** @param ctx - Context whose effects own registered bodies. */ + constructor(private readonly ctx: Context) {} + + /** + * Register one kind's body. The caller holds the returned disposer inside its own `ctx.effect`, so the body + * lives exactly as long as the plugin that contributed it. + * @param descriptor - the kind's action, locale namespace and body. + * @returns idempotent disposer. + * @throws when another body already serves the action. + */ + register>(descriptor: ActionCardDescriptor): () => void { + const { action, locale, Body } = descriptor + const body: RegisteredBody = { locale, render: (props, t) => createElement(Body, { ...props, t }) } + const dispose = this.ctx.effect(() => { + if (this.bodies.has(action)) throw new Error(`action-cards: a body for "${action}" is already registered`) + this.publish(new Map([...this.bodies, [action, body]])) + return () => { + const next = new Map(this.bodies) + next.delete(action) + this.publish(next) + } + }, `action-cards: body ${action}`) + return () => { void dispose() } + } + + private publish(bodies: ReadonlyMap): void { + this.bodies = bodies + for (const listener of [...this.listeners]) listener() + } +} diff --git a/packages/experimental/client-ui-action-cards/src/css-modules.d.ts b/packages/experimental/client-ui-action-cards/src/css-modules.d.ts new file mode 100644 index 0000000000..24a27bda3f --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: Readonly> + export default classes +} diff --git a/packages/experimental/client-ui-action-cards/src/index.ts b/packages/experimental/client-ui-action-cards/src/index.ts new file mode 100644 index 0000000000..8524d1b9ae --- /dev/null +++ b/packages/experimental/client-ui-action-cards/src/index.ts @@ -0,0 +1,4 @@ +/** Browser registration is discovered through the package client declaration. */ + +/** Register the browser-only contribution without Host state. */ +export function apply(): void {} diff --git a/packages/experimental/client-ui-action-cards/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-action-cards/tests/browser-plugin.client.spec.ts new file mode 100644 index 0000000000..87f32a57c3 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/tests/browser-plugin.client.spec.ts @@ -0,0 +1,74 @@ +/** Real registries bind actions to the displayed session and remove every contribution on unload. */ +import { isValidElement } from 'react' +import { Context } from '@deepseek-ai/cordis' +import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' +import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { SessionId } from '@deepseek-ai/dsh-session/types' +import { ToolCallId } from '@deepseek-ai/dsh-llm/brand' +import { expect, it, onTestFinished, vi } from 'vitest' +import { mountActionCards, inject } from '../src/client/mount.ts' +import { apply as hostApply } from '../src/index.ts' +import type { TailInjected } from '../src/client/Card.tsx' + +const apply = (ctx: Context) => mountActionCards(ctx, { package: '@deepseek-ai/dsh-action-cards', descriptors: [] }) +const Body = () => null + +it('binds actions to the displayed session, serves registered bodies and removes registrations on unload', async () => { + hostApply() + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SlotRegistry) + const slots = ctx.slots + slots.register({ name: 'root', children: { + 'conversation.chat.turnTail': { kind: 'list', scope: 'session' }, + 'tool.call.toolview': { kind: 'keyed', scope: 'session' }, + } }, (_props: { renderSlot?: unknown }) => null) + const act = vi.fn().mockResolvedValue({ ok: true, value: { status: 'accepted' } }) + const unmount = vi.fn(async () => {}) + ctx.provide('remote', { actionCards: { act }, $mount: vi.fn(async () => unmount) }) + ctx.provide('remote.actionCards', { act }) + const locale = new LocaleRuntime(ctx) + ctx.provide('locale', locale) + const fiber = await ctx.plugin({ inject, apply }) + const entry = slots.entries('conversation.chat.turnTail')[0]! + expect(slots.entries('tool.call.toolview').map(item => item.options.key)).toEqual(['propose_action']) + const resolve = entry.inject as NonNullable & ((sessionId: SessionId) => TailInjected) + const bound = resolve(SessionId('session-one')) + expect(bound.sessionId).toBe('session-one') + expect(await bound.act(ToolCallId('card'), 'create', { slot: 'page' })).toEqual({ status: 'accepted' }) + expect(act).toHaveBeenCalledWith('session-one', 'card', 'create', { slot: 'page' }) + act.mockResolvedValueOnce({ ok: false }) + expect(await bound.act(ToolCallId('card'), 'dismiss')).toBeUndefined() + expect(bound.bind('actionCards')('dismiss')).toBe('Not now') + const changes = vi.fn() + const unsubscribe = bound.hooks.bodies.subscribe(changes) + const kind = await ctx.plugin({ inject: ['actionCards'], apply: (inner: Context) => { + inner.effect(() => inner.actionCards.register({ action: 'test_action', locale: 'actionCards', Body })) + } }) + expect([...bound.hooks.bodies.getSnapshot().keys()]).toEqual(['test_action']) + const translate = (key: string) => key + const element = bound.hooks.bodies.getSnapshot().get('test_action')!.render({ proposal: { callId: ToolCallId('card'), turn: 1, + action: 'test_action', title: 'Counter', brief: 'Keep count.', details: {} }, decision: undefined, + sessionId: SessionId('session-one'), summary: null, Folded: Body, act: vi.fn() }, translate) + expect(isValidElement(element) && element.type === Body && (element.props as { t: unknown }).t === translate).toBe(true) + expect(() => ctx.actionCards.register({ action: 'test_action', locale: 'actionCards', Body })).toThrow('a body for "test_action" is already registered') + await kind.dispose() + expect(bound.hooks.bodies.getSnapshot().size).toBe(0) + expect(changes).toHaveBeenCalledTimes(2) + unsubscribe() + const release = ctx.actionCards.register({ action: 'other_action', locale: 'actionCards', Body }) + expect(bound.hooks.bodies.getSnapshot().has('other_action')).toBe(true) + release() + expect(bound.hooks.bodies.getSnapshot().has('other_action')).toBe(false) + expect(changes).toHaveBeenCalledTimes(2) + await fiber.dispose() + expect(unmount).toHaveBeenCalledOnce() + expect(slots.entries('conversation.chat.turnTail')).toHaveLength(0) + expect(slots.entries('tool.call.toolview')).toHaveLength(0) + expect(ctx.get('actionCards')).toBeUndefined() + vi.spyOn(locale, 'register').mockImplementation(() => { throw new Error('registration failed') }) + const failed = ctx.plugin({ inject, apply }) + await expect(failed.await()).rejects.toThrow('registration failed') + await failed.dispose() + expect(unmount).toHaveBeenCalledTimes(2) +}) diff --git a/packages/experimental/client-ui-action-cards/tests/card.client.spec.tsx b/packages/experimental/client-ui-action-cards/tests/card.client.spec.tsx new file mode 100644 index 0000000000..c286a52937 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/tests/card.client.spec.tsx @@ -0,0 +1,141 @@ +// @vitest-environment jsdom +import { memo, type ComponentProps } from 'react' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' +import { ToolCallId } from '@deepseek-ai/dsh-llm/brand' +import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { ActionCardDecision, ActionCardResult, ActionCardView } from '@deepseek-ai/dsh-action-cards/types' +import { ActionCard, ActionCardTail, FoldedText } from '../src/client/Card.tsx' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { en, zh } from '../src/client/locales.ts' +import type { ActionCardBodyProps, RegisteredBody } from '../src/client/registry.ts' + +afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals() }) + +type CardProps = ComponentProps +const callId = ToolCallId('card-1') +const proposal = { callId, turn: 3, action: 'test_action', title: 'Counter', brief: 'Keep my task count visible.', details: { value: 'a' } } + +function props(bodies: ReadonlyMap = new Map(), decision?: ActionCardDecision, chinese = false): CardProps { + return { proposal, decision, sessionId: SessionId('session-1'), + act: vi.fn(async (): Promise => ({ status: 'settled', decision: { callId, action: 'test_action', phase: 'dismissed' } })), + bind: vi.fn(() => (key: string) => `body:${key}`), + useBodies: selector => selector(bodies), + t: makeTranslate(chinese ? zh : en) } +} + +it('places the card admitted in its turn once the turn has ended with an answer, and leaves other turns empty', () => { + type Tail = ComponentProps + const value = props() + let view: ActionCardView | undefined + const turnData = vi.fn().mockReturnValue({ closing: {} }) + const ended = { type: 'turn/end', seq: SessionSeq(1), time: 0, data: { turn: 3, reason: { kind: 'max-tokens' } } } as const + const tail = (end: typeof ended | undefined): Tail => ({ ...value, useProjection: () => view, + turn: { turn: 3, start: undefined, end, status: end === undefined ? 'open' : 'closed', steps: [], + data: { get: turnData, source: () => ({ getSnapshot: () => undefined, subscribe: () => () => {} }) } } }) + const card = (id: string, turn: number) => ({ ...proposal, callId: ToolCallId(id), turn }) + const { rerender } = render() + expect(screen.queryByRole('region')).toBeNull() + view = { decisions: {}, proposals: { earlier: card('earlier', 2) } } + rerender() + expect(screen.queryByRole('region')).toBeNull() + view = { decisions: {}, proposals: { ...view.proposals, b: card('b', 3), a: card('a', 3), c: card('c', 3) } } + rerender() + expect(screen.getAllByRole('region').map(region => region.getAttribute('data-action-card'))).toEqual(['a']) + expect(screen.getByRole('region', { name: 'Counter' }).getAttribute('data-action')).toBe('test_action') + expect(turnData).toHaveBeenCalledWith('turn-tail') + rerender() + expect(screen.queryByRole('region')).toBeNull() + turnData.mockReturnValueOnce({ closing: null }) + rerender() + expect(screen.queryByRole('region')).toBeNull() + view = { ...view, decisions: { a: { callId: ToolCallId('a'), action: 'test_action', phase: 'dismissed' } } } + rerender() + expect(screen.getByRole('status').textContent).toBe(en.dismissed) + // A call id naming an inherited object member reads no decision. + view = { decisions: {}, proposals: { toString: card('toString', 3) } } + rerender() + expect(screen.getByRole('region').getAttribute('data-action-card')).toBe('toString') + expect(screen.getByRole('button', { name: en.dismiss })).toBeTruthy() +}) + +it('renders a registered body with the frame summary, the fold, its own translator and an action bound to the card', async () => { + const seen: ActionCardBodyProps[] = [] + const Body = memo((body: ActionCardBodyProps) => { + seen.push(body) + return
{body.summary} +
+ }) + const bodies = new Map([['test_action', { locale: 'testCard', render: (frame, t) => }]]) + const value = props(bodies, { callId, action: 'test_action', phase: 'running' }) + render() + expect(value.bind).toHaveBeenCalledWith('testCard') + expect(screen.getByRole('region', { name: 'Counter' }).textContent).toContain('Keep my task count visible.') + expect(screen.getByText('Long details')).toBeTruthy() + expect(seen.at(-1)).toMatchObject({ proposal, decision: { phase: 'running' }, sessionId: 'session-1', Folded: FoldedText }) + fireEvent.click(screen.getByRole('button', { name: 'body:start' })) + expect(value.act).toHaveBeenCalledWith(callId, 'start', { value: 'x' }) +}) + +it('offers only Not now for a card whose kind registered no body, and reports refusals and lost replies', async () => { + const value = props(new Map(), undefined, true) + const pending = Promise.withResolvers() + value.act = vi.fn().mockReturnValueOnce(pending.promise).mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('disconnected')).mockResolvedValueOnce({ status: 'unconfirmed', reason: 'closing' }) + .mockResolvedValueOnce({ status: 'accepted' }) + render() + expect(screen.getByRole('region', { name: 'Counter' }).textContent).toContain('Keep my task count visible.') + const dismiss = screen.getByRole('button', { name: zh.dismiss }) + fireEvent.click(dismiss) + expect(dismiss.hasAttribute('disabled')).toBe(true) + expect(value.act).toHaveBeenCalledWith(callId, 'dismiss', undefined) + await act(async () => { pending.resolve({ status: 'refused', reason: 'unknown' }) }) + expect(screen.getByRole('alert').textContent).toBe(zh.unavailable) + for (const text of [zh.transport, zh.transport, zh.unavailable]) { + fireEvent.click(screen.getByRole('button', { name: zh.dismiss })) + await vi.waitFor(() => { expect(screen.getByRole('alert').textContent).toBe(text) }) + } + fireEvent.click(screen.getByRole('button', { name: zh.dismiss })) + await vi.waitFor(() => { expect(screen.queryByRole('alert')).toBeNull() }) +}) + +it('dismisses a card through the fallback Not now when the Host records the dismissal without the card kind', async () => { + const value = props() + render() + const dismiss = screen.getByRole('button', { name: en.dismiss }) + fireEvent.click(dismiss) + expect(value.act).toHaveBeenCalledWith(callId, 'dismiss', undefined) + await vi.waitFor(() => { expect(dismiss.hasAttribute('disabled')).toBe(false) }) + expect(screen.queryByRole('alert')).toBeNull() +}) + +it.each([['dismissed', en.dismissed], ['replaced', en.replaced], ['finished', undefined], ['toString', undefined]] as const)( + 'shows the fallback status of a %s card without actions', (phase, text) => { + render() + expect(screen.queryByRole('status')?.textContent).toBe(text) + expect(screen.queryByRole('button', { name: en.dismiss })).toBeNull() + }) + +it('only offers expansion for omitted text and preserves the full brief without layout measurement', () => { + vi.stubGlobal('ResizeObserver', undefined) + const short = 'Keep my task count visible.\n\nPersist across conversations.\nVerify removal.' + const view = render() + expect(screen.queryByRole('button', { name: zh.expandBrief })).toBeNull() + expect(screen.getByText(/Keep my task count visible/).textContent).toBe(short) + // 129 wide graphemes (258 units) plus one narrow emoji fill the 260-unit budget; the next grapheme folds. + const prefix = '计'.repeat(129) + '👩🏽‍💻' + const long = prefix + '\n\n保留完整方案,点击创建后交给 Creator。' + view.rerender() + const expand = screen.getByRole('button', { name: zh.expandBrief }) + const paragraph = document.getElementById(expand.getAttribute('aria-controls')!)! + expect(paragraph.textContent).toBe(prefix + '…') + expect(expand.getAttribute('aria-expanded')).toBe('false') + fireEvent.click(expand) + expect(screen.getByRole('button', { name: zh.collapseBrief }).getAttribute('aria-expanded')).toBe('true') + expect(paragraph.textContent).toBe(long) + fireEvent.click(screen.getByRole('button', { name: zh.collapseBrief })) + expect(paragraph.textContent).toBe(prefix + '…') + view.rerender() + expect(screen.queryByRole('button', { name: zh.expandBrief })).toBeNull() + expect(paragraph.textContent).toBe(prefix) +}) diff --git a/packages/experimental/client-ui-action-cards/tests/row.client.spec.tsx b/packages/experimental/client-ui-action-cards/tests/row.client.spec.tsx new file mode 100644 index 0000000000..ed197519ae --- /dev/null +++ b/packages/experimental/client-ui-action-cards/tests/row.client.spec.tsx @@ -0,0 +1,55 @@ +// @vitest-environment jsdom +/** The process row reads the card title from the recorded arguments and expands only a refused call. */ +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' +import type { StartedToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { PartialArguments } from '@deepseek-ai/dsh-util-values' +import { ProposeRow } from '../src/client/ProposeRow.tsx' +import { en } from '../src/client/locales.ts' + +afterEach(cleanup) +type Props = Parameters[0] +const argsRaw = JSON.stringify({ action: 'test_action', title: 'Counter', brief: 'Keep count.', details: {} }) +const running: StartedToolCall = { phase: 'start', args: PartialArguments.fromText(argsRaw), callId: 'c', name: 'propose_action', argsRaw, turn: 1, step: 1, time: 1, subCalls: [] } +const settled: ToolResultNode = { kind: 'tool-result', seq: 2, time: 2, callId: 'c', name: 'propose_action', args: PartialArguments.fromText(argsRaw), + call: { name: 'propose_action', argsRaw }, callTime: 1, content: [{ type: 'text', text: 'Card shown below your answer. Do not restate it. Invite the user.' }], isError: false, subCalls: [] } +function props(block: Props['block'], inspect?: () => void): Props { + return { ...('kind' in block ? { phase: 'result' as const, block } : { phase: block.phase, block }), + callId: 'c', toolName: 'propose_action', openFile: vi.fn(), inspect, t: makeTranslate(en) } as Props +} + +it('shows a proposed card on one line without exposing the model-facing result', () => { + const view = render() + expect(view.container.querySelector('[data-tool="propose_action"]')?.getAttribute('data-state')).toBe('ok') + expect(view.getByText('Proposed')).toBeTruthy() + expect(view.getByText('Counter')).toBeTruthy() + expect(view.queryByRole('button')).toBeNull() + expect(view.container.textContent).not.toContain('Card shown below your answer. Do not restate it. Invite the user.') +}) + +it('explains a refused call and offers call inspection', () => { + const inspect = vi.fn() + const view = render() + expect(view.getByText('Not shown')).toBeTruthy() + const row = view.getByRole('button') + fireEvent.click(row) + expect(view.container.querySelector('pre')?.textContent).toBe('Error: Load skill test-skill first, then call propose_action.') + fireEvent.click(view.getByRole('button', { name: 'Inspect call' })) + expect(inspect).toHaveBeenCalledOnce() + fireEvent.click(row) + expect(row.getAttribute('aria-expanded')).toBe('false') +}) + +it.each([ + ['preparing', { phase: 'preparing', args: PartialArguments.EMPTY, callId: 'c', name: 'propose_action', turn: 1, step: 1, time: 1, subCalls: [] }, 'Preparing a card'], + ['running', running, 'Proposing'], + ['stopped', { ...settled, isError: true, content: [], error: { name: 'Interrupted', code: 'interrupted' } }, 'Interrupted'], +] as const)('renders the %s call stage without claiming a card', (state, block, label) => { + const view = render() + expect(view.container.querySelector('[data-tool="propose_action"]')?.getAttribute('data-state')).toBe(state) + expect(view.getAllByText(label).length).toBeGreaterThan(0) + expect(view.queryByText('Proposed')).toBeNull() + expect(view.queryByRole('button')).toBeNull() +}) diff --git a/packages/experimental/client-ui-action-cards/tsconfig.json b/packages/experimental/client-ui-action-cards/tsconfig.json new file mode 100644 index 0000000000..fec07b193a --- /dev/null +++ b/packages/experimental/client-ui-action-cards/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { "path": "../../client/ui-renderer" }, + { "path": "../../../vendor/cordis" }, + { "path": "../../api/remotes/tsconfig.client.json" }, + { "path": "../../client/locale/tsconfig.client.json" }, + { "path": "../../client/ui-conversation/tsconfig.client.json" }, + { "path": "../../client/ui-chat/tsconfig.client.json" }, + { "path": "../../client/ui-session" }, + { "path": "../../client/ui-slots" }, + { "path": "../../client/ui-primitives" }, + { "path": "../../client/ui-tool" }, + { "path": "../../llm/llm" }, + { "path": "../../core/session" }, + { "path": "../action-cards" }, + { "path": "../../typert/protocol" }, + { "path": "../../util/values" } + ] +} diff --git a/packages/experimental/client-ui-action-cards/tsdown.config.ts b/packages/experimental/client-ui-action-cards/tsdown.config.ts new file mode 100644 index 0000000000..71d5bd77f8 --- /dev/null +++ b/packages/experimental/client-ui-action-cards/tsdown.config.ts @@ -0,0 +1,3 @@ +import { clientBundle } from '../../client/tsdown.client.ts' + +export default clientBundle(import.meta.dirname, ['lib/types/index.js']) diff --git a/packages/experimental/client-ui-agent-team/locale/de.json b/packages/experimental/client-ui-agent-team/locale/de.json new file mode 100644 index 0000000000..0d51f033bf --- /dev/null +++ b/packages/experimental/client-ui-agent-team/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Teampanel", + "description": "Zeigen Sie Teammitglieder und freigegebene Aufgaben an und navigieren Sie zu Mitgliedergesprächen." + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/es.json b/packages/experimental/client-ui-agent-team/locale/es.json new file mode 100644 index 0000000000..378b5febee --- /dev/null +++ b/packages/experimental/client-ui-agent-team/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panel de equipo", + "description": "Vea los miembros del equipo y las tareas compartidas, y navegue hasta las conversaciones de los miembros." + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/fr.json b/packages/experimental/client-ui-agent-team/locale/fr.json new file mode 100644 index 0000000000..fde7d9a4d1 --- /dev/null +++ b/packages/experimental/client-ui-agent-team/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panneau d'équipe", + "description": "Affichez les membres de l'équipe et les tâches partagées, et accédez aux conversations des membres." + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/pt.json b/packages/experimental/client-ui-agent-team/locale/pt.json new file mode 100644 index 0000000000..8a7909fe18 --- /dev/null +++ b/packages/experimental/client-ui-agent-team/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Painel da equipe", + "description": "Veja os membros da equipe e as tarefas compartilhadas e navegue até as conversas dos membros." + } +} diff --git a/packages/experimental/client-ui-agent-team/locale/ru.json b/packages/experimental/client-ui-agent-team/locale/ru.json new file mode 100644 index 0000000000..91fd1e9567 --- /dev/null +++ b/packages/experimental/client-ui-agent-team/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Панель команды", + "description": "Просматривайте участников команды и общие задачи, а также переходите к беседам участников." + } +} diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index fdde630b95..beb53d7d2a 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/src/client/locales.ts b/packages/experimental/client-ui-agent-team/src/client/locales.ts index 5ba6255bac..f1ed57b9ee 100644 --- a/packages/experimental/client-ui-agent-team/src/client/locales.ts +++ b/packages/experimental/client-ui-agent-team/src/client/locales.ts @@ -92,3 +92,148 @@ export const ja = { 'status.in_progress': '進行中', 'status.completed': '完了', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + trigger: 'equipo de agentes', + loading: 'Cargando equipo…', + unavailable: 'El equipo no está disponible', + failure: 'Registro de equipo persistente no válido: {message}', + empty: 'Aún no hay tareas compartidas. Créelos a través de la conversación.', + roster: 'Miembros', + tasks: 'Tareas compartidas', + model: 'modelo', + open: 'Abrir conversación de miembros', + current: 'charla actual', + owner: 'propietario', + unowned: 'Sin dueño', + blockedBy: 'Bloqueado por', + writeScopes: 'Escribir alcances', + ready: 'Listo', + blocked: 'Bloqueado por dependencias', + 'task.expand': 'Mostrar más', + 'task.collapse': 'Mostrar menos', + 'memberStatus.running': 'corriendo', + 'memberStatus.inactive': 'Inactivo', + 'memberStatus.provisioning': 'Aprovisionamiento', + 'memberStatus.failed': 'Fallido', + 'status.pending': 'Pendiente', + 'status.in_progress': 'En progreso', + 'status.completed': 'Completado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + trigger: "Équipe d'agents", + loading: "Chargement de l'équipe…", + unavailable: "L'équipe n'est pas disponible", + failure: "Enregistrement d'équipe persistant non valide : {message}", + empty: "Aucune tâche partagée pour l'instant. Créez-les à travers la conversation.", + roster: 'Membres', + tasks: 'Tâches partagées', + model: 'Modèle', + open: 'Conversation ouverte avec les membres', + current: 'Discussion actuelle', + owner: 'Propriétaire', + unowned: 'Sans propriétaire', + blockedBy: 'Bloqué par', + writeScopes: 'Écrire des étendues', + ready: 'Prêt', + blocked: 'Bloqué par les dépendances', + 'task.expand': 'Afficher plus', + 'task.collapse': 'Afficher moins', + 'memberStatus.running': 'Courir', + 'memberStatus.inactive': 'Inactif', + 'memberStatus.provisioning': 'Approvisionnement', + 'memberStatus.failed': 'Échec', + 'status.pending': 'En attente', + 'status.in_progress': 'En cours', + 'status.completed': 'Terminé', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + trigger: 'Agententeam', + loading: 'Team wird geladen…', + unavailable: 'Team ist nicht verfügbar', + failure: 'Ungültiger beibehaltener Teamdatensatz: {message}', + empty: 'Noch keine geteilten Aufgaben. Erstellen Sie sie durch das Gespräch.', + roster: 'Mitglieder', + tasks: 'Geteilte Aufgaben', + model: 'Modell', + open: 'Öffnen Sie die Mitgliederkonversation', + current: 'Aktueller Chat', + owner: 'Besitzer', + unowned: 'Unbesessen', + blockedBy: 'Blockiert von', + writeScopes: 'Bereiche schreiben', + ready: 'Bereit', + blocked: 'Blockiert durch Abhängigkeiten', + 'task.expand': 'Mehr anzeigen', + 'task.collapse': 'Weniger anzeigen', + 'memberStatus.running': 'Laufen', + 'memberStatus.inactive': 'Inaktiv', + 'memberStatus.provisioning': 'Bereitstellung', + 'memberStatus.failed': 'Fehlgeschlagen', + 'status.pending': 'Ausstehend', + 'status.in_progress': 'In Bearbeitung', + 'status.completed': 'Abgeschlossen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + trigger: 'Команда агентов', + loading: 'Загрузка команды…', + unavailable: 'Команда недоступна', + failure: 'Неверный сохраненный командный рекорд: {message}.', + empty: 'Общие задачи пока отсутствуют. Создайте их посредством разговора.', + roster: 'Члены', + tasks: 'Общие задачи', + model: 'Модель', + open: 'Открыть беседу участников', + current: 'Текущий чат', + owner: 'Владелец', + unowned: 'Невладеющий', + blockedBy: 'Заблокировано', + writeScopes: 'Запись областей', + ready: 'Готово', + blocked: 'Заблокировано зависимостями', + 'task.expand': 'Показать больше', + 'task.collapse': 'Показать меньше', + 'memberStatus.running': 'Бег', + 'memberStatus.inactive': 'Неактивный', + 'memberStatus.provisioning': 'обеспечение', + 'memberStatus.failed': 'Не удалось', + 'status.pending': 'Ожидается', + 'status.in_progress': 'В процессе', + 'status.completed': 'Завершено', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + trigger: 'Equipe de agentes', + loading: 'Carregando equipe…', + unavailable: 'A equipe não está disponível', + failure: 'Registro de equipe persistente inválido: {message}', + empty: 'Ainda não há tarefas compartilhadas. Crie-os através da conversa.', + roster: 'Membros', + tasks: 'Tarefas compartilhadas', + model: 'Modelo', + open: 'Abrir conversa entre membros', + current: 'Bate-papo atual', + owner: 'Proprietário', + unowned: 'Sem dono', + blockedBy: 'Bloqueado por', + writeScopes: 'Escrever escopos', + ready: 'Pronto', + blocked: 'Bloqueado por dependências', + 'task.expand': 'Mostrar mais', + 'task.collapse': 'Mostrar menos', + 'memberStatus.running': 'Correndo', + 'memberStatus.inactive': 'Inativo', + 'memberStatus.provisioning': 'Provisionamento', + 'memberStatus.failed': 'Falha', + 'status.pending': 'Pendente', + 'status.in_progress': 'Em andamento', + 'status.completed': 'Concluído', +} satisfies Record diff --git a/packages/experimental/client-ui-agent-team/src/client/mount.ts b/packages/experimental/client-ui-agent-team/src/client/mount.ts index a3c044b596..c1e13c8cfa 100644 --- a/packages/experimental/client-ui-agent-team/src/client/mount.ts +++ b/packages/experimental/client-ui-agent-team/src/client/mount.ts @@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' import { TeamAction, type TeamActionInjected } from './TeamAction.tsx' -import { en, ja, NS, zh, type TeamKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type TeamKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -27,7 +27,7 @@ export const inject = ['sessions', 'uiWorkspace', 'slots', 'locale'] * @param ctx - Client Context carrying the injected navigation, locale, slot, and Session services. */ export function registerAgentTeamUi(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'client-ui-agent-team: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'client-ui-agent-team: dictionaries') const sessions = ctx.sessions const leadSessionId = (sessionId: SessionId): SessionId => { const address = sessions.binding(sessionId)?.session.getSnapshot().subagent?.address diff --git a/packages/experimental/client-ui-claude-code-mods/locale/de.json b/packages/experimental/client-ui-claude-code-mods/locale/de.json new file mode 100644 index 0000000000..fda7d714dd --- /dev/null +++ b/packages/experimental/client-ui-claude-code-mods/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code Mods-Schnittstelle", + "description": "Zeigen Sie den Erweiterungsbaum jeder Sitzung über dem Eingabefeld an und senden Sie Schaltflächenaktionen an den Erweiterungsdienst." + } +} diff --git a/packages/experimental/client-ui-claude-code-mods/locale/es.json b/packages/experimental/client-ui-claude-code-mods/locale/es.json new file mode 100644 index 0000000000..b8eadd3e26 --- /dev/null +++ b/packages/experimental/client-ui-claude-code-mods/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de modificaciones Claude Code", + "description": "Muestre el árbol de extensión de cada sesión encima del cuadro de entrada y envíe acciones de botones al servicio de extensión." + } +} diff --git a/packages/experimental/client-ui-claude-code-mods/locale/fr.json b/packages/experimental/client-ui-claude-code-mods/locale/fr.json new file mode 100644 index 0000000000..140a56a210 --- /dev/null +++ b/packages/experimental/client-ui-claude-code-mods/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de mods Claude Code", + "description": "Affichez l'arborescence des extensions de chaque session au-dessus de la zone de saisie et envoyez les actions des boutons au service d'extension." + } +} diff --git a/packages/experimental/client-ui-claude-code-mods/locale/pt.json b/packages/experimental/client-ui-claude-code-mods/locale/pt.json new file mode 100644 index 0000000000..fad6e03f7c --- /dev/null +++ b/packages/experimental/client-ui-claude-code-mods/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code interface de mods", + "description": "Exiba a árvore de extensão de cada sessão acima da caixa de entrada e envie ações de botão para o serviço de extensão." + } +} diff --git a/packages/experimental/client-ui-claude-code-mods/locale/ru.json b/packages/experimental/client-ui-claude-code-mods/locale/ru.json new file mode 100644 index 0000000000..bdf1e5a1f9 --- /dev/null +++ b/packages/experimental/client-ui-claude-code-mods/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Claude Code интерфейс модов", + "description": "Отобразите дерево расширений каждого сеанса над полем ввода и отправьте действия кнопок в службу расширений." + } +} diff --git a/packages/experimental/client-ui-claude-code-mods/package.json b/packages/experimental/client-ui-claude-code-mods/package.json index 89b0ac8619..37a5656e69 100644 --- a/packages/experimental/client-ui-claude-code-mods/package.json +++ b/packages/experimental/client-ui-claude-code-mods/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-claude-code-mods", "description": "Web band above the prompt for Claude Code mods: draws each session's mod tree and sends button presses back to the bridge", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-claude-code-mods/src/client/locales.ts b/packages/experimental/client-ui-claude-code-mods/src/client/locales.ts index 62e530c87e..781c856736 100644 --- a/packages/experimental/client-ui-claude-code-mods/src/client/locales.ts +++ b/packages/experimental/client-ui-claude-code-mods/src/client/locales.ts @@ -29,3 +29,43 @@ export const ja = { 'press.failed': 'ボタンが失敗しました: {message}', 'press.stale': 'そのボタンは以前の描画のものです。バンドは更新されました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + band: 'Modificaciones Claude Code', + pressing: 'Trabajando…', + 'press.failed': 'El botón falló: {message}', + 'press.stale': 'Ese botón pertenecía a un dibujo anterior; la banda se ha renovado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + band: 'Mods Claude Code', + pressing: 'Travailler…', + 'press.failed': 'Le bouton a échoué : {message}', + 'press.stale': "Ce bouton appartenait à un dessin antérieur ; le groupe s'est rafraîchi", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + band: 'Claude Code-Mods', + pressing: 'Arbeiten…', + 'press.failed': 'Die Schaltfläche ist fehlgeschlagen: {message}', + 'press.stale': 'Dieser Knopf gehörte zu einer früheren Zeichnung; Die Band hat sich erfrischt', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + band: 'Claude Code моды', + pressing: 'Работаю…', + 'press.failed': 'Кнопка не удалась: {message}', + 'press.stale': 'Эта кнопка принадлежала более раннему рисунку; группа обновилась', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + band: 'Claude Code modificações', + pressing: 'Trabalhando…', + 'press.failed': 'O botão falhou: {message}', + 'press.stale': 'Esse botão pertencia a um desenho anterior; a banda se atualizou', +} satisfies Record diff --git a/packages/experimental/client-ui-claude-code-mods/src/client/mount.ts b/packages/experimental/client-ui-claude-code-mods/src/client/mount.ts index 498e4a2d28..4bf030cc77 100644 --- a/packages/experimental/client-ui-claude-code-mods/src/client/mount.ts +++ b/packages/experimental/client-ui-claude-code-mods/src/client/mount.ts @@ -11,7 +11,7 @@ import type { SurfaceSnapshot } from '@deepseek-ai/dsh-experimental-claude-code- import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' import { Band, type BandInjected } from './Band.tsx' -import { en, ja, NS, zh, type ModsBandKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ModsBandKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -75,7 +75,7 @@ function watchSession(ctx: Context, sessionId: SessionId, onEnd: () => void): { } function registerUi(ctx: Context): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'client-ui-claude-code-mods: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'client-ui-claude-code-mods: dictionaries') const watches = new Map>() ctx.effect(() => () => { for (const watch of watches.values()) watch.dispose() diff --git a/packages/experimental/client-ui-cot-translation/locale/de.json b/packages/experimental/client-ui-cot-translation/locale/de.json new file mode 100644 index 0000000000..62b1c1a24b --- /dev/null +++ b/packages/experimental/client-ui-cot-translation/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Maschinelle Übersetzung der Argumentation", + "description": "Zeigen Sie mithilfe maschineller Übersetzung erweiterte Argumente in einer anderen Sprache an." + } +} diff --git a/packages/experimental/client-ui-cot-translation/locale/es.json b/packages/experimental/client-ui-cot-translation/locale/es.json new file mode 100644 index 0000000000..50cece4f3c --- /dev/null +++ b/packages/experimental/client-ui-cot-translation/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Traducción automática de razonamiento", + "description": "Muestre un razonamiento ampliado en otro idioma mediante la traducción automática." + } +} diff --git a/packages/experimental/client-ui-cot-translation/locale/fr.json b/packages/experimental/client-ui-cot-translation/locale/fr.json new file mode 100644 index 0000000000..61bc647450 --- /dev/null +++ b/packages/experimental/client-ui-cot-translation/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Traduction automatique du raisonnement", + "description": "Affichez un raisonnement développé dans une autre langue à l’aide de la traduction automatique." + } +} diff --git a/packages/experimental/client-ui-cot-translation/locale/pt.json b/packages/experimental/client-ui-cot-translation/locale/pt.json new file mode 100644 index 0000000000..433e8c3282 --- /dev/null +++ b/packages/experimental/client-ui-cot-translation/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tradução automática de raciocínio", + "description": "Exiba o raciocínio expandido em outro idioma usando tradução automática." + } +} diff --git a/packages/experimental/client-ui-cot-translation/locale/ru.json b/packages/experimental/client-ui-cot-translation/locale/ru.json new file mode 100644 index 0000000000..1e04a7c94b --- /dev/null +++ b/packages/experimental/client-ui-cot-translation/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Машинный перевод рассуждений", + "description": "Отображение развернутых рассуждений на другом языке с помощью машинного перевода." + } +} diff --git a/packages/experimental/client-ui-cot-translation/package.json b/packages/experimental/client-ui-cot-translation/package.json index 7a9c0bcd27..1cc014a2d7 100644 --- a/packages/experimental/client-ui-cot-translation/package.json +++ b/packages/experimental/client-ui-cot-translation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-cot-translation", "description": "Optional machine translation of expanded reasoning, preserving original Session text", - "version": "0.2.1-rc.2", + "version": "0.2.1-rc.3", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-cot-translation/src/client/locales.ts b/packages/experimental/client-ui-cot-translation/src/client/locales.ts index 94d5acd14e..ab461c4f40 100644 --- a/packages/experimental/client-ui-cot-translation/src/client/locales.ts +++ b/packages/experimental/client-ui-cot-translation/src/client/locales.ts @@ -87,3 +87,93 @@ export const ja = { 'unwrap': '折り返しを解除', 'footnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + provider: 'Servicio de traducción', google: 'google', bing: 'Bing', + deepseekAccount: 'DeepSeek Flash — cuenta (pago)', deepseekOfficial: 'Flash DeepSeek: API oficial (de pago)', + providerUnavailableOption: '{provider} — no disponible', + providerUnavailable: 'Este proveedor no está disponible. Configura tu cuenta DeepSeek o proveedor oficial de API, o elige un servicio disponible.', + paidNotice: 'Cada fragmento no almacenado en caché envía una solicitud Flash DeepSeek paga por separado a través de la cuenta seleccionada o las credenciales oficiales de API. El razonamiento de reapertura reutiliza los resultados guardados; cambiar el proveedor, el idioma, el texto fuente o la configuración de la solicitud puede enviar una nueva solicitud paga.', + anonymousNotice: 'Bing y Google no necesitan inicio de sesión de traducción ni clave API. Sus puntos finales anónimos pueden rechazar o limitar solicitudes.', targetLanguage: 'Idioma de destino', + targetLanguageHint: 'auto sigue el idioma de la interfaz de usuario; Los códigos de idioma como zh, en y ja también funcionan.', + privacy: 'Se envía el razonamiento ampliado al servicio seleccionado. Puede contener código privado o detalles de la conversación.', + translation: 'Ver traducción', original: 'Ver original', retry: 'Reintentar', failed: 'La traducción falló. Mostrando el original.', + translating: 'Traducir razonamiento', invalidProvider: 'Elija un servicio de traducción.', invalidLanguage: 'Ingrese auto o un código de idioma, como zh, en o ja.', + overridden: 'Anulado', reset: 'Restablecer los valores predeterminados', + unavailable: 'Las preferencias de traducción no están disponibles.', readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', save: 'Guardar', saving: 'Guardando…', + copy: 'Copiar', copied: 'Copiado', codeTitle: 'Bloque de código', wrap: 'Ajustar líneas', unwrap: 'Desenvolver líneas', footnotes: 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + provider: 'Service de traduction', google: 'Google', bing: 'Bing', + deepseekAccount: 'DeepSeek Flash — compte (payant)', deepseekOfficial: 'DeepSeek Flash — API officiel (payant)', + providerUnavailableOption: '{provider} — indisponible', + providerUnavailable: "Ce fournisseur n'est pas disponible. Configurez votre compte DeepSeek ou votre fournisseur officiel API, ou choisissez un service disponible.", + paidNotice: "Chaque fragment non mis en cache envoie une demande Flash DeepSeek payante distincte via le compte sélectionné ou les informations d'identification officielles API. La réouverture du raisonnement réutilise les résultats enregistrés ; la modification du fournisseur, de la langue, du texte source ou des paramètres de la demande peut envoyer une nouvelle demande payante.", + anonymousNotice: "Bing et Google n'ont pas besoin de connexion de traduction ni de clé API. Leurs points de terminaison anonymes peuvent rejeter ou limiter les demandes.", targetLanguage: 'Langue cible', + targetLanguageHint: "auto suit la langue de l'interface utilisateur ; les codes de langue tels que zh, en et ja fonctionnent également.", + privacy: 'Le raisonnement étendu est envoyé au service sélectionné. Il peut contenir du code privé ou des détails de conversation.', + translation: 'Voir la traduction', original: "Voir l'original", retry: 'Réessayer', failed: "La traduction a échoué. Montrant l'original.", + translating: 'Traduire le raisonnement', invalidProvider: 'Choisissez un service de traduction.', invalidLanguage: 'Saisissez auto ou un code de langue, tel que zh, en ou ja.', + overridden: 'Remplacé', reset: 'Réinitialiser aux valeurs par défaut', + unavailable: 'Les préférences de traduction ne sont pas disponibles.', readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", save: 'Enregistrer', saving: 'Sauvegarde…', + copy: 'Copier', copied: 'Copié', codeTitle: 'Bloc de code', wrap: 'Activer le retour à la ligne', unwrap: 'Dérouler les lignes', footnotes: 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + provider: 'Übersetzungsdienst', google: 'Google', bing: 'Bing', + deepseekAccount: 'DeepSeek Flash – Konto (kostenpflichtig)', deepseekOfficial: 'DeepSeek Flash – offizieller API (kostenpflichtig)', + providerUnavailableOption: '{provider} – nicht verfügbar', + providerUnavailable: 'Dieser Anbieter ist nicht verfügbar. Konfigurieren Sie Ihr DeepSeek-Konto oder Ihren offiziellen API-Anbieter oder wählen Sie einen verfügbaren Dienst.', + paidNotice: 'Jedes nicht zwischengespeicherte Fragment sendet eine separate kostenpflichtige DeepSeek-Flash-Anfrage über das ausgewählte Konto oder die offiziellen API-Anmeldeinformationen. Beim erneuten Öffnen der Argumentation werden gespeicherte Ergebnisse wiederverwendet. Wenn Sie den Anbieter, die Sprache, den Quelltext oder die Anfrageeinstellungen ändern, wird möglicherweise eine neue kostenpflichtige Anfrage gesendet.', + anonymousNotice: 'Für Bing und Google ist weder ein Übersetzungs-Login noch ein API-Schlüssel erforderlich. Ihre anonymen Endpunkte können Anfragen ablehnen oder einschränken.', targetLanguage: 'Zielsprache', + targetLanguageHint: 'folgt automatisch der UI-Sprache; Sprachcodes wie zh, en und ja funktionieren ebenfalls.', + privacy: 'Die erweiterte Begründung wird an den ausgewählten Dienst gesendet. Es kann private Code- oder Konversationsdetails enthalten.', + translation: 'Übersetzung ansehen', original: 'Original ansehen', retry: 'Erneut versuchen', failed: 'Die Übersetzung ist fehlgeschlagen. Zeigt das Original.', + translating: 'Argumentation übersetzen', invalidProvider: 'Wählen Sie einen Übersetzungsdienst.', invalidLanguage: 'Geben Sie auto oder einen Sprachcode ein, z. B. zh, en oder ja.', + overridden: 'Überschrieben', reset: 'Auf Standard zurücksetzen', + unavailable: 'Übersetzungseinstellungen sind nicht verfügbar.', readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', save: 'Speichern', saving: 'Speichern…', + copy: 'Kopieren', copied: 'Kopiert', codeTitle: 'Codeblock', wrap: 'Zeilen umbrechen', unwrap: 'Zeilen abwickeln', footnotes: 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + provider: 'Услуги перевода', google: 'Гугл', bing: 'Бинг', + deepseekAccount: 'DeepSeek Flash — аккаунт (платный)', deepseekOfficial: 'DeepSeek Flash — официальный API (платный)', + providerUnavailableOption: '{provider} — недоступен', + providerUnavailable: 'Этот провайдер недоступен. Настройте свою учетную запись DeepSeek или официального провайдера API или выберите доступную услугу.', + paidNotice: 'Каждый некэшированный фрагмент отправляет отдельный платный запрос DeepSeek Flash через выбранную учетную запись или официальные учетные данные API. При повторном открытии рассуждений повторно используются сохраненные результаты; изменение провайдера, языка, исходного текста или настроек запроса может привести к отправке нового платного запроса.', + anonymousNotice: 'Bing и Google не требуют входа в систему для перевода или ключа API. Их анонимные конечные точки могут отклонять или ограничивать запросы.', targetLanguage: 'Целевой язык', + targetLanguageHint: 'автоматически следует языку пользовательского интерфейса; коды языков, такие как zh, en и ja, также работают.', + privacy: 'Развернутое обоснование отправляется в выбранную службу. Он может содержать личный код или детали разговора.', + translation: 'Посмотреть перевод', original: 'Посмотреть оригинал', retry: 'Повторить', failed: 'Перевод не удался. Показываю оригинал.', + translating: 'Перевод рассуждений', invalidProvider: 'Выберите услугу перевода.', invalidLanguage: 'Введите auto или код языка, например zh, en или ja.', + overridden: 'Переопределено', reset: 'Сбросить настройки по умолчанию', + unavailable: 'Настройки перевода недоступны.', readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', save: 'Сохранить', saving: 'Сохранение…', + copy: 'Копировать', copied: 'Скопировано', codeTitle: 'Блок кода', wrap: 'Переносить строки', unwrap: 'Развернуть линии', footnotes: 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + provider: 'Serviço de tradução', google: 'Google', bing: 'Google', + deepseekAccount: 'DeepSeek Flash — conta (paga)', deepseekOfficial: 'DeepSeek Flash — oficial API (pago)', + providerUnavailableOption: '{provider} — indisponível', + providerUnavailable: 'Este provedor não está disponível. Configure sua conta DeepSeek ou provedor API oficial ou escolha um serviço disponível.', + paidNotice: 'Cada fragmento não armazenado em cache envia uma solicitação Flash DeepSeek paga separada por meio da conta selecionada ou das credenciais API oficiais. A reabertura do raciocínio reutiliza resultados salvos; alterar o provedor, o idioma, o texto fonte ou as configurações da solicitação pode enviar uma nova solicitação paga.', + anonymousNotice: 'O Bing e o Google não precisam de login de tradução ou chave API. Seus endpoints anônimos podem rejeitar ou limitar solicitações.', targetLanguage: 'Idioma alvo', + targetLanguageHint: 'auto segue o idioma da UI; códigos de idioma como zh, en e ja também funcionam.', + privacy: 'O raciocínio expandido é enviado ao serviço selecionado. Pode conter código privado ou detalhes da conversa.', + translation: 'Ver tradução', original: 'Ver original', retry: 'Tentar novamente', failed: 'A tradução falhou. Mostrando o original.', + translating: 'Traduzindo o raciocínio', invalidProvider: 'Escolha um serviço de tradução.', invalidLanguage: 'Insira auto ou um código de idioma, como zh, en ou ja.', + overridden: 'Substituído', reset: 'Redefinir para o padrão', + unavailable: 'As preferências de tradução não estão disponíveis.', readOnly: 'Esta implantação armazena configurações somente leitura.', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', save: 'Salvar', saving: 'Salvando…', + copy: 'Copiar', copied: 'Copiado', codeTitle: 'Bloco de código', wrap: 'Quebrar linhas', unwrap: 'Desembrulhar linhas', footnotes: 'Notas de rodapé', +} satisfies Record diff --git a/packages/experimental/client-ui-cot-translation/src/client/mount.ts b/packages/experimental/client-ui-cot-translation/src/client/mount.ts index 10e81f52fa..18b8e5b592 100644 --- a/packages/experimental/client-ui-cot-translation/src/client/mount.ts +++ b/packages/experimental/client-ui-cot-translation/src/client/mount.ts @@ -14,7 +14,7 @@ import type { CotTranslationPreferences, CotTranslationSnapshot } from '../prefe import { TranslationBody, type TranslationBodyInjected } from './TranslationBody.tsx' import { TranslationSettings } from './TranslationSettings.tsx' import { TranslationPreferencesForm } from './preferences-form.ts' -import { en, ja, NS, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh } from './locales.ts' /** Services required to mount the optional Remote and UI contributions. */ export const inject = ['remote', 'slots', 'locale', 'configForms'] @@ -80,7 +80,7 @@ async function registerUi(ctx: Context, lifetime: AbortController): Promise('cot-translation') ctx.effect(() => scope.subscribe(invalidate)) - ctx.effect(() => ctx.locale.register(NS, { en, zh, ja })) + ctx.effect(() => ctx.locale.register(NS, { en, zh, ja, es, fr, de, ru, pt })) const form = new TranslationPreferencesForm(scope) ctx.effect(() => () => { form.dispose() }) const injected = (sessionId: SessionId): TranslationBodyInjected => ({ diff --git a/packages/experimental/client-ui-creation-recommendations/README.i18n.yaml b/packages/experimental/client-ui-creation-recommendations/README.i18n.yaml new file mode 100644 index 0000000000..6b6cffa9fd --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/README.i18n.yaml @@ -0,0 +1,40 @@ +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/experimental/client-ui-creation-recommendations/README.md +/: + en: e332dc3625021ad9 + zh: a6637d7d8696ba5a +/deepseek-ai-dsh-client-ui-creation-recommendations: + en: b4af39c5d085f16e + zh: 19fd311580db7e2b +/deepseek-ai-dsh-client-ui-creation-recommendations/summary: + en: c2b93e3c50f80ecd + zh: da8aa68e686abddd +/deepseek-ai-dsh-client-ui-creation-recommendations/table-of-contents: + en: d152484eb41ac6b4 + zh: b00dddf4c88f0a45 +/deepseek-ai-dsh-client-ui-creation-recommendations/use-this-package: + en: 328d7d1d81bb2830 + zh: 425df912753bae83 +/deepseek-ai-dsh-client-ui-creation-recommendations/use-this-package/outcomes-and-recovery: + en: 5a3df9cd58832f91 + zh: 2068dca0a62394fe +/deepseek-ai-dsh-client-ui-creation-recommendations/understand-the-implementation: + en: 26b049c5e3f38846 + zh: 15768ae54aec3511 +/deepseek-ai-dsh-client-ui-creation-recommendations/further-exploration: + en: 6543c44016da2cdf + zh: fb0d365a5e411b04 +/deepseek-ai-dsh-client-ui-creation-recommendations/model-experience: + en: 176de8a02931b042 + zh: b1f1228f011ab056 +/deepseek-ai-dsh-client-ui-creation-recommendations/model-experience/kv-cache-effect: + en: 6a708541b49367ae + zh: 5722b25a6a8e6724 +/deepseek-ai-dsh-client-ui-creation-recommendations/known-limitations-and-deferred-work: + en: 93594f90162451d5 + zh: ca27b35469b537be +/deepseek-ai-dsh-client-ui-creation-recommendations/known-limitations-and-deferred-work/dev-note: + en: e67f633dc6fe7773 + zh: 8ec3e73528ddb1a6 diff --git a/packages/experimental/client-ui-creation-recommendations/README.md b/packages/experimental/client-ui-creation-recommendations/README.md new file mode 100644 index 0000000000..808e077ab3 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/README.md @@ -0,0 +1,88 @@ +--- +description: "Show completed-answer creation cards with location choice, Keep and Undo." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-creation-recommendations + +English | [中文](README.zh.md) + +## Summary + +Users see one creation proposal below the completed answer. Short plans appear in full; longer ones fold after about two lines and expand on request. A dropdown offers Right sidebar, Session tab, Global page, or Write your own. The cost notice opens until dismissed; this browser remembers the choice across sessions. The information button reopens it. Only Create starts background work; chatting can continue. Progress appears in the header. Keep confirms retention; Undo removes the plugin, and Restore reinstalls it. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Enable the [Personalized recommendations bundle](../personalized-recommendations/README.md). This browser contribution has no configuration fields; it fills the shared [action card frame](../client-ui-action-cards/README.md) for `create_personalized_page` cards. + +### Outcomes and recovery + +An unstarted card stays available until a newer proposal in the same conversation replaces it. The replaced card keeps its history with a Replaced label and no creation or location controls. Supplied initial content appears below the plan as folded data, so the user sees what the Creator will receive. Started tasks retain their progress and retry actions. The shared card projection owns this state, so reload does not reactivate a card. + +Creation, installation, retention and removal receipts come from the Session projection. The card restores them after reconnecting. A lost action reply shows an actionable error; retry uses the Host’s recorded state. Pending requests disable repeated actions. During generation, Cancel stops the child and waits for cleanup before retry. Accepted installations finish without cancellation. The information disclosure contains the source directory and explains that Undo keeps those files. The removed card offers Restore; when the retained files are gone, it asks for a new proposal. + +Write your own reveals a placement field, limited to 1,000 characters; blank input cannot start creation. Choosing a preset ignores that custom text. Accepted requests persist it with the receipt for reconnect and retry, and Creator receives it with the full brief. Editing the draft, opening the menu and reading the cost notice make no model request. Dismissing the notice or choosing Create/Not now acknowledges it in browser-local storage; a new browser or cleared/unavailable storage can show it again. Historical settled cards keep the notice collapsed. + +Plan disclosure depends on omitted text, not rendered height. Long previews collapse whitespace and preserve grapheme boundaries, including combined emoji; expanding restores the original brief. Short plans remain complete at every viewport width. Lines wrap naturally, with no fixed three-line limit, DOM measurement, or ResizeObserver. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +The plugin registers its body with the shared card frame, which places the card after its Turn and supplies the title, the folded brief, the fold component and card-bound actions. The body validates the card's details and recorded data, then renders the header with the cost notice toggle, the location picker, written placement, folded initial content, progress, Keep, Undo and Restore. The cost notice preference is a browser-local store owned by the plugin. The shared Menu handles portal positioning, keyboard selection and dismissal. The body registration and its dictionaries share one plugin lifetime. + +- [Source](src/client/Card.tsx) +- [Tests](tests/) + +
+ +----- + + +## Further Exploration + +- [Creation service](../creation-recommendations/README.md) — creation ownership and persistence. +- [Action card display](../client-ui-action-cards/README.md) — the frame, fold and actions this body uses. +- [Slots](../../client/ui-slots/README.md) — completed-Turn presentation. + + +## Model Experience + +Indirectly, through an accepted Create action: the Host starts Creator with the full brief and selected or human-written placement, without adding browser tools or resuming the parent model. + +#### KV Cache effect + +Rendering and changing the location do not affect the model cache. + +## Known Limitations and Deferred Work + + + +- The displayed brief is model-authored. Necessary existing data travels separately in the proposal’s optional `initialContent`. +- Historical receipts do not continuously track later manual changes on the Plugins page. + + +### Dev Note + +
+Working context for maintainers — click to expand + +None. + +
diff --git a/packages/experimental/client-ui-creation-recommendations/README.zh.md b/packages/experimental/client-ui-creation-recommendations/README.zh.md new file mode 100644 index 0000000000..d6562250b5 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/README.zh.md @@ -0,0 +1,88 @@ +--- +description: "在完整回答末尾显示支持位置选择、保留和撤销的创建卡片。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-creation-recommendations + +[English](README.md) | 中文 + +## 概述 + +用户在完整回答下方看到一条创建建议。短方案完整显示,长方案约两行后折叠,可展开全文。下拉菜单提供右侧栏、会话 Tab、全局页面和自行撰写四项。消耗提示在用户收起前默认展开,本浏览器跨会话记住选择,信息按钮可重新打开。只有点击创建才启动后台任务,主对话可继续。进度显示在顶部,保留用于确认继续使用,撤销则移除插件,恢复会重新安装它。 + +## 目录 + +- [使用本包](#use-this-package) +- [实现原理](#understand-the-implementation) +- [延伸阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +启用[个性化推荐 Bundle](../personalized-recommendations/README.zh.md)。本浏览器组件没有配置字段;它为 `create_personalized_page` 卡片填充共享的[操作卡片框架](../client-ui-action-cards/README.zh.md)。 + +### 操作结果与恢复 + +尚未创建的卡片一直可用,直到同一会话里更新的方案替换它。被替换的卡片保留历史内容和“已替换”标签,没有创建和位置控件。提供的预填内容以折叠形式显示在方案下方,用户能看到交给 Creator 的内容。已开始的任务保留进度和重试操作。共享卡片投影拥有这个状态,所以刷新不会重新激活卡片。 + +创建、安装、保留和移除记录来自 Session 投影,重新连接后会在卡片中恢复。操作回复丢失时显示可采取行动的错误信息;重试以 Host 已记录的状态为准。请求处理中禁用重复操作。生成阶段可取消,等待子任务清理后才能重试;已接纳的安装不再取消。信息说明中包含源码目录,并明确撤销会保留这些文件。已移除的卡片提供“恢复”;保留的文件已不在时,提示需要重新提案。 + +自行撰写会显示位置输入框,最多 1,000 字,空白内容不能启动创建;改选预设位置后不提交自定义文字。已接受的请求将文字存入操作记录,供重连和重试恢复,并连同完整方案交给 Creator。编辑草稿、打开菜单或查看消耗说明都不会调用模型。收起提示或点击创建、暂不需要后,浏览器本地存储记住已读;更换浏览器、清除或无法使用存储时可能再次显示。历史已结束的卡片默认收起提示。 + +是否展开只取决于有没有省略内容,不取决于渲染高度。长方案预览合并空白,并保留组合 emoji 等字素边界;展开后恢复原始全文。短方案在任何窗口宽度都完整显示。文字自然换行,不固定三行,不读取 DOM 尺寸或使用 ResizeObserver。 + +----- + + +## 实现原理 + +
+实现细节 — 点击展开 + +插件把正文注册到共享卡片框架,由框架把卡片放在所属回合之后,并提供标题、折叠后的说明、折叠组件和绑定到卡片的操作。正文先校验卡片的 details 和已记录的数据,再渲染带消耗说明开关的标题栏、位置选择、手写位置、折叠的预填内容、进度,以及保留、撤销和恢复。消耗说明的偏好是插件自有的浏览器本地存储。共享 Menu 处理浮层定位、键盘选择和关闭。正文注册和语言字典使用同一插件生命周期。 + +- [源码](src/client/Card.tsx) +- [测试](tests/) + +
+ +----- + + +## 延伸阅读 + +- [创建服务](../creation-recommendations/README.zh.md) — 创建任务与持久化。 +- [操作卡片显示](../client-ui-action-cards/README.zh.md) — 本正文使用的框架、折叠和操作。 +- [Slots](../../client/ui-slots/README.zh.md) — 回合末尾呈现. + + +## 模型体验 + +间接影响来自用户接受的创建操作:Host 启动 Creator,传入完整方案和选定或手写的位置,不增加浏览器侧模型工具,也不恢复父模型执行。 + +#### KV Cache effect + +渲染卡片和修改位置不会影响模型缓存。 + +## 已知限制与后续工作 + + + +- 展示方案由模型编写,所需已有数据通过方案的可选 `initialContent` 单独交接。 +- 历史记录不会持续跟踪之后在插件页手动进行的修改。 + + +### 开发备注 + +
+维护者工作上下文 — 点击展开 + +无。 + +
diff --git a/packages/experimental/client-ui-creation-recommendations/locale/de.json b/packages/experimental/client-ui-creation-recommendations/locale/de.json new file mode 100644 index 0000000000..e9a5d99164 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Karten für personalisierte Seiten", + "description": "Zeigt die Vorschlagskarte nach einer Antwort mit Ortswahl, Erstellungsfortschritt, Behalten und Rückgängigmachen an." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/en.json b/packages/experimental/client-ui-creation-recommendations/locale/en.json new file mode 100644 index 0000000000..05dbfd4e43 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personalized page cards", + "description": "Show the proposal card after an answer with location choice, creation progress, retention and undo." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/es.json b/packages/experimental/client-ui-creation-recommendations/locale/es.json new file mode 100644 index 0000000000..8f7e016149 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tarjetas de páginas personalizadas", + "description": "Muestra la tarjeta de propuesta después de una respuesta, con elección de ubicación, progreso de creación, conservación y deshacer." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/fr.json b/packages/experimental/client-ui-creation-recommendations/locale/fr.json new file mode 100644 index 0000000000..1890ba69ee --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cartes de pages personnalisées", + "description": "Affiche la carte de proposition après une réponse, avec le choix de l'emplacement, la progression de la création, la conservation et l'annulation." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/ja.json b/packages/experimental/client-ui-creation-recommendations/locale/ja.json new file mode 100644 index 0000000000..4e776bc2de --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/ja.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "パーソナルページのカード", + "description": "回答の末尾に提案カードを表示し、配置の選択、作成の進行状況、保持と取り消しを扱います。" + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/pt.json b/packages/experimental/client-ui-creation-recommendations/locale/pt.json new file mode 100644 index 0000000000..37bd61d6a4 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cartões de páginas personalizadas", + "description": "Mostra o cartão de proposta após uma resposta, com escolha de local, progresso da criação, manutenção e desfazer." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/ru.json b/packages/experimental/client-ui-creation-recommendations/locale/ru.json new file mode 100644 index 0000000000..d9c778829a --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Карточки персональных страниц", + "description": "Показывает карточку предложения после ответа с выбором места, ходом создания, сохранением и отменой." + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/locale/zh.json b/packages/experimental/client-ui-creation-recommendations/locale/zh.json new file mode 100644 index 0000000000..debb5f1b11 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "个性化页面卡片", + "description": "在回答末尾显示方案卡片,含位置选择、创建进度、保留和撤销。" + } +} diff --git a/packages/experimental/client-ui-creation-recommendations/package.json b/packages/experimental/client-ui-creation-recommendations/package.json new file mode 100644 index 0000000000..f7198b8f1f --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/package.json @@ -0,0 +1,64 @@ +{ + "version": "0.2.1-rc.3", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "license": "MIT", + "name": "@deepseek-ai/dsh-client-ui-creation-recommendations", + "description": "Body of the personalized page card: location choice, creation progress, Keep, Undo and Restore", + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/experimental/client-ui-creation-recommendations" + }, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json", + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./locale/*.json": "./locale/*.json" + }, + "dsh": { + "client": { + "platform": "web", + "inject": [ + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-ui-action-cards" + ] + } + }, + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", + "@deepseek-ai/dsh-client-locale": "workspace:*", + "@deepseek-ai/dsh-client-ui-slots": "workspace:*", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", + "react": "^18.2.0", + "@deepseek-ai/dsh-creation-recommendations": "workspace:*", + "@deepseek-ai/dsh-client-store": "workspace:*", + "@deepseek-ai/dsh-client-ui-action-cards": "workspace:*", + "@deepseek-ai/dsh-action-cards": "workspace:*", + "zod": "4.4.3", + "@deepseek-ai/dsh-client-test-runtime": "workspace:*", + "@deepseek-ai/dsh-llm": "workspace:*", + "@deepseek-ai/dsh-session": "workspace:*" + }, + "files": [ + "locale/*.json", + "lib/index.js", + "lib/client.js", + "lib/types/**/*.d.ts" + ] +} diff --git a/packages/experimental/client-ui-creation-recommendations/src/client/Card.module.css b/packages/experimental/client-ui-creation-recommendations/src/client/Card.module.css new file mode 100644 index 0000000000..b1cc0590ec --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/src/client/Card.module.css @@ -0,0 +1,28 @@ +.header { display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; padding: 12px 20px 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; } +.heading { display: inline-flex; align-items: center; gap: 4px; } +.infoButton { width: 28px; padding: 0; color: var(--dsw-alias-label-secondary); } +.info { margin: 12px 20px 0; padding: 12px; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; } +.info p { margin: 0; } +.info p + p { margin-top: 4px; } +.info strong { font-weight: 500; } +.phase { color: var(--dsw-alias-label-primary); } +.body { padding: 12px 20px 20px; } +.content { margin-top: 12px; } +.contentLabel { display: block; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); } +.description { margin: 16px 0 0; padding-top: 12px; border-top: 0.5px solid var(--dsw-alias-border-l1); font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; } +.footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; flex-wrap: wrap; } +.scope { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary); } +.actions { display: flex; align-items: center; gap: 8px; margin-left: auto; } +.status { margin: 12px 0 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); } +.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l1); } +.controls .footer { margin: 0 0 0 auto; } +.locationRow { display: flex; align-items: center; gap: 8px; min-width: 0; } +.locationLabel { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); } +.locationButton { gap: 8px; font-size: 13px; } +.locationMenu { width: 264px; max-width: calc(100vw - 24px); } +.locationOption { display: flex; flex-direction: column; gap: 2px; white-space: normal; line-height: 1.5; } +.locationOption > span + span { color: var(--dsw-alias-label-secondary); font-size: 12px; } +.customLocation { display: flex; flex-direction: column; gap: 6px; flex: 1 0 100%; min-width: 0; } +.customLocation textarea { box-sizing: border-box; width: 100%; min-height: 72px; resize: vertical; padding: 8px 10px; border: 0.5px solid var(--dsw-alias-border-l4); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; line-height: 1.7; } +.customLocation textarea:focus-visible { outline: 1px solid var(--dsw-alias-state-business-primary); outline-offset: 1px; } +.customLocation textarea::placeholder { color: var(--dsw-alias-label-secondary); } diff --git a/packages/experimental/client-ui-creation-recommendations/src/client/Card.tsx b/packages/experimental/client-ui-creation-recommendations/src/client/Card.tsx new file mode 100644 index 0000000000..5fffa80e65 --- /dev/null +++ b/packages/experimental/client-ui-creation-recommendations/src/client/Card.tsx @@ -0,0 +1,169 @@ +/** Body of a `create_personalized_page` card: location choice, creation progress, retention, undo and restore. */ +import { useCallback, useId, useState, useSyncExternalStore } from 'react' +import { z } from 'zod' +import { Button, IconChevronDownOutlineRegular, IconWarningTriangleOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' +import type { SnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { ActionCardBodyProps } from '@deepseek-ai/dsh-client-ui-action-cards/client' +import type { CreationSlot, PageData, PageDetails, PageVerb } from '@deepseek-ai/dsh-creation-recommendations/types' +import type { RecommendationKey } from './locales.ts' +import css from './Card.module.css' + +/** Body props: the frame's card props plus this browser's cost-notice preference. */ +export type PageCardProps = ActionCardBodyProps<'creationRecommendations'> & { readonly infoDismissed: SnapshotStore } + +type LocationChoice = CreationSlot | 'custom' +type CardVerb = PageVerb | 'dismiss' + +const slotSchema = z.enum(['sidebar-right', 'page', 'sidebar-left']) +const detailsSchema = z.object({ slot: slotSchema, initialContent: z.string().optional() }) satisfies z.ZodType +const dataSchema = z.object({ + slot: slotSchema, customLocation: z.string().optional(), bundleName: z.string().optional(), + application: z.enum(['applied', 'restart-required', 'overridden', 'failed', 'cancelled']).optional(), +}) satisfies z.ZodType + +/** Phases with their own status copy. */ +const PHASE_KEYS: readonly RecommendationKey[] = [ + 'dismissed', 'replaced', 'creating', 'installing', 'cancelled', 'installed', 'kept', 'removed', 'failed', 'interrupted', 'unconfirmed', +] +/** Refusal reasons with their own copy; any other reason reads as unavailable. */ +const REASON_KEYS: readonly RecommendationKey[] = [ + 'unknown', 'access-denied', 'not-live-root', 'closing', 'unavailable', 'invalid-location', 'read-only', 'source-missing', +] + +/** Application outcomes have their own copy; the phase copy of failed and cancelled describes creation instead. */ +const APPLICATION_KEYS: Readonly, 'applied'>, RecommendationKey>> = { + 'restart-required': 'restart-required', overridden: 'overridden', failed: 'applicationFailed', cancelled: 'applicationCancelled', +} + +function keyOf(keys: readonly RecommendationKey[], value: string): RecommendationKey | undefined { + return keys.find(key => key === value) +} + +function LocationPicker({ value, disabled, onChange, t }: { + value: LocationChoice + disabled: boolean + onChange: (value: LocationChoice) => void +} & PropsLocale<'creationRecommendations'>) { + const [open, setOpen] = useState(false) + return { setOpen(false) }} + onSelect={(id) => { onChange(id as LocationChoice); setOpen(false) }} + items={(['sidebar-right', 'page', 'sidebar-left', 'custom'] as const).map(id => ({ + id, label: {t(id)}{t(`${id}Hint`)}, + }))} + anchor={} /> +} + +/** + * Display creation, persistent installation, reversible removal and restoration below the frame's summary. + * @param props - card, state, actions, folded-text component and notice preference. + * @returns the card body. + */ +export function PageCardBody({ proposal, decision, summary, Folded, act, infoDismissed, t }: PageCardProps) { + const details = detailsSchema.safeParse(proposal.details) + const parsed = decision === undefined ? undefined : dataSchema.safeParse(decision.data) + const data = parsed?.success === true ? parsed.data : undefined + const phase = decision === undefined ? undefined : keyOf(PHASE_KEYS, decision.phase) + const [selected, setSelected] = useState() + const [custom, setCustom] = useState() + const [info, setInfo] = useState() + const subscribe = useCallback((listener: () => void) => infoDismissed.subscribe(listener), [infoDismissed]) + const noticeDismissed = useSyncExternalStore(subscribe, () => infoDismissed.getSnapshot()) + const infoId = useId() + const customId = useId() + const [pending, setPending] = useState() + const [error, setError] = useState() + if (!details.success) return
{summary}
+ const busy = pending !== undefined + const slot = selected !== undefined && selected !== 'custom' ? selected : data?.slot ?? details.data.slot + const location = selected ?? (data?.customLocation === undefined ? slot : 'custom') + const customLocation = custom ?? data?.customLocation ?? '' + const offer = decision === undefined || phase === 'cancelled' || phase === 'failed' || phase === 'interrupted' || phase === 'unconfirmed' + const infoOpen = info ?? (offer && !noticeDismissed) + const installed = phase === 'installed' || phase === 'kept' + const run = async (verb: CardVerb) => { + setPending(verb) + setError(undefined) + if (verb === 'create' || verb === 'dismiss') { infoDismissed.set(true); setInfo(false) } + const placement = { slot, ...location === 'custom' ? { customLocation: customLocation.trim() } : {} } + try { + const result = await act(verb, verb === 'create' || verb === 'dismiss' ? placement : undefined) + if (result === undefined) setError('transport') + else if (result.status === 'refused' || result.status === 'unconfirmed') setError(keyOf(REASON_KEYS, result.reason) ?? 'unavailable') + } catch { + // A lost reply does not prove failure; replay supplies the durable decision. + setError('transport') + } finally { setPending(undefined) } + } + return <> +
{t('heading')} + + + + + {phase !== undefined && {t(phase)}} +
+ {infoOpen &&
+

{t('creationMethod')}

+

{t('creationCost')}

+

{t('creationBackground')}

+ {data?.bundleName !== undefined &&

{t('sourceRetained')}

} +
} +
+ {summary} + {details.data.initialContent !== undefined &&
+ {t('initialContent')} + +
} + {data?.application !== undefined && data.application !== 'applied' && phase !== 'failed' + &&

{t(APPLICATION_KEYS[data.application])}

} + {error !== undefined &&

{t(error)}

} + {offer ?
+
+ {t('location')} + +
+ {location === 'custom' &&
+ +