dsh-code 1.0.5 → 1.0.7

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 (47) hide show
  1. package/README.en.md +338 -286
  2. package/README.md +68 -16
  3. package/bin/deepseek.mjs +204 -4
  4. package/cordis.patch.yml +105 -7
  5. package/lib/index.mjs +2148 -439
  6. package/lib/session-query.mjs +149 -0
  7. package/lib/types/app.d.ts +34 -8
  8. package/lib/types/attachments.d.ts +36 -4
  9. package/lib/types/index.d.ts +38 -2
  10. package/lib/types/kernel-panels.d.ts +23 -0
  11. package/lib/types/provider-settings.d.ts +6 -11
  12. package/lib/types/render/animations.d.ts +74 -7
  13. package/lib/types/render/editor.d.ts +4 -3
  14. package/lib/types/render/export.d.ts +0 -6
  15. package/lib/types/render/fuzzy.d.ts +21 -0
  16. package/lib/types/render/ime-cursor.d.ts +60 -0
  17. package/lib/types/render/projection.d.ts +80 -4
  18. package/lib/types/render/status.d.ts +1 -1
  19. package/lib/types/session-directory.d.ts +48 -13
  20. package/lib/types/session-query.d.ts +92 -0
  21. package/lib/types/store.d.ts +3 -0
  22. package/lib/types/terminal-title.d.ts +58 -0
  23. package/lib/types/update-panel.d.ts +49 -0
  24. package/lib/types/update.d.ts +66 -0
  25. package/package.json +307 -162
  26. package/src/app.ts +730 -266
  27. package/src/attachments.ts +110 -11
  28. package/src/commands.ts +35 -5
  29. package/src/index.ts +1986 -1779
  30. package/src/internals.ts +66 -40
  31. package/src/kernel-panels.ts +89 -3
  32. package/src/provider-settings.ts +12 -12
  33. package/src/render/animations.ts +606 -403
  34. package/src/render/editor.ts +5 -4
  35. package/src/render/export.ts +13 -3
  36. package/src/render/fuzzy.ts +83 -0
  37. package/src/render/ime-cursor.ts +147 -0
  38. package/src/render/projection.ts +1974 -1621
  39. package/src/render/status.ts +18 -4
  40. package/src/session-directory.ts +94 -16
  41. package/src/session-query.ts +235 -0
  42. package/src/skills.ts +23 -9
  43. package/src/store.ts +39 -1
  44. package/src/subagents.ts +26 -3
  45. package/src/terminal-title.ts +173 -0
  46. package/src/update-panel.ts +246 -0
  47. package/src/update.ts +110 -0
@@ -1,10 +1,10 @@
1
- /** Terminal image-file adapter over the Harness durable attachment service. */
1
+ /** Terminal image- and file-attachment adapter over the Harness durable attachment service. */
2
2
 
3
3
  import { open, readFile, stat } from 'node:fs/promises'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { basename, extname, isAbsolute, resolve } from 'node:path'
6
- import type { AttachmentStore, ImageMediaType, SaveImageAttachment } from '@deepseek-ai/dsh-attachment'
7
- import type { ImageBlock } from '@deepseek-ai/dsh-llm'
6
+ import type { AttachmentStore, ImageMediaType, SaveFileAttachment, SaveImageAttachment } from '@deepseek-ai/dsh-attachment'
7
+ import type { FileBlock, ImageBlock } from '@deepseek-ai/dsh-llm'
8
8
 
9
9
  /** A validated path retained in the editor until submission persists it. */
10
10
  export interface ImagePathInspection {
@@ -14,6 +14,22 @@ export interface ImagePathInspection {
14
14
  readonly bytes: number
15
15
  }
16
16
 
17
+ /** A validated non-image file path retained the same way (0.1.5 file blocks). */
18
+ export interface FilePathInspection {
19
+ readonly path: string
20
+ readonly name: string
21
+ readonly bytes: number
22
+ }
23
+
24
+ /**
25
+ * Terminal-side file admission bounds. Upstream exposes image limits through
26
+ * the attachment service but no file limits (files ride verbatim storage);
27
+ * these keep a dragged file from silently ingesting a disk-sized blob and
28
+ * bound one message the way the image batch is bounded.
29
+ */
30
+ export const MAX_FILE_BYTES = 8 * 1024 * 1024
31
+ export const MAX_FILES_PER_MESSAGE = 8
32
+
17
33
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif'])
18
34
 
19
35
  /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
@@ -36,11 +52,27 @@ export function looksLikeImagePath(path: string): boolean {
36
52
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase())
37
53
  }
