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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/lib/index.mjs +6679 -5232
- package/lib/startup.mjs +1 -1
- package/lib/{theme-7u5Qo3dF.mjs → theme-B3orFUYz.mjs} +8 -0
- package/lib/types/app.d.ts +32 -45
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/completion.d.ts +29 -0
- package/lib/types/composer.d.ts +150 -0
- package/lib/types/git-workflow.d.ts +6 -0
- package/lib/types/index.d.ts +6 -188
- package/lib/types/locales/en.d.ts +65 -5
- package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
- package/lib/types/panels/completion-panel.d.ts +13 -0
- package/lib/types/panels/interaction-bars.d.ts +38 -0
- package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +19 -38
- package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
- package/lib/types/panels/model-panels.d.ts +86 -0
- package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
- package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +22 -1
- package/lib/types/provider-settings.d.ts +11 -0
- package/lib/types/render/inspector.d.ts +8 -0
- package/lib/types/render/status.d.ts +4 -1
- package/lib/types/render/text.d.ts +4 -0
- package/lib/types/runner/harness-gate.d.ts +83 -0
- package/lib/types/runner/input-history.d.ts +31 -0
- package/lib/types/runner/mode-cycle.d.ts +44 -0
- package/lib/types/runner/preferences.d.ts +40 -0
- package/lib/types/runner/quit.d.ts +27 -0
- package/lib/types/runner/search-rows.d.ts +38 -0
- package/lib/types/runner/session-io.d.ts +46 -0
- package/lib/types/runner/session-target.d.ts +42 -0
- package/lib/types/runner/startup-config.d.ts +33 -0
- package/lib/types/runner/submissions.d.ts +87 -0
- package/lib/types/session/attach.d.ts +39 -0
- package/lib/types/{history.d.ts → session/history.d.ts} +10 -0
- package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +25 -1
- package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +8 -0
- package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
- package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
- package/lib/types/settings-file.d.ts +10 -0
- package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
- package/lib/types/ui/panel-gap.d.ts +6 -0
- package/lib/types/ui/query-editor.d.ts +10 -0
- package/lib/types/ui/styled-rows.d.ts +8 -0
- package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
- package/lib/types/ui/ui-contract.d.ts +12 -0
- package/lib/types/ui/use-frames.d.ts +6 -0
- package/lib/types/ui/use-stable-input.d.ts +7 -0
- package/lib/types/version.d.ts +2 -0
- package/package.json +7 -5
- package/src/app.ts +707 -4108
- package/src/attachments.ts +65 -19
- package/src/completion.ts +117 -0
- package/src/composer.ts +1956 -0
- package/src/git-workflow.ts +18 -0
- package/src/index.ts +164 -645
- package/src/input-split.ts +24 -4
- package/src/internals.ts +1 -1
- package/src/locales/en.ts +66 -5
- package/src/locales/zh.ts +66 -5
- package/src/{authorization-panel.ts → panels/authorization-panel.ts} +30 -8
- package/src/panels/completion-panel.ts +79 -0
- package/src/panels/interaction-bars.ts +567 -0
- package/src/{kernel-panels.ts → panels/kernel-panels.ts} +142 -90
- package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
- package/src/panels/model-panels.ts +1021 -0
- package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
- package/src/{update-panel.ts → panels/update-panel.ts} +117 -10
- package/src/provider-settings.ts +38 -0
- package/src/rainbow.ts +13 -3
- package/src/render/inspector.ts +23 -0
- package/src/render/status.ts +80 -29
- package/src/render/text.ts +10 -1
- package/src/runner/harness-gate.ts +168 -0
- package/src/runner/input-history.ts +77 -0
- package/src/runner/mode-cycle.ts +49 -0
- package/src/runner/preferences.ts +67 -0
- package/src/runner/quit.ts +53 -0
- package/src/runner/search-rows.ts +55 -0
- package/src/runner/session-io.ts +206 -0
- package/src/runner/session-target.ts +81 -0
- package/src/runner/startup-config.ts +54 -0
- package/src/runner/submissions.ts +157 -0
- package/src/session/attach.ts +87 -0
- package/src/{fork.ts → session/fork.ts} +11 -7
- package/src/{history.ts → session/history.ts} +14 -0
- package/src/{session-directory.ts → session/session-directory.ts} +83 -4
- package/src/{session-switch.ts → session/session-switch.ts} +14 -0
- package/src/{store.ts → session/store.ts} +20 -2
- package/src/{subagents.ts → session/subagents.ts} +12 -1
- package/src/settings-file.ts +19 -1
- package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
- package/src/ui/panel-gap.ts +9 -0
- package/src/ui/query-editor.ts +16 -0
- package/src/ui/styled-rows.ts +124 -0
- package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
- package/src/ui/ui-contract.ts +10 -0
- package/src/ui/use-frames.ts +23 -0
- package/src/ui/use-stable-input.ts +17 -0
- package/src/version.ts +5 -0
- /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
package/src/attachments.ts
CHANGED
|
@@ -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
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
65
|
-
*
|
|
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
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
117
|
+
return 'image'
|
|
118
|
+
}
|
|
119
|
+
if (looksLikeDroppedFile(path) || looksLikeFilesystemDrop(path)) {
|
|
120
|
+
files.push(path)
|
|
121
|
+
return 'file'
|
|
94
122
|
}
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|