dsh-code 1.0.0 → 1.0.2

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/src/internals.ts CHANGED
@@ -8,7 +8,16 @@
8
8
 
9
9
  import { render } from 'ink'
10
10
  import type { ReactElement } from 'react'
11
- import { BRACKETED_PASTE_DISABLE, BRACKETED_PASTE_ENABLE, KEYBOARD_ENHANCE_DISABLE, KEYBOARD_ENHANCE_ENABLE } from './keyboard.ts'
11
+ import {
12
+ BRACKETED_PASTE_DISABLE,
13
+ BRACKETED_PASTE_ENABLE,
14
+ KEYBOARD_ENHANCE_DISABLE,
15
+ KEYBOARD_ENHANCE_ENABLE,
16
+ TERMINAL_FOCUS_REPORT_DISABLE,
17
+ TERMINAL_FOCUS_REPORT_ENABLE,
18
+ isVsCodeTerminalEnv,
19
+ shouldEnableKeyboardEnhancement,
20
+ } from './keyboard.ts'
12
21
 
13
22
  /** A mounted terminal app instance; the runner owns unmount ordering. */
14
23
  export interface TuiMount {
@@ -29,11 +38,16 @@ export const internals: {
29
38
  stderr: { write(chunk: string): unknown }
30
39
  } = {
31
40
  mount: (element: ReactElement): TuiMount => {
32
- // Codex keyboard_modes parity: push the kitty keyboard protocol so
33
- // Shift+Enter arrives as CSI 13;2u instead of a bare CR, and enable
34
- // bracketed paste so pasted newlines insert instead of submitting. Both
35
- // are inert in terminals without support.
36
- process.stdout.write(KEYBOARD_ENHANCE_ENABLE + BRACKETED_PASTE_ENABLE)
41
+ // VS Code's integrated terminal can route Tab to the workbench when Kitty
42
+ // enhancement is enabled. Keep bracketed paste everywhere, but only push
43
+ // the keyboard protocol on terminals that can safely own those key events.
44
+ const keyboardEnhanced = shouldEnableKeyboardEnhancement()
45
+ const focusReporting = isVsCodeTerminalEnv()
46
+ process.stdout.write(
47
+ (keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : '')
48
+ + BRACKETED_PASTE_ENABLE
49
+ + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ''),
50
+ )
37
51
  // App owns Ctrl+C's deliberate three-state contract (interrupt, clear
38
52
  // draft, quit). Ink's default `exitOnCtrlC: true` would intercept the
39
53
  // normalized control byte first, unmount only its renderer, and leave the
@@ -45,9 +59,12 @@ export const internals: {
45
59
  },
46
60
  unmount(): void {
47
61
  instance.unmount()
48
- // Pop the stack so the parent shell does not inherit enhanced key
49
- // reporting (Codex resets even harder on forced exit).
50
- process.stdout.write(KEYBOARD_ENHANCE_DISABLE + BRACKETED_PASTE_DISABLE)
62
+ // Pop only a stack this mount pushed, then disable bracketed paste.
63
+ process.stdout.write(
64
+ (keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : '')
65
+ + BRACKETED_PASTE_DISABLE
66
+ + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ''),
67
+ )
51
68
  },
52
69
  }
53
70
  },
package/src/keyboard.ts CHANGED
@@ -2,33 +2,74 @@
2
2
  * Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
3
3
  * kitty CSI-u normalization layer.
4
4
  *
