@huanlin/dsh-plugin-merge-tool-calls 0.2.2 → 0.3.0

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/client.js CHANGED
@@ -27,8 +27,6 @@ let react = require("react");
27
27
  react = __toESM(react);
28
28
  let __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
29
29
  __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
30
- let __deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
31
- __deepseek_ai_dsh_client_runtime_client = __toESM(__deepseek_ai_dsh_client_runtime_client);
32
30
  let react_jsx_runtime = require("react/jsx-runtime");
33
31
  react_jsx_runtime = __toESM(react_jsx_runtime);
34
32
 
@@ -576,6 +574,8 @@ const zh = {
576
574
  countCommands: "{n} 条命令",
577
575
  countPrograms: "{n} 段代码",
578
576
  countCalls: "{n} 次调用",
577
+ copy: "复制",
578
+ copied: "已复制",
579
579
  "terminal.signal": "信号 {signal}",
580
580
  "terminal.exitCode": "退出码 {code}",
581
581
  "terminal.running": "运行中",
@@ -587,7 +587,28 @@ const zh = {
587
587
  "terminal.collapseAria": "收起输出",
588
588
  "terminal.collapse": "折叠",
589
589
  "terminal.expandAria": "展开其余 {n} 行输出",
590
- "terminal.expandRest": "… 其余 {n} 行"
590
+ "terminal.expandRest": "… 其余 {n} 行",
591
+ "read.window": "显示 {shown} / {total} 行",
592
+ "read.collapseAria": "收起内容",
593
+ "read.expandAria": "展开其余 {count} 行",
594
+ "read.expandRest": "… 其余 {count} 行",
595
+ "search.paths": "{shown} 个路径",
596
+ "search.paths.truncated": "显示 {shown} / 共 {total} 个路径",
597
+ "search.matches": "{shown} 处匹配 · {files} 个文件",
598
+ "search.matches.truncated": "显示 {shown} / 共 {total} 处匹配 · {files} 个文件",
599
+ "search.noResults": "无结果",
600
+ "search.collapseAria": "收起结果",
601
+ "search.expandAria": "展开其余 {count} 行结果",
602
+ "search.expandRest": "… 其余 {count} 行",
603
+ "diff.collapseAria": "收起差异",
604
+ "diff.expandAria": "展开其余 {count} 行差异",
605
+ "diff.expandRest": "… 其余 {count} 行",
606
+ "diff.files": "{count} 个文件",
607
+ "web.noResults": "未找到结果",
608
+ "web.sourcesTruncated": "来源列表已截断",
609
+ "web.http": "HTTP",
610
+ "web.contentTruncated": "内容已截断",
611
+ "markdown.footnotes": "脚注"
591
612
  };
592
613
  const en = {
593
614
  running: "Running",
@@ -603,6 +624,8 @@ const en = {
603
624
  countCommands: "{n} Commands",
604
625
  countPrograms: "{n} Programs",
605
626
  countCalls: "{n} Calls",
627
+ copy: "Copy",
628
+ copied: "Copied",
606
629
  "terminal.signal": "signal {signal}",
607
630
  "terminal.exitCode": "exit code {code}",
608
631
  "terminal.running": "Running",
@@ -614,7 +637,28 @@ const en = {
614
637
  "terminal.collapseAria": "Collapse output",
615
638
  "terminal.collapse": "Collapse",
616
639
  "terminal.expandAria": "Expand the remaining {n} output lines",
617
- "terminal.expandRest": "… {n} more lines"
640
+ "terminal.expandRest": "… {n} more lines",
641
+ "read.window": "Showing {shown} of {total} lines",
642
+ "read.collapseAria": "Collapse content",
643
+ "read.expandAria": "Expand {count} more lines",
644
+ "read.expandRest": "… {count} more lines",
645
+ "search.paths": "{shown} paths",
646
+ "search.paths.truncated": "Showing {shown} of {total} paths",
647
+ "search.matches": "{shown} matches · {files} files",
648
+ "search.matches.truncated": "Showing {shown} of {total} matches · {files} files",
649
+ "search.noResults": "No results",
650
+ "search.collapseAria": "Collapse results",
651
+ "search.expandAria": "Expand {count} more result lines",
652
+ "search.expandRest": "… {count} more lines",
653
+ "diff.collapseAria": "Collapse diff",
654
+ "diff.expandAria": "Expand {count} more diff lines",
655
+ "diff.expandRest": "… {count} more lines",
656
+ "diff.files": "{count} files",
657
+ "web.noResults": "No results found",
658
+ "web.sourcesTruncated": "Source list truncated",
659
+ "web.http": "HTTP",
660
+ "web.contentTruncated": "Content truncated",
661
+ "markdown.footnotes": "Footnotes"
618
662
  };
619
663
  const NS = "merge-tool-calls";
620
664
 
@@ -694,6 +738,36 @@ function classifyTool(toolName) {
694
738
 
695
739
  //#endregion
696
740
  //#region src/client/card-model.ts
741
+ /**
742
+ * Parse the call head paired with one immutable Tool block.
743
+ * @param block - running or settled Tool block.
744
+ * @returns the Tool name and object arguments, or null when unavailable.
745
+ */
746
+ function parsedArgsOf(block) {
747
+ const call = "kind" in block ? block.call : block;
748
+ if (call === null) return null;
749
+ let value;
750
+ try {
751
+ value = JSON.parse(call.argsRaw);
752
+ } catch {
753
+ return null;
754
+ }
755
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
756
+ return {
757
+ name: call.name,
758
+ args: value
759
+ };
760
+ }
761
+ /**
762
+ * Read the exact single text block consumed by first-party card derivations.
763
+ * @param block - settled Tool result.
764
+ * @returns its text, or undefined for any other content layout.
765
+ */
766
+ function singleResultText(block) {
767
+ if (block.content.length !== 1) return void 0;
768
+ const only = block.content[0];
769
+ return only?.type === "text" ? only.text : void 0;
770
+ }
697
771
  /** True when a settled terminal card reports a failing exit (mirrors ui-tool). */
698
772
  function terminalFailed(model) {
699
773
  const { exitCode, signal, running } = model.card;
@@ -707,7 +781,7 @@ function terminalFailed(model) {
707
781
  function resultText(node) {
708
782
  const parts = [];
709
783
  for (const block of node.content) if (block.type === "text") parts.push(block.text);
710
- else parts.push(JSON.stringify(block));
784
+ else parts.push(JSON.stringify(block, null, 2));
711
785
  if (parts.length === 0 && node.error !== void 0) parts.push(`${node.error.name}: ${node.error.code}`);
712
786
  return parts.join("\n");
713
787
  }
@@ -722,6 +796,36 @@ function relativizeToCwd(text, cwd) {
722
796
  if (text.startsWith(`${root}/`) || text.startsWith(`${root}\\`)) return text.slice(root.length + 1);
723
797
  return text;
724
798
  }
799
+ /**
800
+ * Resolve a Workspace-relative path into the Host-facing spelling (local
801
+ * mirror of `@deepseek-ai/dsh-util-workspace-path`'s `resolveWorkspacePath`:
802
+ * that static utility package is not a dynamic client bundle row, so the
803
+ * browser half carries this one-function copy).
804
+ * @param cwd - Session Workspace root, when known.
805
+ * @param path - Absolute or Workspace-relative path.
806
+ * @returns an absolute path when a Workspace root is available, otherwise the original path.
807
+ */
808
+ function resolveWorkspacePath(cwd, path) {
809
+ if (path.startsWith("/") || /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\")) return path;
810
+ if (cwd === void 0 || cwd === "") return path;
811
+ return `${cwd.replace(/[/\\]+$/, "")}/${path.replace(/^[/\\]+/, "")}`;
812
+ }
813
+ /**
814
+ * Abbreviate a POSIX home directory for display (local mirror of
815
+ * `@deepseek-ai/dsh-util-workspace-path`'s `abbreviateHomePath`).
816
+ * @param path - Absolute or already-short display path.
817
+ * @param home - Host account home; absent skips abbreviation.
818
+ * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
819
+ */
820
+ function abbreviateHomePath(path, home) {
821
+ if (home === void 0 || home === "") return path;
822
+ if (/^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") || /^[A-Za-z]:[/\\]/.test(home) || home.startsWith("\\\\")) return path;
823
+ const root = home.replace(/\/+$/, "");
824
+ if (root === "" || root === "/") return path;
825
+ if (path.replace(/\/+$/, "") === root) return "~";
826
+ if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`;
827
+ return path;
828
+ }
725
829
  function parseArgs(argsRaw) {
726
830
  try {
727
831
  const parsed = JSON.parse(argsRaw);
@@ -757,6 +861,10 @@ const SUMMARY_KEYS = {
757
861
  function deriveSummary(variant, argsRaw) {
758
862
  const parsed = parseArgs(argsRaw);
759
863
  if (parsed === void 0) return firstLine(argsRaw);
864
+ if (variant === "search" && Array.isArray(parsed.queries)) {
865
+ const queries = parsed.queries.filter((query) => typeof query === "string" && query !== "");
866
+ if (queries.length > 0) return queries.map(firstLine).join(", ");
867
+ }
760
868
  const picked = pickString(parsed, SUMMARY_KEYS[variant]);
761
869
  if (picked !== void 0) return firstLine(picked);
762
870
  for (const value of Object.values(parsed)) if (typeof value === "string" && value !== "") return firstLine(value);
@@ -787,23 +895,23 @@ function deriveBody(variant, argsRaw) {
787
895
  return JSON.stringify(parsed, null, 2);
788
896
  }
789
897
  /** Derive the row model for one call of a grouped tool. */
790
- function callRowModel(toolName, block, cwd) {
898
+ function callRowModel(toolName, block, cwd, home) {
791
899
  const done = "kind" in block;
792
900
  const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? "";
793
901
  const state = !done ? "running" : block.error?.code === "interrupted" ? "stopped" : block.isError ? "error" : "ok";
794
902
  const variant = classifyTool(toolName);
795
903
  const terminal = terminalCardOf(block, cwd);
796
- const read = readCardOf(block, cwd);
904
+ const read = readCardOf(block, cwd, home);
797
905
  const diff = diffCardOf(block);
798
906
  const search = searchCardOf(block);
799
907
  const web = webCardOf(block);
800
- const base = argsRaw === "" ? block.callId : relativizeToCwd(deriveSummary(variant, argsRaw), cwd);
908
+ const base = argsRaw === "" ? block.callId : abbreviateHomePath(relativizeToCwd(deriveSummary(variant, argsRaw), cwd), home);
801
909
  const toolTitle = TOOL_TITLES[toolName];
802
910
  const argsSummary = variant === "others" && toolName !== "" && toolTitle === void 0 ? `${toolName} · ${base}` : base;
803
911
  const filePath = deriveFilePath(variant, argsRaw);
804
912
  const output = done ? resultText(block) || null : null;
805
913
  const errorSummary = state === "error" && output !== null ? firstLine(output) : null;
806
- const summary = terminal?.description ?? search?.title ?? argsSummary;
914
+ const summary = terminal?.description ?? argsSummary;
807
915
  const body = filePath !== void 0 ? null : deriveBody(variant, argsRaw);
808
916
  const finalState = state === "ok" && terminal !== null && terminalFailed(terminal) ? "error" : state;
809
917
  const expandable = terminal !== null || diff !== null || read !== null || search !== null || web !== null || body !== null || output !== null;
@@ -825,20 +933,50 @@ function callRowModel(toolName, block, cwd) {
825
933
  };
826
934
  }
827
935
  /** Read-card derivation, or null when this call is not a read card (mirrors ui-tool). */
828
- function readCardOf(block, cwd) {
829
- if (!("kind" in block)) return null;
830
- const result = block.resultView?.card === "read" ? block.resultView : null;
831
- if (result === null) return null;
832
- if (!Array.isArray(result.lines)) return null;
833
- const lines = result.lines.filter((line) => typeof line === "object" && line !== null && typeof line.number === "number" && typeof line.text === "string").map((line) => ({
834
- number: line.number,
835
- text: line.text
836
- }));
936
+ function readCardOf(block, cwd, home) {
937
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
938
+ const parsed = parsedArgsOf(block);
939
+ if (parsed?.name !== "read") return null;
940
+ const { file_path: path, offset, limit } = parsed.args;
941
+ if (typeof path !== "string" || path.trim() === "") return null;
942
+ if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)) return null;
943
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)) return null;
944
+ const meta = readMeta(block.meta);
945
+ if (meta === null) return null;
946
+ const text = singleResultText(block);
947
+ if (text === void 0) return null;
948
+ if (/^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u.exec(text)?.[1] === void 0) return null;
949
+ return {
950
+ label: abbreviateHomePath(relativizeToCwd(meta.path, cwd), home),
951
+ lines: meta.lines,
952
+ totalLines: meta.totalLines,
953
+ lang: meta.lang
954
+ };
955
+ }
956
+ function readMeta(meta) {
957
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
958
+ const { path, offset, lines, totalLines, lang } = meta;
959
+ if (typeof path !== "string" || typeof offset !== "number" || !Number.isInteger(offset) || offset < 1) return null;
960
+ if (typeof totalLines !== "number" || !Number.isInteger(totalLines) || totalLines < 0 || !Array.isArray(lines)) return null;
961
+ if (lang !== void 0 && typeof lang !== "string") return null;
962
+ const narrowed = [];
963
+ let previous = offset - 1;
964
+ for (const line of lines) {
965
+ if (typeof line !== "object" || line === null || Array.isArray(line)) return null;
966
+ const { number, text } = line;
967
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number <= previous) return null;
968
+ if (number > totalLines || typeof text !== "string") return null;
969
+ previous = number;
970
+ narrowed.push({
971
+ number,
972
+ text
973
+ });
974
+ }
837
975
  return {
838
- label: result.title ?? relativizeToCwd(result.path, cwd),
839
- lines,
840
- totalLines: typeof result.totalLines === "number" ? result.totalLines : lines.length,
841
- lang: typeof result.lang === "string" ? result.lang : void 0
976
+ path,
977
+ lines: narrowed,
978
+ totalLines,
979
+ ...lang === void 0 ? {} : { lang }
842
980
  };
843
981
  }
844
982
  function isValidFiles(files) {
@@ -850,39 +988,50 @@ function flattenContent(content) {
850
988
  }
851
989
  /** Search-card derivation, or null when this call is not a search card (mirrors ui-tool). */
852
990
  function searchCardOf(block) {
853
- if (!("kind" in block)) return null;
854
- const result = block.resultView?.card === "search" ? block.resultView : null;
855
- if (result === null) return null;
991
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
992
+ const parsed = parsedArgsOf(block);
993
+ if (parsed === null || parsed.name !== "grep" && parsed.name !== "glob") return null;
994
+ const { pattern, path } = parsed.args;
995
+ if (typeof pattern !== "string") return null;
996
+ if (parsed.name === "grep" && pattern === "") return null;
997
+ if (parsed.name === "glob" && pattern.trim() === "") return null;
998
+ if (path !== void 0 && (typeof path !== "string" || path.trim() === "")) return null;
999
+ if (parsed.name === "grep") {
1000
+ const include = parsed.args.include;
1001
+ if (include !== void 0 && typeof include !== "string") return null;
1002
+ }
1003
+ if (typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
1004
+ const meta = block.meta;
1005
+ if (typeof meta.truncated !== "boolean") return null;
1006
+ if (typeof meta.total !== "number" || !Number.isInteger(meta.total) || meta.total < 0) return null;
856
1007
  const common = {
857
- truncated: result.truncated,
858
- total: result.total
1008
+ truncated: meta.truncated,
1009
+ total: meta.total
859
1010
  };
860
- const recovery = result.truncated ? flattenContent(block.content) : void 0;
861
- if (result.shape === "matches") {
862
- if (!isValidFiles(result.files)) return null;
1011
+ const recovery = meta.truncated ? flattenContent(block.content) : void 0;
1012
+ if (meta.shape === "matches") {
1013
+ if (!isValidFiles(meta.files)) return null;
863
1014
  return {
864
- title: result.title,
865
1015
  recovery,
866
1016
  card: {
867
1017
  kind: "matches",
868
- files: result.files,
1018
+ files: meta.files,
869
1019
  ...common
870
1020
  }
871
1021
  };
872
1022
  }
873
- if (result.shape !== "paths") return null;
874
- if (!Array.isArray(result.paths) || !result.paths.every((path) => typeof path === "string")) return null;
1023
+ if (meta.shape !== "paths") return null;
1024
+ if (!Array.isArray(meta.paths) || !meta.paths.every((path$1) => typeof path$1 === "string")) return null;
875
1025
  return {
876
- title: result.title,
877
1026
  recovery,
878
1027
  card: {
879
1028
  kind: "paths",
880
- paths: result.paths,
1029
+ paths: meta.paths,
881
1030
  ...common
882
1031
  }
883
1032
  };
884
1033
  }
885
- /** Narrow a wire `card:'diff'` view's `diffs` to well-formed hunks (mirrors ui-tool). */
1034
+ /** Narrow a result metadata `diffs` to well-formed hunks (mirrors ui-tool). */
886
1035
  function narrowDiffs(diffs) {
887
1036
  if (!Array.isArray(diffs) || diffs.length === 0) return null;
888
1037
  const out = [];
@@ -900,86 +1049,262 @@ function narrowDiffs(diffs) {
900
1049
  }
901
1050
  return out;
902
1051
  }
1052
+ function intendedDiff(block) {
1053
+ const parsed = parsedArgsOf(block);
1054
+ if (parsed === null) return null;
1055
+ if (parsed.name === "str_replace_editor") {
1056
+ const { command, path: path$1, file_text: fileText, old_str: oldText$1, new_str: newText$1 } = parsed.args;
1057
+ if (typeof path$1 !== "string" || path$1.trim() === "") return null;
1058
+ if (command === "create") {
1059
+ if (fileText !== void 0 && typeof fileText !== "string") return null;
1060
+ return {
1061
+ tool: "str_replace_editor",
1062
+ diff: {
1063
+ path: path$1,
1064
+ oldText: null,
1065
+ newText: fileText ?? ""
1066
+ }
1067
+ };
1068
+ }
1069
+ if (command === "str_replace") {
1070
+ if (oldText$1 !== void 0 && typeof oldText$1 !== "string") return null;
1071
+ if (newText$1 !== void 0 && typeof newText$1 !== "string") return null;
1072
+ return {
1073
+ tool: "str_replace_editor",
1074
+ diff: {
1075
+ path: path$1,
1076
+ oldText: oldText$1 ?? null,
1077
+ newText: newText$1 ?? ""
1078
+ }
1079
+ };
1080
+ }
1081
+ return null;
1082
+ }
1083
+ const { file_path: path } = parsed.args;
1084
+ if (typeof path !== "string" || path.trim() === "") return null;
1085
+ if (parsed.name === "write") {
1086
+ const { content } = parsed.args;
1087
+ return typeof content === "string" ? {
1088
+ tool: "write",
1089
+ diff: {
1090
+ path,
1091
+ oldText: null,
1092
+ newText: content
1093
+ }
1094
+ } : null;
1095
+ }
1096
+ if (parsed.name !== "edit") return null;
1097
+ const { old_string: oldText, new_string: newText, replace_all: replaceAll } = parsed.args;
1098
+ if (typeof oldText !== "string" || typeof newText !== "string") return null;
1099
+ if (replaceAll !== void 0 && typeof replaceAll !== "boolean") return null;
1100
+ return {
1101
+ tool: "edit",
1102
+ diff: {
1103
+ path,
1104
+ oldText: oldText || null,
1105
+ newText
1106
+ }
1107
+ };
1108
+ }
1109
+ function appliedDiffs(meta) {
1110
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
1111
+ const diffs = meta.diffs;
1112
+ if (!Array.isArray(diffs)) return null;
1113
+ if (diffs.length === 0) return "empty";
1114
+ return narrowDiffs(diffs);
1115
+ }
903
1116
  /** Diff-card derivation, or null when this call is not a diff card (mirrors ui-tool). */
904
1117
  function diffCardOf(block) {
905
- if (!("kind" in block)) {
906
- const call = block.callView?.card === "diff" ? block.callView : null;
907
- const diffs$1 = call === null ? null : narrowDiffs(call.diffs);
908
- return diffs$1 === null ? null : { card: { diffs: diffs$1 } };
1118
+ if (block.parentCallId !== void 0) return null;
1119
+ const intended = intendedDiff(block);
1120
+ if (intended === null) return null;
1121
+ if (!("kind" in block)) return { card: { diffs: [intended.diff] } };
1122
+ if (intended.tool === "str_replace_editor") return null;
1123
+ if (block.isError) return null;
1124
+ const applied = appliedDiffs(block.meta);
1125
+ if (applied === null || applied === "empty") return intended.tool === "write" ? { card: { diffs: [intended.diff] } } : null;
1126
+ return { card: { diffs: applied } };
1127
+ }
1128
+ function shellCallOf(name, args) {
1129
+ if (name !== "bash" && name !== "pwsh") return null;
1130
+ const { command, description, timeoutMs, workdir, run_in_background: background } = args;
1131
+ if (typeof command !== "string" || command.trim() === "") return null;
1132
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0)) return null;
1133
+ if (workdir !== void 0 && typeof workdir !== "string") return null;
1134
+ if (background !== void 0 && typeof background !== "boolean") return null;
1135
+ if (description === void 0) return {
1136
+ command,
1137
+ description: void 0,
1138
+ workdir: void 0,
1139
+ persistent: true,
1140
+ background: false
1141
+ };
1142
+ if (typeof description !== "string" || description.trim() === "") return null;
1143
+ return {
1144
+ command,
1145
+ description,
1146
+ workdir,
1147
+ persistent: false,
1148
+ background: background === true
1149
+ };
1150
+ }
1151
+ /**
1152
+ * Parse the result text's trailing exit-status marker (mirrors the shell
1153
+ * renderer's literal contract without importing that Host-only package).
1154
+ * @param text - rendered shell result text.
1155
+ * @returns output with a trailing exit-code or signal marker extracted.
1156
+ */
1157
+ function parseExitStatus(text) {
1158
+ const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
1159
+ if (signal?.[1] !== void 0) return {
1160
+ output: text.slice(0, signal.index),
1161
+ signal: signal[1]
1162
+ };
1163
+ const exit = /\n\[exit code: (\d+)\]$/.exec(text);
1164
+ if (exit?.[1] !== void 0) return {
1165
+ output: text.slice(0, exit.index),
1166
+ exitCode: Number(exit[1])
1167
+ };
1168
+ return {
1169
+ output: text,
1170
+ exitCode: 0
1171
+ };
1172
+ }
1173
+ /**
1174
+ * Collapse `.` and `..` segments so the prompt label names the directory the
1175
+ * command actually ran in (local mirror of ui-tool's terminal normalization).
1176
+ * @param path - a joined or absolute path, possibly carrying `.`/`..` segments.
1177
+ * @returns the same path with those segments resolved.
1178
+ */
1179
+ function normalizeSegments(path) {
1180
+ if (!/(?:^|[/\\])\.\.?(?:[/\\]|$)/.test(path)) return path;
1181
+ const separator = path.includes("\\") && !path.includes("/") ? "\\" : "/";
1182
+ const rooted = /^[/\\]/.test(path);
1183
+ const drive = /^[A-Za-z]:/.exec(path)?.[0] ?? "";
1184
+ const body = collapse(path.slice(drive.length), rooted || drive !== "", separator);
1185
+ const leading = rooted ? separator : "";
1186
+ return drive === "" ? `${leading}${body}` : `${drive}${rooted ? leading : separator}${body}`;
1187
+ }
1188
+ function collapse(body, rooted, separator = "/") {
1189
+ const kept = [];
1190
+ for (const segment of body.split(/[/\\]/)) {
1191
+ if (segment === "" || segment === ".") continue;
1192
+ if (segment === "..") {
1193
+ if (kept.length > 0 && kept[kept.length - 1] !== "..") kept.pop();
1194
+ else if (!rooted) kept.push(segment);
1195
+ continue;
1196
+ }
1197
+ kept.push(segment);
909
1198
  }
910
- const result = block.resultView?.card === "diff" ? block.resultView : null;
911
- const diffs = result === null ? null : narrowDiffs(result.diffs);
912
- return diffs === null ? null : { card: { diffs } };
1199
+ return kept.join(separator);
913
1200
  }
914
1201
  /**
915
- * Resolve a terminal view's working directory the way the render-intent
916
- * contract assigns it: an absolute path is used as-is, a relative one joins
917
- * under the session workspace, and an omitted one IS the session workspace.
918
- * @param viewCwd - the cwd the terminal call view carries, if any.
1202
+ * Resolve a shell call's workdir for display: an absolute path is used as-is,
1203
+ * a relative one joins under the session workspace, and an omitted one IS the
1204
+ * session workspace.
1205
+ * @param workdir - the raw call's workdir, if any.
919
1206
  * @param sessionCwd - the session workspace root, if the caller knows it.
920
1207
  * @returns the working directory for the prompt label, or undefined.
921
1208
  */
922
- function resolveTerminalCwd(viewCwd, sessionCwd) {
923
- if (viewCwd === void 0 || viewCwd === "") return sessionCwd;
924
- if (sessionCwd === void 0 || sessionCwd === "") return viewCwd;
925
- return (0, __deepseek_ai_dsh_client_runtime_client.resolveWorkspacePath)(sessionCwd, viewCwd);
1209
+ function resolveTerminalCwd(workdir, sessionCwd) {
1210
+ if (workdir === void 0 || workdir === "") return sessionCwd;
1211
+ if (sessionCwd === void 0 || sessionCwd === "") return normalizeSegments(workdir);
1212
+ return normalizeSegments(resolveWorkspacePath(sessionCwd, workdir));
926
1213
  }
927
1214
  /** Terminal-card derivation, or null when this call is not a terminal card (mirrors ui-tool). */
928
1215
  function terminalCardOf(block, sessionCwd) {
929
- const call = block.callView?.card === "terminal" ? block.callView : null;
930
- if (!("kind" in block)) return call === null ? null : {
1216
+ if (block.parentCallId !== void 0) return null;
1217
+ const parsed = parsedArgsOf(block);
1218
+ if (parsed === null) return null;
1219
+ if (parsed.name !== "bash" && parsed.name !== "pwsh") return null;
1220
+ const call = shellCallOf(parsed.name, parsed.args);
1221
+ if (call === null || call.background) return null;
1222
+ const cwd = resolveTerminalCwd(call.workdir, sessionCwd);
1223
+ if (!("kind" in block)) return {
931
1224
  description: call.description,
932
1225
  card: {
933
- command: call.title,
934
- cwd: resolveTerminalCwd(call.cwd, sessionCwd),
1226
+ command: call.command,
1227
+ cwd,
935
1228
  output: void 0,
936
1229
  exitCode: void 0,
937
1230
  signal: void 0,
938
1231
  running: true
939
1232
  }
940
1233
  };
941
- const result = block.resultView?.card === "terminal" ? block.resultView : null;
942
- if (result === null) return null;
1234
+ if (block.isError || call.persistent) return null;
1235
+ const output = singleResultText(block);
1236
+ if (output === void 0) return null;
1237
+ const status = parseExitStatus(output);
943
1238
  return {
944
- description: call?.description,
1239
+ description: call.description,
945
1240
  card: {
946
- command: result.title ?? call?.title ?? "",
947
- cwd: call === null ? void 0 : resolveTerminalCwd(call.cwd, sessionCwd),
948
- output: result.output,
949
- exitCode: result.exitCode,
950
- signal: result.signal,
1241
+ command: call.command,
1242
+ cwd,
1243
+ output: status.output,
1244
+ exitCode: status.exitCode,
1245
+ signal: status.signal,
951
1246
  running: false
952
1247
  }
953
1248
  };
954
1249
  }
955
1250
  /** Web-card derivation, or null when this call is not a web card (mirrors ui-tool). */
956
1251
  function webCardOf(block) {
957
- if (!("kind" in block)) return null;
958
- const result = block.resultView;
959
- if (result?.card !== "web") return null;
960
- if (result.kind === "search") return {
961
- kind: "search",
962
- answer: result.answer,
963
- sources: result.sources.map((source) => ({
964
- url: source.url,
965
- title: source.title,
966
- snippet: source.snippet,
967
- publishedAt: source.publishedAt
968
- })),
969
- truncated: result.truncated
970
- };
971
- if (result.kind === "fetch") return {
1252
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
1253
+ const parsed = parsedArgsOf(block);
1254
+ if (parsed === null) return null;
1255
+ if (parsed.name === "web_search") {
1256
+ const { queries } = parsed.args;
1257
+ if (!Array.isArray(queries) || queries.length === 0) return null;
1258
+ if (!queries.every((query) => typeof query === "string" && query.trim() !== "")) return null;
1259
+ } else if (parsed.name === "web_fetch") {
1260
+ const { url } = parsed.args;
1261
+ if (typeof url !== "string" || url.trim() === "") return null;
1262
+ } else return null;
1263
+ if (typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
1264
+ const meta = block.meta;
1265
+ if (typeof meta.truncated !== "boolean") return null;
1266
+ if (parsed.name === "web_search") {
1267
+ const sources = webSources(meta.sources);
1268
+ if (sources === null || meta.answer !== void 0 && typeof meta.answer !== "string") return null;
1269
+ return {
1270
+ kind: "search",
1271
+ answer: meta.answer,
1272
+ sources,
1273
+ truncated: meta.truncated
1274
+ };
1275
+ }
1276
+ if (typeof meta.url !== "string") return null;
1277
+ if (typeof meta.statusCode !== "number" || !Number.isInteger(meta.statusCode)) return null;
1278
+ return {
972
1279
  kind: "fetch",
973
- url: result.url,
974
- statusCode: result.statusCode,
975
- truncated: result.truncated
1280
+ url: meta.url,
1281
+ statusCode: meta.statusCode,
1282
+ truncated: meta.truncated
976
1283
  };
977
- return null;
1284
+ }
1285
+ function webSources(value) {
1286
+ if (!Array.isArray(value)) return null;
1287
+ const sources = [];
1288
+ for (const source of value) {
1289
+ if (typeof source !== "object" || source === null) return null;
1290
+ const { url, title, snippet, publishedAt } = source;
1291
+ if (typeof url !== "string") return null;
1292
+ if (title !== void 0 && typeof title !== "string") return null;
1293
+ if (snippet !== void 0 && typeof snippet !== "string") return null;
1294
+ if (publishedAt !== void 0 && typeof publishedAt !== "string") return null;
1295
+ sources.push({
1296
+ url,
1297
+ ...title === void 0 ? {} : { title },
1298
+ ...snippet === void 0 ? {} : { snippet },
1299
+ ...publishedAt === void 0 ? {} : { publishedAt }
1300
+ });
1301
+ }
1302
+ return sources;
978
1303
  }
979
1304
 
980
1305
  //#endregion
981
1306
  //#region src/client/merge-run.ts
982
- /** Tool-call node kind registered by ui-conversation's built-in tool definition. */
1307
+ /** Tool-call node kind registered by ui-chat's built-in tool definition. */
983
1308
  const TOOL_CALL_KIND = "tool-call";
984
1309
  /** Extract a root call block from any chat node, when it is a tool-call node. */
985
1310
  function toolRootOf(node) {
@@ -1004,7 +1329,10 @@ function stepIdOf(node) {
1004
1329
  * Compute the merged group this call belongs to.
1005
1330
  *
1006
1331
  * 1. Locates this call's node in the chat order; null when it is not a chat
1007
- * tool-call node (e.g. a read dispatched as a subcall).
1332
+ * tool-call node (e.g. a read dispatched as a subcall). The node key
1333
+ * format stays a ui-conversation internal, so the seat finds itself by
1334
+ * scanning the store for the tool-call node owning its call id instead of
1335
+ * recomputing the key.
1008
1336
  * 2. Walks backward/forward to the maximal consecutive run containing it. A
1009
1337
  * call continues the run when it is a grouped tool AND same-tool-same-run:
1010
1338
  * the identical wire name, or a sibling of the same known variant family
@@ -1025,8 +1353,7 @@ function stepIdOf(node) {
1025
1353
  * @returns the group partition, or null when the call is not a chat tool-call node.
1026
1354
  */
1027
1355
  function readRun(order, nodes, myCallId, tools, groupBy, maxGroupSize) {
1028
- const myKey = (0, __deepseek_ai_dsh_client_runtime_client.conversationContextKey)(TOOL_CALL_KIND, myCallId);
1029
- const myIndex = order.indexOf(myKey);
1356
+ const myIndex = myIndexOf(order, nodes, myCallId);
1030
1357
  if (myIndex < 0) return null;
1031
1358
  const myRoot = rootAtNode(order, nodes, myIndex);
1032
1359
  if (myRoot === null) return null;
@@ -1061,6 +1388,17 @@ function readRun(order, nodes, myCallId, tools, groupBy, maxGroupSize) {
1061
1388
  blocks
1062
1389
  };
1063
1390
  }
1391
+ /** Index in the chat order of the tool-call node owning `callId`; -1 when absent. */
1392
+ function myIndexOf(order, nodes, callId) {
1393
+ for (let index = 0; index < order.length; index++) {
1394
+ const key = order[index];
1395
+ if (key === void 0) continue;
1396
+ const node = nodes.get(key);
1397
+ if (node?.kind !== TOOL_CALL_KIND) continue;
1398
+ if (toolRootOf(node)?.callId === callId) return index;
1399
+ }
1400
+ return -1;
1401
+ }
1064
1402
  function rootAtNode(order, nodes, index) {
1065
1403
  const key = order[index];
1066
1404
  const node = key === void 0 ? void 0 : nodes.get(key);
@@ -1137,29 +1475,100 @@ function terminalLabels(t) {
1137
1475
  expand: (hidden) => t("terminal.expandRest", { n: hidden })
1138
1476
  };
1139
1477
  }
1478
+ /** ReadBlock display copy from the plugin's own dictionary. */
1479
+ function readLabels(t) {
1480
+ return {
1481
+ window: (shown, total) => t("read.window", {
1482
+ shown,
1483
+ total
1484
+ }),
1485
+ copy: t("copy"),
1486
+ copied: t("copied"),
1487
+ collapseAria: t("read.collapseAria"),
1488
+ expandAria: (count) => t("read.expandAria", { count }),
1489
+ collapse: t("collapse"),
1490
+ expand: (count) => t("read.expandRest", { count })
1491
+ };
1492
+ }
1493
+ /** SearchBlock display copy from the plugin's own dictionary. */
1494
+ function searchLabels(t) {
1495
+ return {
1496
+ pathsSummary: (shown, total, truncated) => t(truncated ? "search.paths.truncated" : "search.paths", {
1497
+ shown,
1498
+ total
1499
+ }),
1500
+ matchesSummary: (shown, total, files, truncated) => t(truncated ? "search.matches.truncated" : "search.matches", {
1501
+ shown,
1502
+ total,
1503
+ files
1504
+ }),
1505
+ copy: t("copy"),
1506
+ copied: t("copied"),
1507
+ noResults: t("search.noResults"),
1508
+ collapseAria: t("search.collapseAria"),
1509
+ expandAria: (count) => t("search.expandAria", { count }),
1510
+ collapse: t("collapse"),
1511
+ expand: (count) => t("search.expandRest", { count })
1512
+ };
1513
+ }
1514
+ /** DiffBlock display copy from the plugin's own dictionary. */
1515
+ function diffLabels(t) {
1516
+ return {
1517
+ copy: t("copy"),
1518
+ copied: t("copied"),
1519
+ collapseAria: t("diff.collapseAria"),
1520
+ expandAria: (count) => t("diff.expandAria", { count }),
1521
+ collapse: t("collapse"),
1522
+ expand: (count) => t("diff.expandRest", { count }),
1523
+ files: (count) => t("diff.files", { count })
1524
+ };
1525
+ }
1526
+ /** WebBlock display copy from the plugin's own dictionary. */
1527
+ function webLabels(t) {
1528
+ return {
1529
+ noResults: t("web.noResults"),
1530
+ sourcesTruncated: t("web.sourcesTruncated"),
1531
+ http: t("web.http"),
1532
+ contentTruncated: t("web.contentTruncated"),
1533
+ markdown: {
1534
+ code: {
1535
+ copyLabel: t("copy"),
1536
+ copiedLabel: t("copied")
1537
+ },
1538
+ footnotes: t("markdown.footnotes")
1539
+ }
1540
+ };
1541
+ }
1140
1542
  /** One call's expanded-body card, mirroring the built-in ToolRow body. */
1141
- function CardBody({ model, t }) {
1543
+ function CardBody({ model, home, t }) {
1142
1544
  if (model.terminal !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1143
1545
  ...model.terminal.card,
1546
+ home,
1144
1547
  maxLines: Infinity,
1145
1548
  labels: terminalLabels(t)
1146
1549
  });
1147
1550
  if (model.diff !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
1148
1551
  ...model.diff.card,
1552
+ labels: diffLabels(t),
1149
1553
  maxLines: CHAT_DIFF_MAX_LINES
1150
1554
  });
1151
1555
  if (model.read !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
1152
1556
  ...model.read,
1557
+ labels: readLabels(t),
1153
1558
  maxLines: CHAT_READ_MAX_LINES
1154
1559
  });
1155
1560
  if (model.search !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
1156
1561
  ...model.search.card,
1562
+ labels: searchLabels(t),
1157
1563
  maxLines: CHAT_SEARCH_MAX_LINES
1158
1564
  }), model.search.recovery !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1159
1565
  className: "mtc-recovery",
1160
1566
  children: model.search.recovery
1161
1567
  })] });
1162
- if (model.web !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.WebBlock, { ...model.web });
1568
+ if (model.web !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.WebBlock, {
1569
+ ...model.web,
1570
+ labels: webLabels(t)
1571
+ });
1163
1572
  const hasBody = model.body !== null;
1164
1573
  const hasOutput = model.output !== null;
1165
1574
  if (!hasBody && !hasOutput) return null;
@@ -1202,8 +1611,8 @@ function CardBody({ model, t }) {
1202
1611
  * call is rendered as the first child row so every file path lands on a child
1203
1612
  * row, never on the main row.
1204
1613
  */
1205
- const RowCard = (0, react.memo)(function RowCard$1({ toolName, block, cwd, openFile, inspect, t, mergedCount, children }) {
1206
- const model = callRowModel(toolName, block, cwd);
1614
+ const RowCard = (0, react.memo)(function RowCard$1({ toolName, block, cwd, home, openFile, inspect, t, mergedCount, children }) {
1615
+ const model = callRowModel(toolName, block, cwd, home);
1207
1616
  const [expanded, setExpanded] = (0, react.useState)(false);
1208
1617
  const hasChildren = mergedCount > 0;
1209
1618
  const expandable = model.expandable || hasChildren;
@@ -1256,6 +1665,7 @@ const RowCard = (0, react.memo)(function RowCard$1({ toolName, block, cwd, openF
1256
1665
  className: "mtc-card-body",
1257
1666
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, {
1258
1667
  model,
1668
+ home,
1259
1669
  t
1260
1670
  }), inspect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1261
1671
  type: "button",
@@ -1289,8 +1699,8 @@ const RowCard = (0, react.memo)(function RowCard$1({ toolName, block, cwd, openF
1289
1699
  * renders as an open-file link (the sidebar preview) that stops propagation,
1290
1700
  * exactly like the main row's summary link.
1291
1701
  */
1292
- const ChildRow = (0, react.memo)(function ChildRow$1({ toolName, block, cwd, openFile, t }) {
1293
- const model = callRowModel(toolName, block, cwd);
1702
+ const ChildRow = (0, react.memo)(function ChildRow$1({ toolName, block, cwd, home, openFile, t }) {
1703
+ const model = callRowModel(toolName, block, cwd, home);
1294
1704
  const [open, setOpen] = (0, react.useState)(false);
1295
1705
  const stateLabel = stateStatus(model.state, t);
1296
1706
  const expandable = model.expandable;
@@ -1342,6 +1752,7 @@ const ChildRow = (0, react.memo)(function ChildRow$1({ toolName, block, cwd, ope
1342
1752
  className: "mtc-child-body",
1343
1753
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, {
1344
1754
  model,
1755
+ home,
1345
1756
  t
1346
1757
  })
1347
1758
  })]
@@ -1359,8 +1770,8 @@ const ChildRow = (0, react.memo)(function ChildRow$1({ toolName, block, cwd, ope
1359
1770
  * and indents the children so their dots and paths land on the main row's
1360
1771
  * columns — no font/title constants to keep in sync.
1361
1772
  */
1362
- function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg, useSession }) {
1363
- const run = useSession((snapshot) => readRun(snapshot.chat.order, snapshot.chat.nodes, callId, cfg.tools, cfg.groupBy, cfg.maxGroupSize));
1773
+ function MergedToolRow({ callId, toolName, block, cwd, home, openFile, inspect, t, cfg, useChat }) {
1774
+ const run = useChat((snapshot) => readRun(snapshot.order, snapshot.nodes, callId, cfg.tools, cfg.groupBy, cfg.maxGroupSize));
1364
1775
  const rootRef = (0, react.useRef)(null);
1365
1776
  /** Main separator dot's left offset from the card root; null before first measure. */
1366
1777
  const [sepLeft, setSepLeft] = (0, react.useState)(null);
@@ -1383,6 +1794,7 @@ function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg
1383
1794
  toolName,
1384
1795
  block,
1385
1796
  cwd,
1797
+ home,
1386
1798
  openFile,
1387
1799
  inspect,
1388
1800
  t,
@@ -1399,6 +1811,7 @@ function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg
1399
1811
  toolName,
1400
1812
  block: run.blocks[0] ?? block,
1401
1813
  cwd,
1814
+ home,
1402
1815
  openFile,
1403
1816
  inspect,
1404
1817
  t,
@@ -1407,6 +1820,7 @@ function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg
1407
1820
  toolName,
1408
1821
  block: child,
1409
1822
  cwd,
1823
+ home,
1410
1824
  openFile,
1411
1825
  t
1412
1826
  }, child.callId))