38
54
 
39
- /** Parse a terminal paste/drop containing only one or more image paths. */
40
- export function parsePastedImagePaths(input: string): readonly string[] {
55
+ /**
56
+ * Parse a paste/drop into its image and file paths: image-suffixed tokens
57
+ * stay images, other path-shaped tokens ride as file attachments (0.1.5
58
+ * file blocks), and anything that is neither leaves both empty — the caller
59
+ * then treats the paste as plain text.
60
+ *
61
+ * File tokens are held to an absolute-path-with-shape bar (drive/backslash
62
+ * or a dot-suffixed leaf after a separator): a dropped terminal path always
63
+ * carries one of those, while prose, slash commands, and option flags never
64
+ * do. A POSIX absolute path without any dot-suffixed leaf falls through as
65
+ * text — the @ mention route still attaches such files deliberately.
66
+ */
67
+ export function parsePastedAttachmentPaths(input: string): { readonly images: readonly string[]; readonly files: readonly string[] } {
41
68
  const text = input.trim()
42
- if (text === '') return []
43
- const tokens: string[] = []
69
+ if (text === '') return { images: [], files: [] }
70
+ const images: string[] = []
71
+ const files: string[] = []
72
+ const looksLikeDroppedFile = (path: string): boolean =>
73
+ /^[A-Za-z]:[\\/]/u.test(path)
74
+ || /^\\\\/u.test(path)
75
+ || (/^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path))
44
76
  const matcher = /"([^"]+)"|'([^']+)'|(\S+)/gu
45
77
  for (const match of text.matchAll(matcher)) {
46
78
  const token = match[1] ?? match[2] ?? match[3]
@@ -50,13 +82,20 @@ export function parsePastedImagePaths(input: string): readonly string[] {
50
82
  try {
51
83
  path = fileURLToPath(path)
52
84
  } catch {
53
- return []
85
+ return { images: [], files: [] }
54
86
  }
87
+ if (looksLikeImagePath(path)) images.push(path)
88
+ else files.push(path)
89
+ continue
55
90
  }
56
- if (!looksLikeImagePath(path)) return []
57
- tokens.push(path)
91
+ if (looksLikeImagePath(path)) {
92
+ images.push(path)
93
+ continue
94
+ }
95
+ if (!looksLikeDroppedFile(path)) return { images: [], files: [] }
96
+ files.push(path)
58
97
  }
59
- return tokens
98
+ return { images, files }
60
99
  }
61
100
 
62
101
  /** Validate path, byte size and encoded signature without writing an attachment object. */
@@ -133,3 +172,63 @@ export async function saveImagePaths(
133
172
  checkCancelled()
134
173
  return refs.map(attachment => ({ type: 'image', attachment }))
135
174
  }
