@xmanrui/dsh-im 4.19.0 → 4.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -224,7 +224,7 @@ DingTalk menus use a shared template built into the plugin; no template setup is
224
224
 
225
225
  - Registers one top-level **IM Bot** settings page containing the built-in IM channels and one AI Office Connector.
226
226
  - Maintains the Host, client, and runtime sources for the built-in channels and the Office Connector in this repository without external standalone plugins.
227
- - Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English. Bot chat messages follow the Host's `language` config (Chinese by default; `en` switches them to English), with Chinese always as the fallback so untranslated text is sent verbatim.
227
+ - Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English. Bot chat messages, command help, and the Telegram command menu follow the same interface language and switch live, with Chinese always as the fallback so untranslated text is sent verbatim.
228
228
  - Uses logos for WeChat, Feishu, DingTalk, WeCom, QQ, Slack, Telegram, Discord, WhatsApp, iMessage, and AI Office navigation without enable/disable switches.
229
229
  - Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by IM channel; the Office Connector separately owns Device credentials, Job leases, approval waits, and concurrency limits.
230
230
  - Returns only QR codes, the public Slack Manifest, redacted status data, and access modes or allowlist identifiers explicitly saved for the current Telegram or WhatsApp bot. Manually entered secrets and Tokens travel one way to the local Host; no RPC response returns App Secrets, `bot_token`, DingTalk `client_secret`, WeCom Secrets, QQ `app_secret`, Slack Bot/App Tokens, Telegram/Discord Bot Tokens, WhatsApp linked-device keys, AI Office Device Tokens, or other raw user identifiers observed from platform messages.
@@ -257,7 +257,23 @@ After building the plugin, run `node scripts/verify-lan-management.mjs /path/to/
257
257
 
258
258
  ### Bot chat message language
259
259
 
260
- Bot chat messages are in Chinese by default. To switch them to English, set `language: en` in the plugin config (also accepts `en-US` or `english`), or set the `DSH_IM_LANGUAGE=en` environment variable:
260
+ **Nothing to configure.** Bot chat messages, command help, and the Telegram command menu follow the DeepSeek Harness interface language. Set DSH to English in **Settings → General → Language** and the bot answers in English the change applies live, with no Host restart and no bot reconnect.
261
+
262
+ The language is resolved from the first of these that names one:
263
+
264
+ 1. The plugin's own `language` option (or the `DSH_IM_LANGUAGE` environment variable). This is the operator override described below; when it is set, DSH's interface language is ignored.
265
+ 2. The explicit selection in DSH's **Language** row, read from the Host user-settings document. This is what "DSH is set to English" means, and it applies to every channel.
266
+ 3. The interface language the settings page is actually rendered in. DSH stores no preference when the language came from the browser's language list, so dsh-im mirrors the effective language to the Host and keeps it in `~/.dsh/integrations/dsh-im/interface-language.json`. That way bots keep answering in your language after a Host restart, before any browser has connected.
267
+
268
+ Chinese is always the fallback language — any text missing from the English dictionary is sent verbatim in Chinese, so this never changes the behavior of existing Chinese users.
269
+
270
+ Bot messages switch from the next message onward. The Telegram command menu is re-sent to Telegram right away, but **Telegram clients cache the `/` menu**, so your own app can keep showing the previous language for a while even though Telegram already holds the new one. Reopening the app refreshes it, and `getMyCommands` always shows what was actually stored.
271
+
272
+ One Telegram label is outside a bot's control and does not follow this setting: the blue **Menu** button beside the input box. dsh-im sets it to `MenuButtonCommands`, which the Bot API defines with no text field, so Telegram renders that label in the *reader's own app language*. Only `MenuButtonWebApp` carries text, and it requires a Web App URL.
273
+
274
+ Run `node scripts/verify-interface-language.mjs /path/to/deepseek-harness` to verify this end to end against an unmodified DSH CLI: it uses an isolated temporary home, asserts each resolution layer over the real `/api` carrier, and checks that a restart and an operator-pinned `language` both behave. Set `DSH_IM_TELEGRAM_TOKEN` to additionally bind a real bot and assert the command menu Telegram itself stores, which is restored when the check finishes.
275
+
276
+ To pin one language regardless of who is reading the interface, set `language` in the plugin config (also accepts `en-US` or `english`), or set the `DSH_IM_LANGUAGE=en` environment variable:
261
277
 
262
278
  ```yaml
