@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.
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +1 -1
- package/package.json +5 -4
- package/plugins/session-metrics/view.mjs +10 -2
- package/vendor/tui/lib/app.mjs +6674 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +64 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2198 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1862 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /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
|
+
}
|