175
+
176
+ /** Validate path and byte size for non-image file attachments without writing. */
177
+ export async function inspectFilePaths(
178
+ paths: readonly string[],
179
+ attachments: AttachmentStore | undefined,
180
+ cwd = process.cwd(),
181
+ ): Promise<readonly FilePathInspection[]> {
182
+ if (paths.length === 0) return []
183
+ if (attachments === undefined) throw new Error('file attachments are unavailable in this profile')
184
+ if (paths.length > MAX_FILES_PER_MESSAGE) {
185
+ throw new Error(`too many files (${paths.length}; limit ${MAX_FILES_PER_MESSAGE})`)
186
+ }
187
+ const inspected: FilePathInspection[] = []
188
+ for (const raw of paths) {
189
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw)
190
+ let facts: Awaited<ReturnType<typeof stat>>
191
+ try {
192
+ facts = await stat(path)
193
+ } catch (error: unknown) {
194
+ throw new Error(`cannot read file "${raw}": ${error instanceof Error ? error.message : String(error)}`)
195
+ }
196
+ if (!facts.isFile()) throw new Error(`file path is not a file: "${raw}"`)
197
+ if (facts.size > MAX_FILE_BYTES) {
198
+ throw new Error(`file "${basename(path)}" is ${facts.size} bytes; limit ${MAX_FILE_BYTES}`)
199
+ }
200
+ inspected.push({ path, name: basename(path), bytes: facts.size })
201
+ }
202
+ return inspected
203
+ }
204
+
205
+ /** Read and persist an ordered non-image file path list as model file blocks. */
206
+ export async function saveFilePaths(
207
+ paths: readonly string[],
208
+ attachments: AttachmentStore | undefined,
209
+ signal?: AbortSignal,
210
+ ): Promise<readonly FileBlock[]> {
211
+ if (paths.length === 0) return []
212
+ if (attachments === undefined) throw new Error('file attachments are unavailable in this profile')
213
+ // The bounds are re-checked here so a draft inspected earlier still guards
214
+ // the actual read at submission time.
215
+ await inspectFilePaths(paths, attachments)
216
+ const checkCancelled = (): void => {
217
+ if (signal?.aborted === true) throw new Error('file submission cancelled')
218
+ }
219
+ const inputs: SaveFileAttachment[] = []
220
+ for (const path of paths) {
221
+ checkCancelled()
222
+ let data: Uint8Array
223
+ try {
224
+ data = await readFile(path)
225
+ } catch (error: unknown) {
226
+ throw new Error(`cannot read file "${path}": ${error instanceof Error ? error.message : String(error)}`)
227
+ }
228
+ inputs.push({ data, name: basename(path) })
229
+ }
230
+ checkCancelled()
231
+ const refs = await Promise.all(inputs.map(input => attachments.saveFile(input)))
232
+ checkCancelled()
233
+ return refs.map(attachment => ({ type: 'file', attachment }))
234
+ }
package/src/commands.ts CHANGED
@@ -43,20 +43,50 @@ export function watchCommands(ctx: Context): CommandsView {
43
43
  // another session's commands completable here.
44
44
  let loadedFor: Agent | undefined
45
45
  const listeners = new Set<() => void>()
46
+ // Content gate: the host registry allocates a fresh array on every list()
47
+ // call and 0.1.5 emits commands/change for every scoped register AND
48
+ // dispose (a startup registration wave lands inside React's commit
49
+ // windows). A fresh identity per event chains nested passive updates past
50
+ // React's 50-deep limit ("Maximum update depth exceeded"), so an unchanged
51
+ // catalog keeps the previous array identity and notifies nobody — the same
52
+ // discipline the skills gate and the store's frame throttle established.
53
+ const descriptorFingerprint = (list: readonly CommandDescriptor[]): string =>
54
+ JSON.stringify(list.map(descriptor => [descriptor.name, descriptor.description, descriptor.input?.hint ?? '', descriptor.input?.attachments === true]))
55
+ let lastFingerprint = '[]'
56
+ let lastNotifiedError: string | undefined
57
+ const changed = (next: readonly CommandDescriptor[], nextError: string | undefined): boolean =>
58
+ descriptorFingerprint(next) !== lastFingerprint || nextError !== lastNotifiedError
59
+ // Frame throttle: coalesce a same-tick event storm into one notification
60
+ // (the transcript store's NOTIFY_FRAME_MS contract).
61
+ let notifyScheduled = false
62
+ const notify = (): void => {
63
+ if (notifyScheduled) return
64
+ notifyScheduled = true
65
+ setImmediate(() => {
66
+ notifyScheduled = false
67
+ for (const listener of listeners) listener()
68
+ })
69
+ }
46
70
  const refresh = (): void => {
47
71
  if (commands === undefined || agent === undefined) return
72
+ let next: readonly CommandDescriptor[]
73
+ let nextError: string | undefined
48
74
  try {
49
- descriptors = commands.list(agent)
75
+ next = commands.list(agent)
50
76
  loadedFor = agent
51
- error = undefined
52
77
  } catch (cause: unknown) {
53
78
  // Keep the last good catalog for the SAME agent, but change its identity
54
79
  // so subscribers can render the recoverable failure in /help; an agent
55
80
  // that never loaded starts from empty.
56
- descriptors = loadedFor === agent ? [...descriptors] : []
57
- error = cause instanceof Error ? cause.message : String(cause)
81
+ next = loadedFor === agent ? [...descriptors] : []
82
+ nextError = cause instanceof Error ? cause.message : String(cause)
58
83
  }
59
- for (const listener of listeners) listener()
84
+ if (!changed(next, nextError)) return
85
+ descriptors = next
86
+ error = nextError
87
+ lastFingerprint = descriptorFingerprint(next)
88
+ lastNotifiedError = nextError
89
+ notify()
60
90
  }
61
91
  if (commands !== undefined) {
62
92
  ctx.on('commands/change', () => refresh())