@toddzheng024/dscode-bundle 0.7.12 → 0.7.13

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.
Files changed (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Display-boundary sanitization for externally sourced text (model output,
3
+ * tool payloads, skill descriptions). Control characters — including ANSI
4
+ * CSI/OSC escape sequences — would otherwise pass through Ink into the
5
+ * terminal, letting output rewrite the screen or inject prompts. Newlines
6
+ * survive; everything else in C0/C1 plus DEL becomes a visible `\xNN`
7
+ * escape, and bidi overrides / invisible format controls / Unicode line and
8
+ * paragraph separators become a visible `\uXXXX` escape (terminal emulators
9
+ * that render bidirectional text would otherwise reorder the displayed
10
+ * glyphs and let a command read as something it is not).
11
+ *
12
+ * @module @deepseek-ai/dsh-code/render/text
13
+ */
14
+ import { graphemeWidth, splitGraphemes, stringWidth } from './width.mjs';
15
+ /**
16
+ * Compact token count: exact below 1K, then one-decimal-ish K/M.
17
+ * @param n - token count.
18
+ * @returns display string.
19
+ */
20
+ export function formatTokens(n) {
21
+ const scaled = (v) => v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
22
+ if (n < 1_000)
23
+ return String(n);
24
+ if (n < 1_000_000)
25
+ return scaled(n / 1_000) + 'K';
26
+ return scaled(n / 1_000_000) + 'M';
27
+ }
28
+ /** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
29
+ const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu;
30
+ /**
31
+ * Bidi overrides and isolates (U+202A-202E, U+2066-2069), the Arabic Letter
32
+ * Mark (U+061C), directional and zero-width format characters (U+200B,
33
+ * U+200E/200F, U+2060-2064, U+FEFF), and Unicode line/paragraph separators
34
+ * (U+2028/2029). Terminal emulators with bidi support (Windows Terminal,
35
+ * iTerm2, kitty, WezTerm) reorder or hide these, so they must never reach
36
+ * the terminal raw.
37
+ */
38
+ const INVISIBLE_ESCAPE = /[\u061c\u200b\u200e\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu;
39
+ /**
40
+ * Escape control and deceptive characters so externally sourced text cannot
41
+ * drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
42
+ * invisible-format, and separator controls render as a literal `\uXXXX`
43
+ * escape. Newlines and tabs survive (budgeted callers normalize tabs).
44
+ * @param text - raw text from a session event, tool payload, or catalog.
45
+ * @returns display-safe text with every injectable character made visible.
46
+ */
47
+ export function displayText(text) {
48
+ return text
49
+ .replace(CONTROL_ESCAPE, char => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
50
+ .replace(INVISIBLE_ESCAPE, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
51
+ }
52
+ /** Collapse external text to one terminal-safe logical row. */
53
+ export function singleLineText(text) {
54
+ return displayText(text).replace(/\r?\n/gu, ' ↵ ').replace(/\t/gu, ' ');
55
+ }
56
+ /**
57
+ * Truncate one display-safe row without ever exceeding its physical-column
58
+ * budget. The ellipsis is included inside the budget, matching Codex's popup
59
+ * truncation contract; the cut walks grapheme clusters so emoji and
60
+ * combining sequences never split mid-cluster.
61
+ */
62
+ export function truncateColumns(text, columns) {
63
+ const limit = Math.max(0, Math.floor(columns));
64
+ if (limit === 0)
65
+ return '';
66
+ if (stringWidth(text) <= limit)
67
+ return text;
68
+ const contentLimit = limit - 1;
69
+ let used = 0;
70
+ let result = '';
71
+ for (const cluster of splitGraphemes(text)) {
72
+ const width = graphemeWidth(cluster);
73
+ if (used + width > contentLimit)
74
+ break;
75
+ result += cluster;
76
+ used += width;
77
+ }
78
+ return `${result}…`;
79
+ }
80
+ /** Punctuation that must never START a physical row (CJK kinsoku tail set). */
81
+ const ROW_START_FORBIDDEN = ',。、;:!?)】」』〉》…‥';
82
+ /** Punctuation that must never END a physical row (CJK kinsoku head set). */
83
+ const ROW_END_FORBIDDEN = '(【「『〈《';
84
+ /**
85
+ * Keep the newest display-safe text that fits a terminal rectangle, wrapping
86
+ * FORWARD from the start of the text and slicing the tail rows.
87
+ *
88
+ * Forward wrapping is what keeps a streaming tail calm: rows already produced
89
+ * never re-wrap as tokens append (a backward scan recomputes every wrap point
90
+ * per chunk and the whole visible block jumps), and the wrap rules match the
91
+ * settled text's renderer so the flush at turn end does not reflow the block
92
+ * a second time. CJK kinsoku applies at both edges: closing punctuation
93
+ * overhangs up to two cells onto the filled row instead of starting the next
94
+ * one (within the caret column the caller reserves), and opening punctuation
95
+ * moves down instead of dangling at a row end. Tabs expand to two spaces so
96
+ * terminal tab stops cannot inflate the physical row count; clusters carry
97
+ * emoji presentation and combining marks whole.
98
+ * @param text - raw externally sourced text.
99
+ * @param columns - available terminal columns.
100
+ * @param rows - available terminal rows.
101
+ * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
102
+ */
103
+ export function displayTail(text, columns, rows) {
104
+ const columnLimit = Math.max(1, Math.floor(columns));
105
+ const rowLimit = Math.max(1, Math.floor(rows));
106
+ const wrapped = [];
107
+ let current = '';
108
+ let used = 0;
109
+ let lastCluster = '';
110
+ const flush = () => {
111
+ wrapped.push(current);
112
+ current = '';
113
+ used = 0;
114
+ lastCluster = '';
115
+ };
116
+ for (const cluster of splitGraphemes(text)) {
117
+ if (cluster === '\n') {
118
+ flush();
119
+ continue;
120
+ }
121
+ const safe = cluster === '\t' ? ' ' : displayText(cluster);
122
+ // safe can be a multi-character escape literal (\xNN / \uXXXX); only
123
+ // stringWidth budgets the whole visible escape, never its first byte.
124
+ const width = stringWidth(safe);
125
+ if (used > 0 && used + width > columnLimit) {
126
+ const overhang = width <= 2 && ROW_START_FORBIDDEN.includes(cluster);
127
+ if (!overhang) {
128
+ // Kinsoku head: an opening mark at the row edge moves down with the
129
+ // incoming cluster instead of dangling at the end of the filled row.
130
+ if (lastCluster !== '' && ROW_END_FORBIDDEN.includes(lastCluster)) {
131
+ const carried = lastCluster;
132
+ current = current.slice(0, current.length - carried.length);
133
+ flush();
134
+ current = carried;
135
+ used = stringWidth(carried);
136
+ }
137
+ else {
138
+ flush();
139
+ }
140
+ }
141
+ }
142
+ current += safe;
143
+ used += width;
144
+ lastCluster = cluster;
145
+ }
146
+ // A trailing newline means one deliberate empty caret row, but that row
147
+ // must never evict real content or fake a truncation marker: flush the
148
+ // content first, decide truncation on content alone, then append the blank
149
+ // row only when the whole tail still fits the budget.
150
+ const trailingBlank = current === '' && wrapped.length > 0 && text.endsWith('\n');
151
+ if (current !== '')
152
+ flush();
153
+ const truncated = wrapped.length > rowLimit;
154
+ const kept = truncated ? wrapped.slice(-rowLimit) : wrapped;
155
+ const keptRows = trailingBlank && kept.length < rowLimit ? [...kept, ''] : kept;
156
+ return { text: keptRows.join('\n'), truncated };
157
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Expansion payloads for tool cards (the Ctrl+O verbose transcript): the
3
+ * TUI-side consumption of the harness presentation contract. Mutation and
4
+ * read tools persist a structured `tool/result.meta` (`diffs`, read
5
+ * windows, web sources) exactly so a capable UI can replay richer cards than
6
+ * the model-facing text; this module narrows that opaque JSON defensively —
7
+ * mirroring the upstream validators — and pre-formats bounded, render-ready
8
+ * rows. Malformed or absent metadata always degrades to the bounded raw
9
+ * result text, never throws during replay.
10
+ *
11
+ * @module @deepseek-ai/dsh-code/render/tool-detail
12
+ */
13
+ import { displayText, truncateColumns } from './text.mjs';
14
+ /** Budgets keeping one expanded card bounded on a terminal. */
15
+ const MAX_DIFF_LINES = 200;
16
+ const MAX_READ_LINES = 120;
17
+ const MAX_SOURCES = 10;
18
+ const MAX_RAW_CHARS = 6000;
19
+ const MAX_LINE_COLUMNS = 240;
20
+ /** Hard caps on adversarial `tool/result.meta` before any row is built. */
21
+ const MAX_DIFFS = 8;
22
+ const MAX_DIFF_TEXT_CHARS = 24_000;
23
+ /** Truncate one line to the visible-column budget with an ellipsis marker. */
24
+ function clipLine(text) {
25
+ return truncateColumns(displayText(text), MAX_LINE_COLUMNS);
26
+ }
27
+ /** Split text into lines, dropping the trailing empty element of a final newline. */
28
+ function toLines(text) {
29
+ const split = text.split('\n');
30
+ return split.length > 0 && split[split.length - 1] === '' ? split.slice(0, -1) : split;
31
+ }
32
+ /**
33
+ * Render one change as removed-then-added rows, hunked by common prefix and
34
+ * suffix. A null before-image (file create) renders as pure additions. The
35
+ * budget caps emitted rows and reports the cut, so a whole-file overwrite
36
+ * never floods the transcript. Inputs are hard-capped before line splitting
37
+ * and the row list is built incrementally up to the budget — a crafted or
38
+ * replayed giant diff cannot force a full intermediate rows array.
39
+ * @param oldText - prior content, or null for a create.
40
+ * @param newText - content after the change.
41
+ * @param budget - maximum rows to emit.
42
+ * @returns the bounded rows and whether they were cut.
43
+ */
44
+ export function diffRows(oldText, newText, budget) {
45
+ const oldRaw = oldText ?? '';
46
+ const newRaw = newText;
47
+ let inputTruncated = false;
48
+ let oldSource = oldRaw;
49
+ let newSource = newRaw;
50
+ // Bound the working arrays before `toLines` allocates them: keep a
51
+ // combined-characters share of each side proportional to its input size.
52
+ const combined = oldRaw.length + newRaw.length;
53
+ if (combined > MAX_DIFF_TEXT_CHARS) {
54
+ inputTruncated = true;
55
+ const oldShare = Math.min(oldRaw.length, Math.floor(MAX_DIFF_TEXT_CHARS * oldRaw.length / combined));
56
+ const newShare = Math.min(newRaw.length, MAX_DIFF_TEXT_CHARS - oldShare);
57
+ oldSource = oldRaw.slice(0, oldShare);
58
+ newSource = newRaw.slice(0, newShare);
59
+ }
60
+ const oldLines = oldText === null ? [] : toLines(oldSource);
61
+ const newLines = toLines(newSource);
62
+ let prefix = 0;
63
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
64
+ prefix += 1;
65
+ let suffix = 0;
66
+ while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix
67
+ && oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix])
68
+ suffix += 1;
69
+ const removedCount = oldLines.length - prefix - suffix;
70
+ const addedCount = newLines.length - prefix - suffix;
71
+ const truncated = inputTruncated || removedCount + addedCount > budget;
72
+ // Build at most `budget` rows directly; never materialize the full hunk.
73
+ const rows = [];
74
+ const removedLimit = Math.min(removedCount, Math.max(0, budget));
75
+ for (let index = 0; index < removedLimit; index += 1) {
76
+ rows.push({ mark: '-', text: clipLine(oldLines[prefix + index] ?? '') });
77
+ }
78
+ const addedLimit = Math.min(addedCount, Math.max(0, budget - removedLimit));
79
+ for (let index = 0; index < addedLimit; index += 1) {
80
+ rows.push({ mark: '+', text: clipLine(newLines[prefix + index] ?? '') });
81
+ }
82
+ return { lines: rows, truncated };
83
+ }
84
+ /** Whether `value` is a valid upstream FileDiff (defensive narrowing). */
85
+ function isFileDiff(value) {
86
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
87
+ return false;
88
+ const { path, oldText, newText } = value;
89
+ return typeof path === 'string' && (oldText === null || typeof oldText === 'string') && typeof newText === 'string';
90
+ }
91
+ /** Whether `value` is a valid read-window line (defensive narrowing). */
92
+ function isReadLine(value) {
93
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
94
+ return false;
95
+ const { number, text } = value;
96
+ return typeof number === 'number' && Number.isInteger(number) && number >= 1 && typeof text === 'string';
97
+ }
98
+ /** Whether `value` is a valid web source (defensive narrowing). */
99
+ function isWebSource(value) {
100
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
101
+ return false;
102
+ const { url, title, snippet } = value;
103
+ return typeof url === 'string'
104
+ && (title === undefined || typeof title === 'string')
105
+ && (snippet === undefined || typeof snippet === 'string');
106
+ }
107
+ /**
108
+ * Narrow the opaque `tool/result.meta` into one bounded expansion payload,
109
+ * mirroring the upstream presenters' degradation ladder: diffs (write/edit),
110
+ * read windows (read), sources (web_search), fetch summaries (web_fetch), and
111
+ * the bounded raw result text as the universal fallback.
112
+ * @param meta - the persisted presentation metadata, when the tool attached one.
113
+ * @param rawText - the joined text blocks of the result message.
114
+ * @returns the expansion payload, or undefined when nothing renderable exists.
115
+ */
116
+ export function toolResultDetail(meta, rawText) {
117
+ if (typeof meta === 'object' && meta !== null && !Array.isArray(meta)) {
118
+ const record = meta;
119
+ const diffs = record['diffs'];
120
+ // Validate and process only the capped prefix: an adversarial meta with
121
+ // thousands of diffs never runs `.every` over the full array.
122
+ if (Array.isArray(diffs) && diffs.length > 0) {
123
+ const capped = diffs.slice(0, MAX_DIFFS);
124
+ if (capped.every(isFileDiff)) {
125
+ const budget = Math.max(8, Math.floor(MAX_DIFF_LINES / capped.length));
126
+ // Diffs dropped beyond the cap must not vanish silently: the last
127
+ // kept diff reports the cut exactly like a hunk cut does.
128
+ const dropped = diffs.length > capped.length;
129
+ const rendered = capped.map((diff, index) => {
130
+ const rows = diffRows(diff.oldText, diff.newText, budget);
131
+ return dropped && index === capped.length - 1
132
+ ? { path: diff.path, ...rows, truncated: true }
133
+ : { path: diff.path, ...rows };
134
+ });
135
+ return { kind: 'diff', diffs: rendered };
136
+ }
137
+ }
138
+ const { path, offset, lines, totalLines } = record;
139
+ if (typeof path === 'string' && Number.isInteger(offset) && offset >= 1
140
+ && Number.isInteger(totalLines) && totalLines >= 0
141
+ && Array.isArray(lines)) {
142
+ // Validate the bounded window only: lines beyond the display cap are
143
+ // dropped anyway, so a giant persisted window cannot force a full-array
144
+ // validation pass before the slice.
145
+ const window = lines.slice(0, MAX_READ_LINES);
146
+ if (window.every(isReadLine)) {
147
+ const truncated = lines.length > MAX_READ_LINES;
148
+ return {
149
+ kind: 'read',
150
+ path,
151
+ offset: offset,
152
+ lines: window.map(line => ({ number: line.number, text: clipLine(line.text) })),
153
+ totalLines: totalLines,
154
+ truncated,
155
+ };
156
+ }
157
+ }
158
+ const sources = record['sources'];
159
+ if (Array.isArray(sources)) {
160
+ const capped = sources.slice(0, MAX_SOURCES);
161
+ if (capped.every(isWebSource)) {
162
+ const truncated = sources.length > MAX_SOURCES;
163
+ return {
164
+ kind: 'web-search',
165
+ sources: capped.map(source => ({
166
+ url: source.url,
167
+ title: typeof source.title === 'string' ? source.title : undefined,
168
+ snippet: typeof source.snippet === 'string' ? clipLine(source.snippet) : '',
169
+ })),
170
+ truncated,
171
+ };
172
+ }
173
+ }
174
+ const { url, statusCode } = record;
175
+ if (typeof url === 'string' && typeof statusCode === 'number') {
176
+ return { kind: 'web-fetch', url, statusCode: Math.trunc(statusCode) };
177
+ }
178
+ }
179
+ if (rawText === '')
180
+ return undefined;
181
+ const truncated = rawText.length > MAX_RAW_CHARS;
182
+ return {
183
+ kind: 'raw',
184
+ text: truncated ? rawText.slice(0, MAX_RAW_CHARS) : rawText,
185
+ truncated,
186
+ };
187
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Bounded preview line for a tool invocation's raw JSON arguments: the first
3
+ * human-meaningful string among the well-known keys (command, path, query, …)
4
+ * with a fallback to the bounded raw JSON. Shared by the tool card in the
5
+ * transcript and the approval bar's command preview. Arguments longer than
6
+ * {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
7
+ * and a synchronous `JSON.parse` plus string copies of an unbounded model
8
+ * payload must not run on the approval or projection paths.
9
+ *
10
+ * @module @deepseek-ai/dsh-code/render/tool-preview
11
+ */
12
+ /** Keys searched in declaration order when building a preview. */
13
+ const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'];
14
+ /**
15
+ * Raw arguments longer than this are skipped without parsing and fall back
16
+ * to the bounded raw preview. Well above any realistic command/path/query
17
+ * string while keeping the synchronous parse cost negligible.
18
+ */
19
+ const MAX_PARSE_CHARS = 4096;
20
+ /** Bounded raw-arguments fallback shared by the skip-parse and parse-failure paths. */
21
+ function boundedRawPreview(args) {
22
+ return args.length > 80 ? `${args.slice(0, 77)}...` : args;
23
+ }
24
+ /**
25
+ * Resolve one bounded preview for raw tool arguments.
26
+ * @param args - raw JSON arguments string as the model produced it.
27
+ * @param toolName - the tool the arguments belong to (fallback label).
28
+ * @returns the preview line; empty when nothing useful resolves.
29
+ */
30
+ export function toolArgumentsPreview(args, toolName) {
31
+ if (args === '')
32
+ return toolName;
33
+ if (args.length > MAX_PARSE_CHARS)
34
+ return boundedRawPreview(args);
35
+ try {
36
+ const parsed = JSON.parse(args);
37
+ if (parsed !== null && typeof parsed === 'object') {
38
+ const record = parsed;
39
+ for (const key of PREVIEW_KEYS) {
40
+ const value = record[key];
41
+ if (typeof value === 'string' && value !== '')
42
+ return value;
43
+ }
44
+ }
45
+ }
46
+ catch {
47
+ // Raw JSON parse failed: fall through to the bounded raw arguments.
48
+ }
49
+ return boundedRawPreview(args);
50
+ }
51
+ /** Visible budget for one delegation prompt row on the tool card. */
52
+ const MAX_PROMPT_CHARS = 160;
53
+ /**
54
+ * Bounded prompt preview for delegation-style tools (`subagent`): the
55
+ * `prompt` argument rendered as the card's second row, so the transcript
56
+ * shows what the child agent was asked — not just its description label —
57
+ * while it runs (Codex's SpawnAgent card preview). Anything else returns ''.
58
+ * @param toolName - the tool the arguments belong to.
59
+ * @param args - raw JSON arguments string as the model produced it.
60
+ * @returns the one-line prompt preview, or '' when none applies.
61
+ */
62
+ export function toolPromptPreview(toolName, args) {
63
+ if (toolName !== 'subagent' || args === '' || args.length > MAX_PARSE_CHARS)
64
+ return '';
65
+ try {
66
+ const parsed = JSON.parse(args);
67
+ if (parsed === null || typeof parsed !== 'object')
68
+ return '';
69
+ const prompt = parsed['prompt'];
70
+ if (typeof prompt !== 'string' || prompt === '')
71
+ return '';
72
+ const flat = prompt.replace(/\s+/gu, ' ').trim();
73
+ return flat.length > MAX_PROMPT_CHARS ? `${flat.slice(0, MAX_PROMPT_CHARS - 1)}…` : flat;
74
+ }
75
+ catch {
76
+ // Malformed arguments degrade to no prompt row, never a thrown parse.
77
+ return '';
78
+ }
79
+ }