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.
Files changed (59) hide show
  1. package/README.en.md +29 -13
  2. package/README.md +264 -248
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +29 -1
  5. package/lib/index.mjs +2223 -687
  6. package/lib/startup.mjs +21 -11
  7. package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
  8. package/lib/types/app.d.ts +66 -14
  9. package/lib/types/attachments.d.ts +7 -0
  10. package/lib/types/editor.d.ts +6 -0
  11. package/lib/types/fork.d.ts +8 -0
  12. package/lib/types/git-workflow.d.ts +23 -0
  13. package/lib/types/index.d.ts +6 -0
  14. package/lib/types/kernel-panels.d.ts +39 -0
  15. package/lib/types/keyboard.d.ts +43 -0
  16. package/lib/types/mentions.d.ts +28 -38
  17. package/lib/types/presets.d.ts +1 -3
  18. package/lib/types/provider-settings.d.ts +16 -0
  19. package/lib/types/render/animations.d.ts +10 -39
  20. package/lib/types/render/editor.d.ts +137 -0
  21. package/lib/types/render/export.d.ts +1 -1
  22. package/lib/types/render/lines.d.ts +6 -2
  23. package/lib/types/render/markdown.d.ts +3 -1
  24. package/lib/types/render/projection.d.ts +29 -3
  25. package/lib/types/render/status.d.ts +5 -12
  26. package/lib/types/session-directory.d.ts +1 -3
  27. package/lib/types/startup.d.ts +14 -11
  28. package/lib/types/store.d.ts +11 -9
  29. package/lib/types/subagents.d.ts +3 -3
  30. package/lib/types/theme.d.ts +14 -1
  31. package/lib/types/version.d.ts +15 -2
  32. package/package.json +153 -141
  33. package/src/app.ts +4455 -3917
  34. package/src/attachments.ts +44 -0
  35. package/src/editor.ts +51 -0
  36. package/src/fork.ts +31 -0
  37. package/src/git-workflow.ts +87 -0
  38. package/src/index.ts +1510 -1374
  39. package/src/internals.ts +14 -1
  40. package/src/kernel-panels.ts +914 -798
  41. package/src/keyboard.ts +125 -0
  42. package/src/mentions.ts +72 -117
  43. package/src/presets.ts +1 -4
  44. package/src/provider-settings.ts +94 -0
  45. package/src/render/animations.ts +25 -55
  46. package/src/render/editor.ts +398 -0
  47. package/src/render/export.ts +79 -79
  48. package/src/render/lines.ts +342 -236
  49. package/src/render/markdown.ts +99 -26
  50. package/src/render/projection.ts +102 -19
  51. package/src/render/status.ts +713 -650
  52. package/src/render/text.ts +150 -150
  53. package/src/render/tool-detail.ts +3 -1
  54. package/src/session-directory.ts +3 -3
  55. package/src/startup.ts +136 -119
  56. package/src/store.ts +23 -11
  57. package/src/subagents.ts +13 -5
  58. package/src/theme.ts +214 -206
  59. 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
+ }