@xiaoso/dsh-tool-plus 0.1.7 → 0.1.8-beta.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/lib/{abort-Caqff3M6.mjs → abort-BtBy5Svf.mjs} +1 -1
- package/lib/{alibaba-coding-plan-DTupMiAi.mjs → alibaba-coding-plan-oxHCHJtG.mjs} +2 -2
- package/lib/{alibaba-token-plan-Dj3OZhEE.mjs → alibaba-token-plan-B0WzvYnB.mjs} +2 -2
- package/lib/{amazon-bedrock-DbFSYXfJ.mjs → amazon-bedrock-BvfYGhrD.mjs} +6 -6
- package/lib/{anthropic-CXfWlp-x.mjs → anthropic-C6-vdduI.mjs} +1 -1
- package/lib/{anthropic-CTJ3IQVa.mjs → anthropic-Id561lwb.mjs} +13 -13
- package/lib/{anthropic-wire-CKuhDwZ1.mjs → anthropic-wire-Cb8AQ1tS.mjs} +13 -13
- package/lib/{api-key-validation-CGCJFbFr.mjs → api-key-validation-BXJKlsJg.mjs} +1 -1
- package/lib/{azure-openai-responses-ChFbWaGj.mjs → azure-openai-responses-CGws_ijz.mjs} +8 -8
- package/lib/{browser-render-9o0Rknop.mjs → browser-render-i7uYOl4f.mjs} +2 -2
- package/lib/client.js +2045 -19
- package/lib/{cloudflare-ai-gateway-BupNXyKK.mjs → cloudflare-ai-gateway-BK6eadm9.mjs} +2 -2
- package/lib/{cloudflare-ai-gateway-EWo0VkP5.mjs → cloudflare-ai-gateway-B_kWnF30.mjs} +1 -1
- package/lib/{collapse-DOR8sOGf.mjs → collapse-DU3g7d3B.mjs} +1 -1
- package/lib/{coreweave-Ct8cF-da.mjs → coreweave-CkR_FlQi.mjs} +2 -2
- package/lib/{cursor-LKGjucn2.mjs → cursor-BXx_5h4a.mjs} +2 -2
- package/lib/{cursor-DJV_dcgB.mjs → cursor-BcHwv8qq.mjs} +10 -10
- package/lib/{cursor-B95LzVWV.mjs → cursor-BjWbJJLe.mjs} +1 -1
- package/lib/{cursor-proto-B8Wj8bv9.mjs → cursor-proto-DPXXYx8S.mjs} +1 -1
- package/lib/{devin-DSp0Rw_E.mjs → devin-BZBfzyIz.mjs} +6 -6
- package/lib/{devin-BMTdx_hx.mjs → devin-Ce3Wz8_E.mjs} +5 -5
- package/lib/{devin-proto-Br8fdyNl.mjs → devin-proto-B6HwDwHK.mjs} +1 -1
- package/lib/{error-CORgGg5a.mjs → error-DdCn4m1q.mjs} +3 -3
- package/lib/{event-stream-ulcDenZD.mjs → event-stream-ldcUvOA0.mjs} +1 -1
- package/lib/{github-copilot-Dstq1D3s.mjs → github-copilot-BT5dkIJl.mjs} +2 -2
- package/lib/{github-copilot-headers-DOh1gt_w.mjs → github-copilot-headers-BGL8JO5c.mjs} +2 -2
- package/lib/{gitlab-duo-DSCMrU0m.mjs → gitlab-duo-B0qSYCa3.mjs} +1 -1
- package/lib/{google-CtsKkOQK.mjs → google-6cRgRmh0.mjs} +3 -3
- package/lib/{google-antigravity-z-5KsEci.mjs → google-antigravity-D1llOVad.mjs} +3 -3
- package/lib/{google-auth-ieLBaneO.mjs → google-auth-DYhqX1rJ.mjs} +5 -5
- package/lib/{google-gemini-cli-CAIPSl7e.mjs → google-gemini-cli-ByQUuKp9.mjs} +7 -7
- package/lib/{google-gemini-cli-C6NE5mWz.mjs → google-gemini-cli-CjHy6mFG.mjs} +3 -3
- package/lib/{google-oauth-shared-DhMllAkt.mjs → google-oauth-shared-CSwCT0Yl.mjs} +1 -1
- package/lib/{google-shared-B0aNCYEH.mjs → google-shared-DYjJp0mQ.mjs} +5 -5
- package/lib/{google-vertex-Bbd1tYNc.mjs → google-vertex-CbOvlE5j.mjs} +4 -4
- package/lib/{idle-iterator-DlUkEfzW.mjs → idle-iterator-DTBaEIbM.mjs} +2 -2
- package/lib/index.mjs +1278 -184
- package/lib/{kilo-ChJL54wq.mjs → kilo-cVG1aNZM.mjs} +1 -1
- package/lib/{kimi-C2c_Zq8A.mjs → kimi-BFfnFDhu.mjs} +4 -4
- package/lib/{launch-DKDH71En.mjs → launch-Uq7920ng.mjs} +7 -7
- package/lib/{markit-CmRfm4E2.mjs → markit-BmsfOFVh.mjs} +18 -18
- package/lib/{muse-code-Di1VZmB1.mjs → muse-code-CtcevJab.mjs} +1 -1
- package/lib/{ollama-B8ic3sEp.mjs → ollama-B5hR1P1p.mjs} +8 -8
- package/lib/{openai-codex-DuJNC99x.mjs → openai-codex-CxkPyvyt.mjs} +2 -2
- package/lib/{openai-codex-responses-BL29nh9K.mjs → openai-codex-responses-BbM3Q7nJ.mjs} +9 -9
- package/lib/{openai-completions-BK2f2dBv.mjs → openai-completions-CffXVOCL.mjs} +13 -13
- package/lib/{openai-reasoning-fallback-BdB3RCbj.mjs → openai-reasoning-fallback-DFdD1ok5.mjs} +2 -2
- package/lib/{openai-responses-CGQPlIIy.mjs → openai-responses-DgqOvSn_.mjs} +9 -9
- package/lib/{perplexity-TBjz3VMB.mjs → perplexity-JFp3knZN.mjs} +2 -2
- package/lib/{protobuf-aab1UGVn.mjs → protobuf-BJHNU9-E.mjs} +1 -1
- package/lib/{proxy-5nWqRL1f.mjs → proxy-B51IWEMt.mjs} +2 -2
- package/lib/{scrapers-CtQrQ5kG.mjs → scrapers-DU4u4-Co.mjs} +7 -7
- package/lib/{src-BwW51vA5.mjs → src-DUUqyo9y.mjs} +42 -42
- package/lib/{stream-B_7qN-bA.mjs → stream-DHdFrAG4.mjs} +54 -54
- package/lib/{stream-markup-healing-DKT1lnkW.mjs → stream-markup-healing-DlJ9abTw.mjs} +2 -2
- package/lib/{thinking-CiLa2LFB.mjs → thinking-D6LJxkz_.mjs} +1 -1
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/config/settings.d.ts +8 -0
- package/lib/types/tools/ast-grep/adapter/index.d.ts +23 -0
- package/lib/types/tools/edit/adapter/index.d.ts +3 -0
- package/lib/types/tools/glob/adapter/index.d.ts +2 -0
- package/lib/types/tools/grep/adapter/index.d.ts +4 -0
- package/lib/types/tools/read/adapter/index.d.ts +18 -0
- package/lib/types/tools/write/adapter/index.d.ts +2 -0
- package/lib/types/web/client/ToolCardShell.d.ts +111 -0
- package/lib/types/web/client/labels.d.ts +36 -0
- package/lib/types/web/client/registerToolCards.d.ts +35 -0
- package/lib/types/web/client/row-utils.d.ts +177 -0
- package/lib/types/web/client/rows/AstEditRow.d.ts +22 -0
- package/lib/types/web/client/rows/FileMutationRow.d.ts +23 -0
- package/lib/types/web/client/rows/ReadRow.d.ts +31 -0
- package/lib/types/web/client/rows/SearchRow.d.ts +21 -0
- package/lib/types/web/client/rows/TerminalRow.d.ts +24 -0
- package/lib/types/web/client/rows/WriteRow.d.ts +22 -0
- package/lib/types/web/client/rows/image-card.d.ts +60 -0
- package/lib/types/web/contract.d.ts +249 -0
- package/lib/types/web/host/ast-edit.d.ts +10 -0
- package/lib/types/web/host/bash.d.ts +30 -0
- package/lib/types/web/host/edit.d.ts +33 -0
- package/lib/types/web/host/read.d.ts +48 -0
- package/lib/types/web/host/search.d.ts +41 -0
- package/lib/types/web/host/write.d.ts +13 -0
- package/lib/{types-PVueGi9S.mjs → types-DT9vgZ3S.mjs} +1 -1
- package/lib/{utils-CUPhpw9A.mjs → utils-BMmwLB0f.mjs} +2 -2
- package/lib/{utils-ECAwvaPQ.mjs → utils-C-Lo_JC6.mjs} +41 -41
- package/lib/{utils-DeWqNWV8.mjs → utils-D31VFael.mjs} +1 -1
- package/lib/{vision-guard-_XwsL1DF.mjs → vision-guard-BlAAqGUC.mjs} +1 -1
- package/lib/{xai-oauth-CXhnz0gO.mjs → xai-oauth-QPyScm4X.mjs} +1 -1
- package/lib/{xiaomi-psoUItal.mjs → xiaomi-CLzjxHAi.mjs} +1 -1
- package/lib/{zai-ApJ8HBgT.mjs → zai-wDp0g7VC.mjs} +1 -1
- package/lib/{zip-Kd602XNe.mjs → zip-CNtIFfyV.mjs} +7 -7
- package/package.json +3 -1
- package/presets/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
let _deepseek_ai_dsh_client_connection_client = require("@deepseek-ai/dsh-client-connection/client");
|
|
11
|
+
let react_dom = require("react-dom");
|
|
11
12
|
//#region src/config/fields.ts
|
|
12
13
|
/** Field group ids -> locale key of the group heading. */
|
|
13
14
|
const TOOL_PLUS_GROUP_LABELS = {
|
|
@@ -94,6 +95,15 @@ window.__ModuleLoader__.load({
|
|
|
94
95
|
group: "behavior",
|
|
95
96
|
tool: "bash"
|
|
96
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: "webCards",
|
|
100
|
+
kind: "boolean",
|
|
101
|
+
default: true,
|
|
102
|
+
labelKey: "webCards",
|
|
103
|
+
hintKey: "webCardsHint",
|
|
104
|
+
group: "behavior",
|
|
105
|
+
tool: "bash"
|
|
106
|
+
},
|
|
97
107
|
{
|
|
98
108
|
name: "autoBackgroundMs",
|
|
99
109
|
kind: "number",
|
|
@@ -1136,7 +1146,7 @@ window.__ModuleLoader__.load({
|
|
|
1136
1146
|
return SCHEMA_DEFAULTS[field];
|
|
1137
1147
|
}
|
|
1138
1148
|
/** Card chrome CSS, keyed by `data-plugin-css` and injected once (design-token driven). */
|
|
1139
|
-
const CSS$
|
|
1149
|
+
const CSS$2 = `
|
|
1140
1150
|
.tp-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s;margin:0 0 8px}
|
|
1141
1151
|
.tp-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
1142
1152
|
.tp-card.tp-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
@@ -1175,16 +1185,16 @@ window.__ModuleLoader__.load({
|
|
|
1175
1185
|
}
|
|
1176
1186
|
`;
|
|
1177
1187
|
/** Inject the card stylesheet once per page; the loader removes plugin-owned style tags on unload. */
|
|
1178
|
-
let cssInjected$
|
|
1188
|
+
let cssInjected$2 = false;
|
|
1179
1189
|
function injectCardCss() {
|
|
1180
|
-
if (cssInjected$
|
|
1181
|
-
cssInjected$
|
|
1190
|
+
if (cssInjected$2 || typeof document === "undefined") return;
|
|
1191
|
+
cssInjected$2 = true;
|
|
1182
1192
|
const id = "tool-plus-card";
|
|
1183
1193
|
if (document.querySelector("style[data-plugin-css=\"tool-plus-card\"]") !== null) return;
|
|
1184
1194
|
const tag = document.createElement("style");
|
|
1185
1195
|
tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
|
|
1186
1196
|
tag.dataset.pluginCss = id;
|
|
1187
|
-
tag.textContent = CSS$
|
|
1197
|
+
tag.textContent = CSS$2;
|
|
1188
1198
|
document.head.appendChild(tag);
|
|
1189
1199
|
}
|
|
1190
1200
|
/** Text of a staged (or resolved) value for the number controls. */
|
|
@@ -1710,13 +1720,13 @@ window.__ModuleLoader__.load({
|
|
|
1710
1720
|
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
1711
1721
|
});
|
|
1712
1722
|
}
|
|
1713
|
-
function isRecord(value) {
|
|
1723
|
+
function isRecord$3(value) {
|
|
1714
1724
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1715
1725
|
}
|
|
1716
1726
|
function parseConnectionResponse(value) {
|
|
1717
|
-
if (!isRecord(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
|
|
1727
|
+
if (!isRecord$3(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
|
|
1718
1728
|
const result = value.result;
|
|
1719
|
-
if (!isRecord(result)) throw new TypeError("connection: invalid server-response result");
|
|
1729
|
+
if (!isRecord$3(result)) throw new TypeError("connection: invalid server-response result");
|
|
1720
1730
|
if (result.ok === true) return {
|
|
1721
1731
|
rpcId: value.rpcId,
|
|
1722
1732
|
result: {
|
|
@@ -1724,9 +1734,9 @@ window.__ModuleLoader__.load({
|
|
|
1724
1734
|
value: result.value
|
|
1725
1735
|
}
|
|
1726
1736
|
};
|
|
1727
|
-
if (result.ok !== false || !isRecord(result.error)) throw new TypeError("connection: invalid server-response result");
|
|
1737
|
+
if (result.ok !== false || !isRecord$3(result.error)) throw new TypeError("connection: invalid server-response result");
|
|
1728
1738
|
const error = result.error;
|
|
1729
|
-
if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord(error.details)) throw new TypeError("connection: invalid server-response failure");
|
|
1739
|
+
if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord$3(error.details)) throw new TypeError("connection: invalid server-response failure");
|
|
1730
1740
|
return {
|
|
1731
1741
|
rpcId: value.rpcId,
|
|
1732
1742
|
result: {
|
|
@@ -2131,7 +2141,7 @@ window.__ModuleLoader__.load({
|
|
|
2131
2141
|
* off under prefers-reduced-motion (CSS here, matchMedia for the
|
|
2132
2142
|
* JS-driven underline).
|
|
2133
2143
|
*/
|
|
2134
|
-
const CSS = `
|
|
2144
|
+
const CSS$1 = `
|
|
2135
2145
|
.tps-section{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;padding-top:2px}
|
|
2136
2146
|
.tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary);animation:tps-rise .38s cubic-bezier(.22,1,.36,1) both}
|
|
2137
2147
|
.tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);max-width:64ch;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .05s both}
|
|
@@ -2176,16 +2186,16 @@ window.__ModuleLoader__.load({
|
|
|
2176
2186
|
}
|
|
2177
2187
|
`;
|
|
2178
2188
|
/** Inject the section stylesheet once per page. */
|
|
2179
|
-
let cssInjected = false;
|
|
2189
|
+
let cssInjected$1 = false;
|
|
2180
2190
|
function injectSectionCss() {
|
|
2181
|
-
if (cssInjected || typeof document === "undefined") return;
|
|
2182
|
-
cssInjected = true;
|
|
2191
|
+
if (cssInjected$1 || typeof document === "undefined") return;
|
|
2192
|
+
cssInjected$1 = true;
|
|
2183
2193
|
const id = "tool-plus-section";
|
|
2184
2194
|
if (document.querySelector("style[data-plugin-css=\"tool-plus-section\"]") !== null) return;
|
|
2185
2195
|
const tag = document.createElement("style");
|
|
2186
2196
|
tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
|
|
2187
2197
|
tag.dataset.pluginCss = id;
|
|
2188
|
-
tag.textContent = CSS;
|
|
2198
|
+
tag.textContent = CSS$1;
|
|
2189
2199
|
document.head.appendChild(tag);
|
|
2190
2200
|
}
|
|
2191
2201
|
/** Tool tabs with their owned fields, derived from the single-source table. */
|
|
@@ -2621,7 +2631,7 @@ window.__ModuleLoader__.load({
|
|
|
2621
2631
|
//#endregion
|
|
2622
2632
|
//#region src/client/locales.ts
|
|
2623
2633
|
/** Simplified Chinese copy. */
|
|
2624
|
-
const zh = {
|
|
2634
|
+
const zh$1 = {
|
|
2625
2635
|
title: "Bash(bash-plus)",
|
|
2626
2636
|
description: "持久化 shell 工具:超时、后台化、命令拦截与运行环境。",
|
|
2627
2637
|
nav: "Tool Plus",
|
|
@@ -2774,6 +2784,8 @@ window.__ModuleLoader__.load({
|
|
|
2774
2784
|
rmSafeReasonRuntimeNotEffective: "运行时未生效",
|
|
2775
2785
|
useShellCommandWrapper: "用 bash -c 包裹命令",
|
|
2776
2786
|
useShellCommandWrapperHint: "把每条命令用 bash -c 包裹,以获得完整的 bash 环境。",
|
|
2787
|
+
webCards: "Web 工具卡片",
|
|
2788
|
+
webCardsHint: "在对话里用本插件的卡片展示 write / edit / read / grep / glob / ast_grep / ast_edit / bash 的调用结果;关闭后回到官方行。",
|
|
2777
2789
|
outputTruncateStrategy: "截断单位",
|
|
2778
2790
|
outputTruncateStrategyHint: "后台任务完成消息达到阈值时的截断单位:按字节或按行。切换后下方显示对应单位的配置。",
|
|
2779
2791
|
outputTruncateTriggerBytes: "字节触发阈值",
|
|
@@ -2811,7 +2823,7 @@ window.__ModuleLoader__.load({
|
|
|
2811
2823
|
appliesTo: "保存后对新调用生效。"
|
|
2812
2824
|
};
|
|
2813
2825
|
/** English copy. */
|
|
2814
|
-
const en = {
|
|
2826
|
+
const en$1 = {
|
|
2815
2827
|
title: "Bash (bash-plus)",
|
|
2816
2828
|
description: "Persistent shell tool: timeouts, backgrounding, interception, and environment.",
|
|
2817
2829
|
nav: "Tool Plus",
|
|
@@ -2964,6 +2976,8 @@ window.__ModuleLoader__.load({
|
|
|
2964
2976
|
rmSafeReasonRuntimeNotEffective: "not effective at runtime",
|
|
2965
2977
|
useShellCommandWrapper: "Wrap commands in bash -c",
|
|
2966
2978
|
useShellCommandWrapperHint: "Wrap every command in bash -c for a full bash environment.",
|
|
2979
|
+
webCards: "Web tool cards",
|
|
2980
|
+
webCardsHint: "Show this plugin's cards for write / edit / read / grep / glob / ast_grep / ast_edit / bash calls in chat; off falls back to the shipped rows.",
|
|
2967
2981
|
outputTruncateStrategy: "Truncate unit",
|
|
2968
2982
|
outputTruncateStrategyHint: "Unit used when a background completion message crosses its threshold: bytes or lines. Switching shows the matching unit's settings below.",
|
|
2969
2983
|
outputTruncateTriggerBytes: "Byte trigger threshold",
|
|
@@ -3001,6 +3015,2017 @@ window.__ModuleLoader__.load({
|
|
|
3001
3015
|
appliesTo: "Values apply to new calls."
|
|
3002
3016
|
};
|
|
3003
3017
|
//#endregion
|
|
3018
|
+
//#region src/web/contract.ts
|
|
3019
|
+
/**
|
|
3020
|
+
* Web tool-card contract: the payload this plugin's Host half projects into
|
|
3021
|
+
* `output.presentationMeta` and its browser half narrows back before drawing.
|
|
3022
|
+
*
|
|
3023
|
+
* Dependency-free by design (no React, no cordis, no Node built-ins): the Node
|
|
3024
|
+
* half imports it to build metadata, the browser half imports it to validate
|
|
3025
|
+
* it, and because it carries no React import it can never drag the browser
|
|
3026
|
+
* bundle into the Node build.
|
|
3027
|
+
*
|
|
3028
|
+
* `edit` / `read` / `search` mirror the exact shapes the shipped
|
|
3029
|
+
* `@deepseek-ai/dsh-client-ui-tool` card models accept — a `kind` tag rides on
|
|
3030
|
+
* top, and those models ignore unknown keys — so the same payload stays
|
|
3031
|
+
* readable if this plugin ever stops shadowing those rows. `write`,
|
|
3032
|
+
* `ast_edit` and `terminal` are this plugin's own shapes: the shipped rows have
|
|
3033
|
+
* no equivalent (their write card is a diff card; ours is a content preview,
|
|
3034
|
+
* matching the OMP card the model already sees).
|
|
3035
|
+
*
|
|
3036
|
+
* Everything here is defensive: a card never throws. The browser half turns any
|
|
3037
|
+
* malformed, oversized or foreign payload into `null`, and the row falls back
|
|
3038
|
+
* to the generic shell.
|
|
3039
|
+
*
|
|
3040
|
+
* @module @xiaoso/dsh-tool-plus/web/contract
|
|
3041
|
+
*/
|
|
3042
|
+
/**
|
|
3043
|
+
* Cell-shadowing rank of every card entry this plugin registers.
|
|
3044
|
+
*
|
|
3045
|
+
* The shipped `tool.call.toolview` entries all register at the default
|
|
3046
|
+
* priority 0, and the lowest live entry of a cell renders. A large negative
|
|
3047
|
+
* constant keeps this plugin first even if a future release starts ranking its
|
|
3048
|
+
* own rows below zero — the gap is deliberate, not a tuned value.
|
|
3049
|
+
*/
|
|
3050
|
+
const CARD_TAKEOVER_PRIORITY = -1e3;
|
|
3051
|
+
/**
|
|
3052
|
+
* Wire tool names whose `tool.call.toolview` cell this plugin renders.
|
|
3053
|
+
*
|
|
3054
|
+
* `ast_grep` / `ast_edit` are this plugin's own tool names (no shipped entry
|
|
3055
|
+
* claims those keys), so registering them is purely additive; the other six
|
|
3056
|
+
* shadow a shipped row.
|
|
3057
|
+
*/
|
|
3058
|
+
const CARD_TOOL_KEYS = [
|
|
3059
|
+
"bash",
|
|
3060
|
+
"read",
|
|
3061
|
+
"write",
|
|
3062
|
+
"edit",
|
|
3063
|
+
"grep",
|
|
3064
|
+
"glob",
|
|
3065
|
+
"ast_grep",
|
|
3066
|
+
"ast_edit"
|
|
3067
|
+
];
|
|
3068
|
+
/** UTF-8 byte length of `text`, without allocating an encoder. */
|
|
3069
|
+
function utf8ByteLength(text) {
|
|
3070
|
+
let bytes = 0;
|
|
3071
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
3072
|
+
const code = text.charCodeAt(index);
|
|
3073
|
+
if (code < 128) bytes += 1;
|
|
3074
|
+
else if (code < 2048) bytes += 2;
|
|
3075
|
+
else if (code >= 55296 && code <= 56319) {
|
|
3076
|
+
bytes += 4;
|
|
3077
|
+
index += 1;
|
|
3078
|
+
} else bytes += 3;
|
|
3079
|
+
}
|
|
3080
|
+
return bytes;
|
|
3081
|
+
}
|
|
3082
|
+
/** UTF-8 byte length of a value's JSON form; the unit every cap above uses. */
|
|
3083
|
+
function jsonByteLength(value) {
|
|
3084
|
+
const json = JSON.stringify(value);
|
|
3085
|
+
return json === void 0 ? 0 : utf8ByteLength(json);
|
|
3086
|
+
}
|
|
3087
|
+
/** Whether `value` is a plain object (never an array or `null`). */
|
|
3088
|
+
function isRecord$2(value) {
|
|
3089
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3090
|
+
}
|
|
3091
|
+
/** Non-blank string, or `null`. */
|
|
3092
|
+
function nonBlankString(value) {
|
|
3093
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
3094
|
+
}
|
|
3095
|
+
/** 1-based integer, or `null` (the shipped card models' own line rule). */
|
|
3096
|
+
function positiveInteger(value) {
|
|
3097
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null;
|
|
3098
|
+
}
|
|
3099
|
+
/** Non-negative integer, or `null`. */
|
|
3100
|
+
function nonNegativeInteger(value) {
|
|
3101
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
3102
|
+
}
|
|
3103
|
+
/** Optional grammar hint: a non-empty string stays, anything else drops. */
|
|
3104
|
+
function lang(value) {
|
|
3105
|
+
return typeof value === "string" && value !== "" ? { lang: value } : {};
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Narrow opaque metadata to a {@link WriteCardMeta}.
|
|
3109
|
+
* @param meta - the result node's `meta` field.
|
|
3110
|
+
* @returns the narrowed metadata, or `null` when it is not a usable write card.
|
|
3111
|
+
*/
|
|
3112
|
+
function narrowWriteCardMeta(meta) {
|
|
3113
|
+
if (!isRecord$2(meta) || meta.kind !== "write") return null;
|
|
3114
|
+
const path = nonBlankString(meta.path);
|
|
3115
|
+
if (path === null) return null;
|
|
3116
|
+
return {
|
|
3117
|
+
kind: "write",
|
|
3118
|
+
path,
|
|
3119
|
+
...lang(meta.lang),
|
|
3120
|
+
...meta.madeExecutable === true ? { madeExecutable: true } : {}
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Narrow opaque metadata to an {@link EditCardMeta}.
|
|
3125
|
+
*
|
|
3126
|
+
* Mirrors the shipped diff card's own validation (a hunk needs a non-blank
|
|
3127
|
+
* `path`, an explicit `null` or string `oldText`, and a string `newText`) and
|
|
3128
|
+
* additionally rejects a payload past {@link EDIT_META_MAX_BYTES}.
|
|
3129
|
+
* @param meta - the result node's `meta` field.
|
|
3130
|
+
* @returns the narrowed metadata, or `null` when it is not a usable diff card.
|
|
3131
|
+
*/
|
|
3132
|
+
function narrowEditCardMeta(meta) {
|
|
3133
|
+
if (!isRecord$2(meta) || meta.kind !== "edit") return null;
|
|
3134
|
+
if (jsonByteLength(meta) > 131072) return null;
|
|
3135
|
+
const raw = meta.diffs;
|
|
3136
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
3137
|
+
const diffs = [];
|
|
3138
|
+
for (const hunk of raw) {
|
|
3139
|
+
if (!isRecord$2(hunk)) return null;
|
|
3140
|
+
const path = nonBlankString(hunk.path);
|
|
3141
|
+
if (path === null) return null;
|
|
3142
|
+
const { oldText, newText } = hunk;
|
|
3143
|
+
if (oldText !== null && typeof oldText !== "string") return null;
|
|
3144
|
+
if (typeof newText !== "string") return null;
|
|
3145
|
+
diffs.push({
|
|
3146
|
+
path,
|
|
3147
|
+
oldText,
|
|
3148
|
+
newText
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
return {
|
|
3152
|
+
kind: "edit",
|
|
3153
|
+
diffs
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Narrow opaque metadata to an {@link AstEditCardMeta}.
|
|
3158
|
+
* @param meta - the result node's `meta` field.
|
|
3159
|
+
* @returns the narrowed metadata, or `null` when it is not a usable preview.
|
|
3160
|
+
*/
|
|
3161
|
+
function narrowAstEditCardMeta(meta) {
|
|
3162
|
+
if (!isRecord$2(meta) || meta.kind !== "ast_edit") return null;
|
|
3163
|
+
if (jsonByteLength(meta) > 65536) return null;
|
|
3164
|
+
if (typeof meta.preview !== "string") return null;
|
|
3165
|
+
const replacements = nonNegativeInteger(meta.replacements);
|
|
3166
|
+
if (replacements === null || typeof meta.applied !== "boolean") return null;
|
|
3167
|
+
if (!Array.isArray(meta.files)) return null;
|
|
3168
|
+
const files = [];
|
|
3169
|
+
for (const file of meta.files) {
|
|
3170
|
+
if (!isRecord$2(file)) return null;
|
|
3171
|
+
const path = nonBlankString(file.path);
|
|
3172
|
+
const count = nonNegativeInteger(file.count);
|
|
3173
|
+
if (path === null || count === null) return null;
|
|
3174
|
+
files.push({
|
|
3175
|
+
path,
|
|
3176
|
+
count
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
return {
|
|
3180
|
+
kind: "ast_edit",
|
|
3181
|
+
preview: meta.preview,
|
|
3182
|
+
files,
|
|
3183
|
+
replacements,
|
|
3184
|
+
applied: meta.applied
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Narrow opaque metadata to a {@link ReadCardMeta}.
|
|
3189
|
+
*
|
|
3190
|
+
* Mirrors the shipped read card's own validation: a 1-based `offset`, lines
|
|
3191
|
+
* strictly increasing in `number` and never past `totalLines`. A window whose
|
|
3192
|
+
* lines were all elided therefore narrows to a card with an empty body, not to
|
|
3193
|
+
* a malformed one.
|
|
3194
|
+
* @param meta - the result node's `meta` field.
|
|
3195
|
+
* @returns the narrowed metadata, or `null` when it is not a usable read card.
|
|
3196
|
+
*/
|
|
3197
|
+
function narrowReadCardMeta(meta) {
|
|
3198
|
+
if (!isRecord$2(meta) || meta.kind !== "read") return null;
|
|
3199
|
+
if (jsonByteLength(meta) > 262144) return null;
|
|
3200
|
+
const path = nonBlankString(meta.path);
|
|
3201
|
+
const offset = positiveInteger(meta.offset);
|
|
3202
|
+
const totalLines = nonNegativeInteger(meta.totalLines);
|
|
3203
|
+
if (path === null || offset === null || totalLines === null) return null;
|
|
3204
|
+
if (!Array.isArray(meta.lines)) return null;
|
|
3205
|
+
const lines = [];
|
|
3206
|
+
let previous = offset - 1;
|
|
3207
|
+
for (const line of meta.lines) {
|
|
3208
|
+
if (!isRecord$2(line)) return null;
|
|
3209
|
+
const number = positiveInteger(line.number);
|
|
3210
|
+
if (number === null || number <= previous || number > totalLines) return null;
|
|
3211
|
+
if (typeof line.text !== "string") return null;
|
|
3212
|
+
previous = number;
|
|
3213
|
+
lines.push({
|
|
3214
|
+
number,
|
|
3215
|
+
text: line.text
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return {
|
|
3219
|
+
kind: "read",
|
|
3220
|
+
path,
|
|
3221
|
+
offset,
|
|
3222
|
+
lines,
|
|
3223
|
+
totalLines,
|
|
3224
|
+
...lang(meta.lang)
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
/** Narrow one grouped-matches file entry, or `null` when malformed. */
|
|
3228
|
+
function narrowSearchFile(file) {
|
|
3229
|
+
if (!isRecord$2(file)) return null;
|
|
3230
|
+
const path = nonBlankString(file.path);
|
|
3231
|
+
if (path === null || !Array.isArray(file.matches)) return null;
|
|
3232
|
+
const matches = [];
|
|
3233
|
+
for (const match of file.matches) {
|
|
3234
|
+
if (!isRecord$2(match)) return null;
|
|
3235
|
+
const lineNumber = positiveInteger(match.lineNumber);
|
|
3236
|
+
if (lineNumber === null || typeof match.line !== "string") return null;
|
|
3237
|
+
matches.push({
|
|
3238
|
+
lineNumber,
|
|
3239
|
+
line: match.line
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
return {
|
|
3243
|
+
path,
|
|
3244
|
+
matches
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Narrow opaque metadata to a {@link SearchCardMeta}.
|
|
3249
|
+
* @param meta - the result node's `meta` field.
|
|
3250
|
+
* @returns the narrowed metadata, or `null` when it is not a usable search card.
|
|
3251
|
+
*/
|
|
3252
|
+
function narrowSearchCardMeta(meta) {
|
|
3253
|
+
if (!isRecord$2(meta) || meta.kind !== "search") return null;
|
|
3254
|
+
if (jsonByteLength(meta) > 65536) return null;
|
|
3255
|
+
if (typeof meta.truncated !== "boolean") return null;
|
|
3256
|
+
const total = nonNegativeInteger(meta.total);
|
|
3257
|
+
if (total === null) return null;
|
|
3258
|
+
if (meta.shape === "matches") {
|
|
3259
|
+
if (!Array.isArray(meta.files)) return null;
|
|
3260
|
+
const files = [];
|
|
3261
|
+
for (const file of meta.files) {
|
|
3262
|
+
const narrowed = narrowSearchFile(file);
|
|
3263
|
+
if (narrowed === null) return null;
|
|
3264
|
+
files.push(narrowed);
|
|
3265
|
+
}
|
|
3266
|
+
return {
|
|
3267
|
+
kind: "search",
|
|
3268
|
+
shape: "matches",
|
|
3269
|
+
files,
|
|
3270
|
+
truncated: meta.truncated,
|
|
3271
|
+
total
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
if (meta.shape !== "paths" || !Array.isArray(meta.paths)) return null;
|
|
3275
|
+
const paths = [];
|
|
3276
|
+
for (const path of meta.paths) {
|
|
3277
|
+
if (typeof path !== "string") return null;
|
|
3278
|
+
paths.push(path);
|
|
3279
|
+
}
|
|
3280
|
+
return {
|
|
3281
|
+
kind: "search",
|
|
3282
|
+
shape: "paths",
|
|
3283
|
+
paths,
|
|
3284
|
+
truncated: meta.truncated,
|
|
3285
|
+
total
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Narrow opaque metadata to a {@link TerminalCardMeta}.
|
|
3290
|
+
* @param meta - the result node's `meta` field.
|
|
3291
|
+
* @returns the narrowed metadata, or `null` when it is not a usable terminal card.
|
|
3292
|
+
*/
|
|
3293
|
+
function narrowTerminalCardMeta(meta) {
|
|
3294
|
+
if (!isRecord$2(meta) || meta.kind !== "terminal") return null;
|
|
3295
|
+
if (meta.mode === "background") {
|
|
3296
|
+
const jobId = nonBlankString(meta.jobId);
|
|
3297
|
+
return jobId === null ? null : {
|
|
3298
|
+
kind: "terminal",
|
|
3299
|
+
mode: "background",
|
|
3300
|
+
jobId
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
if (meta.mode !== "foreground") return null;
|
|
3304
|
+
const { exitCode, timedOut, aborted, workingDir } = meta;
|
|
3305
|
+
if (exitCode !== null && (typeof exitCode !== "number" || !Number.isInteger(exitCode))) return null;
|
|
3306
|
+
if (typeof timedOut !== "boolean" || typeof aborted !== "boolean") return null;
|
|
3307
|
+
if (workingDir !== void 0 && typeof workingDir !== "string") return null;
|
|
3308
|
+
return {
|
|
3309
|
+
kind: "terminal",
|
|
3310
|
+
mode: "foreground",
|
|
3311
|
+
exitCode,
|
|
3312
|
+
timedOut,
|
|
3313
|
+
aborted,
|
|
3314
|
+
...typeof workingDir === "string" && workingDir !== "" ? { workingDir } : {}
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
//#endregion
|
|
3318
|
+
//#region src/web/client/labels.ts
|
|
3319
|
+
/**
|
|
3320
|
+
* Card copy for the browser tool views: every string the six row components
|
|
3321
|
+
* and the shared shell draw, in its own locale namespace so it can never
|
|
3322
|
+
* collide with the settings card's `tool-plus` bundle (the slots registry
|
|
3323
|
+
* merges namespaces, and re-declaring one would be a compile error).
|
|
3324
|
+
*
|
|
3325
|
+
* zh is the source of truth for the key set; both dictionaries are
|
|
3326
|
+
* `Record<ToolCardLocaleKey, string>`, so a missing or extra key in either is
|
|
3327
|
+
* a compile error. Template parameters use the host's `{name}` placeholder
|
|
3328
|
+
* convention (the seat interpolates the params record into the string).
|
|
3329
|
+
*
|
|
3330
|
+
* The wording mirrors the shipped conversation namespace for the same
|
|
3331
|
+
* primitives (diff / read / search / terminal chrome) so a takeover does not
|
|
3332
|
+
* change how the cards read; tool titles and the generic-row copy are ours.
|
|
3333
|
+
* @module @xiaoso/dsh-tool-plus/web/client/labels
|
|
3334
|
+
*/
|
|
3335
|
+
/** Locale namespace of the tool-card bundle (registered by `registerToolCards`). */
|
|
3336
|
+
const CARD_LOCALE_NS = "tool-plus-cards";
|
|
3337
|
+
/** The bilingual bundle handed to `ctx.locale.register(CARD_LOCALE_NS, …)`. */
|
|
3338
|
+
const cardLocales = {
|
|
3339
|
+
zh: {
|
|
3340
|
+
copy: "复制",
|
|
3341
|
+
copied: "已复制",
|
|
3342
|
+
collapse: "收起",
|
|
3343
|
+
expand: "展开",
|
|
3344
|
+
collapseAria: "收起",
|
|
3345
|
+
expandAria: "展开其余 {count} 行",
|
|
3346
|
+
expandRest: "… 其余 {count} 行",
|
|
3347
|
+
"files.one": "{count} 个文件",
|
|
3348
|
+
"files.other": "{count} 个文件",
|
|
3349
|
+
running: "运行中",
|
|
3350
|
+
done: "完成",
|
|
3351
|
+
failed: "失败",
|
|
3352
|
+
cancelled: "已取消",
|
|
3353
|
+
timedOut: "已超时",
|
|
3354
|
+
noOutput: "无输出",
|
|
3355
|
+
"title.bash": "Bash",
|
|
3356
|
+
"title.read": "读取",
|
|
3357
|
+
"title.write": "写入",
|
|
3358
|
+
"title.edit": "编辑",
|
|
3359
|
+
"title.grep": "Grep",
|
|
3360
|
+
"title.glob": "Glob",
|
|
3361
|
+
"title.astGrep": "AST Grep",
|
|
3362
|
+
"title.astEdit": "AST Edit",
|
|
3363
|
+
"write.lines": "{count} 行",
|
|
3364
|
+
"write.madeExecutable": "已赋予可执行权限",
|
|
3365
|
+
"astEdit.replacements": "{count} 处替换",
|
|
3366
|
+
"astEdit.files": "{count} 个文件",
|
|
3367
|
+
"astEdit.parseErrors": "解析错误",
|
|
3368
|
+
"read.window": "显示 {shown} / {total} 行",
|
|
3369
|
+
"read.collapseAria": "收起内容",
|
|
3370
|
+
"read.expandAria": "展开其余 {count} 行",
|
|
3371
|
+
"read.expandRest": "… 其余 {count} 行",
|
|
3372
|
+
"search.paths": "{shown} 个路径",
|
|
3373
|
+
"search.paths.truncated": "显示 {shown} / 共 {total} 个路径",
|
|
3374
|
+
"search.matches": "{shown} 处匹配 · {files} 个文件",
|
|
3375
|
+
"search.matches.truncated": "显示 {shown} / 共 {total} 处匹配 · {files} 个文件",
|
|
3376
|
+
"search.noResults": "无结果",
|
|
3377
|
+
"search.collapseAria": "收起结果",
|
|
3378
|
+
"search.expandAria": "展开其余 {count} 行结果",
|
|
3379
|
+
"search.expandRest": "… 其余 {count} 行",
|
|
3380
|
+
"terminal.signal": "信号 {signal}",
|
|
3381
|
+
"terminal.exitCode": "退出码 {code}",
|
|
3382
|
+
"terminal.running": "运行中",
|
|
3383
|
+
"terminal.failed": "失败",
|
|
3384
|
+
"terminal.done": "已完成",
|
|
3385
|
+
"terminal.noOutput": "无输出",
|
|
3386
|
+
"bash.background": "后台任务 {jobId}",
|
|
3387
|
+
"image.loading": "加载中",
|
|
3388
|
+
"image.loadFailed": "加载失败",
|
|
3389
|
+
"image.open": "查看原图",
|
|
3390
|
+
"image.openNamed": "查看原图:{name}",
|
|
3391
|
+
"image.dialog": "原图预览",
|
|
3392
|
+
"image.close": "关闭",
|
|
3393
|
+
"image.unnamed": "图片",
|
|
3394
|
+
"image.caption": "{name} · {width}×{height} · {size}",
|
|
3395
|
+
"generic.params": "{count} 个参数",
|
|
3396
|
+
"generic.noDetail": "没有更多可展示的细节",
|
|
3397
|
+
inspect: "查看"
|
|
3398
|
+
},
|
|
3399
|
+
en: {
|
|
3400
|
+
copy: "Copy",
|
|
3401
|
+
copied: "Copied",
|
|
3402
|
+
collapse: "Collapse",
|
|
3403
|
+
expand: "Expand",
|
|
3404
|
+
collapseAria: "Collapse",
|
|
3405
|
+
expandAria: "Expand {count} more lines",
|
|
3406
|
+
expandRest: "… {count} more lines",
|
|
3407
|
+
"files.one": "{count} file",
|
|
3408
|
+
"files.other": "{count} files",
|
|
3409
|
+
running: "Running",
|
|
3410
|
+
done: "Done",
|
|
3411
|
+
failed: "Failed",
|
|
3412
|
+
cancelled: "Cancelled",
|
|
3413
|
+
timedOut: "Timed out",
|
|
3414
|
+
noOutput: "No output",
|
|
3415
|
+
"title.bash": "Bash",
|
|
3416
|
+
"title.read": "Read",
|
|
3417
|
+
"title.write": "Write",
|
|
3418
|
+
"title.edit": "Edit",
|
|
3419
|
+
"title.grep": "Grep",
|
|
3420
|
+
"title.glob": "Glob",
|
|
3421
|
+
"title.astGrep": "AST Grep",
|
|
3422
|
+
"title.astEdit": "AST Edit",
|
|
3423
|
+
"write.lines": "{count} lines",
|
|
3424
|
+
"write.madeExecutable": "Made executable",
|
|
3425
|
+
"astEdit.replacements": "{count} replacements",
|
|
3426
|
+
"astEdit.files": "{count} files",
|
|
3427
|
+
"astEdit.parseErrors": "Parse errors",
|
|
3428
|
+
"read.window": "Showing {shown} of {total} lines",
|
|
3429
|
+
"read.collapseAria": "Collapse content",
|
|
3430
|
+
"read.expandAria": "Expand {count} more lines",
|
|
3431
|
+
"read.expandRest": "… {count} more lines",
|
|
3432
|
+
"search.paths": "{shown} paths",
|
|
3433
|
+
"search.paths.truncated": "Showing {shown} of {total} paths",
|
|
3434
|
+
"search.matches": "{shown} matches · {files} files",
|
|
3435
|
+
"search.matches.truncated": "Showing {shown} of {total} matches · {files} files",
|
|
3436
|
+
"search.noResults": "No results",
|
|
3437
|
+
"search.collapseAria": "Collapse results",
|
|
3438
|
+
"search.expandAria": "Expand {count} more result lines",
|
|
3439
|
+
"search.expandRest": "… {count} more lines",
|
|
3440
|
+
"terminal.signal": "signal {signal}",
|
|
3441
|
+
"terminal.exitCode": "exit code {code}",
|
|
3442
|
+
"terminal.running": "Running",
|
|
3443
|
+
"terminal.failed": "Failed",
|
|
3444
|
+
"terminal.done": "Done",
|
|
3445
|
+
"terminal.noOutput": "No output",
|
|
3446
|
+
"bash.background": "Background job {jobId}",
|
|
3447
|
+
"image.loading": "Loading",
|
|
3448
|
+
"image.loadFailed": "Load failed",
|
|
3449
|
+
"image.open": "View original",
|
|
3450
|
+
"image.openNamed": "View original: {name}",
|
|
3451
|
+
"image.dialog": "Original image",
|
|
3452
|
+
"image.close": "Close",
|
|
3453
|
+
"image.unnamed": "Image",
|
|
3454
|
+
"image.caption": "{name} · {width}×{height} · {size}",
|
|
3455
|
+
"generic.params": "{count} params",
|
|
3456
|
+
"generic.noDetail": "No further detail",
|
|
3457
|
+
inspect: "Inspect"
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
//#endregion
|
|
3461
|
+
//#region src/web/client/row-utils.ts
|
|
3462
|
+
/** Run state word the shell exposes to assistive technology; `ok` says nothing. */
|
|
3463
|
+
const STATE_STATUS_KEY = {
|
|
3464
|
+
running: "running",
|
|
3465
|
+
ok: null,
|
|
3466
|
+
error: "failed",
|
|
3467
|
+
warning: "failed"
|
|
3468
|
+
};
|
|
3469
|
+
/** Parsed-argument cache: the node is immutable, so one parse serves every render. */
|
|
3470
|
+
const parsedArgs = /* @__PURE__ */ new WeakMap();
|
|
3471
|
+
/** Whether a block is the settled half of a call pair. */
|
|
3472
|
+
function isSettled(block) {
|
|
3473
|
+
return block.kind === "tool-result";
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* The call head paired with a block: a running call is its own head; a settled
|
|
3477
|
+
* result carries the head backfilled from the in-window `tool/call`, which is
|
|
3478
|
+
* `null` when window truncation left that event outside.
|
|
3479
|
+
*/
|
|
3480
|
+
function callHead(block) {
|
|
3481
|
+
if (!isSettled(block)) return block;
|
|
3482
|
+
const call = block.call;
|
|
3483
|
+
return typeof call === "object" && call !== null ? call : null;
|
|
3484
|
+
}
|
|
3485
|
+
/** Whether `value` is a plain object, the only argument shape the cards accept. */
|
|
3486
|
+
function isRecord$1(value) {
|
|
3487
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Parse the call's raw argument JSON.
|
|
3491
|
+
*
|
|
3492
|
+
* The parsed object is memoized per node (the node is immutable), so repeated
|
|
3493
|
+
* renders of one card pay for a single parse.
|
|
3494
|
+
* @param block - the frozen running-or-settled call node.
|
|
3495
|
+
* @returns the argument object, or `null` when the head is missing or its JSON
|
|
3496
|
+
* is not an object.
|
|
3497
|
+
*/
|
|
3498
|
+
function parseCardArgs(block) {
|
|
3499
|
+
const cacheable = typeof block === "object" && block !== null ? block : null;
|
|
3500
|
+
if (cacheable !== null) {
|
|
3501
|
+
const hit = parsedArgs.get(cacheable);
|
|
3502
|
+
if (hit !== void 0 || parsedArgs.has(cacheable)) return hit ?? null;
|
|
3503
|
+
}
|
|
3504
|
+
const raw = callHead(block)?.argsRaw;
|
|
3505
|
+
let parsed = null;
|
|
3506
|
+
if (typeof raw === "string") try {
|
|
3507
|
+
const value = JSON.parse(raw);
|
|
3508
|
+
if (isRecord$1(value)) parsed = value;
|
|
3509
|
+
} catch {
|
|
3510
|
+
parsed = null;
|
|
3511
|
+
}
|
|
3512
|
+
if (cacheable !== null) parsedArgs.set(cacheable, parsed);
|
|
3513
|
+
return parsed;
|
|
3514
|
+
}
|
|
3515
|
+
/**
|
|
3516
|
+
* Run state of one call.
|
|
3517
|
+
*
|
|
3518
|
+
* A settled `isError` result is an error; a foreground command that timed out
|
|
3519
|
+
* or was cancelled is amber rather than green (the shipped terminal model
|
|
3520
|
+
* reads only the trailing exit marker, which is exactly what the card owner
|
|
3521
|
+
* was told not to trust). A non-zero exit stays `ok`: bash reports a failing
|
|
3522
|
+
* command as result data, and the card's own status pill carries the red.
|
|
3523
|
+
* @param block - the frozen running-or-settled call node.
|
|
3524
|
+
* @returns the card run state.
|
|
3525
|
+
*/
|
|
3526
|
+
function cardState(block) {
|
|
3527
|
+
if (!isSettled(block)) return "running";
|
|
3528
|
+
if (block.isError === true) return "error";
|
|
3529
|
+
const terminal = narrowTerminalCardMeta(cardMeta(block));
|
|
3530
|
+
if (terminal !== null && terminal.mode === "foreground" && (terminal.timedOut || terminal.aborted)) return "warning";
|
|
3531
|
+
return "ok";
|
|
3532
|
+
}
|
|
3533
|
+
/** Map a card run state onto the primitive's dot semantics. */
|
|
3534
|
+
function dotState(state) {
|
|
3535
|
+
switch (state) {
|
|
3536
|
+
case "running": return "ongoing";
|
|
3537
|
+
case "error": return "error";
|
|
3538
|
+
case "warning": return "warning";
|
|
3539
|
+
default: return "done";
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* The assistive-technology status word for a run state, or `null` when the
|
|
3544
|
+
* state is already carried by the row's content.
|
|
3545
|
+
*/
|
|
3546
|
+
function stateStatus(state, t) {
|
|
3547
|
+
const key = STATE_STATUS_KEY[state];
|
|
3548
|
+
return key === null ? null : t(key);
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Flatten a settled result's text blocks.
|
|
3552
|
+
* @param block - the frozen call node.
|
|
3553
|
+
* @returns the joined text blocks (empty for a running call or non-text output).
|
|
3554
|
+
*/
|
|
3555
|
+
function resultText(block) {
|
|
3556
|
+
if (!isSettled(block) || !Array.isArray(block.content)) return "";
|
|
3557
|
+
const parts = [];
|
|
3558
|
+
for (const part of block.content) {
|
|
3559
|
+
if (!isRecord$1(part)) continue;
|
|
3560
|
+
if (part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
3561
|
+
}
|
|
3562
|
+
return parts.join("\n");
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* The opaque metadata a call's result carried, exactly as the transport
|
|
3566
|
+
* delivered it. Callers must run it through a `narrow*` guard before reading.
|
|
3567
|
+
* @param block - the frozen call node.
|
|
3568
|
+
* @returns the raw metadata, or `undefined` while the call is running.
|
|
3569
|
+
*/
|
|
3570
|
+
function cardMeta(block) {
|
|
3571
|
+
return isSettled(block) ? block.meta : void 0;
|
|
3572
|
+
}
|
|
3573
|
+
/**
|
|
3574
|
+
* Whether the call was dispatched by another tool (a Code Dispatch child).
|
|
3575
|
+
* The card rows fall back to the generic shell for those, mirroring the
|
|
3576
|
+
* shipped rows: a child diff/read/search inside a parent card would nest a
|
|
3577
|
+
* surface inside the parent's own body.
|
|
3578
|
+
*/
|
|
3579
|
+
function isSubCall(block) {
|
|
3580
|
+
return typeof block.parentCallId === "string";
|
|
3581
|
+
}
|
|
3582
|
+
/** Longest summary the shell renders before it is clipped for the row. */
|
|
3583
|
+
const FIRST_LINE_MAX = 200;
|
|
3584
|
+
/**
|
|
3585
|
+
* The first non-empty line of a text, trimmed — the collapsed summary of an
|
|
3586
|
+
* error, and the closing line of the generic shell's body.
|
|
3587
|
+
* @param text - the source text.
|
|
3588
|
+
* @returns the first line, clipped to a row-sized maximum (never throws).
|
|
3589
|
+
*/
|
|
3590
|
+
function firstLine(text) {
|
|
3591
|
+
if (typeof text !== "string") return "";
|
|
3592
|
+
const line = text.trim().split("\n", 1)[0]?.trim() ?? "";
|
|
3593
|
+
return line.length > FIRST_LINE_MAX ? `${line.slice(0, 199)}…` : line;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* A string argument by name.
|
|
3597
|
+
* @param args - parsed arguments, or `null`.
|
|
3598
|
+
* @param name - the argument key.
|
|
3599
|
+
* @returns its value when it is a non-empty string, else `null`.
|
|
3600
|
+
*/
|
|
3601
|
+
function argText(args, name) {
|
|
3602
|
+
const value = args?.[name];
|
|
3603
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Shorten an absolute path against the session workspace so a summary stays
|
|
3607
|
+
* readable in a narrow row.
|
|
3608
|
+
* @param path - the path to display.
|
|
3609
|
+
* @param cwd - the session workspace root, when known.
|
|
3610
|
+
* @returns the path relative to `cwd`, or `path` unchanged.
|
|
3611
|
+
*/
|
|
3612
|
+
function displayPath(path, cwd) {
|
|
3613
|
+
if (cwd === void 0 || cwd === "") return path;
|
|
3614
|
+
const root = cwd.endsWith("/") || cwd.endsWith("\\") ? cwd : `${cwd}/`;
|
|
3615
|
+
const normalized = path.replace(/\\/g, "/");
|
|
3616
|
+
const prefix = root.replace(/\\/g, "/");
|
|
3617
|
+
if (!normalized.startsWith(prefix)) return path;
|
|
3618
|
+
const rest = normalized.slice(prefix.length);
|
|
3619
|
+
return rest === "" ? path : rest;
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* The collapsed summary for a call whose card could not be built: how many
|
|
3623
|
+
* arguments the model sent, so the row is not blank.
|
|
3624
|
+
* @param args - parsed arguments, or `null`.
|
|
3625
|
+
* @param t - the card translate seat.
|
|
3626
|
+
* @returns the localized parameter count.
|
|
3627
|
+
*/
|
|
3628
|
+
function argsSummary(args, t) {
|
|
3629
|
+
const count = args === null ? 0 : Object.keys(args).length;
|
|
3630
|
+
return count === 0 ? t("generic.noDetail") : t("generic.params", { count });
|
|
3631
|
+
}
|
|
3632
|
+
/** Card title key of every wire tool name this plugin renders. */
|
|
3633
|
+
const TITLE_KEYS = {
|
|
3634
|
+
bash: "title.bash",
|
|
3635
|
+
read: "title.read",
|
|
3636
|
+
write: "title.write",
|
|
3637
|
+
edit: "title.edit",
|
|
3638
|
+
grep: "title.grep",
|
|
3639
|
+
glob: "title.glob",
|
|
3640
|
+
ast_grep: "title.astGrep",
|
|
3641
|
+
ast_edit: "title.astEdit"
|
|
3642
|
+
};
|
|
3643
|
+
/**
|
|
3644
|
+
* The card title for a wire tool name.
|
|
3645
|
+
*
|
|
3646
|
+
* The same row component can be registered under several keys (one search row
|
|
3647
|
+
* serves `grep`, `glob`, and `ast_grep`), and a window-truncated call has no
|
|
3648
|
+
* name at all, so the caller names the key it was registered for as a fallback.
|
|
3649
|
+
* @param toolName - the node's wire tool name (empty when truncated).
|
|
3650
|
+
* @param t - the card translate seat.
|
|
3651
|
+
* @param fallback - title key to use when the name is unknown.
|
|
3652
|
+
* @returns the localized title.
|
|
3653
|
+
*/
|
|
3654
|
+
function cardTitle(toolName, t, fallback) {
|
|
3655
|
+
return t((typeof toolName === "string" ? TITLE_KEYS[toolName] : void 0) ?? fallback);
|
|
3656
|
+
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Count a preview's content lines, ignoring the single trailing newline that
|
|
3659
|
+
* terminates the last line.
|
|
3660
|
+
* @param text - the preview text.
|
|
3661
|
+
* @returns its line count (0 for empty text).
|
|
3662
|
+
*/
|
|
3663
|
+
function lineCount(text) {
|
|
3664
|
+
if (typeof text !== "string" || text === "") return 0;
|
|
3665
|
+
return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n").length;
|
|
3666
|
+
}
|
|
3667
|
+
/** `[timed out after 30000ms]` — `@deepseek-ai/dsh-shell/render`'s own literal. */
|
|
3668
|
+
const TIMEOUT_MARKER = /^\[timed out after [^\]\n]*\]$/;
|
|
3669
|
+
/** `[killed by signal: SIGTERM]`. */
|
|
3670
|
+
const SIGNAL_MARKER = /^\[killed by signal: ([^\]\n]+)\]$/;
|
|
3671
|
+
/** `[exit code: 1]`, or `[exit code: null]` for an unknown status. */
|
|
3672
|
+
const EXIT_MARKER = /^\[exit code: (\d+|null)\]$/;
|
|
3673
|
+
/**
|
|
3674
|
+
* Split a settled shell result into its output and the status markers the
|
|
3675
|
+
* renderer appended (timeout, terminating signal, exit code — in that order,
|
|
3676
|
+
* so every marker is at the end and any run of them is stripped).
|
|
3677
|
+
*
|
|
3678
|
+
* The Host metadata already carries the run state; parsing is what keeps the
|
|
3679
|
+
* markers out of the terminal card's output pane, and it is the only source
|
|
3680
|
+
* for a signal name the projection does not carry.
|
|
3681
|
+
* @param text - the result text.
|
|
3682
|
+
* @returns the marker-free output and the facts the markers named.
|
|
3683
|
+
*/
|
|
3684
|
+
function parseShellStatus(text) {
|
|
3685
|
+
let rest = typeof text === "string" ? text : "";
|
|
3686
|
+
let exitCode;
|
|
3687
|
+
let signal;
|
|
3688
|
+
let timedOut = false;
|
|
3689
|
+
for (;;) {
|
|
3690
|
+
const trimmed = rest.endsWith("\n") ? rest.slice(0, -1) : rest;
|
|
3691
|
+
const breakAt = trimmed.lastIndexOf("\n");
|
|
3692
|
+
const last = breakAt === -1 ? trimmed : trimmed.slice(breakAt + 1);
|
|
3693
|
+
const exit = EXIT_MARKER.exec(last);
|
|
3694
|
+
if (exit !== null) {
|
|
3695
|
+
exitCode = exit[1] === "null" ? null : Number(exit[1]);
|
|
3696
|
+
rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
|
|
3697
|
+
continue;
|
|
3698
|
+
}
|
|
3699
|
+
const killed = SIGNAL_MARKER.exec(last);
|
|
3700
|
+
if (killed !== null) {
|
|
3701
|
+
signal = killed[1];
|
|
3702
|
+
rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
if (TIMEOUT_MARKER.test(last)) {
|
|
3706
|
+
timedOut = true;
|
|
3707
|
+
rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
break;
|
|
3711
|
+
}
|
|
3712
|
+
return {
|
|
3713
|
+
output: rest,
|
|
3714
|
+
exitCode,
|
|
3715
|
+
signal,
|
|
3716
|
+
timedOut
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
/** Bind the card dictionary to the diff primitive's chrome labels. */
|
|
3720
|
+
function diffBlockLabels(t) {
|
|
3721
|
+
return {
|
|
3722
|
+
copy: t("copy"),
|
|
3723
|
+
copied: t("copied"),
|
|
3724
|
+
collapseAria: t("collapseAria"),
|
|
3725
|
+
expandAria: (count) => t("expandAria", { count }),
|
|
3726
|
+
collapse: t("collapse"),
|
|
3727
|
+
expand: (count) => t("expandRest", { count }),
|
|
3728
|
+
files: (count) => t(count === 1 ? "files.one" : "files.other", { count })
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
/** Bind the card dictionary to the read primitive's chrome labels. */
|
|
3732
|
+
function readBlockLabels(t) {
|
|
3733
|
+
return {
|
|
3734
|
+
window: (shown, total) => t("read.window", {
|
|
3735
|
+
shown,
|
|
3736
|
+
total
|
|
3737
|
+
}),
|
|
3738
|
+
copy: t("copy"),
|
|
3739
|
+
copied: t("copied"),
|
|
3740
|
+
collapseAria: t("read.collapseAria"),
|
|
3741
|
+
expandAria: (count) => t("read.expandAria", { count }),
|
|
3742
|
+
collapse: t("collapse"),
|
|
3743
|
+
expand: (count) => t("read.expandRest", { count })
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
/** Bind the card dictionary to the search primitive's chrome labels. */
|
|
3747
|
+
function searchBlockLabels(t) {
|
|
3748
|
+
return {
|
|
3749
|
+
pathsSummary: (shown, total, truncated) => t(truncated ? "search.paths.truncated" : "search.paths", {
|
|
3750
|
+
shown,
|
|
3751
|
+
total
|
|
3752
|
+
}),
|
|
3753
|
+
matchesSummary: (shown, total, files, truncated) => t(truncated ? "search.matches.truncated" : "search.matches", {
|
|
3754
|
+
shown,
|
|
3755
|
+
total,
|
|
3756
|
+
files
|
|
3757
|
+
}),
|
|
3758
|
+
copy: t("copy"),
|
|
3759
|
+
copied: t("copied"),
|
|
3760
|
+
noResults: t("search.noResults"),
|
|
3761
|
+
collapseAria: t("search.collapseAria"),
|
|
3762
|
+
expandAria: (count) => t("search.expandAria", { count }),
|
|
3763
|
+
collapse: t("collapse"),
|
|
3764
|
+
expand: (count) => t("search.expandRest", { count })
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
/** Bind the card dictionary to the terminal primitive's chrome labels. */
|
|
3768
|
+
function terminalBlockLabels(t) {
|
|
3769
|
+
return {
|
|
3770
|
+
signal: (signal) => t("terminal.signal", { signal }),
|
|
3771
|
+
exitCode: (exitCode) => t("terminal.exitCode", { code: exitCode }),
|
|
3772
|
+
running: t("terminal.running"),
|
|
3773
|
+
failed: t("terminal.failed"),
|
|
3774
|
+
done: t("terminal.done"),
|
|
3775
|
+
copy: t("copy"),
|
|
3776
|
+
copied: t("copied"),
|
|
3777
|
+
noOutput: t("terminal.noOutput"),
|
|
3778
|
+
collapseAria: t("collapseAria"),
|
|
3779
|
+
collapse: t("collapse"),
|
|
3780
|
+
expandAria: (hidden) => t("expandAria", { count: hidden }),
|
|
3781
|
+
expand: (hidden) => t("expandRest", { count: hidden })
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
//#endregion
|
|
3785
|
+
//#region src/web/client/ToolCardShell.tsx
|
|
3786
|
+
/**
|
|
3787
|
+
* The shared card shell every row renders through.
|
|
3788
|
+
*
|
|
3789
|
+
* The shipped `ToolRow` is not reusable from a plugin (ui-tool is not part of
|
|
3790
|
+
* the client platform baseline), so this is our own: one 24px header line
|
|
3791
|
+
* (state dot + title + separator + ellipsized summary + optional `+N -M`
|
|
3792
|
+
* suffix + clickable path), a keyboard-reachable disclosure, and a body that
|
|
3793
|
+
* is either the card a row built or the flattened result text.
|
|
3794
|
+
*
|
|
3795
|
+
* Two properties matter more than the visuals:
|
|
3796
|
+
* - **Never throw.** Every value is coerced defensively, and {@link CardBoundary}
|
|
3797
|
+
* turns a crash inside a card into the same generic shell, because an
|
|
3798
|
+
* exception reaching the slot retires the cell and abdicates the call to the
|
|
3799
|
+
* shipped row.
|
|
3800
|
+
* - **Theme tokens only.** The sheet below reads `--dsw-alias-*` (plus the two
|
|
3801
|
+
* published content-font vars) and hard-codes no colour, so light and dark
|
|
3802
|
+
* both come out right.
|
|
3803
|
+
* @module @xiaoso/dsh-tool-plus/web/client/ToolCardShell
|
|
3804
|
+
*/
|
|
3805
|
+
/** Chrome CSS, keyed by `data-plugin-css` and injected once per page. */
|
|
3806
|
+
const CSS = `
|
|
3807
|
+
.twc-root{display:flex;flex-direction:column;min-width:0}
|
|
3808
|
+
.twc-row{position:relative;min-width:0}
|
|
3809
|
+
.twc-leading{flex-shrink:0}
|
|
3810
|
+
.twc-chevron{color:var(--dsw-alias-label-secondary)}
|
|
3811
|
+
.twc-title{font-weight:400}
|
|
3812
|
+
.twc-sep{flex:none;width:2px;height:2px;border-radius:1px;margin:0 8px;background:var(--dsw-alias-label-caption)}
|
|
3813
|
+
.twc-summary{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary)}
|
|
3814
|
+
.twc-summarySuffix{flex:none;margin-left:4px;white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary)}
|
|
3815
|
+
.twc-diffStat{margin-left:10px;font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);color:var(--dsw-alias-label-caption)}
|
|
3816
|
+
.twc-errorSummary{color:var(--dsw-alias-state-error-primary)}
|
|
3817
|
+
.twc-fileLink{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0;border:none;background:none;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);text-decoration:underline dotted;text-decoration-color:var(--dsw-alias-label-tertiary);text-decoration-thickness:1px;text-underline-offset:3px;cursor:pointer;border-radius:4px}
|
|
3818
|
+
.twc-fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}
|
|
3819
|
+
.twc-fileLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
3820
|
+
.twc-bodyWrap{display:flex;flex-direction:column;min-width:0}
|
|
3821
|
+
.twc-body{margin:4px 0 4px 4px;min-width:0}
|
|
3822
|
+
.twc-text{margin:4px 0 4px 4px;padding:10px 14px;max-height:260px;overflow:auto;border-radius:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-secondary);font-family:var(--ds-font-family-code);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}
|
|
3823
|
+
.twc-note{margin:4px 0 4px 4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
3824
|
+
.twc-media{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:4px 0 4px 4px;min-width:0}
|
|
3825
|
+
/* Image gallery: the wrapping group the message gallery uses, so a card's
|
|
3826
|
+
images and a message's images wrap the same way. */
|
|
3827
|
+
.twc-imgGallery{display:flex;flex-wrap:wrap;gap:10px;max-width:100%;min-width:0}
|
|
3828
|
+
/* Thumbnail frame (figma message-image rule): a 16px-radius tile that crops
|
|
3829
|
+
with object-fit cover, sized inline for a lone image and 64×64 in a gallery. */
|
|
3830
|
+
.twc-imgFrame{display:grid;flex:0 0 auto;place-items:center;box-sizing:border-box;min-width:44px;min-height:44px;padding:0;overflow:hidden;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:16px;background:var(--dsw-alias-interactive-bg-hover);cursor:zoom-in}
|
|
3831
|
+
.twc-imgFrame[data-variant='tile']{width:64px;height:64px;min-width:64px;min-height:64px}
|
|
3832
|
+
.twc-imgFrame:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
3833
|
+
.twc-mediaImg{display:block;width:100%;height:100%;object-fit:cover}
|
|
3834
|
+
.twc-imgLoading{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
3835
|
+
/* Failed load: the frame itself becomes the retry control (the official
|
|
3836
|
+
behaviour), so a transient failure costs one click instead of a reload. It
|
|
3837
|
+
carries the frame's own box model — border-box, same 44px floor — so the
|
|
3838
|
+
reserved box survives loading → loaded → failed unchanged (no jump). */
|
|
3839
|
+
.twc-imgError{box-sizing:border-box;min-width:44px;min-height:44px;max-width:240px;padding:10px 12px;overflow:hidden;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:10px;background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;text-align:left;cursor:pointer}
|
|
3840
|
+
.twc-imgError[data-variant='tile']{width:64px;height:64px;min-width:64px;min-height:64px;padding:4px;border-radius:16px}
|
|
3841
|
+
.twc-imgError:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
3842
|
+
/* Original-image lightbox: a body portal, a mask on its own layer (blurring
|
|
3843
|
+
the backdrop itself would blur the preview and the close control), and the
|
|
3844
|
+
preview stacked with its caption so the two always read as one object. */
|
|
3845
|
+
.twc-lightbox{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:40px}
|
|
3846
|
+
.twc-lightboxMask{position:absolute;inset:0;background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur)}
|
|
3847
|
+
.twc-lightboxStack{position:relative;display:flex;flex-direction:column;align-items:center;gap:10px;max-height:calc(100vh - 80px);min-width:0}
|
|
3848
|
+
.twc-lightboxImg{max-width:min(100%,1600px);max-height:calc(100vh - 130px);object-fit:contain;border-radius:12px;background:var(--dsw-specific-input-major);box-shadow:var(--dsw-shadow-lv3)}
|
|
3849
|
+
.twc-lightboxClose{position:fixed;top:20px;right:20px;z-index:1;display:grid;place-items:center;width:36px;height:36px;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);cursor:pointer}
|
|
3850
|
+
.twc-lightboxClose:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}
|
|
3851
|
+
.twc-lightboxClose:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
3852
|
+
/* The caption carries the reference's own numbers — name, intrinsic size,
|
|
3853
|
+
byte length — so the enlarged view states what the file actually is. It sits
|
|
3854
|
+
in the stack under the image: fixed to the viewport it read as a separate
|
|
3855
|
+
object from a centred image, and a tall image could squeeze it out. */
|
|
3856
|
+
.twc-lightboxCaption{margin:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:5px 14px;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
3857
|
+
/* Motion is feedback, never decoration: the hover lift, the skeleton pulse,
|
|
3858
|
+
and the lightbox entrance all sit behind the user's motion preference. A
|
|
3859
|
+
reduced-motion reader keeps every state change, just without the glide. */
|
|
3860
|
+
.twc-imgFrame:hover{box-shadow:var(--dsw-shadow-lv1-blur)}
|
|
3861
|
+
@media (prefers-reduced-motion: no-preference){
|
|
3862
|
+
.twc-imgFrame{transition:transform 160ms ease,box-shadow 160ms ease}
|
|
3863
|
+
.twc-imgFrame:hover{transform:scale(1.01)}
|
|
3864
|
+
.twc-imgFrame:active{transform:scale(0.99)}
|
|
3865
|
+
.twc-imgLoading{animation:twc-img-pulse 1.6s ease-in-out infinite}
|
|
3866
|
+
.twc-lightbox{animation:twc-lightbox-in 160ms ease-out}
|
|
3867
|
+
.twc-lightboxImg{animation:twc-lightbox-img-in 180ms ease-out}
|
|
3868
|
+
@keyframes twc-img-pulse{0%,100%{opacity:.55}50%{opacity:1}}
|
|
3869
|
+
@keyframes twc-lightbox-in{from{opacity:0}to{opacity:1}}
|
|
3870
|
+
@keyframes twc-lightbox-img-in{from{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}
|
|
3871
|
+
}
|
|
3872
|
+
.twc-inspect{display:inline-flex;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;border:0.5px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:16px;cursor:pointer;opacity:0;transition:opacity 100ms ease}
|
|
3873
|
+
.twc-root:hover .twc-inspect,.twc-inspect:focus-visible{opacity:1}
|
|
3874
|
+
.twc-inspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}
|
|
3875
|
+
.twc-visuallyHidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
3876
|
+
.twc-fallbackDot{flex:none;width:8px;height:8px;border-radius:999px;background:var(--dsw-alias-state-error-primary)}
|
|
3877
|
+
@media (prefers-reduced-motion: reduce){.twc-inspect{transition:none}}
|
|
3878
|
+
`;
|
|
3879
|
+
/** Inject the card stylesheet once per page; the loader drops plugin style tags on unload. */
|
|
3880
|
+
let cssInjected = false;
|
|
3881
|
+
function injectCss() {
|
|
3882
|
+
if (cssInjected) return;
|
|
3883
|
+
if (typeof document === "undefined") return;
|
|
3884
|
+
cssInjected = true;
|
|
3885
|
+
const id = "tool-plus-cards";
|
|
3886
|
+
if (document.querySelector("style[data-plugin-css=\"tool-plus-cards\"]") !== null) return;
|
|
3887
|
+
const tag = document.createElement("style");
|
|
3888
|
+
tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
|
|
3889
|
+
tag.dataset.pluginCss = id;
|
|
3890
|
+
tag.textContent = CSS;
|
|
3891
|
+
document.head.appendChild(tag);
|
|
3892
|
+
}
|
|
3893
|
+
/** Whether a React node would draw anything. */
|
|
3894
|
+
function hasContent(node) {
|
|
3895
|
+
return node !== void 0 && node !== null && node !== false;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Render the shared card shell.
|
|
3899
|
+
* @param props - see {@link ToolCardShellProps}.
|
|
3900
|
+
* @returns the card row and, when expanded, its body.
|
|
3901
|
+
*/
|
|
3902
|
+
function ToolCardShell(props) {
|
|
3903
|
+
injectCss();
|
|
3904
|
+
const { t, state, title, children, filePath, openFile, inspect } = props;
|
|
3905
|
+
const [expanded, setExpanded] = (0, react.useState)(props.defaultOpen === true);
|
|
3906
|
+
const bodyCard = hasContent(children) ? children : null;
|
|
3907
|
+
const bodyText = typeof props.text === "string" && props.text !== "" ? props.text : null;
|
|
3908
|
+
const expandable = bodyCard !== null || bodyText !== null;
|
|
3909
|
+
const open = expanded && expandable;
|
|
3910
|
+
const status = stateStatus(state, t);
|
|
3911
|
+
const summaryText = typeof props.summary === "string" ? props.summary : "";
|
|
3912
|
+
const suffix = typeof props.summarySuffix === "string" && props.summarySuffix !== "" ? props.summarySuffix : null;
|
|
3913
|
+
const fileLink = filePath !== void 0 && filePath !== null && filePath !== "" && openFile !== void 0;
|
|
3914
|
+
const errorTone = props.summaryTone === "error";
|
|
3915
|
+
const toggle = () => {
|
|
3916
|
+
setExpanded((value) => !value);
|
|
3917
|
+
};
|
|
3918
|
+
const openPath = (event) => {
|
|
3919
|
+
event.stopPropagation();
|
|
3920
|
+
if (!fileLink || openFile === void 0) return;
|
|
3921
|
+
const line = props.filePathLine;
|
|
3922
|
+
if (typeof line === "number") openFile(filePath, { line });
|
|
3923
|
+
else openFile(filePath);
|
|
3924
|
+
};
|
|
3925
|
+
const openPathKeyDown = (event) => {
|
|
3926
|
+
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
|
3927
|
+
};
|
|
3928
|
+
const collapsed = summaryText === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3930
|
+
className: "twc-sep",
|
|
3931
|
+
"aria-hidden": true
|
|
3932
|
+
}),
|
|
3933
|
+
fileLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3934
|
+
type: "button",
|
|
3935
|
+
className: "twc-fileLink",
|
|
3936
|
+
onClick: openPath,
|
|
3937
|
+
onKeyDown: openPathKeyDown,
|
|
3938
|
+
children: summaryText
|
|
3939
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3940
|
+
className: errorTone ? "twc-summary twc-errorSummary" : "twc-summary",
|
|
3941
|
+
children: summaryText
|
|
3942
|
+
}),
|
|
3943
|
+
suffix !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3944
|
+
className: "twc-summarySuffix",
|
|
3945
|
+
children: suffix
|
|
3946
|
+
})
|
|
3947
|
+
] });
|
|
3948
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3949
|
+
className: "twc-root",
|
|
3950
|
+
"data-state": state,
|
|
3951
|
+
"data-tool": title,
|
|
3952
|
+
children: [status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3953
|
+
className: "twc-visuallyHidden",
|
|
3954
|
+
children: status
|
|
3955
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
|
|
3956
|
+
rowClassName: "twc-row",
|
|
3957
|
+
leadingClassName: "twc-leading",
|
|
3958
|
+
titleClassName: "twc-title",
|
|
3959
|
+
chevronClassName: "twc-chevron",
|
|
3960
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(state) }),
|
|
3961
|
+
title,
|
|
3962
|
+
open,
|
|
3963
|
+
expandable,
|
|
3964
|
+
expandOnRowClick: true,
|
|
3965
|
+
keepContentWhenOpen: true,
|
|
3966
|
+
onToggle: toggle,
|
|
3967
|
+
collapsedContent: collapsed,
|
|
3968
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3969
|
+
className: "twc-bodyWrap",
|
|
3970
|
+
children: [bodyCard ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3971
|
+
className: "twc-text",
|
|
3972
|
+
children: bodyText
|
|
3973
|
+
}), inspect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3974
|
+
type: "button",
|
|
3975
|
+
className: "twc-inspect",
|
|
3976
|
+
onClick: inspect,
|
|
3977
|
+
children: t("inspect")
|
|
3978
|
+
})]
|
|
3979
|
+
})
|
|
3980
|
+
})]
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* The generic card: every row's degradation path.
|
|
3985
|
+
*
|
|
3986
|
+
* A card is only drawn once its metadata passed a `narrow*` guard, so anything
|
|
3987
|
+
* else — a running call, a window-truncated call, a Code Dispatch child, a
|
|
3988
|
+
* failure, a payload from another plugin version — lands here with the call's
|
|
3989
|
+
* own title, run state, argument count, and result text.
|
|
3990
|
+
* @param props - see {@link GenericCardProps}.
|
|
3991
|
+
* @returns the generic shell.
|
|
3992
|
+
*/
|
|
3993
|
+
function GenericCard({ t, title, block, args, inspect }) {
|
|
3994
|
+
const state = cardState(block);
|
|
3995
|
+
const output = resultText(block);
|
|
3996
|
+
const failure = state === "error" ? firstLine(output) : "";
|
|
3997
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
3998
|
+
t,
|
|
3999
|
+
state,
|
|
4000
|
+
title,
|
|
4001
|
+
summary: failure === "" ? argsSummary(args, t) : failure,
|
|
4002
|
+
summaryTone: failure === "" ? "default" : "error",
|
|
4003
|
+
text: output,
|
|
4004
|
+
inspect
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
/** Best-effort message for a thrown value; never throws itself. */
|
|
4008
|
+
function failureText(error) {
|
|
4009
|
+
try {
|
|
4010
|
+
if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
|
|
4011
|
+
if (typeof error === "string" && error !== "") return error;
|
|
4012
|
+
} catch {}
|
|
4013
|
+
return "Unknown card error";
|
|
4014
|
+
}
|
|
4015
|
+
/**
|
|
4016
|
+
* Contain a card's render failure: the fallback is a generic shell (never the
|
|
4017
|
+
* card's own chrome), so one malformed payload cannot take the call's cell —
|
|
4018
|
+
* or the whole slot — down with it.
|
|
4019
|
+
*/
|
|
4020
|
+
var CardBoundary = class extends react.Component {
|
|
4021
|
+
state = { failure: null };
|
|
4022
|
+
/** React's error-boundary hook: record the failure and render the fallback. */
|
|
4023
|
+
static getDerivedStateFromError(error) {
|
|
4024
|
+
return { failure: failureText(error) };
|
|
4025
|
+
}
|
|
4026
|
+
/** Report the failure without rethrowing. */
|
|
4027
|
+
componentDidCatch(error) {
|
|
4028
|
+
console.error("[tool-plus] a web tool card failed to render; showing the generic card instead.", error);
|
|
4029
|
+
}
|
|
4030
|
+
render() {
|
|
4031
|
+
const { failure } = this.state;
|
|
4032
|
+
if (failure === null) return this.props.children;
|
|
4033
|
+
const { t, title, block, summary } = this.props;
|
|
4034
|
+
let text = null;
|
|
4035
|
+
try {
|
|
4036
|
+
const output = block === void 0 ? "" : resultText(block);
|
|
4037
|
+
text = output === "" ? null : output;
|
|
4038
|
+
} catch {
|
|
4039
|
+
text = null;
|
|
4040
|
+
}
|
|
4041
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4042
|
+
t,
|
|
4043
|
+
state: "error",
|
|
4044
|
+
title,
|
|
4045
|
+
summary: failure === "" ? summary ?? null : firstLine(failure),
|
|
4046
|
+
summaryTone: "error",
|
|
4047
|
+
text
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
//#endregion
|
|
4052
|
+
//#region src/web/client/rows/AstEditRow.tsx
|
|
4053
|
+
/**
|
|
4054
|
+
* `ast_edit` row: the engine's own change preview.
|
|
4055
|
+
*
|
|
4056
|
+
* The AST engine already renders a grouped preview (file header, replacement
|
|
4057
|
+
* count, the first line of each change) and hands it to the Host as
|
|
4058
|
+
* `displayContent`; that text is what this card shows, because the exact
|
|
4059
|
+
* before/after hunks are dropped inside the ported tool and the write happens
|
|
4060
|
+
* in the native engine. No diff is computed here, and no `±` totals are shown.
|
|
4061
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/AstEditRow
|
|
4062
|
+
*/
|
|
4063
|
+
/** The engine reports parse failures in its result text, not in the metadata. */
|
|
4064
|
+
const PARSE_ERROR = /parse error/i;
|
|
4065
|
+
/**
|
|
4066
|
+
* Render an `ast_edit` call as a change preview.
|
|
4067
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4068
|
+
* @returns the preview card, or the generic shell when there is no preview.
|
|
4069
|
+
*/
|
|
4070
|
+
function AstEditRow(props) {
|
|
4071
|
+
const { t, block } = props;
|
|
4072
|
+
const title = cardTitle(props.toolName, t, "title.astEdit");
|
|
4073
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4074
|
+
t,
|
|
4075
|
+
title,
|
|
4076
|
+
block,
|
|
4077
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AstEditCard, {
|
|
4078
|
+
...props,
|
|
4079
|
+
title
|
|
4080
|
+
})
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* The card itself, inside the boundary.
|
|
4085
|
+
* @param props - row props plus the resolved title.
|
|
4086
|
+
* @returns the preview card.
|
|
4087
|
+
*/
|
|
4088
|
+
function AstEditCard(props) {
|
|
4089
|
+
const { t, block, cwd, inspect, openFile, title } = props;
|
|
4090
|
+
const args = parseCardArgs(block);
|
|
4091
|
+
const meta = narrowAstEditCardMeta(cardMeta(block));
|
|
4092
|
+
if (isSubCall(block) || block.isError === true || meta === null || meta.preview === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4093
|
+
t,
|
|
4094
|
+
title,
|
|
4095
|
+
block,
|
|
4096
|
+
args,
|
|
4097
|
+
inspect
|
|
4098
|
+
});
|
|
4099
|
+
const files = meta.files.length;
|
|
4100
|
+
const single = files === 1 ? meta.files[0]?.path : void 0;
|
|
4101
|
+
const output = resultText(block);
|
|
4102
|
+
const parseErrors = PARSE_ERROR.test(output);
|
|
4103
|
+
const summary = single !== void 0 && single !== "" ? displayPath(single, cwd) : t("astEdit.files", { count: files });
|
|
4104
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4105
|
+
t,
|
|
4106
|
+
state: cardState(block),
|
|
4107
|
+
title,
|
|
4108
|
+
summary,
|
|
4109
|
+
summarySuffix: `${t("astEdit.replacements", { count: meta.replacements })} · ${t("astEdit.files", { count: files })}`,
|
|
4110
|
+
filePath: single,
|
|
4111
|
+
openFile,
|
|
4112
|
+
inspect,
|
|
4113
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4114
|
+
className: "twc-body",
|
|
4115
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
|
|
4116
|
+
code: meta.preview,
|
|
4117
|
+
copyLabel: t("copy"),
|
|
4118
|
+
copiedLabel: t("copied")
|
|
4119
|
+
}), output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4120
|
+
className: "twc-note",
|
|
4121
|
+
children: parseErrors ? `${t("astEdit.parseErrors")}: ${output}` : output
|
|
4122
|
+
})]
|
|
4123
|
+
})
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
//#endregion
|
|
4127
|
+
//#region src/web/client/rows/FileMutationRow.tsx
|
|
4128
|
+
/**
|
|
4129
|
+
* `edit` row: a diff card.
|
|
4130
|
+
*
|
|
4131
|
+
* Two sources, in order: the Host projection (`meta.diffs`, built from the
|
|
4132
|
+
* engine's own unified diff — the change is already computed and needs no
|
|
4133
|
+
* extra I/O), then the replace-form arguments while the call is still running
|
|
4134
|
+
* (or when the Host could not project a diff). Every other edit mode (patch,
|
|
4135
|
+
* hashline, apply_patch) has no diff until it settles, so it falls back to the
|
|
4136
|
+
* generic shell rather than guessing from `patch`/`input` text.
|
|
4137
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/FileMutationRow
|
|
4138
|
+
*/
|
|
4139
|
+
/**
|
|
4140
|
+
* Render an `edit` call as a diff card.
|
|
4141
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4142
|
+
* @returns the diff card, or the generic shell for a mode with no diff yet.
|
|
4143
|
+
*/
|
|
4144
|
+
function FileMutationRow(props) {
|
|
4145
|
+
const { t, block } = props;
|
|
4146
|
+
const title = cardTitle(props.toolName, t, "title.edit");
|
|
4147
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4148
|
+
t,
|
|
4149
|
+
title,
|
|
4150
|
+
block,
|
|
4151
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileMutationCard, {
|
|
4152
|
+
...props,
|
|
4153
|
+
title
|
|
4154
|
+
})
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* The card itself, inside the boundary.
|
|
4159
|
+
* @param props - row props plus the resolved title.
|
|
4160
|
+
* @returns the diff card.
|
|
4161
|
+
*/
|
|
4162
|
+
function FileMutationCard(props) {
|
|
4163
|
+
const { t, block, cwd, inspect, openFile, title } = props;
|
|
4164
|
+
const args = parseCardArgs(block);
|
|
4165
|
+
const meta = narrowEditCardMeta(cardMeta(block));
|
|
4166
|
+
const replaceForm = replaceHunks(args);
|
|
4167
|
+
if (isSubCall(block) || block.isError === true || meta === null && replaceForm === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4168
|
+
t,
|
|
4169
|
+
title,
|
|
4170
|
+
block,
|
|
4171
|
+
args,
|
|
4172
|
+
inspect
|
|
4173
|
+
});
|
|
4174
|
+
const diffs = meta === null ? replaceForm ?? [] : meta.diffs;
|
|
4175
|
+
const paths = [...new Set(diffs.map((hunk) => hunk.path))];
|
|
4176
|
+
const single = paths.length === 1 ? paths[0] : void 0;
|
|
4177
|
+
const totals = (0, _deepseek_ai_dsh_client_ui_primitives.diffTotals)(diffs);
|
|
4178
|
+
const summary = single === void 0 ? t("files.other", { count: paths.length }) : displayPath(single, cwd);
|
|
4179
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4180
|
+
t,
|
|
4181
|
+
state: cardState(block),
|
|
4182
|
+
title,
|
|
4183
|
+
summary,
|
|
4184
|
+
summarySuffix: `+${totals.added} -${totals.removed}`,
|
|
4185
|
+
filePath: single,
|
|
4186
|
+
openFile,
|
|
4187
|
+
inspect,
|
|
4188
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
|
|
4189
|
+
diffs,
|
|
4190
|
+
labels: diffBlockLabels(t),
|
|
4191
|
+
className: "twc-body"
|
|
4192
|
+
})
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* The replace form's diff, straight from the call's arguments: one hunk for
|
|
4197
|
+
* `old_string` → `new_string`. The empty `new_string` is a real value (it
|
|
4198
|
+
* deletes), so only the path and `old_string` are required to be non-empty.
|
|
4199
|
+
* @param args - the call's parsed arguments, or `null`.
|
|
4200
|
+
* @returns one hunk, or `null` when the call is not the replace form.
|
|
4201
|
+
*/
|
|
4202
|
+
function replaceHunks(args) {
|
|
4203
|
+
const path = argText(args, "file_path");
|
|
4204
|
+
const oldText = argText(args, "old_string");
|
|
4205
|
+
const newText = args?.new_string;
|
|
4206
|
+
if (path === null || oldText === null || typeof newText !== "string") return null;
|
|
4207
|
+
return [{
|
|
4208
|
+
path,
|
|
4209
|
+
oldText,
|
|
4210
|
+
newText
|
|
4211
|
+
}];
|
|
4212
|
+
}
|
|
4213
|
+
//#endregion
|
|
4214
|
+
//#region src/web/client/rows/image-card.ts
|
|
4215
|
+
/** Whether `value` is a plain object. */
|
|
4216
|
+
function isRecord(value) {
|
|
4217
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4218
|
+
}
|
|
4219
|
+
/** A finite number — a `NaN` dimension would only break layout later. */
|
|
4220
|
+
function isFiniteNumber(value) {
|
|
4221
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Display box for a lone image — DSH's own rule, copied from
|
|
4225
|
+
* `ui-attachment/src/MessageImage.tsx::singleFit` so a tool-card image and a
|
|
4226
|
+
* message image of the same file occupy the same box: long edge 240px, the
|
|
4227
|
+
* rendered aspect ratio clamped to [0.25, 4] (the clamped overflow is cropped
|
|
4228
|
+
* by `object-fit: cover`), never upscaled past the natural size, and the crop
|
|
4229
|
+
* anchored to the top of very tall images / the left of very wide ones, where
|
|
4230
|
+
* the informative content usually starts.
|
|
4231
|
+
*
|
|
4232
|
+
* The clamp is deliberately not strict: an exactly 4:1 image stays centred.
|
|
4233
|
+
* @param size - the reference's intrinsic dimensions, in pixels.
|
|
4234
|
+
* @returns the rendered box and the crop anchor.
|
|
4235
|
+
*/
|
|
4236
|
+
function imageFitBox(size) {
|
|
4237
|
+
const natural = size.width / size.height;
|
|
4238
|
+
const ratio = Math.min(4, Math.max(.25, natural));
|
|
4239
|
+
const box = ratio >= 1 ? {
|
|
4240
|
+
width: 240,
|
|
4241
|
+
height: 240 / ratio
|
|
4242
|
+
} : {
|
|
4243
|
+
width: 240 * ratio,
|
|
4244
|
+
height: 240
|
|
4245
|
+
};
|
|
4246
|
+
const scale = Math.min(1, size.width / box.width, size.height / box.height);
|
|
4247
|
+
return {
|
|
4248
|
+
width: Math.max(1, Math.round(box.width * scale)),
|
|
4249
|
+
height: Math.max(1, Math.round(box.height * scale)),
|
|
4250
|
+
objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
/**
|
|
4254
|
+
* The call's arguments.
|
|
4255
|
+
*
|
|
4256
|
+
* A settled node carries the paired call head under `call`, and that is the
|
|
4257
|
+
* only source: the node-level `argsRaw` belongs to a *running* call, so reading
|
|
4258
|
+
* it here would let a truncated settled call (head outside the window) render a
|
|
4259
|
+
* card whose argument summary is empty. `parseCardArgs` narrows the same field
|
|
4260
|
+
* the same way.
|
|
4261
|
+
* @param block - the settled call node.
|
|
4262
|
+
* @returns the parsed argument object, or `null`.
|
|
4263
|
+
*/
|
|
4264
|
+
function callArgs(block) {
|
|
4265
|
+
const raw = block.call?.argsRaw;
|
|
4266
|
+
if (typeof raw !== "string") return null;
|
|
4267
|
+
try {
|
|
4268
|
+
const value = JSON.parse(raw);
|
|
4269
|
+
return isRecord(value) ? value : null;
|
|
4270
|
+
} catch {
|
|
4271
|
+
return null;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Validate one wire attachment object as a durable image reference.
|
|
4276
|
+
*
|
|
4277
|
+
* The store's `ImageAttachmentRef` brands `attachmentId` and `mediaType`; the
|
|
4278
|
+
* result's JSON carries plain strings. This is the single boundary where a
|
|
4279
|
+
* validated wire object becomes that typed reference, field by field — and the
|
|
4280
|
+
* copy keeps the optional fields (`name`, `originalDimensions`) the wire may
|
|
4281
|
+
* carry.
|
|
4282
|
+
* @param value - the image block's `attachment` field.
|
|
4283
|
+
* @returns the typed reference, or `null` when any required field is wrong.
|
|
4284
|
+
*/
|
|
4285
|
+
function typedAttachment(value) {
|
|
4286
|
+
if (!isRecord(value)) return null;
|
|
4287
|
+
if (typeof value.attachmentId !== "string") return null;
|
|
4288
|
+
if (typeof value.mediaType !== "string") return null;
|
|
4289
|
+
if (!isFiniteNumber(value.bytes)) return null;
|
|
4290
|
+
if (!isFiniteNumber(value.width)) return null;
|
|
4291
|
+
if (!isFiniteNumber(value.height)) return null;
|
|
4292
|
+
if (value.width <= 0 || value.height <= 0) return null;
|
|
4293
|
+
return { ...value };
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Derive the read card's image material from a settled result.
|
|
4297
|
+
*
|
|
4298
|
+
* Declines — rather than guessing — whenever the result would not be fully
|
|
4299
|
+
* representable: a running call, a failed call, a result whose content carries
|
|
4300
|
+
* a block that is neither text nor image, an image whose reference is
|
|
4301
|
+
* malformed, a text-only result (the text card owns those), or a call whose
|
|
4302
|
+
* arguments name no path.
|
|
4303
|
+
* @param block - the frozen call node.
|
|
4304
|
+
* @param cwd - the session workspace root, for shortening the label.
|
|
4305
|
+
* @returns the material, or `null` to leave the row on its existing path.
|
|
4306
|
+
*/
|
|
4307
|
+
function imageCardMaterial(block, cwd) {
|
|
4308
|
+
if (!isSettled(block) || block.isError === true) return null;
|
|
4309
|
+
const content = block.content;
|
|
4310
|
+
if (!Array.isArray(content)) return null;
|
|
4311
|
+
const images = [];
|
|
4312
|
+
for (const blockContent of content) {
|
|
4313
|
+
if (!isRecord(blockContent)) return null;
|
|
4314
|
+
if (blockContent.type === "text") continue;
|
|
4315
|
+
if (blockContent.type !== "image") return null;
|
|
4316
|
+
const image = typedAttachment(blockContent.attachment);
|
|
4317
|
+
if (image === null) return null;
|
|
4318
|
+
images.push(image);
|
|
4319
|
+
}
|
|
4320
|
+
if (images.length === 0) return null;
|
|
4321
|
+
const path = callArgs(block)?.path;
|
|
4322
|
+
if (typeof path !== "string" || path.trim() === "") return null;
|
|
4323
|
+
return {
|
|
4324
|
+
label: displayPath(path, cwd),
|
|
4325
|
+
path,
|
|
4326
|
+
images
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
//#endregion
|
|
4330
|
+
//#region src/web/client/rows/ReadRow.tsx
|
|
4331
|
+
/**
|
|
4332
|
+
* `read` row: a line-numbered window view, or the image the read returned.
|
|
4333
|
+
*
|
|
4334
|
+
* Two mutually exclusive bodies hang off one call:
|
|
4335
|
+
* - The Host projected a readable window (`meta`) → the line-numbered text card.
|
|
4336
|
+
* - It did not (an image, a directory, a sqlite or archive target, an internal
|
|
4337
|
+
* URL, a multi-range `:raw` read) → this row tries the image branch, which
|
|
4338
|
+
* draws the result's image blocks through the session-authorized loader the
|
|
4339
|
+
* slot hands every tool view. Anything else keeps the generic shell and its
|
|
4340
|
+
* result text.
|
|
4341
|
+
*
|
|
4342
|
+
* The image branch reimplements the shipped message-image interaction — the
|
|
4343
|
+
* `singleFit` box, 64px gallery tiles, a retry control on a failed load, and a
|
|
4344
|
+
* body-portal lightbox for the original — so a tool-card image and a message
|
|
4345
|
+
* image of the same file behave identically. It lives here rather than in
|
|
4346
|
+
* `image-card.ts` because that module stays pure: this file owns the loader
|
|
4347
|
+
* calls, their cancellation, and the DOM.
|
|
4348
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/ReadRow
|
|
4349
|
+
*/
|
|
4350
|
+
/**
|
|
4351
|
+
* Render a `read` call as a line-numbered window or an image.
|
|
4352
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4353
|
+
* @returns the read card, or the generic shell when nothing else fits.
|
|
4354
|
+
*/
|
|
4355
|
+
function ReadRow(props) {
|
|
4356
|
+
const { t, block } = props;
|
|
4357
|
+
const title = cardTitle(props.toolName, t, "title.read");
|
|
4358
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4359
|
+
t,
|
|
4360
|
+
title,
|
|
4361
|
+
block,
|
|
4362
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadCard, {
|
|
4363
|
+
...props,
|
|
4364
|
+
title
|
|
4365
|
+
})
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
/**
|
|
4369
|
+
* The card itself, inside the boundary.
|
|
4370
|
+
* @param props - row props plus the resolved title.
|
|
4371
|
+
* @returns the read card.
|
|
4372
|
+
*/
|
|
4373
|
+
function ReadCard(props) {
|
|
4374
|
+
const { t, block, cwd, inspect, openFile, title } = props;
|
|
4375
|
+
const args = parseCardArgs(block);
|
|
4376
|
+
const meta = narrowReadCardMeta(cardMeta(block));
|
|
4377
|
+
const material = (0, react.useMemo)(() => imageCardMaterial(block, cwd), [block, cwd]);
|
|
4378
|
+
if (isSubCall(block) || block.isError === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4379
|
+
t,
|
|
4380
|
+
title,
|
|
4381
|
+
block,
|
|
4382
|
+
args,
|
|
4383
|
+
inspect
|
|
4384
|
+
});
|
|
4385
|
+
if (meta === null) {
|
|
4386
|
+
if (material === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4387
|
+
t,
|
|
4388
|
+
title,
|
|
4389
|
+
block,
|
|
4390
|
+
args,
|
|
4391
|
+
inspect
|
|
4392
|
+
});
|
|
4393
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageReadCard, {
|
|
4394
|
+
t,
|
|
4395
|
+
block,
|
|
4396
|
+
material,
|
|
4397
|
+
title,
|
|
4398
|
+
inspect,
|
|
4399
|
+
openFile,
|
|
4400
|
+
loadImage: props.loadImage
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4404
|
+
t,
|
|
4405
|
+
state: cardState(block),
|
|
4406
|
+
title,
|
|
4407
|
+
summary: displayPath(meta.path, cwd),
|
|
4408
|
+
filePath: meta.path,
|
|
4409
|
+
filePathLine: meta.offset,
|
|
4410
|
+
openFile,
|
|
4411
|
+
inspect,
|
|
4412
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
|
|
4413
|
+
label: displayPath(meta.path, cwd),
|
|
4414
|
+
lines: meta.lines,
|
|
4415
|
+
totalLines: meta.totalLines,
|
|
4416
|
+
lang: meta.lang,
|
|
4417
|
+
labels: readBlockLabels(t),
|
|
4418
|
+
className: "twc-body"
|
|
4419
|
+
})
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
/**
|
|
4423
|
+
* Ask the loader's synchronous cache for an already-known URL.
|
|
4424
|
+
* @param loader - the host's image loader.
|
|
4425
|
+
* @param attachment - the reference to resolve.
|
|
4426
|
+
* @returns the cached URL, or `null` when there is none.
|
|
4427
|
+
*/
|
|
4428
|
+
function peekUrl(loader, attachment) {
|
|
4429
|
+
if (loader?.peek === void 0) return null;
|
|
4430
|
+
try {
|
|
4431
|
+
const url = loader.peek(attachment);
|
|
4432
|
+
return typeof url === "string" && url !== "" ? url : null;
|
|
4433
|
+
} catch {
|
|
4434
|
+
return null;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
/** Whether two per-image states are the same, so state identity can be kept. */
|
|
4438
|
+
function sameImageState(left, right) {
|
|
4439
|
+
return left.url === right.url && left.failed === right.failed;
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* The image body: one framed thumbnail per reference over the result's own
|
|
4443
|
+
* envelope text.
|
|
4444
|
+
*
|
|
4445
|
+
* A rejected load or a missing loader leaves that image's frame as a retry
|
|
4446
|
+
* control and the text in place: an image card that cannot show its picture
|
|
4447
|
+
* must still show that the call returned one.
|
|
4448
|
+
* @param props - see {@link ImageReadCardProps}.
|
|
4449
|
+
* @returns the image card.
|
|
4450
|
+
*/
|
|
4451
|
+
function ImageReadCard({ t, block, material, title, inspect, openFile, loadImage }) {
|
|
4452
|
+
const images = material.images;
|
|
4453
|
+
const text = resultText(block);
|
|
4454
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ToolCardShell, {
|
|
4455
|
+
t,
|
|
4456
|
+
state: cardState(block),
|
|
4457
|
+
title,
|
|
4458
|
+
summary: material.label,
|
|
4459
|
+
filePath: material.path,
|
|
4460
|
+
openFile,
|
|
4461
|
+
inspect,
|
|
4462
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4463
|
+
className: "twc-media",
|
|
4464
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4465
|
+
className: "twc-imgGallery",
|
|
4466
|
+
children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardImage, {
|
|
4467
|
+
t,
|
|
4468
|
+
image,
|
|
4469
|
+
single: images.length === 1,
|
|
4470
|
+
loadImage
|
|
4471
|
+
}, `${image.attachmentId}:${index}`))
|
|
4472
|
+
})
|
|
4473
|
+
}), text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4474
|
+
className: "twc-note",
|
|
4475
|
+
children: text
|
|
4476
|
+
})]
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
/**
|
|
4480
|
+
* One image: a framed thumbnail that opens the original.
|
|
4481
|
+
*
|
|
4482
|
+
* The load runs under a liveness guard and is re-armed by {@link CardImage}'s
|
|
4483
|
+
* retry counter, so a failed load costs one click rather than a page reload.
|
|
4484
|
+
* @param props - see {@link CardImageProps}.
|
|
4485
|
+
* @returns the frame, or the retry control when the load failed.
|
|
4486
|
+
*/
|
|
4487
|
+
function CardImage({ t, image, single, loadImage }) {
|
|
4488
|
+
const [state, setState] = (0, react.useState)(() => ({
|
|
4489
|
+
url: peekUrl(loadImage, image),
|
|
4490
|
+
failed: false
|
|
4491
|
+
}));
|
|
4492
|
+
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
4493
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
4494
|
+
const close = (0, react.useCallback)(() => {
|
|
4495
|
+
setOpen(false);
|
|
4496
|
+
}, []);
|
|
4497
|
+
const retry = (0, react.useCallback)(() => {
|
|
4498
|
+
setAttempt((value) => value + 1);
|
|
4499
|
+
}, []);
|
|
4500
|
+
const frameRef = (0, react.useRef)(null);
|
|
4501
|
+
const focusOpener = (0, react.useCallback)(() => {
|
|
4502
|
+
frameRef.current?.focus();
|
|
4503
|
+
}, []);
|
|
4504
|
+
(0, react.useEffect)(() => {
|
|
4505
|
+
let live = true;
|
|
4506
|
+
setState((previous) => previous.failed ? {
|
|
4507
|
+
url: peekUrl(loadImage, image),
|
|
4508
|
+
failed: false
|
|
4509
|
+
} : previous);
|
|
4510
|
+
const load = async () => {
|
|
4511
|
+
if (loadImage === void 0) {
|
|
4512
|
+
if (live) setState((previous) => previous.failed ? previous : {
|
|
4513
|
+
url: null,
|
|
4514
|
+
failed: true
|
|
4515
|
+
});
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4518
|
+
try {
|
|
4519
|
+
const url = await loadImage(image);
|
|
4520
|
+
const next = typeof url === "string" && url !== "" ? {
|
|
4521
|
+
url,
|
|
4522
|
+
failed: false
|
|
4523
|
+
} : {
|
|
4524
|
+
url: null,
|
|
4525
|
+
failed: true
|
|
4526
|
+
};
|
|
4527
|
+
if (live) setState((previous) => sameImageState(previous, next) ? previous : next);
|
|
4528
|
+
} catch {
|
|
4529
|
+
if (live) setState((previous) => previous.failed ? previous : {
|
|
4530
|
+
url: null,
|
|
4531
|
+
failed: true
|
|
4532
|
+
});
|
|
4533
|
+
}
|
|
4534
|
+
};
|
|
4535
|
+
load();
|
|
4536
|
+
return () => {
|
|
4537
|
+
live = false;
|
|
4538
|
+
};
|
|
4539
|
+
}, [
|
|
4540
|
+
image,
|
|
4541
|
+
loadImage,
|
|
4542
|
+
attempt
|
|
4543
|
+
]);
|
|
4544
|
+
const variant = single ? "single" : "tile";
|
|
4545
|
+
const name = image.name ?? t("image.unnamed");
|
|
4546
|
+
const fit = (0, react.useMemo)(() => imageFitBox(image), [image]);
|
|
4547
|
+
if (state.failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4548
|
+
type: "button",
|
|
4549
|
+
className: "twc-imgError",
|
|
4550
|
+
"data-variant": variant,
|
|
4551
|
+
style: single ? {
|
|
4552
|
+
width: fit.width,
|
|
4553
|
+
height: fit.height
|
|
4554
|
+
} : void 0,
|
|
4555
|
+
title: t("image.loadFailed"),
|
|
4556
|
+
"aria-label": t("image.loadFailed"),
|
|
4557
|
+
onClick: retry,
|
|
4558
|
+
children: t("image.loadFailed")
|
|
4559
|
+
});
|
|
4560
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4561
|
+
ref: frameRef,
|
|
4562
|
+
type: "button",
|
|
4563
|
+
className: "twc-imgFrame",
|
|
4564
|
+
"data-variant": variant,
|
|
4565
|
+
style: single ? {
|
|
4566
|
+
width: fit.width,
|
|
4567
|
+
height: fit.height
|
|
4568
|
+
} : void 0,
|
|
4569
|
+
title: t("image.open"),
|
|
4570
|
+
"aria-label": t("image.openNamed", { name }),
|
|
4571
|
+
onClick: () => {
|
|
4572
|
+
if (state.url !== null) setOpen(true);
|
|
4573
|
+
},
|
|
4574
|
+
children: state.url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4575
|
+
className: "twc-imgLoading",
|
|
4576
|
+
children: t("image.loading")
|
|
4577
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4578
|
+
className: "twc-mediaImg",
|
|
4579
|
+
src: state.url,
|
|
4580
|
+
alt: name,
|
|
4581
|
+
style: single ? { objectPosition: fit.objectPosition } : void 0
|
|
4582
|
+
})
|
|
4583
|
+
}), open && state.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLightbox, {
|
|
4584
|
+
t,
|
|
4585
|
+
src: state.url,
|
|
4586
|
+
image,
|
|
4587
|
+
name,
|
|
4588
|
+
onClose: close,
|
|
4589
|
+
focusOpener
|
|
4590
|
+
})] });
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* The document-level original-image preview.
|
|
4594
|
+
*
|
|
4595
|
+
* Closes on Escape, a mask press, or the close control, and restores focus to
|
|
4596
|
+
* the opener on unmount. Rendered through a body portal: an opener inside a
|
|
4597
|
+
* transformed or filtered ancestor would otherwise trap the fixed backdrop in
|
|
4598
|
+
* that ancestor's box instead of covering the viewport.
|
|
4599
|
+
* @param props - see {@link CardLightboxProps}.
|
|
4600
|
+
* @returns the modal preview.
|
|
4601
|
+
*/
|
|
4602
|
+
function CardLightbox({ t, src, image, name, onClose, focusOpener }) {
|
|
4603
|
+
const closeRef = (0, react.useRef)(null);
|
|
4604
|
+
(0, react.useEffect)(() => {
|
|
4605
|
+
closeRef.current?.focus();
|
|
4606
|
+
const onKeyDown = (event) => {
|
|
4607
|
+
if (event.key === "Escape") onClose();
|
|
4608
|
+
};
|
|
4609
|
+
window.addEventListener("keydown", onKeyDown);
|
|
4610
|
+
return () => {
|
|
4611
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
4612
|
+
focusOpener();
|
|
4613
|
+
};
|
|
4614
|
+
}, [onClose, focusOpener]);
|
|
4615
|
+
const caption = t("image.caption", {
|
|
4616
|
+
name,
|
|
4617
|
+
width: image.width,
|
|
4618
|
+
height: image.height,
|
|
4619
|
+
size: (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(image.bytes)
|
|
4620
|
+
});
|
|
4621
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4622
|
+
className: "twc-lightbox",
|
|
4623
|
+
role: "dialog",
|
|
4624
|
+
"aria-modal": "true",
|
|
4625
|
+
"aria-label": t("image.dialog"),
|
|
4626
|
+
children: [
|
|
4627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4628
|
+
className: "twc-lightboxMask",
|
|
4629
|
+
"aria-hidden": "true",
|
|
4630
|
+
onMouseDown: (event) => {
|
|
4631
|
+
event.preventDefault();
|
|
4632
|
+
onClose();
|
|
4633
|
+
}
|
|
4634
|
+
}),
|
|
4635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4636
|
+
className: "twc-lightboxStack",
|
|
4637
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4638
|
+
className: "twc-lightboxImg",
|
|
4639
|
+
src,
|
|
4640
|
+
alt: name
|
|
4641
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4642
|
+
className: "twc-lightboxCaption",
|
|
4643
|
+
children: caption
|
|
4644
|
+
})]
|
|
4645
|
+
}),
|
|
4646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4647
|
+
ref: closeRef,
|
|
4648
|
+
type: "button",
|
|
4649
|
+
className: "twc-lightboxClose",
|
|
4650
|
+
"aria-label": t("image.close"),
|
|
4651
|
+
onClick: onClose,
|
|
4652
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 16 })
|
|
4653
|
+
})
|
|
4654
|
+
]
|
|
4655
|
+
}), document.body);
|
|
4656
|
+
}
|
|
4657
|
+
//#endregion
|
|
4658
|
+
//#region src/web/client/rows/SearchRow.tsx
|
|
4659
|
+
/**
|
|
4660
|
+
* `grep` / `glob` / `ast_grep` row: a search result card.
|
|
4661
|
+
*
|
|
4662
|
+
* One component serves all three keys because the Host projects one
|
|
4663
|
+
* `shape`-discriminated metadata form for them: grouped matches for the two
|
|
4664
|
+
* grep engines, a flat path list for glob. The title follows the call's own
|
|
4665
|
+
* wire name, so the same component reads as Grep, Glob, or AST Grep.
|
|
4666
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/SearchRow
|
|
4667
|
+
*/
|
|
4668
|
+
/**
|
|
4669
|
+
* Render a search call as a result card.
|
|
4670
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4671
|
+
* @returns the search card, or the generic shell with the result text.
|
|
4672
|
+
*/
|
|
4673
|
+
function SearchRow(props) {
|
|
4674
|
+
const { t, block } = props;
|
|
4675
|
+
const title = cardTitle(props.toolName, t, "title.grep");
|
|
4676
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4677
|
+
t,
|
|
4678
|
+
title,
|
|
4679
|
+
block,
|
|
4680
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchCard, {
|
|
4681
|
+
...props,
|
|
4682
|
+
title
|
|
4683
|
+
})
|
|
4684
|
+
});
|
|
4685
|
+
}
|
|
4686
|
+
/**
|
|
4687
|
+
* The card itself, inside the boundary.
|
|
4688
|
+
* @param props - row props plus the resolved title.
|
|
4689
|
+
* @returns the search card.
|
|
4690
|
+
*/
|
|
4691
|
+
function SearchCard(props) {
|
|
4692
|
+
const { t, block, inspect, title } = props;
|
|
4693
|
+
const args = parseCardArgs(block);
|
|
4694
|
+
const meta = narrowSearchCardMeta(cardMeta(block));
|
|
4695
|
+
if (isSubCall(block) || block.isError === true || meta === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4696
|
+
t,
|
|
4697
|
+
title,
|
|
4698
|
+
block,
|
|
4699
|
+
args,
|
|
4700
|
+
inspect
|
|
4701
|
+
});
|
|
4702
|
+
const subject = argText(args, "pattern") ?? argText(args, "pat") ?? argText(args, "path");
|
|
4703
|
+
const labels = searchBlockLabels(t);
|
|
4704
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4705
|
+
t,
|
|
4706
|
+
state: cardState(block),
|
|
4707
|
+
title,
|
|
4708
|
+
summary: subject ?? argsSummary(args, t),
|
|
4709
|
+
inspect,
|
|
4710
|
+
children: meta.shape === "matches" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
|
|
4711
|
+
kind: "matches",
|
|
4712
|
+
files: meta.files,
|
|
4713
|
+
truncated: meta.truncated,
|
|
4714
|
+
total: meta.total,
|
|
4715
|
+
labels,
|
|
4716
|
+
className: "twc-body"
|
|
4717
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
|
|
4718
|
+
kind: "paths",
|
|
4719
|
+
paths: meta.paths,
|
|
4720
|
+
truncated: meta.truncated,
|
|
4721
|
+
total: meta.total,
|
|
4722
|
+
labels,
|
|
4723
|
+
className: "twc-body"
|
|
4724
|
+
})
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
//#endregion
|
|
4728
|
+
//#region src/web/client/rows/TerminalRow.tsx
|
|
4729
|
+
/**
|
|
4730
|
+
* `bash` row: a terminal card for foreground runs, a generic row for jobs.
|
|
4731
|
+
*
|
|
4732
|
+
* The card's command comes from the arguments, its working directory from the
|
|
4733
|
+
* arguments falling back to the Host projection and then the session root, and
|
|
4734
|
+
* its exit status from the Host projection — the trailing `[exit code: N]`
|
|
4735
|
+
* marker is only parsed to keep it out of the output pane (and to recover the
|
|
4736
|
+
* signal name the projection does not carry). A call the Host handed to a
|
|
4737
|
+
* background job shows the job id instead: its result is a hand-off notice,
|
|
4738
|
+
* not a finished command.
|
|
4739
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/TerminalRow
|
|
4740
|
+
*/
|
|
4741
|
+
/**
|
|
4742
|
+
* Render a `bash` call as a terminal card.
|
|
4743
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4744
|
+
* @returns the terminal card, the job hand-off row, or the generic shell.
|
|
4745
|
+
*/
|
|
4746
|
+
function TerminalRow(props) {
|
|
4747
|
+
const { t, block } = props;
|
|
4748
|
+
const title = cardTitle(props.toolName, t, "title.bash");
|
|
4749
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4750
|
+
t,
|
|
4751
|
+
title,
|
|
4752
|
+
block,
|
|
4753
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalCard, {
|
|
4754
|
+
...props,
|
|
4755
|
+
title
|
|
4756
|
+
})
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* The card itself, inside the boundary.
|
|
4761
|
+
* @param props - row props plus the resolved title.
|
|
4762
|
+
* @returns the terminal card or the generic shell.
|
|
4763
|
+
*/
|
|
4764
|
+
function TerminalCard(props) {
|
|
4765
|
+
const { t, block, cwd: sessionCwd, home, inspect, title } = props;
|
|
4766
|
+
const args = parseCardArgs(block);
|
|
4767
|
+
const meta = narrowTerminalCardMeta(cardMeta(block));
|
|
4768
|
+
const command = argText(args, "command");
|
|
4769
|
+
const state = cardState(block);
|
|
4770
|
+
if (block.isError === true || command === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4771
|
+
t,
|
|
4772
|
+
title,
|
|
4773
|
+
block,
|
|
4774
|
+
args,
|
|
4775
|
+
inspect
|
|
4776
|
+
});
|
|
4777
|
+
if (meta !== null && meta.mode === "background") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4778
|
+
t,
|
|
4779
|
+
state,
|
|
4780
|
+
title,
|
|
4781
|
+
summary: t("bash.background", { jobId: meta.jobId }),
|
|
4782
|
+
text: resultText(block),
|
|
4783
|
+
inspect
|
|
4784
|
+
});
|
|
4785
|
+
const foreground = meta !== null && meta.mode === "foreground" ? meta : null;
|
|
4786
|
+
const status = parseShellStatus(resultText(block));
|
|
4787
|
+
const running = state === "running";
|
|
4788
|
+
const timedOut = status.timedOut || foreground?.timedOut === true;
|
|
4789
|
+
const aborted = foreground?.aborted === true;
|
|
4790
|
+
const interrupted = timedOut || aborted;
|
|
4791
|
+
const exitCode = running || interrupted ? void 0 : typeof foreground?.exitCode === "number" ? foreground.exitCode : typeof status.exitCode === "number" ? status.exitCode : void 0;
|
|
4792
|
+
const workdir = argText(args, "workdir") ?? foreground?.workingDir ?? sessionCwd;
|
|
4793
|
+
const summary = interrupted ? timedOut ? t("timedOut") : t("cancelled") : argText(args, "description") ?? firstLine(command);
|
|
4794
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4795
|
+
t,
|
|
4796
|
+
state,
|
|
4797
|
+
title,
|
|
4798
|
+
summary,
|
|
4799
|
+
inspect,
|
|
4800
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
|
|
4801
|
+
command,
|
|
4802
|
+
cwd: workdir,
|
|
4803
|
+
home,
|
|
4804
|
+
output: status.output,
|
|
4805
|
+
exitCode,
|
|
4806
|
+
signal: status.signal,
|
|
4807
|
+
running,
|
|
4808
|
+
labels: terminalBlockLabels(t),
|
|
4809
|
+
className: "twc-body"
|
|
4810
|
+
})
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
//#endregion
|
|
4814
|
+
//#region src/web/client/rows/WriteRow.tsx
|
|
4815
|
+
/**
|
|
4816
|
+
* `write` row: a content preview card.
|
|
4817
|
+
*
|
|
4818
|
+
* The engine's write card is a preview of what was written, not a diff, and
|
|
4819
|
+
* the content is in the arguments — so the card is complete while the call is
|
|
4820
|
+
* still running, and the line count is counted on the client exactly as the
|
|
4821
|
+
* engine's own header does. The language hint comes from the Host projection
|
|
4822
|
+
* (`meta.lang`), which is the only part the arguments cannot supply.
|
|
4823
|
+
* @module @xiaoso/dsh-tool-plus/web/client/rows/WriteRow
|
|
4824
|
+
*/
|
|
4825
|
+
/**
|
|
4826
|
+
* Render a `write` call as a content preview.
|
|
4827
|
+
* @param props - the slot's owner props and this plugin's translate seat.
|
|
4828
|
+
* @returns the card, or the generic shell when the arguments are unusable.
|
|
4829
|
+
*/
|
|
4830
|
+
function WriteRow(props) {
|
|
4831
|
+
const { t, block } = props;
|
|
4832
|
+
const title = cardTitle(props.toolName, t, "title.write");
|
|
4833
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
|
|
4834
|
+
t,
|
|
4835
|
+
title,
|
|
4836
|
+
block,
|
|
4837
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WriteCard, {
|
|
4838
|
+
...props,
|
|
4839
|
+
title
|
|
4840
|
+
})
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
/**
|
|
4844
|
+
* The card itself, inside the boundary.
|
|
4845
|
+
* @param props - row props plus the resolved title.
|
|
4846
|
+
* @returns the preview card.
|
|
4847
|
+
*/
|
|
4848
|
+
function WriteCard(props) {
|
|
4849
|
+
const { t, block, cwd, inspect, openFile, title } = props;
|
|
4850
|
+
const args = parseCardArgs(block);
|
|
4851
|
+
const meta = narrowWriteCardMeta(cardMeta(block));
|
|
4852
|
+
const content = argText(args, "content");
|
|
4853
|
+
const path = meta?.path ?? argText(args, "path");
|
|
4854
|
+
if (isSubCall(block) || block.isError === true || content === null || path === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
|
|
4855
|
+
t,
|
|
4856
|
+
title,
|
|
4857
|
+
block,
|
|
4858
|
+
args,
|
|
4859
|
+
inspect
|
|
4860
|
+
});
|
|
4861
|
+
const lines = t("write.lines", { count: lineCount(content) });
|
|
4862
|
+
const skip = meta?.madeExecutable === true ? ` · ${t("write.madeExecutable")}` : "";
|
|
4863
|
+
const output = resultText(block);
|
|
4864
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
|
|
4865
|
+
t,
|
|
4866
|
+
state: cardState(block),
|
|
4867
|
+
title,
|
|
4868
|
+
summary: displayPath(path, cwd),
|
|
4869
|
+
summarySuffix: lines + skip,
|
|
4870
|
+
filePath: path,
|
|
4871
|
+
openFile,
|
|
4872
|
+
inspect,
|
|
4873
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4874
|
+
className: "twc-body",
|
|
4875
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
|
|
4876
|
+
code: content,
|
|
4877
|
+
lang: meta?.lang,
|
|
4878
|
+
lineNumbers: true,
|
|
4879
|
+
copyLabel: t("copy"),
|
|
4880
|
+
copiedLabel: t("copied")
|
|
4881
|
+
}), output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4882
|
+
className: "twc-note",
|
|
4883
|
+
children: output
|
|
4884
|
+
})]
|
|
4885
|
+
})
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4888
|
+
//#endregion
|
|
4889
|
+
//#region src/web/client/registerToolCards.ts
|
|
4890
|
+
/** The shipped atomic tool-view slot every card registers into. */
|
|
4891
|
+
const TOOLVIEW_SLOT = "tool.call.toolview";
|
|
4892
|
+
/** Settings namespace of this plugin (the browser half spells the Host's). */
|
|
4893
|
+
const SETTINGS_NS = "tool-plus";
|
|
4894
|
+
/** Every row component this module owns, for the winner self-check. */
|
|
4895
|
+
const OWN_COMPONENTS = /* @__PURE__ */ new Set([
|
|
4896
|
+
WriteRow,
|
|
4897
|
+
FileMutationRow,
|
|
4898
|
+
AstEditRow,
|
|
4899
|
+
ReadRow,
|
|
4900
|
+
SearchRow,
|
|
4901
|
+
TerminalRow
|
|
4902
|
+
]);
|
|
4903
|
+
/**
|
|
4904
|
+
* Mount the tool cards.
|
|
4905
|
+
*
|
|
4906
|
+
* Registers the card dictionaries and one entry per tool key, then keeps the
|
|
4907
|
+
* registration in step with the `webCards` switch. Everything
|
|
4908
|
+
* installed here rides the caller's fiber, so plugin unload removes it all.
|
|
4909
|
+
* @param ctx - the browser plugin context.
|
|
4910
|
+
*/
|
|
4911
|
+
function registerToolCards(ctx) {
|
|
4912
|
+
ctx.effect(() => ctx.locale.register(CARD_LOCALE_NS, cardLocales), "tool-plus: card dictionaries");
|
|
4913
|
+
let disposeEntries;
|
|
4914
|
+
const installEntries = () => {
|
|
4915
|
+
const disposers = [
|
|
4916
|
+
ctx.slots.register({
|
|
4917
|
+
name: TOOLVIEW_SLOT,
|
|
4918
|
+
key: "write",
|
|
4919
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4920
|
+
locale: CARD_LOCALE_NS
|
|
4921
|
+
}, WriteRow),
|
|
4922
|
+
ctx.slots.register({
|
|
4923
|
+
name: TOOLVIEW_SLOT,
|
|
4924
|
+
key: "edit",
|
|
4925
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4926
|
+
locale: CARD_LOCALE_NS
|
|
4927
|
+
}, FileMutationRow),
|
|
4928
|
+
ctx.slots.register({
|
|
4929
|
+
name: TOOLVIEW_SLOT,
|
|
4930
|
+
key: "ast_edit",
|
|
4931
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4932
|
+
locale: CARD_LOCALE_NS
|
|
4933
|
+
}, AstEditRow),
|
|
4934
|
+
ctx.slots.register({
|
|
4935
|
+
name: TOOLVIEW_SLOT,
|
|
4936
|
+
key: "read",
|
|
4937
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4938
|
+
locale: CARD_LOCALE_NS
|
|
4939
|
+
}, ReadRow),
|
|
4940
|
+
ctx.slots.register({
|
|
4941
|
+
name: TOOLVIEW_SLOT,
|
|
4942
|
+
key: "grep",
|
|
4943
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4944
|
+
locale: CARD_LOCALE_NS
|
|
4945
|
+
}, SearchRow),
|
|
4946
|
+
ctx.slots.register({
|
|
4947
|
+
name: TOOLVIEW_SLOT,
|
|
4948
|
+
key: "glob",
|
|
4949
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4950
|
+
locale: CARD_LOCALE_NS
|
|
4951
|
+
}, SearchRow),
|
|
4952
|
+
ctx.slots.register({
|
|
4953
|
+
name: TOOLVIEW_SLOT,
|
|
4954
|
+
key: "ast_grep",
|
|
4955
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4956
|
+
locale: CARD_LOCALE_NS
|
|
4957
|
+
}, SearchRow),
|
|
4958
|
+
ctx.slots.register({
|
|
4959
|
+
name: TOOLVIEW_SLOT,
|
|
4960
|
+
key: "bash",
|
|
4961
|
+
priority: CARD_TAKEOVER_PRIORITY,
|
|
4962
|
+
locale: CARD_LOCALE_NS
|
|
4963
|
+
}, TerminalRow)
|
|
4964
|
+
];
|
|
4965
|
+
if (!winnersAreOurs(ctx)) console.error(`[tool-plus] web tool cards did not win ${TOOLVIEW_SLOT} at priority ${String(CARD_TAKEOVER_PRIORITY)} for: ${lostCardKeys(ctx).join(", ")}. Keeping our entries; another component renders those cells.`);
|
|
4966
|
+
return disposers;
|
|
4967
|
+
};
|
|
4968
|
+
const install = () => {
|
|
4969
|
+
if (disposeEntries !== void 0) return;
|
|
4970
|
+
disposeEntries = ctx.slots.inject(TOOLVIEW_SLOT, () => installEntries());
|
|
4971
|
+
};
|
|
4972
|
+
const uninstall = () => {
|
|
4973
|
+
disposeEntries?.();
|
|
4974
|
+
disposeEntries = void 0;
|
|
4975
|
+
};
|
|
4976
|
+
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
|
|
4977
|
+
ctx.effect(() => {
|
|
4978
|
+
const sync = () => {
|
|
4979
|
+
if (cardsEnabled(scope.getSnapshot().value)) install();
|
|
4980
|
+
else uninstall();
|
|
4981
|
+
};
|
|
4982
|
+
const stop = scope.subscribe(sync);
|
|
4983
|
+
sync();
|
|
4984
|
+
return stop;
|
|
4985
|
+
}, "tool-plus: web card switch");
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* Whether the cards should be mounted for one settings document.
|
|
4989
|
+
*
|
|
4990
|
+
* An absent field (first load, namespace not exposed to this client, plugin not
|
|
4991
|
+
* yet described) means on: the shipped rows are the fallback, not the target.
|
|
4992
|
+
* @param value - the namespace's last accepted section, when there is one.
|
|
4993
|
+
* @returns `true` unless the field is explicitly `false`.
|
|
4994
|
+
*/
|
|
4995
|
+
function cardsEnabled(value) {
|
|
4996
|
+
return value?.webCards !== false;
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Read the current cell winners back and report whether every one of them is a
|
|
5000
|
+
* row this module registered.
|
|
5001
|
+
*
|
|
5002
|
+
* Called immediately after registering, while the ledger is synchronously
|
|
5003
|
+
* consistent: a cell whose winner is some other component means our entry did
|
|
5004
|
+
* not take the cell — something outranks it, or a boundary retired it.
|
|
5005
|
+
* @param ctx - the browser plugin context owning this registration.
|
|
5006
|
+
* @returns `true` when every card key resolves to one of this module's rows.
|
|
5007
|
+
*/
|
|
5008
|
+
function winnersAreOurs(ctx) {
|
|
5009
|
+
return lostCardKeys(ctx).length === 0;
|
|
5010
|
+
}
|
|
5011
|
+
/**
|
|
5012
|
+
* The card keys whose current cell winner is not one of this module's rows.
|
|
5013
|
+
*
|
|
5014
|
+
* The takeover assumption is unverified host behaviour, so a lost cell is
|
|
5015
|
+
* reported by name rather than swallowed.
|
|
5016
|
+
* @param ctx - the browser plugin context owning this registration.
|
|
5017
|
+
* @returns the lost keys, in contract order (empty when every cell is ours).
|
|
5018
|
+
*/
|
|
5019
|
+
function lostCardKeys(ctx) {
|
|
5020
|
+
const winners = ctx.slots.entriesOfSlot(TOOLVIEW_SLOT);
|
|
5021
|
+
const lost = [];
|
|
5022
|
+
for (const key of CARD_TOOL_KEYS) {
|
|
5023
|
+
const winner = winners.find((entry) => entry.options.key === key);
|
|
5024
|
+
if (winner === void 0 || !OWN_COMPONENTS.has(winner.component)) lost.push(key);
|
|
5025
|
+
}
|
|
5026
|
+
return lost;
|
|
5027
|
+
}
|
|
5028
|
+
//#endregion
|
|
3004
5029
|
//#region src/client/index.ts
|
|
3005
5030
|
/**
|
|
3006
5031
|
* Settings namespace of this plugin, spelled here because a client package
|
|
@@ -3022,8 +5047,8 @@ window.__ModuleLoader__.load({
|
|
|
3022
5047
|
*/
|
|
3023
5048
|
function apply(ctx) {
|
|
3024
5049
|
ctx.effect(() => ctx.locale.register(BASH_PLUS_LOCALE_NS, {
|
|
3025
|
-
zh,
|
|
3026
|
-
en
|
|
5050
|
+
zh: zh$1,
|
|
5051
|
+
en: en$1
|
|
3027
5052
|
}), "tool-plus: card dictionaries");
|
|
3028
5053
|
const scope = ctx.settingsScope.bind({ namespace: BASH_PLUS_CLIENT_NS });
|
|
3029
5054
|
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
@@ -3032,6 +5057,7 @@ window.__ModuleLoader__.load({
|
|
|
3032
5057
|
locale: BASH_PLUS_LOCALE_NS,
|
|
3033
5058
|
inject: () => ({ scope })
|
|
3034
5059
|
}, BashPlusCard));
|
|
5060
|
+
registerToolCards(ctx);
|
|
3035
5061
|
const sectionScope = ctx.settingsScope.bind({ namespace: BASH_PLUS_CLIENT_NS });
|
|
3036
5062
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3037
5063
|
name: "settings.section",
|