dsh-code 0.7.0 → 0.9.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 +30 -7
- package/README.md +30 -7
- package/lib/index.mjs +3791 -853
- package/lib/types/app.d.ts +90 -1
- package/lib/types/approval.d.ts +3 -1
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +48 -0
- package/lib/types/kernel-panels.d.ts +65 -8
- package/lib/types/models.d.ts +15 -1
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +8 -6
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +97 -3
- package/lib/types/render/status.d.ts +26 -36
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +14 -1
- package/lib/types/session-directory.d.ts +61 -2
- package/lib/types/store.d.ts +13 -2
- package/lib/types/subagents.d.ts +60 -0
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +1200 -219
- package/src/approval.ts +161 -126
- package/src/history.ts +20 -5
- package/src/index.ts +577 -167
- package/src/kernel-panels.ts +354 -37
- package/src/models.ts +26 -0
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +32 -18
- package/src/render/lines.ts +236 -218
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +670 -11
- package/src/render/status.ts +68 -162
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +77 -34
- package/src/session-directory.ts +171 -10
- package/src/skills.ts +8 -4
- package/src/store.ts +26 -8
- package/src/subagents.ts +165 -0
- package/src/version.ts +16 -0
package/src/approval.ts
CHANGED
|
@@ -1,126 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The terminal approval answerer: one `approval/request` waterfall listener
|
|
3
|
-
* that renders the pending question as a y/n bar and resolves the decision
|
|
4
|
-
* back into the waterfall. Mirrors the web host's composer takeover — the
|
|
5
|
-
* service (audit pair, policy gate, fail-closed defaults) all live in
|
|
6
|
-
* dsh-base; this module only answers for agents this TUI owns.
|
|
7
|
-
*
|
|
8
|
-
* Vocabulary note: a client answerer may only ever resolve `'allowed-once'`
|
|
9
|
-
* or `'rejected'`; `'cancelled'` belongs to the request signal and
|
|
10
|
-
* `'unavailable'` to the fail-closed waterfall default.
|
|
11
|
-
*
|
|
12
|
-
* @module @deepseek-ai/dsh-code/approval
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
-
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
|
-
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
|
18
|
-
|
|
19
|
-
/** The answer values a client answerer may resolve with. */
|
|
20
|
-
export type ApprovalAnswer = 'allowed-once' | 'rejected'
|
|
21
|
-
|
|
22
|
-
/** One pending approval question, derived from the request for rendering. */
|
|
23
|
-
export interface PendingApproval {
|
|
24
|
-
/** The asker's human-readable explanation, or a generic fallback. */
|
|
25
|
-
headline: string
|
|
26
|
-
/** The tool the question is about. */
|
|
27
|
-
toolName: string
|
|
28
|
-
/** Command-line preview resolved from the paired streaming tool call. */
|
|
29
|
-
command: string
|
|
30
|
-
/** Resolve the ask; calling twice is inert (one-shot latch). */
|
|
31
|
-
answer(outcome: ApprovalAnswer): void
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The pending-question snapshot the renderer subscribes to. */
|
|
35
|
-
export interface ApprovalSnapshot {
|
|
36
|
-
/** The
|
|
37
|
-
pending: PendingApproval | undefined
|
|
38
|
-
/** Presentational: an answer was submitted, the ask has not settled yet. */
|
|
39
|
-
answered: boolean
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The terminal approval answerer: one `approval/request` waterfall listener
|
|
3
|
+
* that renders the pending question as a y/n bar and resolves the decision
|
|
4
|
+
* back into the waterfall. Mirrors the web host's composer takeover — the
|
|
5
|
+
* service (audit pair, policy gate, fail-closed defaults) all live in
|
|
6
|
+
* dsh-base; this module only answers for agents this TUI owns.
|
|
7
|
+
*
|
|
8
|
+
* Vocabulary note: a client answerer may only ever resolve `'allowed-once'`
|
|
9
|
+
* or `'rejected'`; `'cancelled'` belongs to the request signal and
|
|
10
|
+
* `'unavailable'` to the fail-closed waterfall default.
|
|
11
|
+
*
|
|
12
|
+
* @module @deepseek-ai/dsh-code/approval
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
|
+
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
|
18
|
+
|
|
19
|
+
/** The answer values a client answerer may resolve with. */
|
|
20
|
+
export type ApprovalAnswer = 'allowed-once' | 'rejected'
|
|
21
|
+
|
|
22
|
+
/** One pending approval question, derived from the request for rendering. */
|
|
23
|
+
export interface PendingApproval {
|
|
24
|
+
/** The asker's human-readable explanation, or a generic fallback. */
|
|
25
|
+
headline: string
|
|
26
|
+
/** The tool the question is about. */
|
|
27
|
+
toolName: string
|
|
28
|
+
/** Command-line preview resolved from the paired streaming tool call. */
|
|
29
|
+
command: string
|
|
30
|
+
/** Resolve the ask; calling twice is inert (one-shot latch). */
|
|
31
|
+
answer(outcome: ApprovalAnswer): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The pending-question snapshot the renderer subscribes to. */
|
|
35
|
+
export interface ApprovalSnapshot {
|
|
36
|
+
/** The question on screen (queue head), or undefined when none is asked. */
|
|
37
|
+
pending: PendingApproval | undefined
|
|
38
|
+
/** Presentational: an answer was submitted, the ask has not settled yet. */
|
|
39
|
+
answered: boolean
|
|
40
|
+
/** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
|
|
41
|
+
queued: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Store the pending question lands in; the renderer reads, the answerer writes. */
|
|
45
|
+
export interface ApprovalStore {
|
|
46
|
+
/** Subscribe to pending-state changes; returns the unsubscribe function. */
|
|
47
|
+
subscribe(listener: () => void): () => void
|
|
48
|
+
/** Read the current snapshot (identity-stable between changes). */
|
|
49
|
+
getSnapshot(): ApprovalSnapshot
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create the approval store and mount the answerer listener on the context.
|
|
54
|
+
* The listener claims only requests for `owns`-owned agents and defers every
|
|
55
|
+
* other request back into the waterfall (`next()`), so sibling answerers stay
|
|
56
|
+
* usable. An aborted ask never reaches the human. Plugin teardown removes the
|
|
57
|
+
* listener; the service then fails its own question closed.
|
|
58
|
+
* @param ctx - plugin context whose event bus carries `approval/request`.
|
|
59
|
+
* @param owns - agents this terminal answers for.
|
|
60
|
+
* @param preview - resolves a tool-call preview for a pending request (the
|
|
61
|
+
* request contract carries no arguments; the UI self-serves from the
|
|
62
|
+
* transcript projection via `callId`).
|
|
63
|
+
* @returns the store the renderer subscribes to.
|
|
64
|
+
*/
|
|
65
|
+
export function mountApprovalAnswerer(
|
|
66
|
+
ctx: Context,
|
|
67
|
+
owns: (agent: Agent) => boolean,
|
|
68
|
+
preview: (request: ApprovalRequest) => string,
|
|
69
|
+
): ApprovalStore {
|
|
70
|
+
/** One live ask: its pending view plus the one-shot settle plumbing. */
|
|
71
|
+
interface Slot {
|
|
72
|
+
readonly pending: PendingApproval
|
|
73
|
+
answered: boolean
|
|
74
|
+
}
|
|
75
|
+
const queue: Slot[] = []
|
|
76
|
+
let snapshot: ApprovalSnapshot = { pending: undefined, answered: false, queued: 0 }
|
|
77
|
+
const listeners = new Set<() => void>()
|
|
78
|
+
const publish = (): void => {
|
|
79
|
+
const head = queue[0]
|
|
80
|
+
snapshot = {
|
|
81
|
+
pending: head === undefined ? undefined : head.pending,
|
|
82
|
+
answered: head !== undefined && head.answered,
|
|
83
|
+
queued: Math.max(0, queue.length - 1),
|
|
84
|
+
}
|
|
85
|
+
for (const listener of listeners) listener()
|
|
86
|
+
}
|
|
87
|
+
const removeSlot = (slot: Slot): void => {
|
|
88
|
+
const at = queue.indexOf(slot)
|
|
89
|
+
if (at !== -1) queue.splice(at, 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.on('approval/request', (request: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => {
|
|
93
|
+
if (!owns(request.agent)) return next()
|
|
94
|
+
// An already-aborted ask never reaches the human (mirrors the host bridge).
|
|
95
|
+
if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled')
|
|
96
|
+
|
|
97
|
+
let resolved = false
|
|
98
|
+
let settle!: (outcome: ApprovalOutcome) => void
|
|
99
|
+
const signal = request.signal
|
|
100
|
+
const onAbort = (): void => withdraw()
|
|
101
|
+
// Detach on every settle so an answered ask never retains a listener on
|
|
102
|
+
// the tool call's signal (long turns ask many times; each ask must let go).
|
|
103
|
+
const detachAbort = (): void => {
|
|
104
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
105
|
+
}
|
|
106
|
+
const withdraw = (): void => {
|
|
107
|
+
if (resolved) return
|
|
108
|
+
resolved = true
|
|
109
|
+
detachAbort()
|
|
110
|
+
removeSlot(slot)
|
|
111
|
+
publish()
|
|
112
|
+
// The service's signal race would conclude 'cancelled' anyway; settle
|
|
113
|
+
// the same way so this listener never dangles a pending promise.
|
|
114
|
+
settle('cancelled')
|
|
115
|
+
}
|
|
116
|
+
if (signal !== undefined) {
|
|
117
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
118
|
+
}
|
|
119
|
+
const slot: Slot = {
|
|
120
|
+
answered: false,
|
|
121
|
+
pending: {
|
|
122
|
+
headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
|
|
123
|
+
toolName: request.toolName,
|
|
124
|
+
command: preview(request),
|
|
125
|
+
answer: (outcome: ApprovalAnswer): void => {
|
|
126
|
+
// One-shot latch: a second keypress after submission is inert.
|
|
127
|
+
if (resolved) return
|
|
128
|
+
resolved = true
|
|
129
|
+
detachAbort()
|
|
130
|
+
slot.answered = true
|
|
131
|
+
publish()
|
|
132
|
+
settle(outcome)
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
queue.push(slot)
|
|
137
|
+
publish()
|
|
138
|
+
|
|
139
|
+
return new Promise<ApprovalOutcome>((resolve) => {
|
|
140
|
+
settle = resolve
|
|
141
|
+
}).then((outcome) => {
|
|
142
|
+
if (outcome !== 'cancelled') {
|
|
143
|
+
removeSlot(slot)
|
|
144
|
+
publish()
|
|
145
|
+
}
|
|
146
|
+
return outcome
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
subscribe(listener: () => void): () => void {
|
|
152
|
+
listeners.add(listener)
|
|
153
|
+
return () => {
|
|
154
|
+
listeners.delete(listener)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
getSnapshot(): ApprovalSnapshot {
|
|
158
|
+
return snapshot
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/history.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* @module @deepseek-ai/dsh-tui/history
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** Maximum entries retained in the persistent history file. */
|
|
12
|
-
export const HISTORY_MAX_ENTRIES =
|
|
11
|
+
/** Maximum entries retained in the persistent history file and the local recall pool. */
|
|
12
|
+
export const HISTORY_MAX_ENTRIES = 100
|
|
13
13
|
|
|
14
14
|
/** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
|
|
15
15
|
export function serializeHistoryEntry(text: string): string {
|
|
@@ -56,15 +56,30 @@ export function appendHistoryContent(current: string, text: string, max = HISTOR
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Record one in-session submission: empty text is ignored and an adjacent
|
|
59
|
-
* duplicate collapses (Codex `record_local_submission` semantics).
|
|
59
|
+
* duplicate collapses (Codex `record_local_submission` semantics). The local
|
|
60
|
+
* pool shares the persistent pool's cap so the recall space stays bounded.
|
|
60
61
|
* @param local - current in-session entries, oldest first.
|
|
61
62
|
* @param text - the submitted prompt.
|
|
63
|
+
* @param max - the local pool cap.
|
|
62
64
|
* @returns the updated local list.
|
|
63
65
|
*/
|
|
64
|
-
export function recordLocalEntry(local: readonly string[], text: string): readonly string[] {
|
|
66
|
+
export function recordLocalEntry(local: readonly string[], text: string, max = HISTORY_MAX_ENTRIES): readonly string[] {
|
|
65
67
|
if (text === '') return local
|
|
66
68
|
if (local.length > 0 && local[local.length - 1] === text) return local
|
|
67
|
-
return [...local, text]
|
|
69
|
+
return [...local, text].slice(-max)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a capped entry list to the history file format (one JSON line per
|
|
74
|
+
* entry, trailing newline). The runner writes the in-memory list as the whole
|
|
75
|
+
* file, so rapid same-process submissions cannot lose entries to a
|
|
76
|
+
* read-modify-write race (the file is never read back before writing).
|
|
77
|
+
* @param entries - the entries to persist, oldest first.
|
|
78
|
+
* @returns the file content, '' for an empty list.
|
|
79
|
+
*/
|
|
80
|
+
export function serializeHistoryList(entries: readonly string[]): string {
|
|
81
|
+
if (entries.length === 0) return ''
|
|
82
|
+
return entries.map(serializeHistoryEntry).join('\n') + '\n'
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/**
|