dsh-code 0.2.0 → 0.4.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/README.md +13 -2
- package/README.zh.md +11 -2
- package/cordis.patch.yml +6 -0
- package/lib/index.mjs +2793 -394
- package/lib/types/app.d.ts +12 -0
- package/lib/types/mentions.d.ts +70 -0
- package/lib/types/questions.d.ts +48 -0
- package/lib/types/render/animations.d.ts +15 -0
- package/lib/types/render/export.d.ts +15 -0
- package/lib/types/render/inspector.d.ts +30 -0
- package/lib/types/render/lines.d.ts +31 -0
- package/lib/types/render/markdown.d.ts +27 -0
- package/lib/types/render/projection.d.ts +107 -2
- package/lib/types/render/status.d.ts +21 -0
- package/lib/types/render/text.d.ts +18 -0
- package/lib/types/render/tool-detail.d.ts +92 -0
- package/lib/types/render/tool-preview.d.ts +15 -0
- package/lib/types/store.d.ts +2 -0
- package/lib/types/theme.d.ts +4 -0
- package/package.json +20 -2
- package/src/app.ts +1507 -149
- package/src/index.ts +156 -46
- package/src/mentions.ts +193 -0
- package/src/pictures/1.png +0 -0
- package/src/questions.ts +143 -0
- package/src/render/animations.ts +22 -0
- package/src/render/export.ts +81 -0
- package/src/render/inspector.ts +79 -0
- package/src/render/lines.ts +207 -0
- package/src/render/markdown.ts +235 -0
- package/src/render/projection.ts +322 -18
- package/src/render/status.ts +62 -3
- package/src/render/text.ts +79 -0
- package/src/render/tool-detail.ts +197 -0
- package/src/render/tool-preview.ts +34 -0
- package/src/store.ts +8 -0
- package/src/theme.ts +4 -0
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
|
|
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
|
+
|
|
21
|
+
/** One rendered diff row: removed, added, or shared context. */
|
|
22
|
+
export interface DiffLine {
|
|
23
|
+
/** '-' removed, '+' added, ' ' context. */
|
|
24
|
+
mark: '-' | '+' | ' '
|
|
25
|
+
/** The line text, truncated to the column budget. */
|
|
26
|
+
text: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One file's bounded inline diff. */
|
|
30
|
+
export interface ToolDiff {
|
|
31
|
+
/** File path the change belongs to. */
|
|
32
|
+
path: string
|
|
33
|
+
/** Rendered rows in order; '-' block before the '+' block. */
|
|
34
|
+
lines: readonly DiffLine[]
|
|
35
|
+
/** True when the line budget cut the hunk. */
|
|
36
|
+
truncated: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One numbered line of a read window. */
|
|
40
|
+
export interface ToolReadLine {
|
|
41
|
+
/** 1-based file line number. */
|
|
42
|
+
number: number
|
|
43
|
+
/** The line text, truncated to the column budget. */
|
|
44
|
+
text: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One web-search source row. */
|
|
48
|
+
export interface ToolWebSource {
|
|
49
|
+
/** Source URL. */
|
|
50
|
+
url: string
|
|
51
|
+
/** Source title, when the provider returned one. */
|
|
52
|
+
title: string | undefined
|
|
53
|
+
/** Short excerpt, truncated to the column budget. */
|
|
54
|
+
snippet: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The expansion payload a verbose tool card renders; a discriminated union. */
|
|
58
|
+
export type ToolDetail =
|
|
59
|
+
| { kind: 'diff'; diffs: readonly ToolDiff[] }
|
|
60
|
+
| { kind: 'read'; path: string; offset: number; lines: readonly ToolReadLine[]; totalLines: number; truncated: boolean }
|
|
61
|
+
| { kind: 'web-search'; sources: readonly ToolWebSource[]; truncated: boolean }
|
|
62
|
+
| { kind: 'web-fetch'; url: string; statusCode: number }
|
|
63
|
+
| { kind: 'raw'; text: string; truncated: boolean }
|
|
64
|
+
|
|
65
|
+
/** Truncate one line to the visible-column budget with an ellipsis marker. */
|
|
66
|
+
function clipLine(text: string): string {
|
|
67
|
+
return text.length > MAX_LINE_COLUMNS ? `${text.slice(0, MAX_LINE_COLUMNS - 1)}…` : text
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Split text into lines, dropping the trailing empty element of a final newline. */
|
|
71
|
+
function toLines(text: string): string[] {
|
|
72
|
+
const split = text.split('\n')
|
|
73
|
+
return split.length > 0 && split[split.length - 1] === '' ? split.slice(0, -1) : split
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render one change as removed-then-added rows, hunked by common prefix and
|
|
78
|
+
* suffix. A null before-image (file create) renders as pure additions. The
|
|
79
|
+
* budget caps emitted rows and reports the cut, so a whole-file overwrite
|
|
80
|
+
* never floods the transcript.
|
|
81
|
+
* @param oldText - prior content, or null for a create.
|
|
82
|
+
* @param newText - content after the change.
|
|
83
|
+
* @param budget - maximum rows to emit.
|
|
84
|
+
* @returns the bounded rows and whether they were cut.
|
|
85
|
+
*/
|
|
86
|
+
export function diffRows(oldText: string | null, newText: string, budget: number): { lines: readonly DiffLine[]; truncated: boolean } {
|
|
87
|
+
const oldLines = oldText === null ? [] : toLines(oldText)
|
|
88
|
+
const newLines = toLines(newText)
|
|
89
|
+
let prefix = 0
|
|
90
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) prefix += 1
|
|
91
|
+
let suffix = 0
|
|
92
|
+
while (
|
|
93
|
+
suffix < oldLines.length - prefix && suffix < newLines.length - prefix
|
|
94
|
+
&& oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
95
|
+
) suffix += 1
|
|
96
|
+
const removed = oldLines.slice(prefix, oldLines.length - suffix)
|
|
97
|
+
const added = newLines.slice(prefix, newLines.length - suffix)
|
|
98
|
+
const rows: DiffLine[] = [
|
|
99
|
+
...removed.map((text): DiffLine => ({ mark: '-', text: clipLine(text) })),
|
|
100
|
+
...added.map((text): DiffLine => ({ mark: '+', text: clipLine(text) })),
|
|
101
|
+
]
|
|
102
|
+
if (rows.length <= budget) return { lines: rows, truncated: false }
|
|
103
|
+
return { lines: rows.slice(0, budget), truncated: true }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether `value` is a valid upstream FileDiff (defensive narrowing). */
|
|
107
|
+
function isFileDiff(value: unknown): value is { path: string; oldText: string | null; newText: string } {
|
|
108
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
109
|
+
const { path, oldText, newText } = value as Record<string, unknown>
|
|
110
|
+
return typeof path === 'string' && (oldText === null || typeof oldText === 'string') && typeof newText === 'string'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether `value` is a valid read-window line (defensive narrowing). */
|
|
114
|
+
function isReadLine(value: unknown): value is { number: number; text: string } {
|
|
115
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
116
|
+
const { number, text } = value as Record<string, unknown>
|
|
117
|
+
return typeof number === 'number' && Number.isInteger(number) && number >= 1 && typeof text === 'string'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether `value` is a valid web source (defensive narrowing). */
|
|
121
|
+
function isWebSource(value: unknown): value is { url: string; title?: unknown; snippet?: unknown } {
|
|
122
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
123
|
+
const { url, title, snippet } = value as Record<string, unknown>
|
|
124
|
+
return typeof url === 'string'
|
|
125
|
+
&& (title === undefined || typeof title === 'string')
|
|
126
|
+
&& (snippet === undefined || typeof snippet === 'string')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Narrow the opaque `tool/result.meta` into one bounded expansion payload,
|
|
131
|
+
* mirroring the upstream presenters' degradation ladder: diffs (write/edit),
|
|
132
|
+
* read windows (read), sources (web_search), fetch summaries (web_fetch), and
|
|
133
|
+
* the bounded raw result text as the universal fallback.
|
|
134
|
+
* @param meta - the persisted presentation metadata, when the tool attached one.
|
|
135
|
+
* @param rawText - the joined text blocks of the result message.
|
|
136
|
+
* @returns the expansion payload, or undefined when nothing renderable exists.
|
|
137
|
+
*/
|
|
138
|
+
export function toolResultDetail(meta: unknown, rawText: string): ToolDetail | undefined {
|
|
139
|
+
if (typeof meta === 'object' && meta !== null && !Array.isArray(meta)) {
|
|
140
|
+
const record = meta as Record<string, unknown>
|
|
141
|
+
|
|
142
|
+
const diffs = record['diffs']
|
|
143
|
+
if (Array.isArray(diffs) && diffs.length > 0 && diffs.every(isFileDiff)) {
|
|
144
|
+
const budget = Math.max(8, Math.floor(MAX_DIFF_LINES / diffs.length))
|
|
145
|
+
return {
|
|
146
|
+
kind: 'diff',
|
|
147
|
+
diffs: diffs.map(diff => ({
|
|
148
|
+
path: diff.path,
|
|
149
|
+
...diffRows(diff.oldText, diff.newText, budget),
|
|
150
|
+
})),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { path, offset, lines, totalLines } = record
|
|
155
|
+
if (typeof path === 'string' && Number.isInteger(offset) && (offset as number) >= 1
|
|
156
|
+
&& Number.isInteger(totalLines) && (totalLines as number) >= 0
|
|
157
|
+
&& Array.isArray(lines) && lines.every(isReadLine)) {
|
|
158
|
+
const window = lines as { number: number; text: string }[]
|
|
159
|
+
const truncated = window.length > MAX_READ_LINES
|
|
160
|
+
return {
|
|
161
|
+
kind: 'read',
|
|
162
|
+
path,
|
|
163
|
+
offset: offset as number,
|
|
164
|
+
lines: (truncated ? window.slice(0, MAX_READ_LINES) : window)
|
|
165
|
+
.map(line => ({ number: line.number, text: clipLine(line.text) })),
|
|
166
|
+
totalLines: totalLines as number,
|
|
167
|
+
truncated,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sources = record['sources']
|
|
172
|
+
if (Array.isArray(sources) && sources.every(isWebSource)) {
|
|
173
|
+
const truncated = sources.length > MAX_SOURCES
|
|
174
|
+
return {
|
|
175
|
+
kind: 'web-search',
|
|
176
|
+
sources: (truncated ? sources.slice(0, MAX_SOURCES) : sources).map(source => ({
|
|
177
|
+
url: source.url,
|
|
178
|
+
title: typeof source.title === 'string' ? source.title : undefined,
|
|
179
|
+
snippet: typeof source.snippet === 'string' ? clipLine(source.snippet) : '',
|
|
180
|
+
})),
|
|
181
|
+
truncated,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { url, statusCode } = record
|
|
186
|
+
if (typeof url === 'string' && typeof statusCode === 'number') {
|
|
187
|
+
return { kind: 'web-fetch', url, statusCode: Math.trunc(statusCode) }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (rawText === '') return undefined
|
|
191
|
+
const truncated = rawText.length > MAX_RAW_CHARS
|
|
192
|
+
return {
|
|
193
|
+
kind: 'raw',
|
|
194
|
+
text: truncated ? rawText.slice(0, MAX_RAW_CHARS) : rawText,
|
|
195
|
+
truncated,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Keys searched in declaration order when building a preview. */
|
|
11
|
+
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve one bounded preview for raw tool arguments.
|
|
15
|
+
* @param args - raw JSON arguments string as the model produced it.
|
|
16
|
+
* @param toolName - the tool the arguments belong to (fallback label).
|
|
17
|
+
* @returns the preview line; empty when nothing useful resolves.
|
|
18
|
+
*/
|
|
19
|
+
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
20
|
+
if (args === '') return toolName
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(args)
|
|
23
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
24
|
+
const record = parsed as Record<string, unknown>
|
|
25
|
+
for (const key of PREVIEW_KEYS) {
|
|
26
|
+
const value = record[key]
|
|
27
|
+
if (typeof value === 'string' && value !== '') return value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Raw JSON parse failed: fall through to the bounded raw arguments.
|
|
32
|
+
}
|
|
33
|
+
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
34
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface TranscriptStore {
|
|
|
18
18
|
subscribe(listener: () => void): () => void
|
|
19
19
|
/** Fold one session event; ignored events change nothing and notify nobody. */
|
|
20
20
|
apply(event: SessionEvent): void
|
|
21
|
+
/** Drop the folded view entirely (/clear): the next event starts a fresh one. */
|
|
22
|
+
reset(): void
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -49,5 +51,11 @@ export function createTranscriptStore(replay?: readonly SessionEvent[]): Transcr
|
|
|
49
51
|
listener()
|
|
50
52
|
}
|
|
51
53
|
},
|
|
54
|
+
reset(): void {
|
|
55
|
+
view = createTranscriptView()
|
|
56
|
+
for (const listener of listeners) {
|
|
57
|
+
listener()
|
|
58
|
+
}
|
|
59
|
+
},
|
|
52
60
|
}
|
|
53
61
|
}
|
package/src/theme.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const TUI_RGB = {
|
|
|
28
28
|
error: [239, 68, 68],
|
|
29
29
|
/** Warning amber — `--dsw-static-amber-500`. */
|
|
30
30
|
warn: [245, 158, 11],
|
|
31
|
+
/** Default foreground text — `--dsw-static-neutral-50`. */
|
|
32
|
+
text: [236, 240, 246],
|
|
33
|
+
/** Inline/fenced code — soft sky blue, distinct from brand accents. */
|
|
34
|
+
code: [125, 211, 252],
|
|
31
35
|
} as const satisfies Record<string, readonly [number, number, number]>
|
|
32
36
|
|
|
33
37
|
/** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
|