dsh-code 1.2.0 → 1.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.
Files changed (102) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/lib/index.mjs +6679 -5232
  4. package/lib/startup.mjs +1 -1
  5. package/lib/{theme-7u5Qo3dF.mjs → theme-B3orFUYz.mjs} +8 -0
  6. package/lib/types/app.d.ts +32 -45
  7. package/lib/types/attachments.d.ts +16 -7
  8. package/lib/types/completion.d.ts +29 -0
  9. package/lib/types/composer.d.ts +150 -0
  10. package/lib/types/git-workflow.d.ts +6 -0
  11. package/lib/types/index.d.ts +6 -188
  12. package/lib/types/locales/en.d.ts +65 -5
  13. package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
  14. package/lib/types/panels/completion-panel.d.ts +13 -0
  15. package/lib/types/panels/interaction-bars.d.ts +38 -0
  16. package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +19 -38
  17. package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
  18. package/lib/types/panels/model-panels.d.ts +86 -0
  19. package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
  20. package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +22 -1
  21. package/lib/types/provider-settings.d.ts +11 -0
  22. package/lib/types/render/inspector.d.ts +8 -0
  23. package/lib/types/render/status.d.ts +4 -1
  24. package/lib/types/render/text.d.ts +4 -0
  25. package/lib/types/runner/harness-gate.d.ts +83 -0
  26. package/lib/types/runner/input-history.d.ts +31 -0
  27. package/lib/types/runner/mode-cycle.d.ts +44 -0
  28. package/lib/types/runner/preferences.d.ts +40 -0
  29. package/lib/types/runner/quit.d.ts +27 -0
  30. package/lib/types/runner/search-rows.d.ts +38 -0
  31. package/lib/types/runner/session-io.d.ts +46 -0
  32. package/lib/types/runner/session-target.d.ts +42 -0
  33. package/lib/types/runner/startup-config.d.ts +33 -0
  34. package/lib/types/runner/submissions.d.ts +87 -0
  35. package/lib/types/session/attach.d.ts +39 -0
  36. package/lib/types/{history.d.ts → session/history.d.ts} +10 -0
  37. package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +25 -1
  38. package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +8 -0
  39. package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
  40. package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
  41. package/lib/types/settings-file.d.ts +10 -0
  42. package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
  43. package/lib/types/ui/panel-gap.d.ts +6 -0
  44. package/lib/types/ui/query-editor.d.ts +10 -0
  45. package/lib/types/ui/styled-rows.d.ts +8 -0
  46. package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
  47. package/lib/types/ui/ui-contract.d.ts +12 -0
  48. package/lib/types/ui/use-frames.d.ts +6 -0
  49. package/lib/types/ui/use-stable-input.d.ts +7 -0
  50. package/lib/types/version.d.ts +2 -0
  51. package/package.json +7 -5
  52. package/src/app.ts +707 -4108
  53. package/src/attachments.ts +65 -19
  54. package/src/completion.ts +117 -0
  55. package/src/composer.ts +1956 -0
  56. package/src/git-workflow.ts +18 -0
  57. package/src/index.ts +164 -645
  58. package/src/input-split.ts +24 -4
  59. package/src/internals.ts +1 -1
  60. package/src/locales/en.ts +66 -5
  61. package/src/locales/zh.ts +66 -5
  62. package/src/{authorization-panel.ts → panels/authorization-panel.ts} +30 -8
  63. package/src/panels/completion-panel.ts +79 -0
  64. package/src/panels/interaction-bars.ts +567 -0
  65. package/src/{kernel-panels.ts → panels/kernel-panels.ts} +142 -90
  66. package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
  67. package/src/panels/model-panels.ts +1021 -0
  68. package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
  69. package/src/{update-panel.ts → panels/update-panel.ts} +117 -10
  70. package/src/provider-settings.ts +38 -0
  71. package/src/rainbow.ts +13 -3
  72. package/src/render/inspector.ts +23 -0
  73. package/src/render/status.ts +80 -29
  74. package/src/render/text.ts +10 -1
  75. package/src/runner/harness-gate.ts +168 -0
  76. package/src/runner/input-history.ts +77 -0
  77. package/src/runner/mode-cycle.ts +49 -0
  78. package/src/runner/preferences.ts +67 -0
  79. package/src/runner/quit.ts +53 -0
  80. package/src/runner/search-rows.ts +55 -0
  81. package/src/runner/session-io.ts +206 -0
  82. package/src/runner/session-target.ts +81 -0
  83. package/src/runner/startup-config.ts +54 -0
  84. package/src/runner/submissions.ts +157 -0
  85. package/src/session/attach.ts +87 -0
  86. package/src/{fork.ts → session/fork.ts} +11 -7
  87. package/src/{history.ts → session/history.ts} +14 -0
  88. package/src/{session-directory.ts → session/session-directory.ts} +83 -4
  89. package/src/{session-switch.ts → session/session-switch.ts} +14 -0
  90. package/src/{store.ts → session/store.ts} +20 -2
  91. package/src/{subagents.ts → session/subagents.ts} +12 -1
  92. package/src/settings-file.ts +19 -1
  93. package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
  94. package/src/ui/panel-gap.ts +9 -0
  95. package/src/ui/query-editor.ts +16 -0
  96. package/src/ui/styled-rows.ts +124 -0
  97. package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
  98. package/src/ui/ui-contract.ts +10 -0
  99. package/src/ui/use-frames.ts +23 -0
  100. package/src/ui/use-stable-input.ts +17 -0
  101. package/src/version.ts +5 -0
  102. /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
