dsh-code 0.9.1 → 1.0.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.en.md +29 -13
- package/README.md +264 -248
- package/bin/deepseek.mjs +100 -6
- package/cordis.patch.yml +29 -1
- package/lib/index.mjs +2223 -687
- package/lib/startup.mjs +21 -11
- package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
- package/lib/types/app.d.ts +66 -14
- package/lib/types/attachments.d.ts +7 -0
- package/lib/types/editor.d.ts +6 -0
- package/lib/types/fork.d.ts +8 -0
- package/lib/types/git-workflow.d.ts +23 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/kernel-panels.d.ts +39 -0
- package/lib/types/keyboard.d.ts +43 -0
- package/lib/types/mentions.d.ts +28 -38
- package/lib/types/presets.d.ts +1 -3
- package/lib/types/provider-settings.d.ts +16 -0
- package/lib/types/render/animations.d.ts +10 -39
- package/lib/types/render/editor.d.ts +137 -0
- package/lib/types/render/export.d.ts +1 -1
- package/lib/types/render/lines.d.ts +6 -2
- package/lib/types/render/markdown.d.ts +3 -1
- package/lib/types/render/projection.d.ts +29 -3
- package/lib/types/render/status.d.ts +5 -12
- package/lib/types/session-directory.d.ts +1 -3
- package/lib/types/startup.d.ts +14 -11
- package/lib/types/store.d.ts +11 -9
- package/lib/types/subagents.d.ts +3 -3
- package/lib/types/theme.d.ts +14 -1
- package/lib/types/version.d.ts +15 -2
- package/package.json +153 -141
- package/src/app.ts +4455 -3917
- package/src/attachments.ts +44 -0
- package/src/editor.ts +51 -0
- package/src/fork.ts +31 -0
- package/src/git-workflow.ts +87 -0
- package/src/index.ts +1510 -1374
- package/src/internals.ts +14 -1
- package/src/kernel-panels.ts +914 -798
- package/src/keyboard.ts +125 -0
- package/src/mentions.ts +72 -117
- package/src/presets.ts +1 -4
- package/src/provider-settings.ts +94 -0
- package/src/render/animations.ts +25 -55
- package/src/render/editor.ts +398 -0
- package/src/render/export.ts +79 -79
- package/src/render/lines.ts +342 -236
- package/src/render/markdown.ts +99 -26
- package/src/render/projection.ts +102 -19
- package/src/render/status.ts +713 -650
- package/src/render/text.ts +150 -150
- package/src/render/tool-detail.ts +3 -1
- package/src/session-directory.ts +3 -3
- package/src/startup.ts +136 -119
- package/src/store.ts +23 -11
- package/src/subagents.ts +13 -5
- package/src/theme.ts +214 -206
- package/src/version.ts +58 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Terminal image-file adapter over the Harness durable attachment service. */
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { basename } from 'node:path'
|
|
5
|
+
import type { AttachmentStore, ImageMediaType, SaveImageAttachment } from '@deepseek-ai/dsh-attachment'
|
|
6
|
+
import type { ImageBlock } from '@deepseek-ai/dsh-llm'
|
|
7
|
+
|
|
8
|
+
/** Detect the supported encoded raster formats from bytes, never from a path suffix. */
|
|
9
|
+
export function detectImageMediaType(data: Uint8Array): ImageMediaType | undefined {
|
|
10
|
+
if (data.length >= 8 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47
|
|
11
|
+
&& data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a) return 'image/png'
|
|
12
|
+
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) return 'image/jpeg'
|
|
13
|
+
if (data.length >= 6) {
|
|
14
|
+
const signature = String.fromCharCode(...data.subarray(0, 6))
|
|
15
|
+
if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif'
|
|
16
|
+
}
|
|
17
|
+
if (data.length >= 12
|
|
18
|
+
&& String.fromCharCode(...data.subarray(0, 4)) === 'RIFF'
|
|
19
|
+
&& String.fromCharCode(...data.subarray(8, 12)) === 'WEBP') return 'image/webp'
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Read, validate, and persist an ordered image path list as model content blocks. */
|
|
24
|
+
export async function saveImagePaths(
|
|
25
|
+
paths: readonly string[],
|
|
26
|
+
attachments: AttachmentStore | undefined,
|
|
27
|
+
): Promise<readonly ImageBlock[]> {
|
|
28
|
+
if (paths.length === 0) return []
|
|
29
|
+
if (attachments === undefined) throw new Error('image attachments are unavailable in this profile')
|
|
30
|
+
const inputs: SaveImageAttachment[] = []
|
|
31
|
+
for (const path of paths) {
|
|
32
|
+
let data: Uint8Array
|
|
33
|
+
try {
|
|
34
|
+
data = await readFile(path)
|
|
35
|
+
} catch (error: unknown) {
|
|
36
|
+
throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`)
|
|
37
|
+
}
|
|
38
|
+
const mediaType = detectImageMediaType(data)
|
|
39
|
+
if (mediaType === undefined) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`)
|
|
40
|
+
inputs.push({ data, mediaType, name: basename(path) })
|
|
41
|
+
}
|
|
42
|
+
const refs = await attachments.saveImages(inputs)
|
|
43
|
+
return refs.map(attachment => ({ type: 'image', attachment }))
|
|
44
|
+
}
|
package/src/editor.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Host editor and clipboard adapters used by the terminal surface. */
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import type { TranscriptView } from './render/projection.ts'
|
|
5
|
+
|
|
6
|
+
function waitForProcess(command: string, args: readonly string[], input?: string): Promise<void> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, [...args], {
|
|
9
|
+
stdio: input === undefined ? 'inherit' : ['pipe', 'ignore', 'pipe'],
|
|
10
|
+
windowsHide: true,
|
|
11
|
+
})
|
|
12
|
+
let stderr = ''
|
|
13
|
+
child.stderr?.on('data', chunk => { stderr += String(chunk) })
|
|
14
|
+
child.once('error', reject)
|
|
15
|
+
child.once('exit', code => {
|
|
16
|
+
if (code === 0) resolve()
|
|
17
|
+
else reject(new Error(`${command} exited with code ${String(code)}${stderr.trim() === '' ? '' : `: ${stderr.trim()}`}`))
|
|
18
|
+
})
|
|
19
|
+
if (input !== undefined) child.stdin?.end(input)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Copy UTF-8 text through the platform clipboard command. */
|
|
24
|
+
export async function copyText(text: string): Promise<void> {
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
// Windows PowerShell 5 reads redirected stdin using the active console
|
|
27
|
+
// code page by default. Node writes UTF-8, so CJK copied through `$input`
|
|
28
|
+
// became mojibake. Set the pipe encoding before reading it.
|
|
29
|
+
await waitForProcess('powershell.exe', [
|
|
30
|
+
'-NoProfile',
|
|
31
|
+
'-NonInteractive',
|
|
32
|
+
'-Command',
|
|
33
|
+
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); Set-Clipboard -Value ([Console]::In.ReadToEnd())',
|
|
34
|
+
], text)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
await waitForProcess('pbcopy', [], text)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
await waitForProcess('xclip', ['-selection', 'clipboard'], text)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Latest complete assistant text, excluding streaming and reasoning. */
|
|
45
|
+
export function latestAssistantText(view: TranscriptView): string | undefined {
|
|
46
|
+
for (let index = view.entries.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const entry = view.entries[index]
|
|
48
|
+
if (entry?.kind === 'assistant' && entry.text !== '') return entry.text
|
|
49
|
+
}
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
package/src/fork.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Pure session-fork boundary policy shared by the TUI command and tests. */
|
|
2
|
+
|
|
3
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
4
|
+
|
|
5
|
+
export interface ForkSeed {
|
|
6
|
+
readonly boundarySeq: number
|
|
7
|
+
readonly events: readonly SessionEvent[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Select a completed turn and trailing between-turn metadata. */
|
|
11
|
+
export function selectForkSeed(events: readonly SessionEvent[], atSeq?: number): ForkSeed {
|
|
12
|
+
if (atSeq !== undefined && (!Number.isSafeInteger(atSeq) || atSeq < 0)) {
|
|
13
|
+
throw new Error('fork event sequence must be a non-negative integer')
|
|
14
|
+
}
|
|
15
|
+
const lastSeq = events.at(-1)?.seq ?? -1
|
|
16
|
+
const anchored = atSeq === undefined
|
|
17
|
+
? undefined
|
|
18
|
+
: events.find(event => event.type === 'turn/end' && event.seq >= atSeq)
|
|
19
|
+
const boundary = anchored
|
|
20
|
+
?? (atSeq === undefined || atSeq > lastSeq
|
|
21
|
+
? events.findLast(event => event.type === 'turn/end')
|
|
22
|
+
: undefined)
|
|
23
|
+
if (boundary === undefined) {
|
|
24
|
+
throw new Error(atSeq !== undefined && atSeq <= lastSeq
|
|
25
|
+
? `the turn containing event ${atSeq} has not completed`
|
|
26
|
+
: 'this session has no completed turn to fork from')
|
|
27
|
+
}
|
|
28
|
+
let cut = events.indexOf(boundary) + 1
|
|
29
|
+
while (cut < events.length && events[cut]?.type !== 'turn/start') cut += 1
|
|
30
|
+
return { boundarySeq: boundary.seq, events: events.slice(0, cut) }
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Read-only Git inspection used by /diff and /review. */
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
export interface GitDiffSpec {
|
|
6
|
+
readonly label: string
|
|
7
|
+
readonly args: readonly string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** One file section from a unified diff, retained in source order. */
|
|
11
|
+
export interface GitDiffFile {
|
|
12
|
+
readonly path: string
|
|
13
|
+
readonly lines: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A parsed diff ready for a file-oriented terminal viewport. */
|
|
17
|
+
export interface GitDiffView {
|
|
18
|
+
readonly title: string
|
|
19
|
+
readonly files: readonly GitDiffFile[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Split Git's stable `diff --git` framing without interpreting patch content. */
|
|
23
|
+
export function parseGitDiffFiles(text: string): readonly GitDiffFile[] {
|
|
24
|
+
if (text === '') return []
|
|
25
|
+
const chunks = text.split(/(?=^diff --git )/mu).filter(chunk => chunk !== '')
|
|
26
|
+
return chunks.map((chunk, index) => {
|
|
27
|
+
const lines = chunk.replace(/\n$/u, '').split('\n')
|
|
28
|
+
const plus = lines.find(line => line.startsWith('+++ b/'))
|
|
29
|
+
const minus = lines.find(line => line.startsWith('--- a/'))
|
|
30
|
+
const header = /^diff --git a\/(.+) b\/(.+)$/u.exec(lines[0] ?? '')
|
|
31
|
+
const path = plus?.slice(6) || minus?.slice(6) || header?.[2] || header?.[1] || `file ${index + 1}`
|
|
32
|
+
return { path, lines }
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse the intentionally small, option-safe /diff argument vocabulary. */
|
|
37
|
+
export function parseGitDiffSpec(argument: string): GitDiffSpec {
|
|
38
|
+
const value = argument.trim()
|
|
39
|
+
if (value === '') return { label: 'working tree vs HEAD', args: ['diff', '--no-ext-diff', '--unified=3', 'HEAD', '--'] }
|
|
40
|
+
if (value === '--staged' || value === '--cached') {
|
|
41
|
+
return { label: 'staged changes', args: ['diff', '--no-ext-diff', '--unified=3', '--cached', '--'] }
|
|
42
|
+
}
|
|
43
|
+
if (value.startsWith('-') || /\s/u.test(value)) throw new Error('usage: /diff [--staged|git-ref]')
|
|
44
|
+
return { label: `changes since ${value}`, args: ['diff', '--no-ext-diff', '--unified=3', value, '--'] }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function executeGit(cwd: string, args: readonly string[]): Promise<string> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
execFile('git', [...args], { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
50
|
+
if (error !== null) {
|
|
51
|
+
reject(new Error(stderr.trim() || error.message))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
resolve(stdout.replace(/\r\n/gu, '\n'))
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Load one complete textual diff without invoking external diff drivers. */
|
|
60
|
+
export async function loadGitDiff(cwd: string, argument: string): Promise<GitDiffView> {
|
|
61
|
+
const spec = parseGitDiffSpec(argument)
|
|
62
|
+
try {
|
|
63
|
+
const text = await executeGit(cwd, spec.args)
|
|
64
|
+
return { title: `git diff - ${spec.label}`, files: parseGitDiffFiles(text) }
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
// An unborn repository has no HEAD. Preserve useful unstaged output for
|
|
67
|
+
// the default form while still surfacing all other Git failures.
|
|
68
|
+
if (argument.trim() !== '') throw error
|
|
69
|
+
const text = await executeGit(cwd, ['diff', '--no-ext-diff', '--unified=3', '--'])
|
|
70
|
+
return { title: 'git diff - working tree', files: parseGitDiffFiles(text) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Review prompt capped before it reaches a provider context window. */
|
|
75
|
+
export function buildReviewPrompt(diff: string, label: string, maxChars = 200_000): string {
|
|
76
|
+
const truncated = diff.length > maxChars
|
|
77
|
+
const body = truncated ? diff.slice(0, maxChars) : diff
|
|
78
|
+
return [
|
|
79
|
+
'Review the following Git changes. Do not modify files or run write operations.',
|
|
80
|
+
'Lead with concrete bugs, regressions, security risks, and missing tests, ordered by severity.',
|
|
81
|
+
`Scope: ${label}${truncated ? ' (diff truncated by CLI)' : ''}`,
|
|
82
|
+
'',
|
|
83
|
+
'```diff',
|
|
84
|
+
body,
|
|
85
|
+
'```',
|
|
86
|
+
].join('\n')
|
|
87
|
+
}
|