dsh-code 1.0.3 → 1.0.5
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.md +293 -285
- package/bin/deepseek.mjs +245 -12
- package/cordis.patch.yml +12 -14
- package/lib/index.mjs +1939 -903
- package/lib/types/app.d.ts +11 -2
- package/lib/types/commands.d.ts +13 -0
- package/lib/types/git-workflow.d.ts +7 -2
- package/lib/types/history.d.ts +18 -11
- package/lib/types/index.d.ts +28 -0
- package/lib/types/input-split.d.ts +54 -0
- package/lib/types/kernel-panels.d.ts +3 -1
- package/lib/types/keyboard.d.ts +8 -0
- package/lib/types/presets.d.ts +4 -1
- package/lib/types/provider-settings.d.ts +77 -0
- package/lib/types/questions.d.ts +16 -12
- package/lib/types/render/projection.d.ts +9 -2
- package/lib/types/render/status.d.ts +22 -15
- package/lib/types/settings-file.d.ts +8 -0
- package/lib/types/skills.d.ts +1 -1
- package/package.json +49 -46
- package/src/app.ts +5459 -4900
- package/src/approval.ts +8 -3
- package/src/authorization-panel.ts +2 -4
- package/src/commands.ts +27 -3
- package/src/git-workflow.ts +29 -10
- package/src/history.ts +22 -13
- package/src/index.ts +203 -61
- package/src/input-split.ts +191 -0
- package/src/internals.ts +26 -8
- package/src/kernel-panels.ts +26 -10
- package/src/keyboard.ts +123 -88
- package/src/mentions.ts +42 -9
- package/src/permissions.ts +1 -1
- package/src/presets.ts +19 -6
- package/src/provider-settings.ts +204 -0
- package/src/questions.ts +58 -55
- package/src/render/export.ts +7 -7
- package/src/render/lines.ts +24 -12
- package/src/render/markdown.ts +15 -13
- package/src/render/projection.ts +101 -13
- package/src/render/status.ts +76 -71
- package/src/render/text.ts +9 -3
- package/src/settings-file.ts +38 -6
- package/src/skills.ts +19 -6
- package/src/theme-panel.ts +79 -72
package/src/approval.ts
CHANGED
|
@@ -96,6 +96,13 @@ export function mountApprovalAnswerer(
|
|
|
96
96
|
|
|
97
97
|
let resolved = false
|
|
98
98
|
let settle!: (outcome: ApprovalOutcome) => void
|
|
99
|
+
// Established BEFORE the abort listener mounts: a synchronous throw
|
|
100
|
+
// between the listener registration and a later construction site would
|
|
101
|
+
// otherwise leave a subsequent abort invoking an unassigned settle from
|
|
102
|
+
// inside the AbortSignal listener (an uncaughtException).
|
|
103
|
+
const settled = new Promise<ApprovalOutcome>((resolve) => {
|
|
104
|
+
settle = resolve
|
|
105
|
+
})
|
|
99
106
|
const signal = request.signal
|
|
100
107
|
const onAbort = (): void => withdraw()
|
|
101
108
|
// Detach on every settle so an answered ask never retains a listener on
|
|
@@ -136,9 +143,7 @@ export function mountApprovalAnswerer(
|
|
|
136
143
|
queue.push(slot)
|
|
137
144
|
publish()
|
|
138
145
|
|
|
139
|
-
return
|
|
140
|
-
settle = resolve
|
|
141
|
-
}).then((outcome) => {
|
|
146
|
+
return settled.then((outcome) => {
|
|
142
147
|
if (outcome !== 'cancelled') {
|
|
143
148
|
removeSlot(slot)
|
|
144
149
|
publish()
|
|
@@ -196,8 +196,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
|
|
|
196
196
|
if (input !== '' && !key.ctrl && !key.meta) setDraft(current => current + input)
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
if (viewport.maxHeight === 0
|
|
200
|
-
if (viewport.compact) {
|
|
199
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
201
200
|
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('provider login · esc cancel', viewport.contentColumns))
|
|
202
201
|
}
|
|
203
202
|
|
|
@@ -271,8 +270,7 @@ export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }: {
|
|
|
271
270
|
setError(reason instanceof Error ? reason.message : String(reason))
|
|
272
271
|
})
|
|
273
272
|
})
|
|
274
|
-
if (viewport.maxHeight === 0) return createElement(
|
|
275
|
-
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('y logout · n/esc back', viewport.contentColumns))
|
|
273
|
+
if (viewport.maxHeight === 0 || viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('y logout · n/esc back', viewport.contentColumns))
|
|
276
274
|
return createElement(
|
|
277
275
|
Box,
|
|
278
276
|
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
|
package/src/commands.ts
CHANGED
|
@@ -38,16 +38,22 @@ export function watchCommands(ctx: Context): CommandsView {
|
|
|
38
38
|
let agent: Agent | undefined
|
|
39
39
|
let descriptors: readonly CommandDescriptor[] = []
|
|
40
40
|
let error: string | undefined
|
|
41
|
+
// The agent whose scoped view the current descriptors were read for: a
|
|
42
|
+
// failure before this agent ever loaded clears the list instead of keeping
|
|
43
|
+
// another session's commands completable here.
|
|
44
|
+
let loadedFor: Agent | undefined
|
|
41
45
|
const listeners = new Set<() => void>()
|
|
42
46
|
const refresh = (): void => {
|
|
43
47
|
if (commands === undefined || agent === undefined) return
|
|
44
48
|
try {
|
|
45
49
|
descriptors = commands.list(agent)
|
|
50
|
+
loadedFor = agent
|
|
46
51
|
error = undefined
|
|
47
52
|
} catch (cause: unknown) {
|
|
48
|
-
// Keep the last good catalog, but change its identity
|
|
49
|
-
// can render the recoverable failure in /help
|
|
50
|
-
|
|
53
|
+
// Keep the last good catalog for the SAME agent, but change its identity
|
|
54
|
+
// so subscribers can render the recoverable failure in /help; an agent
|
|
55
|
+
// that never loaded starts from empty.
|
|
56
|
+
descriptors = loadedFor === agent ? [...descriptors] : []
|
|
51
57
|
error = cause instanceof Error ? cause.message : String(cause)
|
|
52
58
|
}
|
|
53
59
|
for (const listener of listeners) listener()
|
|
@@ -83,3 +89,21 @@ export function watchCommands(ctx: Context): CommandsView {
|
|
|
83
89
|
export function isSlashLine(line: string): boolean {
|
|
84
90
|
return /^\/[a-z][a-z0-9_-]*(?=$|[\t ])/u.test(line)
|
|
85
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The submission payload for one composer line. Trim is a blank check, not a
|
|
95
|
+
* rewrite: an ordinary prompt keeps its exact leading indentation, inner
|
|
96
|
+
* layout, and trailing spaces (pasted code must reach the model verbatim).
|
|
97
|
+
* Only trailing line terminators are stripped — a draft's final newline is a
|
|
98
|
+
* paste/Enter artifact (an open bracketed paste turns Enter into an inserted
|
|
99
|
+
* newline), never deliberate content. A syntactic slash line still normalizes
|
|
100
|
+
* fully so command routing stays stable (completion inserts a trailing space
|
|
101
|
+
* after `/name`).
|
|
102
|
+
* @param line - the complete draft text.
|
|
103
|
+
* @returns the text to submit verbatim.
|
|
104
|
+
*/
|
|
105
|
+
export function submissionPayload(line: string): string {
|
|
106
|
+
const withoutTrailingNewlines = line.replace(/[\r\n]+$/u, '')
|
|
107
|
+
const trimmed = withoutTrailingNewlines.trim()
|
|
108
|
+
return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines
|
|
109
|
+
}
|
package/src/git-workflow.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Git inspection used by /diff and /review. Every diff
|
|
3
|
+
* invocation carries --no-ext-diff and --no-textconv, so configured
|
|
4
|
+
* external diff drivers and text converters can never execute as a
|
|
5
|
+
* side effect of reading a diff.
|
|
6
|
+
*/
|
|
2
7
|
|
|
3
8
|
import { execFile } from 'node:child_process'
|
|
4
9
|
|
|
@@ -36,12 +41,12 @@ export function parseGitDiffFiles(text: string): readonly GitDiffFile[] {
|
|
|
36
41
|
/** Parse the intentionally small, option-safe /diff argument vocabulary. */
|
|
37
42
|
export function parseGitDiffSpec(argument: string): GitDiffSpec {
|
|
38
43
|
const value = argument.trim()
|
|
39
|
-
if (value === '') return { label: 'working tree vs HEAD', args: ['diff', '--no-ext-diff', '--unified=3', 'HEAD', '--'] }
|
|
44
|
+
if (value === '') return { label: 'working tree vs HEAD', args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', 'HEAD', '--'] }
|
|
40
45
|
if (value === '--staged' || value === '--cached') {
|
|
41
|
-
return { label: 'staged changes', args: ['diff', '--no-ext-diff', '--unified=3', '--cached', '--'] }
|
|
46
|
+
return { label: 'staged changes', args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', '--cached', '--'] }
|
|
42
47
|
}
|
|
43
48
|
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, '--'] }
|
|
49
|
+
return { label: `changes since ${value}`, args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', value, '--'] }
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function executeGit(cwd: string, args: readonly string[], signal?: AbortSignal): Promise<string> {
|
|
@@ -56,8 +61,18 @@ function executeGit(cwd: string, args: readonly string[], signal?: AbortSignal):
|
|
|
56
61
|
})
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** Arguments for the unstaged-only fallback below. */
|
|
65
|
+
const UNSTAGED_DIFF_ARGS = ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', '--'] as const
|
|
66
|
+
|
|
67
|
+
/** Whether the repository has at least one commit (a HEAD revision). */
|
|
68
|
+
function hasHeadRevision(cwd: string, signal?: AbortSignal): Promise<boolean> {
|
|
69
|
+
return executeGit(cwd, ['rev-parse', '--verify', '--quiet', 'HEAD'], signal)
|
|
70
|
+
.then(() => true)
|
|
71
|
+
.catch(() => false)
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
/**
|
|
60
|
-
* Load one complete textual diff without invoking external
|
|
75
|
+
* Load one complete textual diff without invoking external programs.
|
|
61
76
|
* @param signal - aborted by the caller on session switches/quit, killing the
|
|
62
77
|
* git subprocess instead of letting a stale repository's diff land later.
|
|
63
78
|
*/
|
|
@@ -67,11 +82,15 @@ export async function loadGitDiff(cwd: string, argument: string, signal?: AbortS
|
|
|
67
82
|
const text = await executeGit(cwd, spec.args, signal)
|
|
68
83
|
return { title: `git diff - ${spec.label}`, files: parseGitDiffFiles(text) }
|
|
69
84
|
} catch (error: unknown) {
|
|
70
|
-
//
|
|
71
|
-
// the default form
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
// Only a repository without commits (no HEAD to diff against) may
|
|
86
|
+
// narrow the default form to the unstaged fallback. Every other
|
|
87
|
+
// failure — output past the buffer limit, a corrupt index, a missing
|
|
88
|
+
// repository, or the caller aborting between the two calls — must
|
|
89
|
+
// surface, not silently shrink what /diff and /review end up seeing
|
|
90
|
+
// (an aborted probe would otherwise masquerade as an unborn repo).
|
|
91
|
+
if (argument.trim() !== '' || signal?.aborted === true || (await hasHeadRevision(cwd, signal))) throw error
|
|
92
|
+
const text = await executeGit(cwd, UNSTAGED_DIFF_ARGS, signal)
|
|
93
|
+
return { title: 'git diff - working tree (no commits yet)', files: parseGitDiffFiles(text) }
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
|
package/src/history.ts
CHANGED
|
@@ -42,16 +42,25 @@ export function parseHistoryFile(raw: string, max = HISTORY_MAX_ENTRIES): readon
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
45
|
+
* The append unit for the persistent file: one JSON line, so a multi-line
|
|
46
|
+
* draft still occupies exactly one physical line. Each submission appends
|
|
47
|
+
* this unit at the end of the file, so concurrent terminals add entries
|
|
48
|
+
* after each other. Node chunks one append at 512 KiB: a pasted entry
|
|
49
|
+
* beyond that size could interleave mid-line with another writer's
|
|
50
|
+
* chunks, and the damaged line then drops out at the next parse —
|
|
51
|
+
* recall tolerates the loss by design.
|
|
51
52
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
export function historyLine(text: string): string {
|
|
54
|
+
return serializeHistoryEntry(text) + '\n'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the file on disk differs from its canonical form (deduped and
|
|
59
|
+
* capped). True means stale lines have accumulated and the next boot
|
|
60
|
+
* should rewrite it once, atomically.
|
|
61
|
+
*/
|
|
62
|
+
export function needsCompaction(raw: string, max = HISTORY_MAX_ENTRIES): boolean {
|
|
63
|
+
return serializeHistoryList(parseHistoryFile(raw, max)) !== raw
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -70,10 +79,10 @@ export function recordLocalEntry(local: readonly string[], text: string, max = H
|
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/**
|
|
73
|
-
* Serialize a capped entry list to the history file format (one JSON line
|
|
74
|
-
* entry, trailing newline). The
|
|
75
|
-
*
|
|
76
|
-
*
|
|
82
|
+
* Serialize a capped entry list to the history file format (one JSON line
|
|
83
|
+
* per entry, trailing newline). The boot-time compaction writes this
|
|
84
|
+
* canonical form once when stale lines have accumulated; submissions
|
|
85
|
+
* themselves only ever append a single line.
|
|
77
86
|
* @param entries - the entries to persist, oldest first.
|
|
78
87
|
* @returns the file content, '' for an empty list.
|
|
79
88
|
*/
|