@@ -52,17 +52,41 @@ export function looksLikeImagePath(path: string): boolean {
52
52
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase())
53
53
  }
54
54
 
55
+ /** Strip one layer of ASCII or Unicode quotes and shell-escaped spaces. */
56
+ export function unwrapDroppedPath(token: string): string {
57
+ const trimmed = token.trim()
58
+ const wrapped = /^[\u2018\u201C"'](.+)[\u2019\u201D"']$/u.exec(trimmed)
59
+ return (wrapped?.[1] ?? trimmed).replace(/\\ /gu, ' ')
60
+ }
61
+
62
+ /** Absolute/relative drop with a dotted leaf — including unquoted spaces. */
63
+ export function looksLikeFilesystemDrop(path: string): boolean {
64
+ if (path.startsWith('file://')) return true
65
+ if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/)/u.test(path)) return false
66
+ return looksLikeImagePath(path) || /\.[A-Za-z0-9]{1,16}$/u.test(path)
67
+ }
68
+
55
69
  /**
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.
70
+ * A composer draft that is a filesystem path, not a slash command.
71
+ * `/usage` stays a command; `/Users/foo.png` and `C:\temp\a.png` are drops.
72
+ */
73
+ export function looksLikePathDraft(value: string): boolean {
74
+ const path = unwrapDroppedPath(value)
75
+ if (looksLikeFilesystemDrop(path)) return true
76
+ return /^\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes)\//u.test(path)
77
+ }
78
+
79
+ /**
80
+ * Parse a paste or file-drop into image and file paths: image-suffixed
81
+ * tokens stay images, other path-shaped tokens ride as file attachments
82
+ * (0.1.5 file blocks), and anything that is neither leaves both empty —
83
+ * the caller then treats the paste as plain text.
60
84
  *
61
85
  * 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
86
+ * or a dotted leaf after a separator): a dropped terminal path always
63
87
  * 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.
88
+ * do. A POSIX absolute path without a dotted leaf falls through as text —
89
+ * the @ mention route still attaches such files deliberately.
66
90
  */
