dsh-code 1.0.4 → 1.0.6
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 +287 -286
- package/README.md +16 -13
- package/bin/deepseek.mjs +336 -11
- package/cordis.patch.yml +26 -18
- package/lib/index.mjs +1310 -439
- package/lib/types/app.d.ts +25 -6
- package/lib/types/attachments.d.ts +36 -4
- package/lib/types/git-workflow.d.ts +7 -2
- package/lib/types/history.d.ts +18 -11
- package/lib/types/index.d.ts +11 -2
- package/lib/types/presets.d.ts +4 -1
- package/lib/types/provider-settings.d.ts +6 -11
- package/lib/types/questions.d.ts +16 -12
- package/lib/types/render/animations.d.ts +74 -7
- package/lib/types/render/export.d.ts +0 -6
- package/lib/types/render/fuzzy.d.ts +21 -0
- package/lib/types/render/projection.d.ts +47 -5
- package/lib/types/session-directory.d.ts +48 -13
- package/lib/types/settings-file.d.ts +8 -0
- package/lib/types/store.d.ts +3 -0
- package/package.json +168 -159
- package/src/app.ts +480 -199
- package/src/attachments.ts +110 -11
- package/src/commands.ts +35 -5
- package/src/git-workflow.ts +29 -10
- package/src/history.ts +22 -13
- package/src/index.ts +1868 -1752
- package/src/internals.ts +61 -40
- package/src/permissions.ts +1 -1
- package/src/presets.ts +19 -6
- package/src/provider-settings.ts +12 -12
- package/src/questions.ts +57 -74
- package/src/render/animations.ts +606 -403
- package/src/render/export.ts +20 -10
- package/src/render/fuzzy.ts +83 -0
- package/src/render/projection.ts +1833 -1620
- package/src/session-directory.ts +94 -16
- package/src/settings-file.ts +38 -6
- package/src/skills.ts +23 -9
- package/src/store.ts +39 -1
- package/src/subagents.ts +26 -3
package/src/internals.ts
CHANGED
|
@@ -44,46 +44,67 @@ export const internals: {
|
|
|
44
44
|
// the keyboard protocol on terminals that can safely own those key events.
|
|
45
45
|
const keyboardEnhanced = shouldEnableKeyboardEnhancement()
|
|
46
46
|
const focusReporting = isVsCodeTerminalEnv()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
stdin:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
47
|
+
// Enter raw mode BEFORE pushing any protocol: xterm.js answers `?1004h`
|
|
48
|
+
// with an immediate focus report (ESC[I), and while the tty still carries
|
|
49
|
+
// the shell's cooked+ECHO settings that report is echoed to the screen as
|
|
50
|
+
// a literal `^[[I`. Ink only takes raw mode after its first commit, so
|
|
51
|
+
// this mount owns the window; the call is idempotent with Ink's later one.
|
|
52
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(true)
|
|
53
|
+
try {
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
(keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : '')
|
|
56
|
+
+ BRACKETED_PASTE_ENABLE
|
|
57
|
+
+ (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ''),
|
|
58
|
+
)
|
|
59
|
+
// App owns Ctrl+C's deliberate three-state contract (interrupt, clear
|
|
60
|
+
// draft, quit). Ink's default `exitOnCtrlC: true` would intercept the
|
|
61
|
+
// normalized control byte first, unmount only its renderer, and leave the
|
|
62
|
+
// Harness runner plus the pushed keyboard protocol alive.
|
|
63
|
+
// stdin travels through the keypress splitter: Ink parses one chunk as
|
|
64
|
+
// one keypress, so a coalesced space-then-enter would drop both keys.
|
|
65
|
+
const tuiStdin = createSplitStdin(process.stdin)
|
|
66
|
+
// Ink only touches isTTY/setRawMode/ref/read on stdin; the object-mode
|
|
67
|
+
// proxy satisfies that contract without the full ReadStream surface.
|
|
68
|
+
const instance = render(element, {
|
|
69
|
+
exitOnCtrlC: false,
|
|
70
|
+
stdin: tuiStdin.stdin as unknown as NodeJS.ReadStream,
|
|
71
|
+
stdout: process.stdout,
|
|
72
|
+
})
|
|
73
|
+
return {
|
|
74
|
+
rerender(element: ReactElement): void {
|
|
75
|
+
instance.rerender(element)
|
|
76
|
+
},
|
|
77
|
+
unmount(): void {
|
|
78
|
+
// The cleanup below must run even when Ink's unmount throws (a
|
|
79
|
+
// render-teardown failure): a stdin tap or pushed terminal-protocol
|
|
80
|
+
// stack outliving the app wedges the terminal for whatever runs
|
|
81
|
+
// next, and a stray exception here must not skip the exit sequence.
|
|
82
|
+
// Pop the stack while raw mode still hides echo — xterm.js keeps
|
|
83
|
+
// reporting focus changes until `?1004l` lands, and one arriving
|
|
84
|
+
// after Ink restores the cooked tty would print as `^[[I`.
|
|
85
|
+
try {
|
|
86
|
+
process.stdout.write(
|
|
87
|
+
(keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : '')
|
|
88
|
+
+ BRACKETED_PASTE_DISABLE
|
|
89
|
+
+ (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ''),
|
|
90
|
+
)
|
|
91
|
+
} finally {
|
|
92
|
+
try {
|
|
93
|
+
instance.unmount()
|
|
94
|
+
} finally {
|
|
95
|
+
tuiStdin.dispose()
|
|
96
|
+
// Belt and braces: give the tty back its cooked mode even when
|
|
97
|
+
// Ink never took raw mode over (idempotent at the termios level).
|
|
98
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// The synchronous mount path failed before Ink could own the terminal:
|
|
105
|
+
// undo the raw mode entered above so the shell keeps its echo.
|
|
106
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false)
|
|
107
|
+
throw error
|
|
87
108
|
}
|
|
88
109
|
},
|
|
89
110
|
stderr: process.stderr,
|
package/src/permissions.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function effectivePermission(
|
|
|
24
24
|
session: Session | undefined,
|
|
25
25
|
pending: string | undefined,
|
|
26
26
|
): string {
|
|
27
|
-
return session === undefined ? pending ?? service.defaultPreset : service.current(session
|
|
27
|
+
return session === undefined ? pending ?? service.defaultPreset : service.current(session)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Validate a preset and write it only when a durable session already exists. */
|
package/src/presets.ts
CHANGED
|
@@ -21,15 +21,27 @@ export function isBlankSession(events: readonly SessionEvent[]): boolean {
|
|
|
21
21
|
return !events.some(event => event.type === 'turn/start')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Upstream renamed the shipped `code` preset to `ptc` in 0.1.2-rc.1; sessions
|
|
25
|
+
* and CLI choices recorded before the rename keep resolving through this map. */
|
|
26
|
+
const LEGACY_PRESET_IDS: Readonly<Record<string, string>> = { code: 'ptc' }
|
|
27
|
+
|
|
28
|
+
/** Translate a preset id recorded before an upstream rename to its current id. */
|
|
29
|
+
export function normalizePresetId(id: string): string
|
|
30
|
+
export function normalizePresetId(id: string | undefined): string | undefined
|
|
31
|
+
export function normalizePresetId(id: string | undefined): string | undefined {
|
|
32
|
+
return id === undefined ? undefined : LEGACY_PRESET_IDS[id] ?? id
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
/** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
|
|
25
|
-
export function resolvePreset(session: Pick<Session, 'header' | '
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
export function resolvePreset(session: Pick<Session, 'header' | 'snapshotEvents'>): string {
|
|
37
|
+
const events = session.snapshotEvents()
|
|
38
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
39
|
+
const event = events[index] as unknown as { type: string; data?: { agentPreset?: string } }
|
|
28
40
|
if (event.type === 'agent-preset/selected' && event.data?.agentPreset !== undefined) {
|
|
29
|
-
return event.data.agentPreset
|
|
41
|
+
return normalizePresetId(event.data.agentPreset) as string
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
|
-
return session.header.agentPreset ?? 'standard'
|
|
44
|
+
return normalizePresetId(session.header.agentPreset) ?? 'standard'
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
@@ -38,6 +50,7 @@ export async function selectPreset(
|
|
|
38
50
|
agent: Agent | undefined,
|
|
39
51
|
presetId: string,
|
|
40
52
|
): Promise<PresetRow> {
|
|
53
|
+
presetId = normalizePresetId(presetId)
|
|
41
54
|
if (agent !== undefined) return switchPreset(service, agent, presetId)
|
|
42
55
|
const preset = await service.resolve(presetId)
|
|
43
56
|
if (preset.broken !== undefined) throw new Error(preset.broken)
|
|
@@ -50,7 +63,7 @@ export async function switchPreset(
|
|
|
50
63
|
agent: Agent,
|
|
51
64
|
presetId: string,
|
|
52
65
|
): Promise<PresetRow> {
|
|
53
|
-
if (!isBlankSession(agent.session.
|
|
66
|
+
if (!isBlankSession(agent.session.snapshotEvents())) {
|
|
54
67
|
throw new Error('mode is locked after the first turn; use /new <mode>')
|
|
55
68
|
}
|
|
56
69
|
const preset = await service.recompose(agent.ctx, presetId)
|
package/src/provider-settings.ts
CHANGED
|
@@ -33,6 +33,8 @@ interface LlmFace {
|
|
|
33
33
|
readonly settingsNs: string
|
|
34
34
|
readonly settingsPath: readonly string[]
|
|
35
35
|
readonly declared?: boolean
|
|
36
|
+
/** Configuration diagnostic for repair; unaffected models may remain serviceable. */
|
|
37
|
+
readonly error?: string
|
|
36
38
|
}[]
|
|
37
39
|
/**
|
|
38
40
|
* Registered endpoint model discovery; absent on an older service. The
|
|
@@ -256,18 +258,6 @@ export interface DiscoveredModelView {
|
|
|
256
258
|
readonly maxTokens?: number
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
/** One model an endpoint reported about itself (mirrors LlmDiscoveredModel). */
|
|
260
|
-
export interface DiscoveredModelView {
|
|
261
|
-
/** Model id the endpoint accepts. */
|
|
262
|
-
readonly id: string
|
|
263
|
-
/** Human-readable name when the endpoint supplies one. */
|
|
264
|
-
readonly name?: string
|
|
265
|
-
/** Context window when disclosed; adoption still owes it if absent. */
|
|
266
|
-
readonly contextWindow?: number
|
|
267
|
-
/** Output cap when disclosed. */
|
|
268
|
-
readonly maxTokens?: number
|
|
269
|
-
}
|
|
270
|
-
|
|
271
261
|
/**
|
|
272
262
|
* The seven canonical reasoning levels a reasoningEfforts key may name -
|
|
273
263
|
* pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
|
|
@@ -362,6 +352,12 @@ export interface ProviderTargetView {
|
|
|
362
352
|
readonly configuration: ProviderConfiguration
|
|
363
353
|
/** The owning adapter reports this route as hand-declared (absent when it draws no distinction). */
|
|
364
354
|
readonly declared?: boolean
|
|
355
|
+
/**
|
|
356
|
+
* Configuration diagnostic the adapter reported for this route (catalog or
|
|
357
|
+
* profile damage): the row stays listed and repairable instead of the whole
|
|
358
|
+
* provider vanishing; absent when the route reads clean.
|
|
359
|
+
*/
|
|
360
|
+
readonly diagnostic?: string
|
|
365
361
|
}
|
|
366
362
|
|
|
367
363
|
/** The resolved provider/settings/credential join. */
|
|
@@ -431,6 +427,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
|
|
|
431
427
|
settingsNs: string
|
|
432
428
|
settingsPath: readonly string[]
|
|
433
429
|
declared?: boolean
|
|
430
|
+
error?: string
|
|
434
431
|
}> = []
|
|
435
432
|
if (llm.listConfigurableProviders !== undefined) {
|
|
436
433
|
try {
|
|
@@ -462,6 +459,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
|
|
|
462
459
|
settingsNs: string
|
|
463
460
|
settingsPath: readonly string[]
|
|
464
461
|
declared?: boolean
|
|
462
|
+
error?: string
|
|
465
463
|
}> = [
|
|
466
464
|
...directoryEntries.map(entry => ({
|
|
467
465
|
provider: entry.provider,
|
|
@@ -470,6 +468,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
|
|
|
470
468
|
settingsNs: entry.settingsNs,
|
|
471
469
|
settingsPath: entry.settingsPath,
|
|
472
470
|
...entry.declared === undefined ? {} : { declared: entry.declared },
|
|
471
|
+
...entry.error === undefined ? {} : { error: singleLine(entry.error) },
|
|
473
472
|
})),
|
|
474
473
|
...registered
|
|
475
474
|
.filter(provider => !declared.has(provider.id))
|
|
@@ -508,6 +507,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
|
|
|
508
507
|
...credentialRef === undefined ? {} : { credentialRef },
|
|
509
508
|
suggestedRef: deriveCredentialRef(base.provider),
|
|
510
509
|
...base.declared === undefined ? {} : { declared: base.declared },
|
|
510
|
+
...base.error === undefined ? {} : { diagnostic: base.error },
|
|
511
511
|
}
|
|
512
512
|
})
|
|
513
513
|
const refs = [...new Set(rows.flatMap(row => row.credentialRef === undefined ? [] : [row.credentialRef]))]
|
package/src/questions.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The terminal ask_user_question
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The terminal ask_user_question answerer: one `user-questions/request`
|
|
3
|
+
* waterfall listener that drives a FIFO queue — one question request on
|
|
4
|
+
* screen at a time, everything else waiting — then resolves the collected
|
|
5
|
+
* answers back into the waterfall. Mirrors the approval answerer's claim/
|
|
6
|
+
* defer split: only agents this TUI owns are answered, every other request
|
|
7
|
+
* falls through to the next answerer.
|
|
8
8
|
*
|
|
9
|
-
* Plan reviews (`exit_plan_mode`) arrive through the same
|
|
9
|
+
* Plan reviews (`exit_plan_mode`) arrive through the same waterfall with an
|
|
10
10
|
* `intent: { kind: 'plan-review' }` — the renderer highlights the approve
|
|
11
11
|
* option; the answer encoding is identical either way.
|
|
12
12
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
18
|
import {
|
|
18
19
|
UserQuestionError,
|
|
19
20
|
type AskUserQuestionAnswer,
|
|
@@ -56,13 +57,15 @@ const ABORT_ERROR = new UserQuestionError(
|
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Mount the
|
|
60
|
-
* @param ctx - context
|
|
61
|
-
* @
|
|
62
|
-
*
|
|
60
|
+
* Mount the `user-questions/request` answerer over a FIFO queue.
|
|
61
|
+
* @param ctx - plugin context whose event bus carries the waterfall.
|
|
62
|
+
* @param owns - agents this terminal answers for; every other request is
|
|
63
|
+
* deferred back into the waterfall (`next()`), so sibling answerers stay
|
|
64
|
+
* usable. Agent-less asks are claimed: this TUI is the only human surface
|
|
65
|
+
* in the process.
|
|
66
|
+
* @returns the store the renderer subscribes to.
|
|
63
67
|
*/
|
|
64
|
-
export function mountQuestionProvider(ctx: Context): QuestionStore {
|
|
65
|
-
const service = ctx.get('userQuestions')
|
|
68
|
+
export function mountQuestionProvider(ctx: Context, owns: (agent: Agent) => boolean): QuestionStore {
|
|
66
69
|
let snapshot: QuestionSnapshot = { pending: undefined }
|
|
67
70
|
let active: PendingQuestion | undefined
|
|
68
71
|
const queue: PendingQuestion[] = []
|
|
@@ -79,69 +82,49 @@ export function mountQuestionProvider(ctx: Context): QuestionStore {
|
|
|
79
82
|
set({ pending: next })
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
ctx.on('user-questions/request', (
|
|
86
|
+
request: AskUserQuestionRequest,
|
|
87
|
+
next: () => Promise<AskUserQuestionAnswer>,
|
|
88
|
+
): Promise<AskUserQuestionAnswer> => {
|
|
89
|
+
if (request.agent !== undefined && !owns(request.agent)) return next()
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
// Abort settles through the same channel as an Esc cancel: the
|
|
92
|
+
// owning tool/step died, so the answer must not linger.
|
|
93
|
+
const onAbort = (): void => {
|
|
94
|
+
if (active === pending) {
|
|
95
|
+
active = undefined
|
|
96
|
+
set({ pending: undefined })
|
|
97
|
+
advance()
|
|
98
|
+
} else {
|
|
99
|
+
const at = queue.indexOf(pending)
|
|
100
|
+
if (at >= 0) queue.splice(at, 1)
|
|
101
|
+
}
|
|
102
|
+
reject(ABORT_ERROR)
|
|
103
|
+
}
|
|
104
|
+
// Detach on every settle so an answered/cancelled question never
|
|
105
|
+
// retains a listener on the owning tool call's signal.
|
|
106
|
+
const detachAbort = (): void => {
|
|
107
|
+
if (request.signal !== undefined) request.signal.removeEventListener('abort', onAbort)
|
|
108
|
+
}
|
|
109
|
+
const pending: PendingQuestion = {
|
|
110
|
+
request,
|
|
111
|
+
resolve,
|
|
112
|
+
reject,
|
|
113
|
+
detachAbort,
|
|
114
|
+
}
|
|
115
|
+
if (request.signal?.aborted === true) {
|
|
116
|
+
reject(ABORT_ERROR)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
request.signal?.addEventListener('abort', onAbort, { once: true })
|
|
120
|
+
if (active === undefined) {
|
|
121
|
+
active = pending
|
|
122
|
+
set({ pending })
|
|
123
|
+
} else {
|
|
124
|
+
queue.push(pending)
|
|
101
125
|
}
|
|
102
|
-
}
|
|
103
|
-
service.registerProvider({
|
|
104
|
-
ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer> {
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
// Abort settles through the same channel as an Esc cancel: the
|
|
107
|
-
// owning tool/step died, so the answer must not linger.
|
|
108
|
-
const onAbort = (): void => {
|
|
109
|
-
if (active === pending) {
|
|
110
|
-
active = undefined
|
|
111
|
-
set({ pending: undefined })
|
|
112
|
-
advance()
|
|
113
|
-
} else {
|
|
114
|
-
const at = queue.indexOf(pending)
|
|
115
|
-
if (at >= 0) queue.splice(at, 1)
|
|
116
|
-
}
|
|
117
|
-
reject(ABORT_ERROR)
|
|
118
|
-
}
|
|
119
|
-
// Detach on every settle so an answered/cancelled question never
|
|
120
|
-
// retains a listener on the owning tool call's signal.
|
|
121
|
-
const detachAbort = (): void => {
|
|
122
|
-
if (request.signal !== undefined) request.signal.removeEventListener('abort', onAbort)
|
|
123
|
-
}
|
|
124
|
-
const pending: PendingQuestion = {
|
|
125
|
-
request,
|
|
126
|
-
resolve,
|
|
127
|
-
reject,
|
|
128
|
-
detachAbort,
|
|
129
|
-
}
|
|
130
|
-
if (request.signal?.aborted === true) {
|
|
131
|
-
reject(ABORT_ERROR)
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
request.signal?.addEventListener('abort', onAbort, { once: true })
|
|
135
|
-
if (active === undefined) {
|
|
136
|
-
active = pending
|
|
137
|
-
set({ pending })
|
|
138
|
-
} else {
|
|
139
|
-
queue.push(pending)
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
},
|
|
143
126
|
})
|
|
144
|
-
}
|
|
127
|
+
})
|
|
145
128
|
|
|
146
129
|
return {
|
|
147
130
|
subscribe(listener: () => void): () => void {
|