263
279
  - id: xmanrui-dsh-im
@@ -265,8 +281,6 @@ Bot chat messages are in Chinese by default. To switch them to English, set `lan
265
281
  language: en
266
282
  ```
267
283
 
268
- Without a setting, Chinese is used. Chinese is always the fallback language — any text missing from the English dictionary is sent verbatim in Chinese, so this feature never changes the behavior of existing Chinese users.
269
-
270
284
  ---
271
285
 
272
286
  ## Contact
package/README.md CHANGED
@@ -227,7 +227,7 @@ Logo 由 dsh-im 的浏览器适配显示,无需修改 DSH。适配保留原始
227
227
 
228
228
  - Harness 一级设置菜单中只注册一个「IM机器人」设置页,其中包含内置 IM 渠道和一个 AI Office Connector;
229
229
  - 内置渠道及 Office Connector 的 Host、客户端与运行时源码都在本仓库维护,不依赖外部独立插件;
230
- - 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;机器人发出的聊天消息跟随 Host 的 `language` 配置(默认中文;设为 `en` 即为英文),中文始终为兜底,未收录的文案原样输出;
230
+ - 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;机器人发出的聊天消息、命令帮助和 Telegram 命令菜单同样跟随该界面语言并即时切换,中文始终为兜底,未收录的文案原样输出;
231
231
  - 左侧使用 Logo 切换微信、飞书、钉钉、企业微信、企业微信应用、QQ、Slack、Telegram、Discord、WhatsApp、iMessage 和 AI Office,不使用启用/停用开关;
232
232
  - 各 IM 渠道保持独立的 RPC、凭据、连接监督和会话映射;Office Connector 另行维护设备凭据、Job 租约、审批等待与并发上限;
233
233
  - 浏览器只获得二维码、Manifest、脱敏状态,以及用户为当前 Telegram 或 WhatsApp 机器人主动保存的访问模式和白名单标识;手动输入的 Secret 或 Token 仅单向提交给本机 Host,任何 RPC 响应都不会返回 App Secret、`bot_token`、钉钉 `client_secret`、企业微信 Secret、QQ `app_secret`、Slack Bot/App Token、Telegram/Discord Bot Token、WhatsApp 关联设备密钥、AI Office Device Token,或从平台消息中观察到的其他原始用户标识。
@@ -260,7 +260,23 @@ IM 管理接口默认沿用 Harness 的浏览器认证和 Host/Origin 信任
260
260
 
261
261
  ### 聊天消息语言
262
262
 
263
- 机器人发出的聊天消息默认使用中文。要切换为英文,在插件配置中设置 `language: en`(也接受 `en-US`、`english`),或设置环境变量 `DSH_IM_LANGUAGE=en`:
263
+ **无需配置。** 机器人发出的聊天消息、命令帮助和 Telegram 命令菜单跟随 DeepSeek Harness 的界面语言。在 **设置 → 通用 → 语言** 中把 DSH 切换为 English,机器人即以英文回复;切换即时生效,无需重启 Host,也无需重连机器人。
264
+
265
+ 语言按以下顺序取第一个有效值:
266
+
267
+ 1. 插件自身的 `language` 配置(或环境变量 `DSH_IM_LANGUAGE`)。这是下文的运维级固定值;一旦设置,就不再跟随 DSH 的界面语言。
268
+ 2. DSH「语言」设置项中的显式选择,从 Host 用户设置文档读取。这就是「DSH 设为 English」的含义,对所有渠道生效。
269
+ 3. 设置页实际渲染所用的界面语言。当界面语言来自浏览器语言列表时 DSH 不会存储任何偏好,因此 dsh-im 会把生效语言回传 Host 并保存在 `~/.dsh/integrations/dsh-im/interface-language.json`,这样 Host 重启后、尚无浏览器连接时机器人仍以该语言回复。
270
+
271
+ 中文始终是兜底语言,任何未收录到英文词典的文案都会原样以中文输出,因此该功能不会改变现有中文用户的行为。
272
+
273
+ 聊天消息从下一条起即切换语言。Telegram 命令菜单会立即重新下发,但 **Telegram 客户端会缓存 `/` 菜单**,因此即使 Telegram 侧已保存新语言,你自己的客户端仍可能在一段时间内显示切换前的语言;重开客户端即可刷新,`getMyCommands` 始终反映实际存储的内容。
274
+
275
+ 输入框旁的蓝色 **Menu 按钮**不受机器人控制,也不会跟随该设置:dsh-im 将其设为 `MenuButtonCommands`,而 Bot API 中该类型没有文本字段,因此按钮文案由 Telegram 按**阅读者客户端自身的语言**渲染。只有 `MenuButtonWebApp` 带有文本字段,但它需要一个 Web App URL。
276
+
277
+ 端到端验证可运行 `node scripts/verify-interface-language.mjs /path/to/deepseek-harness`:脚本使用原版 DSH CLI 与独立临时 home,通过真实 `/api` 通道逐层校验语言解析顺序,并验证重启后与运维固定 `language` 时的行为。设置 `DSH_IM_TELEGRAM_TOKEN` 可额外接入真实机器人,断言 Telegram 侧实际存储的命令菜单,检查结束后会恢复其原有菜单。
278
+
279
+ 如需固定一种语言、不随阅读者的界面语言变化,可在插件配置中设置 `language`(也接受 `en-US`、`english`),或设置环境变量 `DSH_IM_LANGUAGE=en`:
264
280
 