67
91
  export function parsePastedAttachmentPaths(input: string): { readonly images: readonly string[]; readonly files: readonly string[] } {
68
92
  const text = input.trim()
@@ -73,27 +97,49 @@ export function parsePastedAttachmentPaths(input: string): { readonly images: re
73
97
  /^[A-Za-z]:[\\/]/u.test(path)
74
98
  || /^\\\\/u.test(path)
75
99
  || (/^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path))
76
- const matcher = /"([^"]+)"|'([^']+)'|(\S+)/gu
77
- for (const match of text.matchAll(matcher)) {
78
- const token = match[1] ?? match[2] ?? match[3]
79
- if (token === undefined) continue
80
- let path = token
100
+ const classify = (raw: string): 'image' | 'file' | 'reject' => {
101
+ let path = unwrapDroppedPath(raw)
81
102
  if (path.startsWith('file://')) {
82
103
  try {
83
104
  path = fileURLToPath(path)
84
105
  } catch {
85
- return { images: [], files: [] }
106
+ return 'reject'
107
+ }
108
+ } else if (/%[0-9A-Fa-f]{2}/u.test(path)) {
109
+ try {
110
+ path = decodeURIComponent(path)
111
+ } catch {
112
+ // Keep the raw token when it is not a valid percent-encoding.
86
113
  }
87
- if (looksLikeImagePath(path)) images.push(path)
88
- else files.push(path)
89
- continue
90
114
  }
91
115
  if (looksLikeImagePath(path)) {
92
116
  images.push(path)
93
- continue
117
+ return 'image'
118
+ }
119
+ if (looksLikeDroppedFile(path) || looksLikeFilesystemDrop(path)) {
120
+ files.push(path)
121
+ return 'file'
94
122
  }
95
- if (!looksLikeDroppedFile(path)) return { images: [], files: [] }
96
- files.push(path)
123
+ return 'reject'
124
+ }
125
+ // A single dropped path with spaces often arrives unquoted (macOS, Windows
126
+ // Explorer, VS Code sendText). Use the whole paste only when there is one
127
+ // path start and the text actually contains spaces (or wrapping quotes) —
128
+ // two unquoted `C:\a.png D:\b.txt` tokens must still split.
129
+ const whole = unwrapDroppedPath(text)
130
+ const pathStarts = text.match(/(?:^|[\s"'])(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.?\/|file:\/\/)/gu) ?? []
131
+ const spacedSingleton = pathStarts.length <= 1
132
+ && looksLikeFilesystemDrop(whole)
133
+ && (/\s/u.test(whole) || /^[\u2018\u201C"']/u.test(text))
134
+ if (spacedSingleton) {
135
+ classify(whole)
136
+ return { images, files }
137
+ }
138
+ const matcher = /"([^"]+)"|'([^']+)'|(\S+)/gu
139
+ for (const match of text.matchAll(matcher)) {
140
+ const token = match[1] ?? match[2] ?? match[3]
141
+ if (token === undefined) continue
142
+ if (classify(token) === 'reject') return { images: [], files: [] }
97
143
  }
98
144
  return { images, files }
99
145
  }
@@ -0,0 +1,117 @@
1
+ /** Slash-command parsing and completion candidates shared by the composer and help panel. */
2
+
3
+ import type { CommandDescriptor } from '@deepseek-ai/dsh-commands'
4
+ import { t, type MessageKey } from './i18n.ts'
5
+ import { rankByName } from './render/fuzzy.ts'
6
+ import type { SkillRow } from './skills.ts'
7
+
8
+ /** One TUI-owned slash command: label plus its i18n description key. */
9
+ export interface LocalCommand {
10
+ readonly label: string
11
+ readonly descriptionKey: MessageKey
12
+ }
13
+
14
+ /** One source of truth for TUI-owned slash commands in completion and `/help`. */
15
+ export const LOCAL_COMMANDS: readonly LocalCommand[] = [
16
+ { label: '/help', descriptionKey: 'cmd.help' },
17
+ { label: '/model', descriptionKey: 'cmd.model' },
18
+ { label: '/effort', descriptionKey: 'cmd.effort' },
19
+ { label: '/mode', descriptionKey: 'cmd.mode' },
20
+ { label: '/permission', descriptionKey: 'cmd.permission' },
21
+ { label: '/new', descriptionKey: 'cmd.new' },
22
+ { label: '/fork', descriptionKey: 'cmd.fork' },
23
+ { label: '/resume', descriptionKey: 'cmd.resume' },
24
+ { label: '/search', descriptionKey: 'cmd.search' },
25
+ { label: '/plugin', descriptionKey: 'cmd.plugin' },
26
+ { label: '/update', descriptionKey: 'cmd.update' },
27
+ { label: '/jobs', descriptionKey: 'cmd.jobs' },
28
+ { label: '/schedule', descriptionKey: 'cmd.schedule' },
29
+ { label: '/statusline', descriptionKey: 'cmd.statusline' },
30
+ { label: '/theme', descriptionKey: 'cmd.theme' },
31
+ { label: '/language', descriptionKey: 'cmd.language' },
32
+ { label: '/rainbow', descriptionKey: 'cmd.rainbow' },
33
+ { label: '/animation', descriptionKey: 'cmd.animation' },
34
+ { label: '/history', descriptionKey: 'cmd.history' },
35
+ { label: '/queue', descriptionKey: 'cmd.queue' },
36
+ { label: '/usage', descriptionKey: 'cmd.usage' },
37
+ { label: '/agents', descriptionKey: 'cmd.agents' },
38
+ { label: '/todos', descriptionKey: 'cmd.todos' },
39
+ { label: '/subagent', descriptionKey: 'cmd.subagent' },
40
+ { label: '/vscode-keys', descriptionKey: 'cmd.vscode-keys' },
41
+ { label: '/delete', descriptionKey: 'cmd.delete' },
42
+ { label: '/clear', descriptionKey: 'cmd.clear' },
43
+ { label: '/export', descriptionKey: 'cmd.export' },
44
+ { label: '/title', descriptionKey: 'cmd.title' },
45
+ { label: '/copy', descriptionKey: 'cmd.copy' },
46
+ { label: '/diff', descriptionKey: 'cmd.diff' },
47
+ { label: '/review', descriptionKey: 'cmd.review' },
48
+ { label: '/quit', descriptionKey: 'cmd.quit' },
49
+ ] as const
50
+
51
+ export const LOCAL_COMMAND_NAMES: ReadonlySet<string> = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)))
52
+
53
+ /** TUI-local commands that reject trailing input instead of forwarding it as a prompt. */
54
+ export const BARE_LOCAL_COMMANDS: ReadonlySet<string> = new Set([
55
+ 'quit', 'help', 'clear', 'copy', 'update', 'schedule', 'statusline', 'theme',
56
+ 'history', 'queue', 'usage', 'agents', 'todos', 'subagent',
57
+ ])
58
+
59
+ /** Split a slash line into the command name and any trailing input. */
60
+ export function slashNameAndArgs(text: string): { readonly name: string; readonly args: string } | undefined {
61
+ const match = /^\/([a-z][a-z0-9_-]*)(?:$|[\t ](.*))$/u.exec(text)
62
+ if (match === null || match[1] === undefined) return undefined
63
+ return { name: match[1], args: (match[2] ?? '').trim() }
64
+ }
65
+
66
+ /** One completion candidate row. */
67
+ export interface CompletionCandidate {
68
+ readonly label: string
69
+ readonly description: string
70
+ readonly origin: 'command' | 'skill' | 'mention'
71
+ }
72
+
73
+ /** Wrap one completion-menu cursor step; an empty menu keeps the index at zero. */
74
+ export function stepCompletionIndex(index: number, delta: number, count: number): number {
75
+ if (count <= 0) return 0
76
+ return ((index + delta) % count + count) % count
77
+ }
78
+
79
+ /** Merge local commands, registry descriptors, and skills using dispatch shadowing order. */
80
+ export function completionCandidates(
81
+ value: string,
82
+ descriptors: readonly CommandDescriptor[],
83
+ skills: readonly SkillRow[],
84
+ ): readonly CompletionCandidate[] {
85
+ if (!value.startsWith('/')) return []
86
+ const prefix = value.slice(1).split(' ')[0] ?? ''
87
+ const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({
88
+ label: command.label,
89
+ description: t(command.descriptionKey),
90
+ origin: 'command',
91
+ }))
92
+ const registry = descriptors
93
+ .filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name))
94
+ .map((descriptor): CompletionCandidate => ({
95
+ label: `/${descriptor.name}`,
96
+ description: descriptor.description,
97
+ origin: 'command',
98
+ }))
99
+ const taken = new Set([...local, ...registry].map(candidate => candidate.label.slice(1)))
100
+ const skillRows = skills
101
+ .filter(skill => !taken.has(skill.name))
102
+ .map((skill): CompletionCandidate => ({
103
+ label: `/${skill.name}`,
104
+ description: skill.modelInvocable ? `skill · ${skill.description}` : `skill (user only) · ${skill.description}`,
105
+ origin: 'skill',
106
+ }))
107
+ const seen = new Set<string>()
108
+ const all: CompletionCandidate[] = []
109
+ for (const candidate of [...local, ...registry, ...skillRows]) {
110
+ const name = candidate.label.slice(1)
111
+ if (seen.has(name)) continue
112
+ seen.add(name)
113
+ all.push(candidate)
114
+ }
115
+ return rankByName(all.map(candidate => ({ name: candidate.label.slice(1), candidate })), prefix)
116
+ .map(entry => entry.candidate)
117
+ }