dsh-zen-remote 2.3.5 → 2.3.7
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/AGENTS.md +15 -5
- package/CHANGELOG.md +71 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +12 -6
- package/docs/plugin-bridge.md +2 -2
- package/docs/remote-access.en.md +9 -3
- package/docs/remote-access.md +9 -3
- package/lan-gate.mjs +7 -0
- package/lib/adapters/dsh-better-sidebar/index.js +31 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/client-pairing.js +10 -0
- package/lib/client-pairing.js.map +1 -1
- package/lib/client-routes.js +183 -94
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +589 -97
- package/lib/debug-redact.js +253 -0
- package/lib/debug-redact.js.map +1 -0
- package/lib/debug-report.js +60 -0
- package/lib/debug-report.js.map +1 -0
- package/lib/fetch-route-intercept.js +23 -0
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +306 -45
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -1
- package/lib/intercept.js.map +1 -1
- package/lib/log-ring.js +76 -0
- package/lib/log-ring.js.map +1 -0
- package/lib/plugin-adapter-core.js +30 -1
- package/lib/plugin-adapter-core.js.map +1 -1
- package/lib/plugin-route-intercept.js +506 -11
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/plugin-tunnel.js +210 -18
- package/lib/plugin-tunnel.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/effects/remote-drop.d.ts +20 -8
- package/lib/types/client/effects/remote-drop.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +10 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/styles/content.css.d.ts +1 -1
- package/lib/types/client/styles/content.css.d.ts.map +1 -1
- package/lib/types/client/styles/layout.css.d.ts +1 -1
- package/lib/types/client/styles/layout.css.d.ts.map +1 -1
- package/lib/types/client/styles/misc.css.d.ts +1 -1
- package/lib/types/client/styles/misc.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-drop.d.ts +64 -0
- package/lib/types/client-data/remote-drop.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +3 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-pairing.d.ts +8 -0
- package/lib/types/client-pairing.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +23 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/debug-redact.d.ts +68 -0
- package/lib/types/debug-redact.d.ts.map +1 -0
- package/lib/types/debug-report.d.ts +58 -0
- package/lib/types/debug-report.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +26 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +6 -0
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/log-ring.d.ts +57 -0
- package/lib/types/log-ring.d.ts.map +1 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +8 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/plugin-tunnel.d.ts +37 -2
- package/lib/types/plugin-tunnel.d.ts.map +1 -1
- package/package.json +3 -2
- package/pwa/app.css +13 -2
- package/scripts/check-debug-report.mjs +277 -0
- package/scripts/check-info-sheet-stacking.mjs +85 -0
- package/scripts/check-remote-drop.mjs +130 -0
- package/scripts/check-tablet-safe-area.mjs +49 -0
- package/src/adapters/dsh-better-sidebar/index.ts +31 -2
- package/src/client/MobileSessionInfo.tsx +29 -2
- package/src/client/effects/remote-drop.ts +271 -24
- package/src/client/locales.ts +24 -4
- package/src/client/settings/SettingsSection.tsx +120 -3
- package/src/client/styles/content.css.ts +44 -14
- package/src/client/styles/layout.css.ts +9 -3
- package/src/client/styles/misc.css.ts +38 -0
- package/src/client-data/remote-drop.ts +103 -0
- package/src/client-data/settings-form.ts +3 -0
- package/src/client-pairing.ts +11 -0
- package/src/client-routes.ts +196 -90
- package/src/debug-redact.ts +290 -0
- package/src/debug-report.ts +93 -0
- package/src/fetch-route-intercept.ts +29 -0
- package/src/index.ts +295 -43
- package/src/intercept.ts +46 -0
- package/src/log-ring.ts +101 -0
- package/src/plugin-adapter-core.ts +29 -2
- package/src/plugin-route-intercept.ts +516 -12
- package/src/plugin-tunnel.ts +230 -20
package/lib/client.js
CHANGED
|
@@ -2067,7 +2067,7 @@ __modules["client-data/settings-form.js"] = function (require, module, exports)
|
|
|
2067
2067
|
* (devices / pairing) plus the T14 envelope (ok / gateway / config / viaGateway).
|
|
2068
2068
|
*/
|
|
2069
2069
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2070
|
-
exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.PAIRING_CODE_LENGTH = exports.PAIRING_CODE_ALPHABET = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PLUGIN_BRIDGE_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
|
|
2070
|
+
exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.PAIRING_CODE_LENGTH = exports.PAIRING_CODE_ALPHABET = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.CLIENT_DEBUG_REPORT_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PLUGIN_BRIDGE_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
|
|
2071
2071
|
exports.deriveClientStatusView = deriveClientStatusView;
|
|
2072
2072
|
exports.clientStatusLineOf = clientStatusLineOf;
|
|
2073
2073
|
exports.normalizePairingCode = normalizePairingCode;
|
|
@@ -2100,6 +2100,9 @@ exports.CLIENT_CLAIM_ROUTE = '/_dsh/zen-remote/client/claim';
|
|
|
2100
2100
|
exports.CLIENT_STATUS_ROUTE = '/_dsh/zen-remote/client/status';
|
|
2101
2101
|
/** T43: one immediate reconnect — answered 409 unless the client is offline. */
|
|
2102
2102
|
exports.CLIENT_RECONNECT_ROUTE = '/_dsh/zen-remote/client/reconnect';
|
|
2103
|
+
/** CU-4: the redacted diagnostic report (GET, both roles, machine-local only
|
|
2104
|
+
* — a gateway-forwarded fetch is refused server-side). */
|
|
2105
|
+
exports.CLIENT_DEBUG_REPORT_ROUTE = '/_dsh/zen-remote/client/debug-report';
|
|
2103
2106
|
/** The lightweight client-facing config route (host half, both roles): the
|
|
2104
2107
|
* settings page's FALLBACK role probe (T17) — it registers wherever a
|
|
2105
2108
|
* webServer exists, and since T17 its body carries the effective `role`
|
|
@@ -3083,6 +3086,8 @@ exports.zh = {
|
|
|
3083
3086
|
'activityOpen': '查看子代理与后台任务',
|
|
3084
3087
|
'infoStatTurns': '轮次',
|
|
3085
3088
|
'infoStatSteps': '步骤',
|
|
3089
|
+
'infoStatTurnsSteps': '轮次 / 步数',
|
|
3090
|
+
'infoStatSpeed': '输出速度',
|
|
3086
3091
|
'infoStatTtft': '首字延迟',
|
|
3087
3092
|
'infoStatLlm': '模型耗时',
|
|
3088
3093
|
'infoStatTool': '工具耗时',
|
|
@@ -3301,15 +3306,23 @@ exports.zh = {
|
|
|
3301
3306
|
'settings.client.diagRecentFailures': '最近的远程调用失败',
|
|
3302
3307
|
'settings.client.diagCompatGroups': '指纹有差异的组:{groups}',
|
|
3303
3308
|
'settings.client.diagCompatCalls': '最近的不兼容调用',
|
|
3304
|
-
// --- RD-3
|
|
3305
|
-
'settings.client.diagRemoteDrop': '
|
|
3309
|
+
// --- RD-3/AU-3 附件入口守卫的诊断行(浏览器端计数,渲染时现读) ---
|
|
3310
|
+
'settings.client.diagRemoteDrop': '附件入口守卫',
|
|
3306
3311
|
'settings.client.diagRemoteDropOk': '已安装 · 拦截 {intercepted} · 重派发 {redispatched} · 文件夹 {blocked} · 放行 {passthrough} · 重包失败 {failed}',
|
|
3312
|
+
'settings.client.diagRemoteDropEntries': '分入口 · 拦截 拖拽 {dropI} / 回形针 {pickerI} / 粘贴 {pasteI} · 放行 拖拽 {dropP} / 回形针 {pickerP} / 粘贴 {pasteP}',
|
|
3307
3313
|
'settings.client.diagRemoteDropOff': '未安装({reason})',
|
|
3308
3314
|
// --- RD-6 上传改道(fetch-route 拦截)的诊断行:读 remote-status 的 serviceProbe ---
|
|
3309
3315
|
'settings.client.diagUploadRedirect': '上传改道',
|
|
3310
3316
|
'settings.client.diagUploadRedirectOk': '已安装 · 上传已接管',
|
|
3311
3317
|
'settings.client.diagUploadRedirectRefused': '拒装:{reason}',
|
|
3312
3318
|
'settings.client.diagUploadRedirectPending': '服务注入未触发 · connection {connection} · fileUploads {fileUploads}',
|
|
3319
|
+
// --- CU-4 诊断信息导出(设置页最底部,两种角色都有) ---
|
|
3320
|
+
'settings.debug.title': '诊断信息',
|
|
3321
|
+
'settings.debug.export': '导出诊断信息',
|
|
3322
|
+
'settings.debug.exporting': '正在生成…',
|
|
3323
|
+
'settings.debug.hint': '下载一份脱敏后的诊断文件(版本、配置形态、连接诊断、插件日志)。令牌与密钥、对话内容、附件和完整文件路径一律不包含,服务器地址只保留类型。建议从机与主服务器各导一份,可直接附到 GitHub issue。',
|
|
3324
|
+
'settings.debug.done': '诊断文件已下载',
|
|
3325
|
+
'settings.debug.fail': '导出失败:无法生成诊断报告',
|
|
3313
3326
|
// --- PB-5 插件桥:主服务端同意小节 + 子客户端诊断小节 ---
|
|
3314
3327
|
'settings.bridge.title': '插件桥',
|
|
3315
3328
|
'settings.bridge.empty': '暂无插件登记插件桥',
|
|
@@ -3596,6 +3609,8 @@ exports.en = {
|
|
|
3596
3609
|
'activityOpen': 'View subagents and background tasks',
|
|
3597
3610
|
'infoStatTurns': 'Turns',
|
|
3598
3611
|
'infoStatSteps': 'Steps',
|
|
3612
|
+
'infoStatTurnsSteps': 'Turns / Steps',
|
|
3613
|
+
'infoStatSpeed': 'Output speed',
|
|
3599
3614
|
'infoStatTtft': 'TTFT',
|
|
3600
3615
|
'infoStatLlm': 'LLM time',
|
|
3601
3616
|
'infoStatTool': 'Tool time',
|
|
@@ -3809,16 +3824,24 @@ exports.en = {
|
|
|
3809
3824
|
'settings.client.diagRecentFailures': 'Recent remote call failures',
|
|
3810
3825
|
'settings.client.diagCompatGroups': 'Groups with fingerprint differences: {groups}',
|
|
3811
3826
|
'settings.client.diagCompatCalls': 'Recent incompatible calls',
|
|
3812
|
-
// --- RD-3: the
|
|
3827
|
+
// --- RD-3/AU-3: the attachment-entry guard's diagnostics line (browser-side
|
|
3813
3828
|
// counters, read at render time) ---
|
|
3814
|
-
'settings.client.diagRemoteDrop': '
|
|
3829
|
+
'settings.client.diagRemoteDrop': 'Attachment entry guard',
|
|
3815
3830
|
'settings.client.diagRemoteDropOk': 'Installed · intercepted {intercepted} · redispatched {redispatched} · folders {blocked} · passthrough {passthrough} · rewrap failed {failed}',
|
|
3831
|
+
'settings.client.diagRemoteDropEntries': 'By entry · intercepted drop {dropI} / picker {pickerI} / paste {pasteI} · passthrough drop {dropP} / picker {pickerP} / paste {pasteP}',
|
|
3816
3832
|
'settings.client.diagRemoteDropOff': 'Not installed ({reason})',
|
|
3817
3833
|
// --- RD-6: the upload-redirect (fetch-route intercept) diagnostics row ---
|
|
3818
3834
|
'settings.client.diagUploadRedirect': 'Upload redirect',
|
|
3819
3835
|
'settings.client.diagUploadRedirectOk': 'Installed · upload wrapped',
|
|
3820
3836
|
'settings.client.diagUploadRedirectRefused': 'Refused: {reason}',
|
|
3821
3837
|
'settings.client.diagUploadRedirectPending': 'Service inject never fired · connection {connection} · fileUploads {fileUploads}',
|
|
3838
|
+
// --- CU-4: the diagnostics export (bottom of the settings page, both roles) ---
|
|
3839
|
+
'settings.debug.title': 'Diagnostics',
|
|
3840
|
+
'settings.debug.export': 'Export diagnostics',
|
|
3841
|
+
'settings.debug.exporting': 'Generating…',
|
|
3842
|
+
'settings.debug.hint': 'Download a redacted diagnostics file (versions, config shape, connection diagnostics, plugin log). Tokens and secrets, conversation content, attachments and full file paths are never included; server addresses keep only their type. Export one on the client and one on the server — both can be attached to a GitHub issue as-is.',
|
|
3843
|
+
'settings.debug.done': 'Diagnostics file downloaded',
|
|
3844
|
+
'settings.debug.fail': 'Export failed: the diagnostics report could not be generated',
|
|
3822
3845
|
// --- PB-5 plugin bridge: host consent section + sub-client diagnostics ---
|
|
3823
3846
|
'settings.bridge.title': 'Plugin bridge',
|
|
3824
3847
|
'settings.bridge.empty': 'No plugin has registered a bridge yet',
|
|
@@ -7100,6 +7123,16 @@ function formatDuration(ms) {
|
|
|
7100
7123
|
const whole = Math.round(s);
|
|
7101
7124
|
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
|
7102
7125
|
}
|
|
7126
|
+
/**
|
|
7127
|
+
* Decode-throughput figure: whole tokens from ten up, one decimal below.
|
|
7128
|
+
* Ported digit-for-digit from dsh-client-ui-chat lib/client.js
|
|
7129
|
+
* formatTokensPerSecond (DSH 0.2.x TurnUsagePanel; verified 2026-10-30),
|
|
7130
|
+
* which also owns the "{tps} tok/s" unit the cell appends.
|
|
7131
|
+
*/
|
|
7132
|
+
function formatTokensPerSecond(tps) {
|
|
7133
|
+
const clamped = Math.max(0, tps);
|
|
7134
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
7135
|
+
}
|
|
7103
7136
|
/** Sum of the three disjoint prompt-side billing buckets. */
|
|
7104
7137
|
function billedInputTokens(usage) {
|
|
7105
7138
|
return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
@@ -7426,8 +7459,23 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
|
|
|
7426
7459
|
});
|
|
7427
7460
|
};
|
|
7428
7461
|
const cells = [
|
|
7429
|
-
|
|
7430
|
-
|
|
7462
|
+
/* Turns and steps share one cell ("3 / 12") so the grid stays 2×3 with
|
|
7463
|
+
output speed taking the freed slot (UI-1, 2026-10-30). */
|
|
7464
|
+
{
|
|
7465
|
+
label: t('infoStatTurnsSteps'),
|
|
7466
|
+
value: stats === undefined ? NA : `${stats.turns} / ${stats.steps}`,
|
|
7467
|
+
sub: undefined,
|
|
7468
|
+
},
|
|
7469
|
+
/* Same guard and formula as the official TurnUsagePanel: decodeMs > 0
|
|
7470
|
+
is its "we have a throughput reading" test, so a session with no
|
|
7471
|
+
usage-reporting step shows the placeholder, never "0 tok/s". */
|
|
7472
|
+
{
|
|
7473
|
+
label: t('infoStatSpeed'),
|
|
7474
|
+
value: stats === undefined || stats.decodeMs <= 0
|
|
7475
|
+
? NA
|
|
7476
|
+
: `${formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1e3))} tok/s`,
|
|
7477
|
+
sub: undefined,
|
|
7478
|
+
},
|
|
7431
7479
|
{
|
|
7432
7480
|
label: t('infoStatTtft'),
|
|
7433
7481
|
value: stats === undefined || stats.ttftSteps === 0 ? NA : formatDuration(stats.ttftMs / stats.ttftSteps),
|
|
@@ -8162,12 +8210,18 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8162
8210
|
[data-phase] [class$="_actions"] {
|
|
8163
8211
|
overflow: hidden !important;
|
|
8164
8212
|
}
|
|
8213
|
+
/* The 24h clock ("18:06" / "MM-DD 18:06") is a fixed-size item: it must
|
|
8214
|
+
never be the thing that shrinks. DSH 0.2.x wraps it with the usage
|
|
8215
|
+
button in an inline-flex _endInfo (min-width: 0), so a shrinkable
|
|
8216
|
+
time (the old flex: 0 1 auto + min-width: 0) got squeezed inside that
|
|
8217
|
+
non-wrapping box and "18:06" broke into two lines. flex: none pins its
|
|
8218
|
+
width; the turn-tail rules in content.css.ts let _endInfo wrap as a
|
|
8219
|
+
whole instead. */
|
|
8165
8220
|
[data-phase] [class$="_actions"] [class$="_timeEnd"] {
|
|
8166
|
-
flex:
|
|
8167
|
-
|
|
8221
|
+
flex: none !important;
|
|
8222
|
+
white-space: nowrap !important;
|
|
8168
8223
|
overflow: hidden !important;
|
|
8169
8224
|
text-overflow: ellipsis !important;
|
|
8170
|
-
white-space: nowrap !important;
|
|
8171
8225
|
}
|
|
8172
8226
|
|
|
8173
8227
|
[data-phase] [class$="_scroll"]:has(p) {
|
|
@@ -9736,6 +9790,44 @@ exports.MISC_CSS = ` /* ---------- hero composer on mobile ----------
|
|
|
9736
9790
|
display: none !important;
|
|
9737
9791
|
}
|
|
9738
9792
|
}
|
|
9793
|
+
|
|
9794
|
+
/* ---------- tablet standalone PWA: pay the safe area above 1023px ---------- */
|
|
9795
|
+
/* The gateway serves viewport-fit=cover to every non-desktop device (see
|
|
9796
|
+
coverViewport in lan-gate-server), phone-chrome only manages the meta at
|
|
9797
|
+
<=1023px, and every compensation rule in layout.css lives behind the same
|
|
9798
|
+
1023px breakpoint and the data-mobile-nav frame marker MobileNavOverlay
|
|
9799
|
+
only sets there. A wide tablet's home-screen PWA — iPad landscape, the
|
|
9800
|
+
12.9"/13" iPad portrait at exactly 1024, Android tablets — therefore
|
|
9801
|
+
painted under the status bar with nothing paying the inset: the top row
|
|
9802
|
+
of controls sat in the system area and was hard to tap.
|
|
9803
|
+
|
|
9804
|
+
Anchor: the official AppFrame (ui-layout AppFrame.tsx) is the direct
|
|
9805
|
+
parent of the [data-shell-overlay] layer — the same element
|
|
9806
|
+
MobileNavOverlay's findFrame() resolves — and the frame already pads
|
|
9807
|
+
ITSELF for the Windows titlebar (padding-top: --dsh-windows-titlebar-height
|
|
9808
|
+
in AppFrame.module.css), so padding the frame is the host's own pattern
|
|
9809
|
+
for system bars.
|
|
9810
|
+
|
|
9811
|
+
Structural no-op everywhere else, by construction:
|
|
9812
|
+
- display-mode: standalone matches only an installed PWA; desktop
|
|
9813
|
+
browsers report browser.
|
|
9814
|
+
- The Electron app never receives this stylesheet at all (the
|
|
9815
|
+
isDesktopShell gate in client/index.tsx) and never gets cover.
|
|
9816
|
+
- env() insets are hard 0 in every desktop browser, so even a browser
|
|
9817
|
+
that somehow matched the media query computes 0px padding; on iPads
|
|
9818
|
+
the bottom inset is 0 too (no home indicator) — the bottom line only
|
|
9819
|
+
ever pays out on Android tablets with a gesture/nav bar.
|
|
9820
|
+
box-sizing is load-bearing (the S2.1 lesson, see the frame rule in
|
|
9821
|
+
layout.css): the official frame is height:100% content-box, and padding
|
|
9822
|
+
without border-box would grow it past the viewport instead of reserving
|
|
9823
|
+
the strip. Pinned by scripts/check-tablet-safe-area.mjs. */
|
|
9824
|
+
@media (min-width: 1024px) and (display-mode: standalone) {
|
|
9825
|
+
:has(> [data-shell-overlay]) {
|
|
9826
|
+
box-sizing: border-box !important;
|
|
9827
|
+
padding-top: var(--mnav-sat) !important;
|
|
9828
|
+
padding-bottom: var(--mnav-sab) !important;
|
|
9829
|
+
}
|
|
9830
|
+
}
|
|
9739
9831
|
`;
|
|
9740
9832
|
};
|
|
9741
9833
|
__modules["client/styles/home.css.js"] = function (require, module, exports) {
|
|
@@ -13412,15 +13504,25 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
|
|
|
13412
13504
|
max-width: none !important;
|
|
13413
13505
|
}
|
|
13414
13506
|
|
|
13415
|
-
/* Turn-tail metadata (2026-08-18
|
|
13416
|
-
|
|
13417
|
-
[data-time-hover-root] gate holds
|
|
13418
|
-
:focus-within) — a phone never hovers, so
|
|
13419
|
-
|
|
13420
|
-
plugin's own
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13507
|
+
/* Turn-tail metadata (2026-08-18; reshaped 2026-10-30 for DSH 0.2.x): the
|
|
13508
|
+
tail's 24h clock is desktop-hover-revealed (official
|
|
13509
|
+
[data-time-hover-root] gate holds it at opacity 0 until :hover /
|
|
13510
|
+
:focus-within) — a phone never hovers, so it was simply invisible; and
|
|
13511
|
+
the row is a fixed-height nowrap + overflow:hidden flex line (plus this
|
|
13512
|
+
plugin's own clamp in layout.css.ts), which clipped it.
|
|
13513
|
+
|
|
13514
|
+
DSH 0.2.x (MessageIconActions, dsh-client-ui-chat) groups the usage
|
|
13515
|
+
button (TurnUsagePanel) and the clock (_timeEnd) inside ONE inline-flex
|
|
13516
|
+
_endInfo (min-width: 0; align-items: center; gap: 8px; margin-left: 8px)
|
|
13517
|
+
that never wraps internally. The 0.1.x-era rule here gave _timeEnd
|
|
13518
|
+
flex-basis: 100% + white-space: normal — inside _endInfo that no longer
|
|
13519
|
+
reads as "own full-width line", it reads as "shrink to nothing and
|
|
13520
|
+
break", and with the transcript-wide overflow-wrap: anywhere above,
|
|
13521
|
+
"18:06" rendered as two lines. On phones now: always visible; the row
|
|
13522
|
+
may grow in height and wrap; _endInfo drops to the next line as one
|
|
13523
|
+
unit when icons + usage + clock do not fit; the clock itself never
|
|
13524
|
+
shrinks or breaks. !important + the extra [class$="_actions"] hop
|
|
13525
|
+
out-specifies both upstream and layout.css.ts. */
|
|
13424
13526
|
/* Pin the tail row's width chain before allowing wrap: with wrap enabled
|
|
13425
13527
|
the row's intrinsic (shrink-to-fit) width collapses to its widest
|
|
13426
13528
|
single icon, taking the whole tail down to ~16px. */
|
|
@@ -13428,9 +13530,26 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
|
|
|
13428
13530
|
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
13429
13531
|
width: 100%;
|
|
13430
13532
|
}
|
|
13533
|
+
/* height: auto releases the upstream fixed height: calc(28px + …);
|
|
13534
|
+
overflow: visible beats layout.css.ts's overflow: hidden (same
|
|
13535
|
+
specificity — this file is concatenated last, so it wins the tie). A
|
|
13536
|
+
wrapped second line must not be cut off vertically; min-height keeps
|
|
13537
|
+
the one-line case at the upstream height so nothing shifts when it
|
|
13538
|
+
fits. */
|
|
13431
13539
|
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
13432
13540
|
flex-wrap: wrap;
|
|
13433
13541
|
row-gap: 2px;
|
|
13542
|
+
height: auto !important;
|
|
13543
|
+
min-height: 28px;
|
|
13544
|
+
overflow: visible !important;
|
|
13545
|
+
}
|
|
13546
|
+
/* _endInfo moves as a whole: flex: none so the usage + clock group is
|
|
13547
|
+
never asked to shrink — when the row is too narrow the entire group
|
|
13548
|
+
wraps to the next line instead of compressing the clock inside it.
|
|
13549
|
+
max-width keeps an unusually long usage label inside the column. */
|
|
13550
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_endInfo"] {
|
|
13551
|
+
flex: none;
|
|
13552
|
+
max-width: 100%;
|
|
13434
13553
|
}
|
|
13435
13554
|
/* Wide companion to the rule below: mirrors the upstream hover-gate
|
|
13436
13555
|
selector shape directly, with no assumptions about the tail's DOM
|
|
@@ -13441,13 +13560,16 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
|
|
|
13441
13560
|
opacity: 1 !important;
|
|
13442
13561
|
transition: none !important;
|
|
13443
13562
|
}
|
|
13563
|
+
/* overflow-wrap / word-break: normal undo the [data-chat-flow]
|
|
13564
|
+
overflow-wrap: anywhere inherited from the top of this block — that is
|
|
13565
|
+
the rule that let "18:06" break between digits once its box was
|
|
13566
|
+
squeezed. */
|
|
13444
13567
|
[data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
|
|
13445
13568
|
opacity: 1 !important;
|
|
13446
|
-
flex
|
|
13447
|
-
white-space:
|
|
13448
|
-
overflow:
|
|
13449
|
-
|
|
13450
|
-
padding-left: 0 !important;
|
|
13569
|
+
flex: none !important;
|
|
13570
|
+
white-space: nowrap !important;
|
|
13571
|
+
overflow-wrap: normal !important;
|
|
13572
|
+
word-break: normal !important;
|
|
13451
13573
|
font-size: 12px;
|
|
13452
13574
|
line-height: 18px;
|
|
13453
13575
|
}
|
|
@@ -16158,6 +16280,69 @@ const EXPORT_STYLE = {
|
|
|
16158
16280
|
whiteSpace: 'nowrap',
|
|
16159
16281
|
};
|
|
16160
16282
|
};
|
|
16283
|
+
__modules["client-data/current-session.js"] = function (require, module, exports) {
|
|
16284
|
+
"use strict";
|
|
16285
|
+
/**
|
|
16286
|
+
* T62: the browser half's current-session parsing, as pure functions — the
|
|
16287
|
+
* readable core of the reporter (src/client/current-session-report.ts) and
|
|
16288
|
+
* the one place its rules are unit-tested without a DOM.
|
|
16289
|
+
*
|
|
16290
|
+
* The host DSH persists its selection (the session its UI has open) to
|
|
16291
|
+
* localStorage under `dsh.sessions.current` as a JSON object carrying
|
|
16292
|
+
* `sessionId` (dsh-client-ui-workspace's snapshot store, `replaceMain` /
|
|
16293
|
+
* `clearMain`). The reporter re-reads that key and POSTs it to the backend
|
|
16294
|
+
* whenever it CHANGES; the backend's intercept layer judges close frames
|
|
16295
|
+
* against it — a closed session the user has open keeps its page and its
|
|
16296
|
+
* 「远程已关闭」 banner, anything else hides. The vocabulary mirrors the
|
|
16297
|
+
* intercept side's `CurrentSessionRead`:
|
|
16298
|
+
* - `unavailable`: the key is absent or the value unreadable — NEVER
|
|
16299
|
+
* report; the backend keeps its conservative tombstone behavior;
|
|
16300
|
+
* - `none`: readable, no session open — report `{sessionId: null}`;
|
|
16301
|
+
* - `open`: a session id — report `{sessionId}`.
|
|
16302
|
+
*/
|
|
16303
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16304
|
+
exports.CURRENT_SESSION_STORAGE_KEY = void 0;
|
|
16305
|
+
exports.parseCurrentSessionStorage = parseCurrentSessionStorage;
|
|
16306
|
+
exports.currentSessionReportEquals = currentSessionReportEquals;
|
|
16307
|
+
/** Mirror of src/intercept.ts's key (the client build cannot import the
|
|
16308
|
+
* host file). The host's selection store persists under this exact name. */
|
|
16309
|
+
exports.CURRENT_SESSION_STORAGE_KEY = 'dsh.sessions.current';
|
|
16310
|
+
/** Parse one raw localStorage read. `null` / `undefined` (the key is absent
|
|
16311
|
+
* — the host store only starts persisting with its first write) and any
|
|
16312
|
+
* value that does not parse into an object with a usable shape are
|
|
16313
|
+
* `unavailable`: no signal, never a guess. An object WITHOUT a `sessionId`
|
|
16314
|
+
* (the host's `clearMain` writes `{}`) is a real, reportable "nothing is
|
|
16315
|
+
* open". */
|
|
16316
|
+
function parseCurrentSessionStorage(raw) {
|
|
16317
|
+
if (raw === null || raw === undefined)
|
|
16318
|
+
return { kind: 'unavailable' };
|
|
16319
|
+
let parsed;
|
|
16320
|
+
try {
|
|
16321
|
+
parsed = JSON.parse(raw);
|
|
16322
|
+
}
|
|
16323
|
+
catch {
|
|
16324
|
+
return { kind: 'unavailable' };
|
|
16325
|
+
}
|
|
16326
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
16327
|
+
return { kind: 'unavailable' };
|
|
16328
|
+
const sessionId = parsed.sessionId;
|
|
16329
|
+
if (typeof sessionId === 'string' && sessionId !== '')
|
|
16330
|
+
return { kind: 'open', sessionId };
|
|
16331
|
+
if (sessionId === undefined || sessionId === null || sessionId === '')
|
|
16332
|
+
return { kind: 'none' };
|
|
16333
|
+
return { kind: 'unavailable' };
|
|
16334
|
+
}
|
|
16335
|
+
/** Whether one report differs from another — the reporter posts only on a
|
|
16336
|
+
* change, so this compares content, never object identity: kind first, then
|
|
16337
|
+
* the session id. */
|
|
16338
|
+
function currentSessionReportEquals(a, b) {
|
|
16339
|
+
if (a.kind !== b.kind)
|
|
16340
|
+
return false;
|
|
16341
|
+
if (a.kind === 'open' && b.kind === 'open')
|
|
16342
|
+
return a.sessionId === b.sessionId;
|
|
16343
|
+
return true;
|
|
16344
|
+
}
|
|
16345
|
+
};
|
|
16161
16346
|
__modules["virtual-id.js"] = function (require, module, exports) {
|
|
16162
16347
|
"use strict";
|
|
16163
16348
|
/**
|
|
@@ -16270,6 +16455,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16270
16455
|
exports.isRemoteSessionId = isRemoteSessionId;
|
|
16271
16456
|
exports.decideRemoteDrop = decideRemoteDrop;
|
|
16272
16457
|
exports.rewrapFiles = rewrapFiles;
|
|
16458
|
+
exports.decideRemoteIntake = decideRemoteIntake;
|
|
16273
16459
|
const virtual_id_ts_1 = require("./virtual-id.js");
|
|
16274
16460
|
/** True when the id is one of this plugin's virtual (relay) session ids —
|
|
16275
16461
|
* the `zr~<serverId>~<id>` form. `undefined` (no readable current-session
|
|
@@ -16322,68 +16508,48 @@ function decideRemoteDrop(input) {
|
|
|
16322
16508
|
function rewrapFiles(files) {
|
|
16323
16509
|
return Array.from(files, (file) => new File([file], file.name, { type: file.type, lastModified: file.lastModified }));
|
|
16324
16510
|
}
|
|
16325
|
-
};
|
|
16326
|
-
__modules["client-data/current-session.js"] = function (require, module, exports) {
|
|
16327
|
-
"use strict";
|
|
16328
16511
|
/**
|
|
16329
|
-
*
|
|
16330
|
-
*
|
|
16331
|
-
*
|
|
16512
|
+
* The three gates, then the intervention rule. The gates are decideRemoteDrop's
|
|
16513
|
+
* verbatim (all must affirmatively pass — a missing signal reads as local and
|
|
16514
|
+
* never intervenes); the rule after them:
|
|
16515
|
+
* 1. `items === undefined` — no inventory, pass through (`no-entry-api`;
|
|
16516
|
+
* deciding blind could eat a working flow);
|
|
16517
|
+
* 2. any directory — `block-directories`, ahead of every redirect: a folder
|
|
16518
|
+
* cannot be fixed by single-file upload, the whole intake is stopped with
|
|
16519
|
+
* a toast (same semantics as the drop branch);
|
|
16520
|
+
* 3. no items at all — passthrough (`no-files`; the plain-text paste shape);
|
|
16521
|
+
* 4. otherwise — `redirect` when ANY item is a `drop` item (the frozen
|
|
16522
|
+
* "any file intervenes" semantics) or carries a path while not being an
|
|
16523
|
+
* image (the zero-copy bad branch — exactly what issue #49 broke on);
|
|
16524
|
+
* `no-path-or-image-only` passthrough when none does.
|
|
16332
16525
|
*
|
|
16333
|
-
*
|
|
16334
|
-
*
|
|
16335
|
-
*
|
|
16336
|
-
* `
|
|
16337
|
-
*
|
|
16338
|
-
*
|
|
16339
|
-
*
|
|
16340
|
-
*
|
|
16341
|
-
*
|
|
16342
|
-
|
|
16343
|
-
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
if (raw === null || raw === undefined)
|
|
16361
|
-
return { kind: 'unavailable' };
|
|
16362
|
-
let parsed;
|
|
16363
|
-
try {
|
|
16364
|
-
parsed = JSON.parse(raw);
|
|
16365
|
-
}
|
|
16366
|
-
catch {
|
|
16367
|
-
return { kind: 'unavailable' };
|
|
16368
|
-
}
|
|
16369
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
16370
|
-
return { kind: 'unavailable' };
|
|
16371
|
-
const sessionId = parsed.sessionId;
|
|
16372
|
-
if (typeof sessionId === 'string' && sessionId !== '')
|
|
16373
|
-
return { kind: 'open', sessionId };
|
|
16374
|
-
if (sessionId === undefined || sessionId === null || sessionId === '')
|
|
16375
|
-
return { kind: 'none' };
|
|
16376
|
-
return { kind: 'unavailable' };
|
|
16377
|
-
}
|
|
16378
|
-
/** Whether one report differs from another — the reporter posts only on a
|
|
16379
|
-
* change, so this compares content, never object identity: kind first, then
|
|
16380
|
-
* the session id. */
|
|
16381
|
-
function currentSessionReportEquals(a, b) {
|
|
16382
|
-
if (a.kind !== b.kind)
|
|
16383
|
-
return false;
|
|
16384
|
-
if (a.kind === 'open' && b.kind === 'open')
|
|
16385
|
-
return a.sessionId === b.sessionId;
|
|
16386
|
-
return true;
|
|
16526
|
+
* Drop-entry parity pin: a `drop` item's intervention semantics here are
|
|
16527
|
+
* kept EQUIVALENT to `decideRemoteDrop` — same gates, directory rule and
|
|
16528
|
+
* "any file intervenes" — with exactly ONE deliberate divergence: when
|
|
16529
|
+
* `items === undefined`, `decideRemoteDrop` redispatches (a drop always has
|
|
16530
|
+
* a Files payload worth re-shaping) while this function passes through
|
|
16531
|
+
* (`no-entry-api` — an intake caller that cannot even enumerate its items
|
|
16532
|
+
* must not be intercepted blind). If either function's gate sequence or
|
|
16533
|
+
* drop-entry rule changes, update the other in the same commit; the truth
|
|
16534
|
+
* tables in scripts/check-remote-drop.mjs pin both sides.
|
|
16535
|
+
*/
|
|
16536
|
+
function decideRemoteIntake(input) {
|
|
16537
|
+
if (!input.trusted)
|
|
16538
|
+
return { action: 'passthrough', reason: 'untrusted-event' };
|
|
16539
|
+
if (!input.hasHostPaths)
|
|
16540
|
+
return { action: 'passthrough', reason: 'no-host-paths' };
|
|
16541
|
+
if (!isRemoteSessionId(input.currentSessionId))
|
|
16542
|
+
return { action: 'passthrough', reason: 'not-remote-session' };
|
|
16543
|
+
if (input.items === undefined)
|
|
16544
|
+
return { action: 'passthrough', reason: 'no-entry-api' };
|
|
16545
|
+
if (input.items.some((item) => item.isDirectory))
|
|
16546
|
+
return { action: 'block-directories', reason: 'contains-directory' };
|
|
16547
|
+
if (input.items.length === 0)
|
|
16548
|
+
return { action: 'passthrough', reason: 'no-files' };
|
|
16549
|
+
const intervenes = input.items.some((item) => item.entry === 'drop' || (item.hasPath && !item.isImage));
|
|
16550
|
+
return intervenes
|
|
16551
|
+
? { action: 'redirect', reason: 'remote-path-files' }
|
|
16552
|
+
: { action: 'passthrough', reason: 'no-path-or-image-only' };
|
|
16387
16553
|
}
|
|
16388
16554
|
};
|
|
16389
16555
|
__modules["client/effects/remote-drop.js"] = function (require, module, exports) {
|
|
@@ -16398,18 +16564,60 @@ const locales_ts_1 = require("./client/locales.js");
|
|
|
16398
16564
|
* Re-mounting (a plugin row reload) resets it. */
|
|
16399
16565
|
let diagnostics = freshDiagnostics();
|
|
16400
16566
|
function freshDiagnostics() {
|
|
16401
|
-
return {
|
|
16567
|
+
return {
|
|
16568
|
+
installed: false,
|
|
16569
|
+
notInstalledReason: undefined,
|
|
16570
|
+
intercepted: 0,
|
|
16571
|
+
redispatched: 0,
|
|
16572
|
+
blockedDirectories: 0,
|
|
16573
|
+
passthrough: 0,
|
|
16574
|
+
rewrapFailed: 0,
|
|
16575
|
+
interceptedBy: { drop: 0, picker: 0, paste: 0 },
|
|
16576
|
+
redirectedBy: { drop: 0, picker: 0, paste: 0 },
|
|
16577
|
+
blockedBy: { drop: 0, picker: 0, paste: 0 },
|
|
16578
|
+
passthroughBy: { drop: 0, picker: 0, paste: 0 },
|
|
16579
|
+
};
|
|
16402
16580
|
}
|
|
16403
16581
|
/** A snapshot for the diagnostics carriers (settings page). Copying keeps
|
|
16404
|
-
* callers from holding a live reference into a later mount's state
|
|
16582
|
+
* callers from holding a live reference into a later mount's state — the
|
|
16583
|
+
* per-entry objects are copied too, or a caller would keep counting. */
|
|
16405
16584
|
function remoteDropDiagnostics() {
|
|
16406
|
-
return {
|
|
16585
|
+
return {
|
|
16586
|
+
...diagnostics,
|
|
16587
|
+
interceptedBy: { ...diagnostics.interceptedBy },
|
|
16588
|
+
redirectedBy: { ...diagnostics.redirectedBy },
|
|
16589
|
+
blockedBy: { ...diagnostics.blockedBy },
|
|
16590
|
+
passthroughBy: { ...diagnostics.passthroughBy },
|
|
16591
|
+
};
|
|
16592
|
+
}
|
|
16593
|
+
function bump(counts, entry) {
|
|
16594
|
+
counts[entry] += 1;
|
|
16407
16595
|
}
|
|
16408
16596
|
/** The contextBridge global's existence only — never touched otherwise
|
|
16409
16597
|
* (it is deep-frozen and must stay unwrapped). */
|
|
16410
16598
|
function hostPathsOf() {
|
|
16411
16599
|
return globalThis.__DSH_HOST_PATHS__;
|
|
16412
16600
|
}
|
|
16601
|
+
/** The bridge's `pathFor` — the preload's `webUtils.getPathForFile`. A
|
|
16602
|
+
* non-empty string means the File still carries its disk path, the exact
|
|
16603
|
+
* fact the host's zero-copy branch keys on. Anything else (bridge shape,
|
|
16604
|
+
* non-string answer, a throw) reads as "no path" — the conservative
|
|
16605
|
+
* direction: the file is treated as already byte-uploading and stays
|
|
16606
|
+
* untouched. */
|
|
16607
|
+
function bridgePathFor(bridge, file) {
|
|
16608
|
+
if (bridge === null || typeof bridge !== 'object')
|
|
16609
|
+
return '';
|
|
16610
|
+
const pathFor = bridge.pathFor;
|
|
16611
|
+
if (typeof pathFor !== 'function')
|
|
16612
|
+
return '';
|
|
16613
|
+
try {
|
|
16614
|
+
const result = pathFor.call(bridge, file);
|
|
16615
|
+
return typeof result === 'string' ? result : '';
|
|
16616
|
+
}
|
|
16617
|
+
catch {
|
|
16618
|
+
return '';
|
|
16619
|
+
}
|
|
16620
|
+
}
|
|
16413
16621
|
/** The current-session signal, read AT EVENT TIME (the selection changes
|
|
16414
16622
|
* while the page is open; a mount-time snapshot would go stale). Any
|
|
16415
16623
|
* storage failure reads as "no signal" = local session = passthrough. */
|
|
@@ -16468,6 +16676,28 @@ function dismissHostDropOverlay() {
|
|
|
16468
16676
|
// No window / Event in this host: the overlay is the only casualty.
|
|
16469
16677
|
}
|
|
16470
16678
|
}
|
|
16679
|
+
/** Build the redispatch payload WITHOUT touching any event: re-wrap the files
|
|
16680
|
+
* (path-stripped) and pack them into a fresh DataTransfer. `undefined` means
|
|
16681
|
+
* the file list was empty (nothing to dispatch — the caller passes through);
|
|
16682
|
+
* a throw means a re-wrap or an items.add failed (0-byte / exotic File) and
|
|
16683
|
+
* the caller releases the original event untouched (宁坏勿丢). Shared by all
|
|
16684
|
+
* three entries — the payload shape is the redispatch contract, and the
|
|
16685
|
+
* drop entry's behavior is frozen by its existing tests. */
|
|
16686
|
+
function buildDropPayload(files) {
|
|
16687
|
+
const rewrapped = (0, remote_drop_ts_1.rewrapFiles)(files);
|
|
16688
|
+
if (rewrapped.length === 0)
|
|
16689
|
+
return undefined;
|
|
16690
|
+
const payload = new DataTransfer();
|
|
16691
|
+
for (const file of rewrapped)
|
|
16692
|
+
payload.items.add(file);
|
|
16693
|
+
return payload;
|
|
16694
|
+
}
|
|
16695
|
+
/** Dispatch the host-shaped synthetic drop on document, bubble phase — the
|
|
16696
|
+
* host's own drop handler (mask reset, busy rejection, addFiles) digests it;
|
|
16697
|
+
* our capture listener skips it via the isTrusted gate. */
|
|
16698
|
+
function dispatchSyntheticDrop(payload) {
|
|
16699
|
+
document.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: payload }));
|
|
16700
|
+
}
|
|
16471
16701
|
function mountRemoteDropGuard(ctx) {
|
|
16472
16702
|
ctx.effect(() => {
|
|
16473
16703
|
// Fresh state per mount: a plugin reload re-runs apply() and must not
|
|
@@ -16544,6 +16774,7 @@ function mountRemoteDropGuard(ctx) {
|
|
|
16544
16774
|
});
|
|
16545
16775
|
if (decision.action === 'passthrough') {
|
|
16546
16776
|
diagnostics.passthrough += 1;
|
|
16777
|
+
bump(diagnostics.passthroughBy, 'drop');
|
|
16547
16778
|
return;
|
|
16548
16779
|
}
|
|
16549
16780
|
if (decision.action === 'block-directories') {
|
|
@@ -16553,7 +16784,9 @@ function mountRemoteDropGuard(ctx) {
|
|
|
16553
16784
|
event.stopPropagation();
|
|
16554
16785
|
dismissHostDropOverlay();
|
|
16555
16786
|
diagnostics.intercepted += 1;
|
|
16787
|
+
bump(diagnostics.interceptedBy, 'drop');
|
|
16556
16788
|
diagnostics.blockedDirectories += 1;
|
|
16789
|
+
bump(diagnostics.blockedBy, 'drop');
|
|
16557
16790
|
showToast();
|
|
16558
16791
|
return;
|
|
16559
16792
|
}
|
|
@@ -16562,23 +16795,23 @@ function mountRemoteDropGuard(ctx) {
|
|
|
16562
16795
|
// host still receives today's (path-chip) behavior instead of
|
|
16563
16796
|
// nothing at all.
|
|
16564
16797
|
try {
|
|
16565
|
-
const
|
|
16566
|
-
if (
|
|
16798
|
+
const payload = buildDropPayload(Array.from(transfer.files));
|
|
16799
|
+
if (payload === undefined) {
|
|
16567
16800
|
diagnostics.passthrough += 1;
|
|
16801
|
+
bump(diagnostics.passthroughBy, 'drop');
|
|
16568
16802
|
return;
|
|
16569
16803
|
}
|
|
16570
|
-
const payload = new DataTransfer();
|
|
16571
|
-
for (const file of rewrapped)
|
|
16572
|
-
payload.items.add(file);
|
|
16573
16804
|
event.preventDefault();
|
|
16574
16805
|
event.stopPropagation();
|
|
16575
16806
|
diagnostics.intercepted += 1;
|
|
16807
|
+
bump(diagnostics.interceptedBy, 'drop');
|
|
16576
16808
|
// The host's drop handler sits on document, bubble phase (F4): a
|
|
16577
16809
|
// bubbling synthetic drop on document reaches it. Its own handler
|
|
16578
16810
|
// does the preventDefault / mask reset / addFiles work; our capture
|
|
16579
16811
|
// listener skips this event via the isTrusted gate above.
|
|
16580
|
-
|
|
16812
|
+
dispatchSyntheticDrop(payload);
|
|
16581
16813
|
diagnostics.redispatched += 1;
|
|
16814
|
+
bump(diagnostics.redirectedBy, 'drop');
|
|
16582
16815
|
}
|
|
16583
16816
|
catch {
|
|
16584
16817
|
// A re-wrap or a single items.add threw (0-byte / exotic File):
|
|
@@ -16587,8 +16820,173 @@ function mountRemoteDropGuard(ctx) {
|
|
|
16587
16820
|
}
|
|
16588
16821
|
};
|
|
16589
16822
|
document.addEventListener('drop', onDrop, true);
|
|
16823
|
+
// --- the picker entry (AU-2): the composer's hidden file selector -------
|
|
16824
|
+
// F1: the host composer's picker is an `input[type=file][multiple][hidden]`
|
|
16825
|
+
// with NO accept attribute, and its React onChange feeds intakeFiles →
|
|
16826
|
+
// addFiles — the same zero-copy branch the drop entry guards. Judged by
|
|
16827
|
+
// STRUCTURE, never by class names (hash classes cannot be anchored): an
|
|
16828
|
+
// input with an accept attribute (import / restore pickers), a visible
|
|
16829
|
+
// one, or a `webkitdirectory` folder picker (a third-party composition
|
|
16830
|
+
// could combine all four) is somebody else's and passes through. The
|
|
16831
|
+
// host composer has none of those, so the hit is unchanged.
|
|
16832
|
+
const onChange = (event) => {
|
|
16833
|
+
if (!event.isTrusted)
|
|
16834
|
+
return;
|
|
16835
|
+
const input = event.target;
|
|
16836
|
+
if (!(input instanceof HTMLInputElement))
|
|
16837
|
+
return;
|
|
16838
|
+
if (input.type !== 'file' || !input.multiple || !input.hidden || input.hasAttribute('accept') || input.webkitdirectory)
|
|
16839
|
+
return;
|
|
16840
|
+
const files = Array.from(input.files ?? []);
|
|
16841
|
+
if (files.length === 0)
|
|
16842
|
+
return;
|
|
16843
|
+
// The remote gate first, BEFORE counting: a local session's everyday
|
|
16844
|
+
// picker use must not show up in the diagnostics at all (the counters
|
|
16845
|
+
// are a record of what the guard saw in remote sessions).
|
|
16846
|
+
if (!(0, remote_drop_ts_1.isRemoteSessionId)(readCurrentSessionId()))
|
|
16847
|
+
return;
|
|
16848
|
+
const bridge = hostPathsOf();
|
|
16849
|
+
const items = files.map((file) => ({
|
|
16850
|
+
entry: 'picker',
|
|
16851
|
+
// A picker yields plain files; directories are structurally
|
|
16852
|
+
// impossible, so the block-directories branch is unreachable here
|
|
16853
|
+
// (defensive passthrough below).
|
|
16854
|
+
isDirectory: false,
|
|
16855
|
+
hasPath: bridgePathFor(bridge, file) !== '',
|
|
16856
|
+
isImage: file.type.startsWith('image/'),
|
|
16857
|
+
}));
|
|
16858
|
+
const decision = (0, remote_drop_ts_1.decideRemoteIntake)({
|
|
16859
|
+
hasHostPaths: bridge !== undefined,
|
|
16860
|
+
currentSessionId: readCurrentSessionId(),
|
|
16861
|
+
trusted: event.isTrusted,
|
|
16862
|
+
items,
|
|
16863
|
+
});
|
|
16864
|
+
if (decision.action !== 'redirect') {
|
|
16865
|
+
// passthrough, or the defensive block branch that cannot happen for
|
|
16866
|
+
// a picker — either way zero intervention.
|
|
16867
|
+
diagnostics.passthrough += 1;
|
|
16868
|
+
bump(diagnostics.passthroughBy, 'picker');
|
|
16869
|
+
return;
|
|
16870
|
+
}
|
|
16871
|
+
// Build the payload BEFORE stopping the event (宁坏勿丢): a re-wrap
|
|
16872
|
+
// failure releases the change untouched — the host keeps today's
|
|
16873
|
+
// (path-chip) behavior instead of losing the pick entirely.
|
|
16874
|
+
try {
|
|
16875
|
+
const payload = buildDropPayload(files);
|
|
16876
|
+
if (payload === undefined) {
|
|
16877
|
+
diagnostics.passthrough += 1;
|
|
16878
|
+
bump(diagnostics.passthroughBy, 'picker');
|
|
16879
|
+
return;
|
|
16880
|
+
}
|
|
16881
|
+
// stopPropagation keeps the event from reaching React's delegated
|
|
16882
|
+
// onChange at the root container (F5); clearing the value lets the
|
|
16883
|
+
// SAME file trigger a later change again (the host's own onPickFiles
|
|
16884
|
+
// habit, applied here since the host's handler no longer runs).
|
|
16885
|
+
event.stopPropagation();
|
|
16886
|
+
input.value = '';
|
|
16887
|
+
diagnostics.intercepted += 1;
|
|
16888
|
+
bump(diagnostics.interceptedBy, 'picker');
|
|
16889
|
+
dispatchSyntheticDrop(payload);
|
|
16890
|
+
diagnostics.redispatched += 1;
|
|
16891
|
+
bump(diagnostics.redirectedBy, 'picker');
|
|
16892
|
+
}
|
|
16893
|
+
catch {
|
|
16894
|
+
diagnostics.rewrapFailed += 1;
|
|
16895
|
+
}
|
|
16896
|
+
};
|
|
16897
|
+
document.addEventListener('change', onChange, true);
|
|
16898
|
+
// --- the paste entry (AU-2): the editor's clipboard files ---------------
|
|
16899
|
+
// F2: the composer editor (a contenteditable root) reads file items off
|
|
16900
|
+
// clipboardData; a path-carrying non-image lands on the same zero-copy
|
|
16901
|
+
// branch. Pasting into any other target (settings fields, search) is not
|
|
16902
|
+
// ours. clipboardData is read-only, so a redirect can only re-dispatch
|
|
16903
|
+
// — preventDefault keeps the editor from inserting anything.
|
|
16904
|
+
const onPaste = (event) => {
|
|
16905
|
+
if (!event.isTrusted)
|
|
16906
|
+
return;
|
|
16907
|
+
const transfer = event.clipboardData;
|
|
16908
|
+
if (transfer === null)
|
|
16909
|
+
return;
|
|
16910
|
+
const firstElement = event.composedPath().find((node) => node instanceof Element);
|
|
16911
|
+
if (firstElement === undefined || firstElement.closest('[contenteditable="true"]') === null)
|
|
16912
|
+
return;
|
|
16913
|
+
// The remote gate BEFORE any per-item work (same rationale as picker):
|
|
16914
|
+
// a local session's paste — including pasted FILES — returns here, so
|
|
16915
|
+
// it never runs bridgePathFor per file and never reaches the counters.
|
|
16916
|
+
// Everything after this point still runs inside this one synchronous
|
|
16917
|
+
// handler, within the dispatch lifetime clipboardData requires.
|
|
16918
|
+
if (!(0, remote_drop_ts_1.isRemoteSessionId)(readCurrentSessionId()))
|
|
16919
|
+
return;
|
|
16920
|
+
const bridge = hostPathsOf();
|
|
16921
|
+
const files = [];
|
|
16922
|
+
const items = [];
|
|
16923
|
+
for (const item of Array.from(transfer.items)) {
|
|
16924
|
+
if (item.kind !== 'file')
|
|
16925
|
+
continue;
|
|
16926
|
+
const file = item.getAsFile();
|
|
16927
|
+
if (file === null)
|
|
16928
|
+
continue;
|
|
16929
|
+
// Directory detection with the host's own default: no entry API
|
|
16930
|
+
// reads as "not a directory". Items without a File behind them never
|
|
16931
|
+
// reach the descriptors, so a plain-text paste arrives as an empty
|
|
16932
|
+
// list — the pure no-files passthrough.
|
|
16933
|
+
const entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null;
|
|
16934
|
+
files.push(file);
|
|
16935
|
+
items.push({
|
|
16936
|
+
entry: 'paste',
|
|
16937
|
+
isDirectory: entry?.isDirectory === true,
|
|
16938
|
+
hasPath: bridgePathFor(bridge, file) !== '',
|
|
16939
|
+
isImage: file.type.startsWith('image/'),
|
|
16940
|
+
});
|
|
16941
|
+
}
|
|
16942
|
+
const decision = (0, remote_drop_ts_1.decideRemoteIntake)({
|
|
16943
|
+
hasHostPaths: bridge !== undefined,
|
|
16944
|
+
currentSessionId: readCurrentSessionId(),
|
|
16945
|
+
trusted: event.isTrusted,
|
|
16946
|
+
items,
|
|
16947
|
+
});
|
|
16948
|
+
if (decision.action === 'block-directories') {
|
|
16949
|
+
// A folder paste cannot be fixed by single-file upload: same toast
|
|
16950
|
+
// semantics as the drop branch (no overlay to dismiss — paste never
|
|
16951
|
+
// opened one).
|
|
16952
|
+
event.preventDefault();
|
|
16953
|
+
event.stopPropagation();
|
|
16954
|
+
diagnostics.intercepted += 1;
|
|
16955
|
+
bump(diagnostics.interceptedBy, 'paste');
|
|
16956
|
+
diagnostics.blockedDirectories += 1;
|
|
16957
|
+
bump(diagnostics.blockedBy, 'paste');
|
|
16958
|
+
showToast();
|
|
16959
|
+
return;
|
|
16960
|
+
}
|
|
16961
|
+
if (decision.action !== 'redirect') {
|
|
16962
|
+
diagnostics.passthrough += 1;
|
|
16963
|
+
bump(diagnostics.passthroughBy, 'paste');
|
|
16964
|
+
return;
|
|
16965
|
+
}
|
|
16966
|
+
try {
|
|
16967
|
+
const payload = buildDropPayload(files);
|
|
16968
|
+
if (payload === undefined) {
|
|
16969
|
+
diagnostics.passthrough += 1;
|
|
16970
|
+
bump(diagnostics.passthroughBy, 'paste');
|
|
16971
|
+
return;
|
|
16972
|
+
}
|
|
16973
|
+
event.preventDefault();
|
|
16974
|
+
event.stopPropagation();
|
|
16975
|
+
diagnostics.intercepted += 1;
|
|
16976
|
+
bump(diagnostics.interceptedBy, 'paste');
|
|
16977
|
+
dispatchSyntheticDrop(payload);
|
|
16978
|
+
diagnostics.redispatched += 1;
|
|
16979
|
+
bump(diagnostics.redirectedBy, 'paste');
|
|
16980
|
+
}
|
|
16981
|
+
catch {
|
|
16982
|
+
diagnostics.rewrapFailed += 1;
|
|
16983
|
+
}
|
|
16984
|
+
};
|
|
16985
|
+
document.addEventListener('paste', onPaste, true);
|
|
16590
16986
|
return () => {
|
|
16591
16987
|
document.removeEventListener('drop', onDrop, true);
|
|
16988
|
+
document.removeEventListener('change', onChange, true);
|
|
16989
|
+
document.removeEventListener('paste', onPaste, true);
|
|
16592
16990
|
removeToast();
|
|
16593
16991
|
// Keep the counters readable after a dispose (the settings page may
|
|
16594
16992
|
// still be open mid-reload); `installed` is the armed flag only.
|
|
@@ -16639,7 +17037,9 @@ const react_1 = require("react");
|
|
|
16639
17037
|
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
16640
17038
|
const settings_form_ts_1 = require("./client-data/settings-form.js");
|
|
16641
17039
|
const plugin_bridge_ts_1 = require("./client-data/plugin-bridge.js");
|
|
16642
|
-
const
|
|
17040
|
+
const current_session_ts_1 = require("./client-data/current-session.js");
|
|
17041
|
+
const remote_drop_ts_1 = require("./client-data/remote-drop.js");
|
|
17042
|
+
const remote_drop_ts_2 = require("./client/effects/remote-drop.js");
|
|
16643
17043
|
const shares_ts_1 = require("./client-data/shares.js");
|
|
16644
17044
|
/** Device roles the pairing picker offers, in display order. */
|
|
16645
17045
|
const PAIR_ROLES = ['web', 'desktop-client'];
|
|
@@ -16665,6 +17065,45 @@ async function postJson(url, payload) {
|
|
|
16665
17065
|
return { ok: false, sent: 0, failed: 0 };
|
|
16666
17066
|
}
|
|
16667
17067
|
}
|
|
17068
|
+
/** CU-4: the export's download name, `zen-remote-debug-<role>-<yyyymmdd-hhmm>.json`,
|
|
17069
|
+
* stamped in the browser's local time (the file is for the user's own
|
|
17070
|
+
* folder; the report's `_meta.generatedAt` carries the exact UTC instant). */
|
|
17071
|
+
function debugExportFileName(role, now) {
|
|
17072
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
17073
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
17074
|
+
return `zen-remote-debug-${role}-${stamp}.json`;
|
|
17075
|
+
}
|
|
17076
|
+
/** CU-4: one transient notice for the export's outcome — the same inline-
|
|
17077
|
+
* styled overlay habit the remote-drop guard's folder toast uses (styles
|
|
17078
|
+
* stay inline: nothing of this goes into pwa/app.css or the style sheets),
|
|
17079
|
+
* one at a time, gone after 3 s, never intercepting taps. */
|
|
17080
|
+
let debugToast;
|
|
17081
|
+
let debugToastTimer;
|
|
17082
|
+
function showDebugToast(text) {
|
|
17083
|
+
if (debugToastTimer !== undefined)
|
|
17084
|
+
window.clearTimeout(debugToastTimer);
|
|
17085
|
+
debugToast?.remove();
|
|
17086
|
+
const el = document.createElement('div');
|
|
17087
|
+
el.dataset.zenRemote = 'debug-export-toast';
|
|
17088
|
+
el.textContent = text;
|
|
17089
|
+
el.style.cssText = [
|
|
17090
|
+
'position:fixed', 'left:50%', 'transform:translateX(-50%)',
|
|
17091
|
+
'bottom:calc(env(safe-area-inset-bottom, 0px) + 32px)',
|
|
17092
|
+
'z-index:2147483000',
|
|
17093
|
+
'background:rgba(0,0,0,.82)', 'color:#fff',
|
|
17094
|
+
'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
|
|
17095
|
+
'padding:10px 16px', 'border-radius:10px', 'max-width:90vw', 'text-align:center',
|
|
17096
|
+
'pointer-events:none',
|
|
17097
|
+
].join(';');
|
|
17098
|
+
document.body.appendChild(el);
|
|
17099
|
+
debugToast = el;
|
|
17100
|
+
debugToastTimer = window.setTimeout(() => {
|
|
17101
|
+
el.remove();
|
|
17102
|
+
if (debugToast === el)
|
|
17103
|
+
debugToast = undefined;
|
|
17104
|
+
debugToastTimer = undefined;
|
|
17105
|
+
}, 3000);
|
|
17106
|
+
}
|
|
16668
17107
|
function SettingsSection(props) {
|
|
16669
17108
|
// The summary view is a one-liner only — it must not even fetch
|
|
16670
17109
|
// admin/status, so every hook (and the fetch) lives in the page component.
|
|
@@ -16738,6 +17177,8 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
16738
17177
|
// JS dialog methods, so renames edit inline).
|
|
16739
17178
|
const [renaming, setRenaming] = (0, react_1.useState)(null);
|
|
16740
17179
|
const [pushTest, setPushTest] = (0, react_1.useState)({ state: 'idle' });
|
|
17180
|
+
// CU-4: the debug export's in-flight marker.
|
|
17181
|
+
const [debugBusy, setDebugBusy] = (0, react_1.useState)(false);
|
|
16741
17182
|
// The client group's pairing form (T16). The address prefills from the row
|
|
16742
17183
|
// (once the shared form's document is served) until the user types into it;
|
|
16743
17184
|
// the code normalizes as typed. The registered name is the shared 设备名称
|
|
@@ -16835,7 +17276,7 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
16835
17276
|
// time — no snapshot state of its own: the initial mount, the 15s
|
|
16836
17277
|
// connected-status poll and the countdown ticker all re-render this page,
|
|
16837
17278
|
// and each render takes the values as they are right then.
|
|
16838
|
-
const dropDiag = (0,
|
|
17279
|
+
const dropDiag = (0, remote_drop_ts_2.remoteDropDiagnostics)();
|
|
16839
17280
|
const clientRetryAt = clientView?.nextRetryAt;
|
|
16840
17281
|
const clientCountingDown = clientRetryAt !== undefined;
|
|
16841
17282
|
// Offline with a pending retry: refresh the status shortly after the
|
|
@@ -17089,6 +17530,50 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17089
17530
|
const result = await postJson(settings_form_ts_1.ADMIN_PUSH_TEST_ROUTE, {});
|
|
17090
17531
|
setPushTest(result.ok ? { state: 'ok', sent: result.sent, failed: result.failed } : { state: 'fail' });
|
|
17091
17532
|
};
|
|
17533
|
+
// CU-4 导出诊断信息: the backend's redacted report (both roles answer
|
|
17534
|
+
// their own), merged with the two facts only THIS page can read — the
|
|
17535
|
+
// attachment-entry guard's browser-side counters and whether the session
|
|
17536
|
+
// open right now is a remote one — then downloaded as one JSON file. The
|
|
17537
|
+
// browser plate carries counters and a boolean, nothing else: no session
|
|
17538
|
+
// id, no storage dump.
|
|
17539
|
+
const runDebugExport = async () => {
|
|
17540
|
+
setDebugBusy(true);
|
|
17541
|
+
try {
|
|
17542
|
+
const res = await fetch(settings_form_ts_1.CLIENT_DEBUG_REPORT_ROUTE);
|
|
17543
|
+
if (!res.ok)
|
|
17544
|
+
throw new StatusError(res.status);
|
|
17545
|
+
const report = await res.json();
|
|
17546
|
+
let remote = false;
|
|
17547
|
+
try {
|
|
17548
|
+
const current = (0, current_session_ts_1.parseCurrentSessionStorage)(window.localStorage.getItem(current_session_ts_1.CURRENT_SESSION_STORAGE_KEY));
|
|
17549
|
+
remote = current.kind === 'open' && (0, remote_drop_ts_1.isRemoteSessionId)(current.sessionId);
|
|
17550
|
+
}
|
|
17551
|
+
catch { /* storage unreadable: report not-remote */ }
|
|
17552
|
+
const merged = {
|
|
17553
|
+
...report,
|
|
17554
|
+
browser: {
|
|
17555
|
+
remoteDrop: (0, remote_drop_ts_2.remoteDropDiagnostics)(),
|
|
17556
|
+
currentSession: { remote },
|
|
17557
|
+
},
|
|
17558
|
+
};
|
|
17559
|
+
const blob = new Blob([`${JSON.stringify(merged, null, 2)}\n`], { type: 'application/json' });
|
|
17560
|
+
const url = URL.createObjectURL(blob);
|
|
17561
|
+
const anchor = document.createElement('a');
|
|
17562
|
+
anchor.href = url;
|
|
17563
|
+
anchor.download = debugExportFileName(clientRole ? 'client' : 'host', new Date());
|
|
17564
|
+
document.body.appendChild(anchor);
|
|
17565
|
+
anchor.click();
|
|
17566
|
+
anchor.remove();
|
|
17567
|
+
// Revoked on the next tick: some engines start the download only
|
|
17568
|
+
// after the click handler returns.
|
|
17569
|
+
window.setTimeout(() => { URL.revokeObjectURL(url); }, 0);
|
|
17570
|
+
showDebugToast(t('settings.debug.done'));
|
|
17571
|
+
}
|
|
17572
|
+
catch {
|
|
17573
|
+
showDebugToast(t('settings.debug.fail'));
|
|
17574
|
+
}
|
|
17575
|
+
setDebugBusy(false);
|
|
17576
|
+
};
|
|
17092
17577
|
// --- client group (T16) ----------------------------------------------------
|
|
17093
17578
|
// The address prefill reads the row's stored serverUrl while the user has
|
|
17094
17579
|
// not typed anything of their own.
|
|
@@ -17306,7 +17791,14 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17306
17791
|
passthrough: dropDiag.passthrough,
|
|
17307
17792
|
failed: dropDiag.rewrapFailed,
|
|
17308
17793
|
})
|
|
17309
|
-
: t('settings.client.diagRemoteDropOff', { reason: dropDiag.notInstalledReason ?? '—' }) })
|
|
17794
|
+
: t('settings.client.diagRemoteDropOff', { reason: dropDiag.notInstalledReason ?? '—' }) }), dropDiag.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.diagRemoteDropEntries', {
|
|
17795
|
+
dropI: dropDiag.interceptedBy.drop,
|
|
17796
|
+
pickerI: dropDiag.interceptedBy.picker,
|
|
17797
|
+
pasteI: dropDiag.interceptedBy.paste,
|
|
17798
|
+
dropP: dropDiag.passthroughBy.drop,
|
|
17799
|
+
pickerP: dropDiag.passthroughBy.picker,
|
|
17800
|
+
pasteP: dropDiag.passthroughBy.paste,
|
|
17801
|
+
}) }))] })), clientView?.uploadProbe !== undefined && (() => {
|
|
17310
17802
|
const probe = clientView.uploadProbe;
|
|
17311
17803
|
const seen = probe.uploadInjectSeen === true;
|
|
17312
17804
|
const refused = seen && probe.fetchRoute?.shape?.reasons instanceof Array && probe.fetchRoute.shape.reasons.length > 0;
|
|
@@ -17322,7 +17814,7 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17322
17814
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagUploadRedirect') }) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: text })] }));
|
|
17323
17815
|
})(), clientView.compat !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [clientView.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: clientView.compat.mismatchedGroups.join(', ') }) })), clientView.compat.recentCalls.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagCompatCalls') }) }), clientView.compat.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), (clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.bridge.title') }) }), clientView.pluginBridge.map((row) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [row.plugin, " \u00B7 ", row.localVersion !== '' ? `v${row.localVersion}` : '—', " \u00B7", ' ', row.stateKey === 'settings.bridge.stateVersionMismatch'
|
|
17324
17816
|
? t(row.stateKey, { version: row.serverVersion ?? '?' })
|
|
17325
|
-
: t(row.stateKey)] }, row.plugin)))] })), clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' }) }))] }))] }))] }));
|
|
17817
|
+
: t(row.stateKey)] }, row.plugin)))] })), clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' }) }))] }))] })), (0, jsx_runtime_1.jsx)("div", { className: "zr-settings-card", children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.debug.title') }), (0, jsx_runtime_1.jsx)("span", { style: { flex: 1 } }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: debugBusy, onClick: () => { void runDebugExport(); }, children: debugBusy ? t('settings.debug.exporting') : t('settings.debug.export') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.debug.hint') })] }) })] }));
|
|
17326
17818
|
}
|
|
17327
17819
|
/** Display name of a value's source layer for the invalid-saved-value note. */
|
|
17328
17820
|
function sourceName(source, t) {
|