5
- * The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
6
- * and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`), which makes terminals
7
- * report Shift+Enter as `CSI 13;2u` instead of a bare CR. Event types are
8
- * deliberately NOT requested: Ink 5's parser cannot decode the
9
- * `:event-type` suffix, and repeat/release reporting buys this surface
10
- * nothing.
5
+ * The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
6
+ * and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
7
+ * deliberately NOT requested: Ink 5's parser cannot decode the
8
+ * `:event-type` suffix, and repeat/release reporting buys this surface
9
+ * nothing.
11
10
  *
12
11
  * Ink 5 also cannot parse most CSI-u forms at all — they fall through its
13
12
  * regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
14
13
  * stdin read patch therefore rewrites every CSI-u form it can decode back
15
14
  * to the legacy byte or canonical sequence the existing key handling
16
- * already understands, before Ink ever parses the chunk.
17
- *
18
- * @module @deepseek-ai/dsh-code/keyboard
19
- */
20
-
21
- /** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
22
- export const KEYBOARD_ENHANCE_ENABLE = '\x1b[>4;0m\x1b[>5u'
15
+ * already understands, before Ink ever parses the chunk.
16
+ * @module @deepseek-ai/dsh-code/keyboard
17
+ */
18
+
19
+ /** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
20
+ export const KEYBOARD_ENHANCE_ENABLE = '\x1b[>4;0m\x1b[>5u'
23
21
 
24
22
  /** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
25
- export const KEYBOARD_ENHANCE_DISABLE = '\x1b[<u\x1b[>4;0m'
26
-
27
- /** Enable bracketed paste reporting. */
28
- export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h'
23
+ export const KEYBOARD_ENHANCE_DISABLE = '\x1b[<u\x1b[>4;0m'
24
+
25
+ /** Explicit environment overrides for terminal keyboard enhancement. */
26
+ export const DSH_DISABLE_KEYBOARD_ENHANCEMENT = 'DSH_DISABLE_KEYBOARD_ENHANCEMENT'
27
+ export const DSH_ENABLE_KEYBOARD_ENHANCEMENT = 'DSH_ENABLE_KEYBOARD_ENHANCEMENT'
28
+
29
+ function parseBooleanEnv(value: string | undefined): boolean | undefined {
30
+ if (value === undefined) return undefined
31
+ const normalized = value.trim().toLowerCase()
32
+ if (normalized === '1' || normalized === 'true' || normalized === 'yes') return true
33
+ if (normalized === '0' || normalized === 'false' || normalized === 'no') return false
34
+ return undefined
35
+ }
36
+
37
+ /** True when the process is running inside the VS Code integrated terminal. */
38
+ export function isVsCodeTerminalEnv(env: NodeJS.ProcessEnv = process.env): boolean {
39
+ return env.TERM_PROGRAM?.trim().toLowerCase() === 'vscode' || env.VSCODE_INJECTION === '1'
40
+ }
41
+
42
+ /** Whether to push Kitty keyboard enhancement for the current terminal. */
43
+ export function shouldEnableKeyboardEnhancement(env: NodeJS.ProcessEnv = process.env): boolean {
44
+ const explicitDisable = parseBooleanEnv(env[DSH_DISABLE_KEYBOARD_ENHANCEMENT])
45
+ if (explicitDisable === true) return false
46
+ const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT])
47
+ if (explicitEnable !== undefined) return explicitEnable
48
+ return !isVsCodeTerminalEnv(env)
49
+ }
29
50
 
30
- /** Disable bracketed paste reporting. */
31
- export const BRACKETED_PASTE_DISABLE = '\x1b[?2004l'
51
+ /** Enable bracketed paste reporting. */
52
+ export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h'
53
+
54
+ /** Disable bracketed paste reporting. */
55
+ export const BRACKETED_PASTE_DISABLE = '\x1b[?2004l'
56
+
57
+ /** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
58
+ export const TERMINAL_FOCUS_REPORT_ENABLE = '\x1b[?1004h'
59
+
60
+ /** Disable terminal focus-in/focus-out reporting. */
61
+ export const TERMINAL_FOCUS_REPORT_DISABLE = '\x1b[?1004l'
62
+
63
+ /**
64
+ * Remove xterm focus reports from one input chunk and update the caller's
65
+ * focus state. Focus reports are terminal protocol, not composer text.
66
+ */
67
+ export function stripTerminalFocusEvents(chunk: string, onFocus: (focused: boolean) => void): string {
68
+ return chunk.replace(/\x1b\[(I|O)/gu, (_whole, event: string) => {
69
+ onFocus(event === 'I')
70
+ return ''
71
+ })
72
+ }
32
73
 
33
74
  /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
34
75
  export const PASTE_START_MARKER = '[200~'
@@ -39,9 +80,13 @@ export const PASTE_END_MARKER = '[201~'
39
80
  * `input` text, where an unhandled paste would otherwise persist the literal
40
81
  * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
41
82
  */
42
- export function stripPasteMarkers(text: string): string {
43
- return text.replaceAll(PASTE_START_MARKER, '').replaceAll(PASTE_END_MARKER, '')
44
- }
83
+ export function stripPasteMarkers(text: string): string {
84
+ return text
85
+ .replaceAll(`\x1b${PASTE_START_MARKER}`, '')
86
+ .replaceAll(`\x1b${PASTE_END_MARKER}`, '')
87
+ .replaceAll(PASTE_START_MARKER, '')
88
+ .replaceAll(PASTE_END_MARKER, '')
89
+ }
45
90
 
46
91
  /** One decoded CSI-u keypress: key code, 1-based modifier param, alternate code. */
47
92
  interface CsiUKey {
@@ -51,21 +96,20 @@ interface CsiUKey {
51
96
  }
52
97
 
53
98
  /** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
54
- const CSI_U_SOURCE = '\x1b\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u'
55
-
56
- /** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
57
- function legacyForKey(key: CsiUKey): string | undefined {
99
+ const CSI_U_SOURCE = '\x1b\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u'
100
+
101
+ /** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
102
+ function legacyForKey(key: CsiUKey): string | undefined {
58
103
  const bits = Math.max(0, key.modifiers - 1)
59
104
  const shift = (bits & 1) !== 0
60
105
  const alt = (bits & 2) !== 0
61
- const ctrl = (bits & 4) !== 0
62
- if (key.code === 13) {
63
- // Enter: shift keeps the canonical enhanced form the composer detects;
64
- // ctrl maps to LF (the Ctrl+J newline binding), alt to ESC+CR.
65
- if (shift) return '\x1b[13;2u'
66
- if (ctrl) return '\n'
67
- if (alt) return '\x1b\r'
68
- return '\r'
106
+ const ctrl = (bits & 4) !== 0
107
+ if (key.code === 13) {
108
+ // Modified Enter has no dedicated composer behavior. Preserve the legacy
109
+ // Ctrl/Alt bytes and collapse every other form to ordinary Enter.
110
+ if (ctrl) return '\n'
111
+ if (alt) return '\x1b\r'
112
+ return '\r'
69
113
  }
70
114
  if (key.code === 27) return '\x1b'
71
115
  if (key.code === 9) return shift ? '\x1b[Z' : '\t'
@@ -111,10 +155,10 @@ function legacyForKey(key: CsiUKey): string | undefined {
111
155
  * sequences pass through untouched, so terminals without the protocol are
112
156
  * unaffected.
113
157
  */
114
- export function normalizeKeyboardChunk(chunk: string): string {
115
- if (!chunk.includes('\x1b[') || !chunk.includes('u')) return chunk
116
- const pattern = new RegExp(CSI_U_SOURCE, 'g')
117
- return chunk.replace(pattern, (whole, code: string, mods?: string, third?: string) => {
158
+ export function normalizeKeyboardChunk(chunk: string): string {
159
+ if (!chunk.includes('\x1b[') || !chunk.includes('u')) return chunk
160
+ const pattern = new RegExp(CSI_U_SOURCE, 'g')
161
+ return chunk.replace(pattern, (whole, code: string, mods?: string, third?: string) => {
118
162
  const legacy = legacyForKey({
119
163
  code: Number.parseInt(code, 10),
120
164
  modifiers: mods === undefined || mods === '' ? 1 : Math.max(1, Number.parseInt(mods, 10)),
@@ -122,4 +166,85 @@ export function normalizeKeyboardChunk(chunk: string): string {
122
166
  })
123
167
  return legacy ?? whole
124
168
  })
125
- }
169
+ }
170
+
171
+ /** Editor actions Ink cannot distinguish reliably when terminal bytes batch. */
172
+ export type RawEditorToken =
173
+ | { readonly kind: 'text'; readonly text: string }
174
+ | { readonly kind: 'home' | 'end' | 'delete-backward' | 'delete-word-backward' | 'delete-forward' | 'delete-word-forward' }
175
+
176
+ const HOME_SEQUENCES = ['\x1b[H', '\x1b[1~', '\x1b[7~', '\x1bOH'] as const
177
+ const END_SEQUENCES = ['\x1b[F', '\x1b[4~', '\x1b[8~', '\x1bOF'] as const
178
+
179
+ /** Parse one CSI functional-key sequence at `offset`. */
180
+ function functionalToken(chunk: string, offset: number): { token: RawEditorToken; length: number } | undefined {
181
+ const tail = chunk.slice(offset)
182
+ for (const sequence of HOME_SEQUENCES) {
183
+ if (tail.startsWith(sequence)) return { token: { kind: 'home' }, length: sequence.length }
184
+ }
185
+ for (const sequence of END_SEQUENCES) {
186
+ if (tail.startsWith(sequence)) return { token: { kind: 'end' }, length: sequence.length }
187
+ }
188
+ const modifiedHome = /^\x1b\[1;(\d+)H/u.exec(tail)
189
+ if (modifiedHome !== null) return { token: { kind: 'home' }, length: modifiedHome[0].length }
190
+ const modifiedEnd = /^\x1b\[1;(\d+)F/u.exec(tail)
191
+ if (modifiedEnd !== null) return { token: { kind: 'end' }, length: modifiedEnd[0].length }
192
+ const modifiedDelete = /^\x1b\[3(?:;(\d+))?~/u.exec(tail)
193
+ if (modifiedDelete !== null) {
194
+ const modifiers = Number.parseInt(modifiedDelete[1] ?? '1', 10) - 1
195
+ const byWord = (modifiers & 2) !== 0 || (modifiers & 4) !== 0
196
+ return {
197
+ token: { kind: byWord ? 'delete-word-forward' : 'delete-forward' },
198
+ length: modifiedDelete[0].length,
199
+ }
200
+ }
201
+ return undefined
202
+ }
203
+
204
+ /**
205
+ * Tokenize a stdin chunk containing at least one editor-only key. Ink calls
206
+ * `useInput` once for a pasted/batched chunk, so preserving each action here
207
+ * prevents repeated Backspace/Home/End/Delete presses from collapsing into
208
+ * one blurred key event. Unknown escape sequences return undefined and stay
209
+ * under Ink's ownership.
210
+ */
211
+ export function tokenizeRawEditorChunk(chunk: string): readonly RawEditorToken[] | undefined {
212
+ const tokens: RawEditorToken[] = []
213
+ let text = ''
214
+ let special = false
215
+ const flushText = (): void => {
216
+ if (text === '') return
217
+ tokens.push({ kind: 'text', text })
218
+ text = ''
219
+ }
220
+ for (let offset = 0; offset < chunk.length;) {
221
+ if (chunk.startsWith('\x1b\x7f', offset) || chunk.startsWith('\x1b\b', offset)) {
222
+ flushText()
223
+ tokens.push({ kind: 'delete-word-backward' })
224
+ special = true
225
+ offset += 2
226
+ continue
227
+ }
228
+ const functional = functionalToken(chunk, offset)
229
+ if (functional !== undefined) {
230
+ flushText()
231
+ tokens.push(functional.token)
232
+ special = true
233
+ offset += functional.length
234
+ continue
235
+ }
236
+ const char = chunk[offset]!
237
+ if (char === '\x7f' || char === '\b') {
238
+ flushText()
239
+ tokens.push({ kind: 'delete-backward' })
240
+ special = true
241
+ offset += 1
242
+ continue
243
+ }
244
+ if (char === '\x1b') return undefined
245
+ text += char
246
+ offset += 1
247
+ }
248
+ flushText()
249
+ return special ? tokens : undefined
250
+ }
package/src/mentions.ts CHANGED
@@ -22,6 +22,7 @@
22
22
 
23
23
  import type { Context } from '@deepseek-ai/cordis'
24
24
  import type { Agent } from '@deepseek-ai/dsh-agent'
25
+ import { isAbsolute, resolve } from 'node:path'
25
26
  import {
26
27
  DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
27
28
  DEFAULT_FILE_SEARCH_MAX_ENTRIES,
@@ -46,6 +47,8 @@ export interface MentionCandidate {
46
47
  description: string
47
48
  /** Origin kind for icon/coloring decisions. */
48
49
  kind: 'file' | 'directory' | 'session'
50
+ /** Absolute path for file candidates; never rendered or persisted directly. */
51
+ path?: string
49
52
  }
50
53
 
51
54
  /** Prepared submission: readable content plus optional injected context. */
@@ -72,6 +75,11 @@ interface FileReferenceServiceLike {
72
75
  /** Menu cap on file rows; the service owns ranking and default rows. */
73
76
  const MAX_FILE_ROWS = 20
74
77
 
78
+ /** Whether a mention token is already navigating a filesystem path. */
79
+ export function isPathLikeMentionQuery(query: string): boolean {
80
+ return /[\\/]/u.test(query)
81
+ }
82
+
75
83
  /** The mention API the input editor and the runner share. */
76
84
  export interface MentionsApi {
77
85
  /** Ranked menu candidates for the typed `@` query. */
@@ -133,7 +141,7 @@ export function createMentions(ctx: Context, agent: Agent | undefined, cwd: stri
133
141
  : agent === undefined
134
142
  ? preSessionFiles(needle, signal).catch(() => [] as readonly ServiceFileCandidate[])
135
143
  : Promise.resolve([] as readonly ServiceFileCandidate[]),
136
- sessionCapable && needle !== '' && agent !== undefined
144
+ sessionCapable && needle !== '' && !isPathLikeMentionQuery(needle) && agent !== undefined
137
145
  ? resolver!.listCandidates(agent, needle, 10, signal).catch(() => [] as readonly SessionReferenceCandidate[])
138
146
  : Promise.resolve([] as readonly SessionReferenceCandidate[]),
139
147
  ])
@@ -144,6 +152,9 @@ export function createMentions(ctx: Context, agent: Agent | undefined, cwd: stri
144
152
  label: candidate.path,
145
153
  description: candidate.kind === 'directory' ? 'Folder' : 'File',
146
154
  kind: candidate.kind,
155
+ ...candidate.kind === 'file'
156
+ ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) }
157
+ : {},
147
158
  }))
148
159
  const sessionRows: MentionCandidate[] = sessions.map(candidate => ({
149
160
  label: formatSessionReferenceMention(candidate),
package/src/models.ts CHANGED
@@ -13,10 +13,11 @@ import type { ModelSelection } from '@deepseek-ai/dsh-agent'
13
13
  import {
14
14
  ReasoningEffortId,
15
15
  type LlmCallConfig,
16
- type LlmModelInfo,
17
- type LlmModelReasoningInfo,
18
- type LlmResolvedModelInfo,
19
- } from '@deepseek-ai/dsh-llm'
16
+ type LlmModelInfo,
17
+ type LlmModelReasoningInfo,
18
+ type LlmResolvedModelInfo,
19
+ type ModelModality,
20
+ } from '@deepseek-ai/dsh-llm'
20
21
 
21
22
  /** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
22
23
  export interface ModelReasoningEffort {
@@ -44,9 +45,11 @@ export interface ModelRow {
44
45
  providerName: string
45
46
  /** Provider-owned model id. */
46
47
  model: string
47
- /** Human-readable model name. */
48
- modelName: string
49
- /** Adapter-owned selectable reasoning levels when the model exposes any. */
48
+ /** Human-readable model name. */
49
+ modelName: string
50
+ /** Request modalities advertised for this exact route; absent means unknown. */
51
+ inputModalities?: readonly ModelModality[]
52
+ /** Adapter-owned selectable reasoning levels when the model exposes any. */
50
53
  reasoning?: ModelReasoning
51
54
  }
52
55
 
@@ -194,16 +197,19 @@ export async function loadModelDirectory(ctx: Context): Promise<ModelDirectory>
194
197
  const rows = await Promise.all(models.map(async (model): Promise<ModelRow> => {
195
198
  const row: ModelRow = {
196
199
  provider: provider.id,
197
- providerName: provider.name,
198
- model: model.id,
199
- modelName: model.name,
200
- }
200
+ providerName: provider.name,
201
+ model: model.id,
202
+ modelName: model.name,
203
+ ...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
204
+ }
201
205
  if (llmResolve.resolveModelInfo === undefined) return row
202
206
  try {
203
207
  const resolved = await llmResolve.resolveModelInfo(provider.id, model.id)
204
- return resolved.reasoning === undefined
205
- ? row
206
- : { ...row, reasoning: mapReasoning(resolved.reasoning) }
208
+ return {
209
+ ...row,
210
+ ...resolved.inputModalities === undefined ? {} : { inputModalities: [...resolved.inputModalities] },
211
+ ...resolved.reasoning === undefined ? {} : { reasoning: mapReasoning(resolved.reasoning) },
212
+ }
207
213
  } catch {
208
214
  reasoningFailures.push(`${provider.id}/${model.id}`)
209
215
  return row
@@ -1,7 +1,8 @@
1
1
  /** Permission-preset policy for pending and active TUI sessions. */
2
2
 
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
- import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
4
+ import type { PermissionPresetService } from '@deepseek-ai/dsh-permission-presets'
5
+ import type { Session } from '@deepseek-ai/dsh-session'
5
6
 
6
7
  /** One selectable permission preset row for the /permission panel. */
7
8
  export interface PermissionRow {
@@ -9,20 +10,12 @@ export interface PermissionRow {
9
10
  readonly description?: string
10
11
  }
11
12
 
12
- /** Structural boundary over Harness permission presets; values stay service-owned. */
13
- export interface PermissionPresetsService {
14
- readonly names: readonly string[]
15
- readonly defaultPreset: string
16
- resolve(name: string): unknown
17
- current(events: readonly SessionEvent[]): string
18
- set(session: Session, preset: string): void
19
- /** Client presentation metadata for one preset; may reject unknown names. */
20
- optionOf?(name: string): { name: string; description?: string } | undefined
21
- }
13
+ /** Public compatibility alias for the official upstream permission service. */
14
+ export type PermissionPresetsService = PermissionPresetService
22
15
 
23
16
  /** Read the optional Harness service without importing its runtime package. */
24
17
  export function permissionPresetsFrom(ctx: Context): PermissionPresetsService | undefined {
25
- return (ctx as unknown as { get(name: string): unknown }).get('permissionPresets') as PermissionPresetsService | undefined
18
+ return ctx.get('permissionPresets')
26
19
  }
27
20
 
28
21
  /** Effective label for either an active session or the not-yet-created first one. */
@@ -75,7 +68,6 @@ export function applyPendingPermission(
75
68
  */
76
69
  export function listPermissionRows(service: PermissionPresetsService): readonly PermissionRow[] {
77
70
  return service.names.map((id) => {
78
- if (service.optionOf === undefined) return { id }
79
71
  try {
80
72
  return { id, description: service.optionOf(id)?.description }
81
73
  } catch {
package/src/presets.ts CHANGED
@@ -2,31 +2,18 @@
2
2
 
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
4
  import type { Agent } from '@deepseek-ai/dsh-agent'
5
+ import type { AgentPreset, AgentPresets } from '@deepseek-ai/dsh-agent-presets'
5
6
  import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
6
7
 
7
8
  /** One discoverable agent composition. */
8
- export interface PresetRow {
9
- readonly id: string
10
- readonly trust: 'system' | 'user'
11
- readonly name?: string
12
- readonly description?: string
13
- readonly order?: number
14
- readonly broken?: string
15
- }
9
+ export type PresetRow = AgentPreset
16
10
 
17
- /** Structural boundary for the optional upstream AgentPresets service. */
18
- export interface AgentPresetsService {
19
- readonly defaultId: string
20
- list(): Promise<PresetRow[]>
21
- resolve(id?: string): Promise<PresetRow>
22
- mount(agentCtx: Context, id?: string): Promise<PresetRow>
23
- recompose(agentCtx: Context, id: string): Promise<PresetRow>
24
- composedPreset(agentCtx: Context): string | undefined
25
- }
11
+ /** Public compatibility alias for the official upstream service type. */
12
+ export type AgentPresetsService = AgentPresets
26
13
 
27
14
  /** Read an optional Cordis service without requiring its package at build time. */
28
15
  export function agentPresetsFrom(ctx: Context): AgentPresetsService | undefined {
29
- return (ctx as unknown as { get(name: string): unknown }).get('agentPresets') as AgentPresetsService | undefined
16
+ return ctx.get('agentPresets')
30
17
  }
31
18
 
32
19
  /** A preset may change only before the first durable turn begins. */
@@ -249,7 +249,7 @@ export interface ProviderSettingsDirectory {
249
249
 
250
250
  /** Events that invalidate the official Models provider/settings/credential join. */
251
251
  const PROVIDER_SETTINGS_EVENTS = [
252
- 'credentials/updated',
252
+ 'credentials/reference-updated',
253
253
  'settings/document-updated',
254
254
  'llm/adapters-updated',
255
255
  ] as const