265
281
  ```yaml
266
282
  - id: xmanrui-dsh-im
@@ -268,8 +284,6 @@ IM 管理接口默认沿用 Harness 的浏览器认证和 Host/Origin 信任
268
284
  language: en
269
285
  ```
270
286
 
271
- 未设置时保持中文;中文始终是兜底语言,任何未收录到英文词典的文案都会原样以中文输出,因此该功能不会改变现有中文用户的行为。
272
-
273
287
  ---
274
288
 
275
289
  ## 联系方式
package/lib/client.js CHANGED
@@ -582,7 +582,7 @@ var React30 = __toESM(require("react"), 1);
582
582
  // package.json
583
583
  var package_default = {
584
584
  name: "@xmanrui/dsh-im",
585
- version: "4.19.0",
585
+ version: "4.19.2",
586
586
  description: "\u628A\u5341\u4E00\u79CD IM \u6E20\u9053\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
587
587
  keywords: [
588
588
  "deepseek-harness",
@@ -634,6 +634,10 @@ var package_default = {
634
634
  {
635
635
  name: "geekyfoxlab",
636
636
  url: "https://github.com/geekyfoxlab"
637
+ },
638
+ {
639
+ name: "grloper",
640
+ url: "https://github.com/grloper"
637
641
  }
638
642
  ],
639
643
  license: "MIT",
@@ -3105,12 +3109,109 @@ function AliasDialog({ bot, onSave, onClose }) {
3105
3109
  );
3106
3110
  return globalThis.document?.body ? (0, import_react_dom.createPortal)(content, document.body) : content;
3107
3111
  }
3112
+ function BotNameTooltip({ anchorRef, id: id6, name: name2, onDismiss }) {
3113
+ const tooltipRef = React5.useRef(null);
3114
+ const [position, setPosition] = React5.useState(null);
3115
+ React5.useLayoutEffect(() => {
3116
+ const anchor = anchorRef.current;
3117
+ const tooltip = tooltipRef.current;
3118
+ if (!anchor || !tooltip) return void 0;
3119
+ const document2 = anchor.ownerDocument;
3120
+ const view = document2.defaultView;
3121
+ const place = () => {
3122
+ const rect = anchor.getBoundingClientRect();
3123
+ const { width, height } = tooltip.getBoundingClientRect();
3124
+ const viewport = document2.documentElement;
3125
+ const margin = 8;
3126
+ const below = rect.bottom + 6;
3127
+ setPosition({
3128
+ left: Math.max(margin, Math.min(rect.left, viewport.clientWidth - width - margin)),
3129
+ top: Math.max(margin, Math.min(
3130
+ below + height <= viewport.clientHeight - margin ? below : rect.top - height - 6,
3131
+ viewport.clientHeight - height - margin
3132
+ ))
3133
+ });
3134
+ };
3135
+ const dismiss = (event) => {
3136
+ if (event.key === "Escape") {
3137
+ event.stopPropagation();
3138
+ onDismiss();
3139
+ }
3140
+ };
3141
+ place();
3142
+ view.addEventListener("resize", place);
3143
+ document2.addEventListener("scroll", onDismiss, true);
3144
+ document2.addEventListener("keydown", dismiss, true);
3145
+ return () => {
3146
+ view.removeEventListener("resize", place);
3147
+ document2.removeEventListener("scroll", onDismiss, true);
3148
+ document2.removeEventListener("keydown", dismiss, true);
3149
+ };
3150
+ }, [anchorRef, name2, onDismiss]);
3151
+ const body = anchorRef.current?.ownerDocument.body;
3152
+ return body ? (0, import_react_dom.createPortal)(h2("span", {
3153
+ ref: tooltipRef,
3154
+ id: id6,
3155
+ role: "tooltip",
3156
+ className: "dim-botNameTooltip",
3157
+ style: position ?? { visibility: "hidden" }
3158
+ }, name2), body) : null;
3159
+ }
3108
3160
  function BotName({ bot, id: id6, disabled = false, onSave }) {
3109
3161
  const [open, setOpen] = React5.useState(false);
3162
+ const nameRef = React5.useRef(null);
3163
+ const tooltipId = React5.useId();
3164
+ const [truncated, setTruncated] = React5.useState(false);
3165
+ const [hovered, setHovered] = React5.useState(false);
3166
+ const [focused, setFocused] = React5.useState(false);
3167
+ const [dismissed, setDismissed] = React5.useState(false);
3168
+ const dismissTooltip = React5.useCallback(() => setDismissed(true), []);
3169
+ const measure = React5.useCallback(() => {
3170
+ const node = nameRef.current;
3171
+ setTruncated(Boolean(node && node.scrollWidth > node.clientWidth));
3172
+ }, []);
3173
+ React5.useEffect(() => {
3174
+ const node = nameRef.current;
3175
+ if (!node) return void 0;
3176
+ measure();
3177
+ const view = node.ownerDocument.defaultView;
3178
+ const observer = view.ResizeObserver ? new view.ResizeObserver(measure) : null;
3179
+ observer?.observe(node);
3180
+ view.addEventListener("resize", measure);
3181
+ return () => {
3182
+ observer?.disconnect();
3183
+ view.removeEventListener("resize", measure);
3184
+ };
3185
+ }, [bot.name, measure]);
3186
+ const showTooltip = truncated && !dismissed && !open && (hovered || focused);
3110
3187
  return h2(
3111
3188
  "div",
3112
3189
  { className: "dim-aliasName" },
3113
- h2("h3", { id: id6, title: bot.name }, bot.name),
3190
+ h2("h3", {
3191
+ id: id6,
3192
+ ref: nameRef,
3193
+ tabIndex: truncated ? 0 : void 0,
3194
+ "aria-describedby": showTooltip ? tooltipId : void 0,
3195
+ onMouseEnter: () => {
3196
+ measure();
3197
+ setHovered(true);
3198
+ setDismissed(false);
3199
+ },
3200
+ onMouseLeave: () => setHovered(false),
3201
+ onFocus: () => {
3202
+ measure();
3203
+ setFocused(true);
3204
+ setDismissed(false);
3205
+ },
3206
+ onBlur: () => setFocused(false),
3207
+ onClick: dismissTooltip
3208
+ }, bot.name),
3209
+ showTooltip ? h2(BotNameTooltip, {
3210
+ anchorRef: nameRef,
3211
+ id: tooltipId,
3212
+ name: bot.name,
3213
+ onDismiss: dismissTooltip
3214
+ }) : null,
3114
3215
  h2(
3115
3216
  "span",
3116
3217
  {
@@ -13841,6 +13942,68 @@ var channel4 = createTokenChannelSettings({
13841
13942
  var IMessageSettingsTab = channel4.SettingsTab;
13842
13943
  var IMessageAccountCard = channel4.AccountCard;
13843
13944
 
13945
+ // src/channels/shared/interface-language.mjs
13946
+ var LANGUAGE_TAG = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
13947
+ var HOST_LANGUAGE_SOURCES = Object.freeze(["config", "settings", "mirror"]);
13948
+ function normalizeInterfaceLanguageTag(value) {
13949
+ if (typeof value !== "string") return null;
13950
+ const trimmed = value.trim();
13951
+ return LANGUAGE_TAG.test(trimmed) ? trimmed : null;
13952
+ }
13953
+
13954
+ // plugin-src/client/interface-language.js
13955
+ var HOST_LANGUAGE_RPC_CHANNEL = "/dsh-im-language";
13956
+ var HOST_LANGUAGE_ENDPOINTS = Object.freeze({
13957
+ get: "settings.language.get",
13958
+ mirror: "settings.language.mirror"
13959
+ });
13960
+ var RETRY_DELAYS_MS = Object.freeze([1e3, 4e3, 15e3]);
13961
+ function installInterfaceLanguageMirror(ctx, {
13962
+ rpcCall,
13963
+ setTimeoutFn = (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
13964
+ clearTimeoutFn = (timer) => globalThis.clearTimeout(timer),
13965
+ retryDelaysMs = RETRY_DELAYS_MS
13966
+ } = {}) {
13967
+ if (typeof rpcCall !== "function" || typeof ctx?.locale?.getLocale !== "function") {
13968
+ return () => {
13969
+ };
13970
+ }
13971
+ const delays = retryDelaysMs.length > 0 ? retryDelaysMs : RETRY_DELAYS_MS;
13972
+ const scheduler = createPollScheduler({ setTimeoutFn, clearTimeoutFn });
13973
+ let mirrored = null;
13974
+ let attempt = 0;
13975
+ const retry = (active) => {
13976
+ if (mirrored === active) mirrored = null;
13977
+ scheduler.schedule(report, delays[Math.min(attempt, delays.length - 1)]);
13978
+ attempt += 1;
13979
+ };
13980
+ const report = () => {
13981
+ if (scheduler.disposed) return;
13982
+ const active = normalizeInterfaceLanguageTag(ctx.locale.getLocale()?.active);
13983
+ if (active === null || active === mirrored) return;
13984
+ mirrored = active;
13985
+ Promise.resolve(rpcCall(HOST_LANGUAGE_ENDPOINTS.mirror, { locale: active })).then(
13986
+ (result) => {
13987
+ if (result?.ok === true) {
13988
+ attempt = 0;
13989
+ return;
13990
+ }
13991
+ retry(active);
13992
+ },
13993
+ () => retry(active)
13994
+ );
13995
+ };
13996
+ report();
13997
+ const off = typeof ctx.on === "function" ? ctx.on("locale/change", () => {
13998
+ attempt = 0;
13999
+ report();
14000
+ }) : null;
14001
+ return () => {
14002
+ scheduler.dispose();
14003
+ if (typeof off === "function") off();
14004
+ };
14005
+ }
14006
+
13844
14007
  // plugin-src/client/delivery-settings.js
13845
14008
  var React27 = __toESM(require("react"), 1);
13846
14009
 
@@ -16178,9 +16341,15 @@ var IM_STYLE_ID = "xmanrui-dsh-im-settings";
16178
16341
  var CSS13 = String.raw`
16179
16342
  .dim-aliasName { display: flex; align-items: center; gap: 4px; min-width: 0; }
16180
16343
  .dim-aliasName h3 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
16344
+ .dim-aliasName h3:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 3px; }
16345
+ .dim-botNameTooltip { position: fixed; z-index: 1000; box-sizing: border-box; width: max-content; max-width: min(320px, calc(100vw - 16px)); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font: 500 12px/18px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; white-space: normal; overflow-wrap: anywhere; pointer-events: none; animation: dim-botNameTooltip-in .15s ease; }
16346
+ @keyframes dim-botNameTooltip-in { from { opacity: 0; } to { opacity: 1; } }
16347
+ @media (prefers-reduced-motion: reduce) { .dim-botNameTooltip { animation: none; } }
16181
16348
  .dim-aliasEntry { display: inline-flex; flex: none; }
16182
- .dim-aliasEdit { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; cursor: pointer; }
16183
- .dim-aliasEdit:hover:not(:disabled) { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
16349
+ .dim-aliasEdit { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; color: var(--dsw-alias-label-tertiary, #8f959e); background: transparent; cursor: pointer; }
16350
+ .dim-aliasEdit svg { opacity: .55; transition: opacity .15s ease; }
16351
+ .dim-aliasName:hover .dim-aliasEdit:not(:disabled) svg, .dim-aliasEdit:focus-visible svg { opacity: 1; }
16352
+ .dim-aliasEdit:hover:not(:disabled), .dim-aliasEdit:focus-visible { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
16184
16353
  .dim-aliasDialog { box-sizing: border-box; width: min(380px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; padding: 22px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 12px 36px rgb(0 0 0 / 18%); font: 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
16185
16354
  .dim-aliasDialog * { box-sizing: border-box; }
16186
16355
  .dim-aliasDialog::backdrop { background: rgb(15 17 21 / 30%); }
@@ -16576,9 +16745,9 @@ var CSS13 = String.raw`
16576
16745
  /* Header tooltips may extend beyond the card; the collapsible body clips its own content. */
16577
16746
  .dim-panel .dim-botCard { position: relative; min-width: 0; width: 100%; max-width: 100%; overflow: visible; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
16578
16747
  .dim-panel .dim-botCard::before { display: none; }
16579
- .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px; }
16748
+ .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px 8px; }
16580
16749
  .dim-collapsibleAccount { min-width: 0; display: flex; flex-direction: column; }
16581
- .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; -webkit-user-select: none; }
16750
+ .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; -webkit-user-select: none; }
16582
16751
  .dim-collapsibleHead:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 8px; }
16583
16752
  .dim-collapsibleHeaderContent { min-width: 0; flex: 1 1 auto; display: flex; align-items: center; }
16584
16753
  .dim-collapsibleChevron { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 9px; height: 9px; border-right: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); border-bottom: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); transform: rotate(-45deg); transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
@@ -16588,14 +16757,17 @@ var CSS13 = String.raw`
16588
16757
  .dim-collapsibleAccount.is-open > .dim-collapsibleBody { grid-template-rows: 1fr; }
16589
16758
  .dim-collapsibleBodyInner { min-height: 0; overflow: hidden; }
16590
16759
  .dim-collapsibleAccount:not(.is-open) .dim-collapsibleBodyInner { visibility: hidden; }
16591
- .dim-panel .dim-botCardTop { min-width: 0; max-width: 100%; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
16592
- .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 10px; }
16760
+ /* Reclaim horizontal spacing for names while keeping status on the same row,
16761
+ including when a channel's mobile stylesheet requests a column layout. */
16762
+ .dim-panel .dim-botCardTop { min-width: 0; width: 100%; max-width: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: flex-start; justify-content: space-between; gap: 6px; }
16763
+ .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 6px; }
16593
16764
  .dim-panel .dim-botAvatar { flex: none; width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden; border-radius: 11px; box-shadow: none; }
16594
16765
  .dim-panel .dim-botAvatar svg { width: 27px; height: 27px; }
16595
- .dim-panel .dim-botName { min-width: 0; }
16766
+ .dim-panel .dim-botName { min-width: 0; flex: 1; }
16767
+ .dim-panel .dim-aliasName { gap: 2px; }
16596
16768
  .dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
16597
16769
  .dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
16598
- .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 8px; }
16770
+ .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 4px; }
16599
16771
  .dim-panel .dim-botHealthGroup { min-width: 0; max-width: 100%; flex: none; display: grid; justify-items: end; gap: 5px; }
16600
16772
  .dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
16601
16773
  .dim-panel .dim-lastChecked { display: inline-flex; align-items: baseline; gap: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font: inherit; font-size: 11px; font-weight: 400; line-height: normal; white-space: nowrap; }
@@ -17978,6 +18150,12 @@ function apply(ctx) {
17978
18150
  );
17979
18151
  const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
17980
18152
  setImTranslator(t);
18153
+ ctx.effect(
18154
+ () => installInterfaceLanguageMirror(ctx, {
18155
+ rpcCall: (endpoint, payload, signal) => callManagementRpc(ctx.connection, HOST_LANGUAGE_RPC_CHANNEL, endpoint, payload, signal)
18156
+ }),
18157
+ "im-settings: mirror the DSH interface language"
18158
+ );
17981
18159
  ctx.effect(() => installSessionChannelLogos(), "im-settings: Session channel logos");
17982
18160
  ctx.effect(() => {
17983
18161
  const disposers = [