dsh-code 1.0.7 → 1.3.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.
Files changed (89) hide show
  1. package/README.en.md +70 -24
  2. package/README.md +71 -25
  3. package/bin/deepseek.mjs +202 -39
  4. package/cordis.patch.yml +13 -4
  5. package/lib/index.mjs +5002 -1061
  6. package/lib/session-query.mjs +3 -2
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-B3orFUYz.mjs} +665 -20
  9. package/lib/types/app.d.ts +120 -63
  10. package/lib/types/attachments.d.ts +16 -7
  11. package/lib/types/authorization-panel.d.ts +3 -3
  12. package/lib/types/git-workflow.d.ts +91 -2
  13. package/lib/types/history.d.ts +10 -0
  14. package/lib/types/i18n.d.ts +39 -0
  15. package/lib/types/index.d.ts +74 -2
  16. package/lib/types/input-split.d.ts +1 -1
  17. package/lib/types/kernel-panels.d.ts +89 -32
  18. package/lib/types/language-panel.d.ts +12 -0
  19. package/lib/types/locales/en.d.ts +498 -0
  20. package/lib/types/locales/zh.d.ts +9 -0
  21. package/lib/types/mentions.d.ts +7 -3
  22. package/lib/types/models.d.ts +14 -0
  23. package/lib/types/panel-accent.d.ts +28 -0
  24. package/lib/types/rainbow.d.ts +69 -0
  25. package/lib/types/render/animations.d.ts +42 -0
  26. package/lib/types/render/inspector.d.ts +26 -0
  27. package/lib/types/render/lines.d.ts +21 -1
  28. package/lib/types/render/markdown.d.ts +1 -1
  29. package/lib/types/render/projection.d.ts +95 -4
  30. package/lib/types/render/status.d.ts +12 -9
  31. package/lib/types/render/text.d.ts +6 -0
  32. package/lib/types/render/usage.d.ts +113 -0
  33. package/lib/types/session-directory.d.ts +42 -1
  34. package/lib/types/session-switch.d.ts +8 -0
  35. package/lib/types/startup.d.ts +1 -1
  36. package/lib/types/terminal-title.d.ts +8 -0
  37. package/lib/types/theme-panel.d.ts +2 -2
  38. package/lib/types/theme.d.ts +271 -52
  39. package/lib/types/update-panel.d.ts +27 -6
  40. package/lib/types/update.d.ts +10 -1
  41. package/lib/types/version.d.ts +6 -3
  42. package/package.json +26 -7
  43. package/src/app.ts +1426 -627
  44. package/src/approval.ts +166 -166
  45. package/src/attachments.ts +65 -19
  46. package/src/authorization-panel.ts +24 -18
  47. package/src/editor-keys.ts +371 -371
  48. package/src/fork.ts +11 -7
  49. package/src/git-workflow.ts +229 -3
  50. package/src/history.ts +14 -0
  51. package/src/i18n.ts +68 -0
  52. package/src/index.ts +503 -128
  53. package/src/input-split.ts +27 -7
  54. package/src/kernel-panels.ts +528 -113
  55. package/src/keyboard.ts +5 -4
  56. package/src/language-panel.ts +53 -0
  57. package/src/locales/en.ts +538 -0
  58. package/src/locales/zh.ts +537 -0
  59. package/src/mentions.ts +8 -4
  60. package/src/models.ts +264 -212
  61. package/src/panel-accent.ts +41 -0
  62. package/src/presets.ts +1 -1
  63. package/src/provider-settings.ts +1 -1
  64. package/src/rainbow.ts +218 -0
  65. package/src/render/animations.ts +104 -6
  66. package/src/render/editor.ts +20 -20
  67. package/src/render/export.ts +116 -95
  68. package/src/render/inspector.ts +42 -0
  69. package/src/render/lines.ts +628 -415
  70. package/src/render/markdown.ts +15 -3
  71. package/src/render/projection.ts +429 -19
  72. package/src/render/status.ts +119 -62
  73. package/src/render/text.ts +15 -0
  74. package/src/render/tool-preview.ts +77 -77
  75. package/src/render/usage.ts +430 -0
  76. package/src/render/width.ts +2 -2
  77. package/src/session-directory.ts +90 -9
  78. package/src/session-query.ts +8 -4
  79. package/src/session-switch.ts +14 -0
  80. package/src/startup.ts +3 -3
  81. package/src/store.ts +19 -1
  82. package/src/subagents.ts +229 -229
  83. package/src/terminal-title.ts +22 -5
  84. package/src/theme-panel.ts +17 -21
  85. package/src/theme.ts +281 -33
  86. package/src/update-panel.ts +148 -31
  87. package/src/update.ts +19 -3
  88. package/src/version.ts +63 -20
  89. package/src/whale-glyph.ts +23 -23
package/src/models.ts CHANGED
@@ -1,232 +1,284 @@
1
- /**
2
- * Model directory for the `/model` panel: the in-process equivalent of the
3
- * web host's model catalog (`buildModelCatalog`), reading the advisory
4
- * `ctx.llm` registry directly. Catalog membership is advisory — a route
5
- * serving a model it stopped advertising stays usable — so selection never
6
- * fails on catalog absence alone.
7
- *
8
- * @module @deepseek-ai/dsh-tui/models
9
- */
10
-
11
- import type { Context } from '@deepseek-ai/cordis'
12
- import type { ModelSelection } from '@deepseek-ai/dsh-agent'
13
- import {
14
- ReasoningEffortId,
15
- type LlmCallConfig,
1
+ /**
2
+ * Model directory for the `/model` panel: the in-process equivalent of the
3
+ * web host's model catalog (`buildModelCatalog`), reading the advisory
4
+ * `ctx.llm` registry directly. Catalog membership is advisory — a route
5
+ * serving a model it stopped advertising stays usable — so selection never
6
+ * fails on catalog absence alone.
7
+ *
8
+ * @module @deepseek-ai/dsh-tui/models
9
+ */
10
+
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import type { ModelSelection } from '@deepseek-ai/dsh-agent'
13
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
14
+ import {
15
+ ReasoningEffortId,
16
+ type LlmCallConfig,
16
17
  type LlmModelInfo,
17
18
  type LlmModelReasoningInfo,
18
19
  type LlmResolvedModelInfo,
19
20
  type ModelModality,
20
21
  } from '@deepseek-ai/dsh-llm'
21
-
22
- /** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
23
- export interface ModelReasoningEffort {
24
- /** Opaque value accepted by the model's `GenerateOptions.reasoningEffort`. */
25
- id: string
26
- /** Human-readable effort name for selectors. */
27
- name: string
28
- /** Optional user-facing distinction from otherwise similar efforts. */
29
- description?: string
30
- }
31
-
32
- /** Selectable reasoning efforts for one model (mirrors `LlmModelReasoningInfo`). */
33
- export interface ModelReasoning {
34
- /** Supported efforts in adapter-preferred display order. */
35
- efforts: readonly ModelReasoningEffort[]
36
- /** Adapter-configured default materialized when callers omit an effort. */
37
- defaultEffort?: string
38
- }
39
-
40
- /** One selectable row in the `/model` panel. */
41
- export interface ModelRow {
42
- /** Registered provider route. */
43
- provider: string
44
- /** Display name of the provider route. */
45
- providerName: string
46
- /** Provider-owned model id. */
47
- model: string
22
+
23
+ /** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
24
+ export interface ModelReasoningEffort {
25
+ /** Opaque value accepted by the model's `GenerateOptions.reasoningEffort`. */
26
+ id: string
27
+ /** Human-readable effort name for selectors. */
28
+ name: string
29
+ /** Optional user-facing distinction from otherwise similar efforts. */
30
+ description?: string
31
+ }
32
+
33
+ /** Selectable reasoning efforts for one model (mirrors `LlmModelReasoningInfo`). */
34
+ export interface ModelReasoning {
35
+ /** Supported efforts in adapter-preferred display order. */
36
+ efforts: readonly ModelReasoningEffort[]
37
+ /** Adapter-configured default materialized when callers omit an effort. */
38
+ defaultEffort?: string
39
+ }
40
+
41
+ /** One selectable row in the `/model` panel. */
42
+ export interface ModelRow {
43
+ /** Registered provider route. */
44
+ provider: string
45
+ /** Display name of the provider route. */
46
+ providerName: string
47
+ /** Provider-owned model id. */
48
+ model: string
48
49
  /** Human-readable model name. */
49
50
  modelName: string
50
51
  /** Request modalities advertised for this exact route; absent means unknown. */
51
52
  inputModalities?: readonly ModelModality[]
52
53
  /** Adapter-owned selectable reasoning levels when the model exposes any. */
53
- reasoning?: ModelReasoning
54
- }
55
-
56
- /** The resolved directory: rows plus per-provider discovery failures. */
57
- export interface ModelDirectory {
58
- /** Advisory rows, provider-major in registry order. */
59
- rows: readonly ModelRow[]
60
- /** Provider ids whose model listing failed; those providers contribute no rows. */
61
- failures: readonly string[]
62
- /**
63
- * `provider/model` labels whose per-model capability lookup failed. Those
64
- * rows still appear (advisory degrade, mirroring the web catalog), but
65
- * without an effort picker — a picker caller must not misread the absence
66
- * as "this model exposes no reasoning" (e.g. deepseek-v4-flash always
67
- * advertises off/high/max unless thinking is disabled). Optional for
68
- * callers that shape a directory by hand; {@link loadModelDirectory}
69
- * always populates it (empty when nothing failed).
70
- */
71
- reasoningFailures?: readonly string[]
72
- }
73
-
74
- /** Map an adapter's reasoning capability onto the panel's plain-id shape. */
75
- export function mapReasoning(reasoning: LlmModelReasoningInfo): ModelReasoning {
76
- return {
77
- efforts: reasoning.efforts.map(effort => ({
78
- id: effort.id,
79
- name: effort.name,
80
- ...effort.description === undefined ? {} : { description: effort.description },
81
- })),
82
- ...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
83
- }
84
- }
85
-
86
- /**
87
- * Resolve the effective model selection for one live session, in the
88
- * documented precedence: the in-process explicit pick, then the session's
89
- * last `request/header` config, then the deployment default.
90
- * @param picked - the explicit selection made in this process, when any.
91
- * @param logged - the last logged request header config, when any.
92
- * @param defaults - the deployment default selection.
93
- * @returns the effective selection, carrying a reasoning effort when one is in force.
94
- */
95
- export function resolveEffectiveSelection(
96
- picked: ModelSelection | undefined,
97
- logged: { provider: string; model: string; reasoningEffort?: string } | undefined,
98
- defaults: ModelSelection,
99
- ): ModelSelection {
100
- if (picked !== undefined) return picked
101
- if (logged !== undefined) {
102
- return {
103
- provider: logged.provider,
104
- model: logged.model,
105
- ...logged.reasoningEffort === undefined
106
- ? {}
107
- : { reasoningEffort: ReasoningEffortId(logged.reasoningEffort) },
108
- }
109
- }
110
- return defaults
111
- }
112
-
113
- /**
114
- * Build the selection one `/model` pick applies, rejecting an effort the
115
- * row does not advertise. The row is the picker's source of truth, so a
116
- * stale directory cannot smuggle an unsupported effort into the next step
117
- * (the request pipeline would reject it before network I/O regardless). The
118
- * empty string is the picker's "provider default" sentinel — an explicit
119
- * choice to leave the effort to the model's own default, exactly like an
120
- * absent effort.
121
- * @param row - the picked model row.
122
- * @param effortId - the chosen advertised effort, '' or undefined for the model default.
123
- * @returns the selection the runner records for the next assembled step.
124
- */
125
- export function buildModelSelection(row: ModelRow, effortId?: string): ModelSelection {
126
- const selected = effortId === undefined || effortId === '' ? undefined : effortId
127
- if (selected !== undefined
128
- && (row.reasoning === undefined || !row.reasoning.efforts.some(effort => effort.id === selected))) {
129
- throw new Error(`model ${row.provider}/${row.model} does not support reasoning effort "${selected}"`)
130
- }
131
- return {
132
- provider: row.provider,
133
- model: row.model,
134
- ...selected === undefined ? {} : { reasoningEffort: ReasoningEffortId(selected) },
135
- }
136
- }
137
-
138
- /** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
139
- export function modelSelectionLabel(selection: ModelSelection): string {
140
- return selection.reasoningEffort === undefined
141
- ? `${selection.provider}/${selection.model}`
142
- : `${selection.provider}/${selection.model}@${selection.reasoningEffort}`
143
- }
144
-
145
- /**
146
- * Apply one model selection onto a resolved request config — the exact
147
- * semantics of the kernel's `installModelSelection` request listener,
148
- * extracted so the TUI can mirror it for subagent-origin requests: children
149
- * spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
150
- * which a mid-session /model switch never touches, so delegated work would
151
- * otherwise keep running on the launch-time route. An absent effort strips
152
- * any inherited effort (restoring the selected model's provider default),
153
- * matching the kernel listener field-for-field.
154
- * @param resolved - the config the inner chain produced.
155
- * @param selection - the selection to enforce.
156
- * @returns the overridden config.
157
- */
158
- export function applyModelSelectionToConfig(resolved: LlmCallConfig, selection: ModelSelection): LlmCallConfig {
159
- const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
160
- return {
161
- ...withoutInheritedEffort,
162
- provider: selection.provider,
163
- model: selection.model,
164
- ...selection.reasoningEffort === undefined
165
- ? {}
166
- : { reasoningEffort: selection.reasoningEffort },
167
- }
168
- }
169
-
170
- /**
171
- * Load the selectable model directory from the live `ctx.llm` registry.
172
- * Providers are listed synchronously; each provider's models are discovered
173
- * with a bounded parallel fan-out whose failures degrade to that provider
174
- * contributing no rows (mirrors the web catalog's per-provider failures).
175
- * Each row's reasoning levels are resolved per exact model like the web
176
- * catalog (`buildModelCatalog`); a single model's capability lookup failure
177
- * degrades to that row having no effort picker rather than hiding the model,
178
- * and the failure rides `reasoningFailures` so the caller can tell "no
179
- * advertised reasoning" from "capability lookup failed".
180
- * @param ctx - context carrying the `llm` service.
181
- * @returns the resolved directory; empty rows when `llm` is unavailable.
182
- */
183
- export async function loadModelDirectory(ctx: Context): Promise<ModelDirectory> {
184
- const llm = ctx.get('llm')
185
- if (llm === undefined) return { rows: [], failures: [], reasoningFailures: [] }
186
- // Call resolveModelInfo AS A METHOD (llm.resolveModelInfo(...)): destructured
187
- // off the service it loses `this` — this.registration() then throws on the
188
- // first provider, the catch swallows it, and every row lands in
189
- // reasoningFailures ("capability lookup failed") even though the adapter
190
- // itself never fails.
191
- const llmResolve = llm as { resolveModelInfo?: (provider: string, model: string) => Promise<LlmResolvedModelInfo> }
192
- const providers = llm.listProviders()
193
- const listed = await Promise.all(providers.map(async (provider) => {
194
- try {
195
- const models: readonly LlmModelInfo[] = await llm.listModels(provider.id)
196
- const reasoningFailures: string[] = []
197
- const rows = await Promise.all(models.map(async (model): Promise<ModelRow> => {
198
- const row: ModelRow = {
199
- provider: provider.id,
54
+ reasoning?: ModelReasoning
55
+ }
56
+
57
+ /** The resolved directory: rows plus per-provider discovery failures. */
58
+ export interface ModelDirectory {
59
+ /** Advisory rows, provider-major in registry order. */
60
+ rows: readonly ModelRow[]
61
+ /** Provider ids whose model listing failed; those providers contribute no rows. */
62
+ failures: readonly string[]
63
+ /**
64
+ * `provider/model` labels whose per-model capability lookup failed. Those
65
+ * rows still appear (advisory degrade, mirroring the web catalog), but
66
+ * without an effort picker — a picker caller must not misread the absence
67
+ * as "this model exposes no reasoning" (e.g. deepseek-v4-flash always
68
+ * advertises off/high/max unless thinking is disabled). Optional for
69
+ * callers that shape a directory by hand; {@link loadModelDirectory}
70
+ * always populates it (empty when nothing failed).
71
+ */
72
+ reasoningFailures?: readonly string[]
73
+ }
74
+
75
+ /** Map an adapter's reasoning capability onto the panel's plain-id shape. */
76
+ export function mapReasoning(reasoning: LlmModelReasoningInfo): ModelReasoning {
77
+ return {
78
+ efforts: reasoning.efforts.map(effort => ({
79
+ id: effort.id,
80
+ name: effort.name,
81
+ ...effort.description === undefined ? {} : { description: effort.description },
82
+ })),
83
+ ...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Resolve the effective model selection for one live session, in the
89
+ * documented precedence: the in-process explicit pick, then the session's
90
+ * last `request/header` config, then the deployment default.
91
+ * @param picked - the explicit selection made in this process, when any.
92
+ * @param logged - the last logged request header config, when any.
93
+ * @param defaults - the deployment default selection.
94
+ * @returns the effective selection, carrying a reasoning effort when one is in force.
95
+ */
96
+ export function resolveEffectiveSelection(
97
+ picked: ModelSelection | undefined,
98
+ logged: { provider: string; model: string; reasoningEffort?: string } | undefined,
99
+ defaults: ModelSelection,
100
+ ): ModelSelection {
101
+ if (picked !== undefined) return picked
102
+ if (logged !== undefined) {
103
+ return {
104
+ provider: logged.provider,
105
+ model: logged.model,
106
+ ...logged.reasoningEffort === undefined
107
+ ? {}
108
+ : { reasoningEffort: ReasoningEffortId(logged.reasoningEffort) },
109
+ }
110
+ }
111
+ return defaults
112
+ }
113
+
114
+ /** Structural equality of two selections (upstream `sameSelection`). */
115
+ function sameModelSelection(left: ModelSelection, right: ModelSelection): boolean {
116
+ return left.provider === right.provider
117
+ && left.model === right.model
118
+ && left.reasoningEffort === right.reasoningEffort
119
+ }
120
+
121
+ /**
122
+ * The session log's unconsumed explicit model selection (the resume layer of
123
+ * the documented precedence). `model/selection` events are log-only facts the
124
+ * web host's session-controller appends when its user picks a model for the
125
+ * NEXT request; a request header assembling exactly that selection retires
126
+ * it, while a header for different values (another route forced the request)
127
+ * leaves it armed. Seeding the resumed pick from here restores a selection
128
+ * that was made but never sent — the request header alone would answer with
129
+ * the older model.
130
+ * @param events - the full persisted event list of the resumed session.
131
+ * @returns the still-pending selection, when one exists.
132
+ */
133
+ export function pendingModelSelection(events: readonly SessionEvent[]): ModelSelection | undefined {
134
+ let pending: ModelSelection | undefined
135
+ for (const event of events) {
136
+ // The event map merge lives in @deepseek-ai/dsh-api-session-controller,
137
+ // which this bundle does not depend on; the string guard keeps the fold
138
+ // decoupled while matching the typed upstream projection exactly.
139
+ if ((event.type as string) === 'model/selection') {
140
+ const data = event.data as Partial<ModelSelection>
141
+ if (typeof data.provider === 'string' && typeof data.model === 'string') {
142
+ const selection: ModelSelection = {
143
+ provider: data.provider,
144
+ model: data.model,
145
+ ...data.reasoningEffort === undefined ? {} : { reasoningEffort: data.reasoningEffort },
146
+ }
147
+ if (pending === undefined || !sameModelSelection(pending, selection)) pending = selection
148
+ }
149
+ continue
150
+ }
151
+ if (event.type === 'request/header') {
152
+ const config = event.data.header.config
153
+ if (pending !== undefined && sameModelSelection(pending, {
154
+ provider: config.provider,
155
+ model: config.model,
156
+ ...config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort },
157
+ })) {
158
+ pending = undefined
159
+ }
160
+ }
161
+ }
162
+ return pending
163
+ }
164
+
165
+ /**
166
+ * Build the selection one `/model` pick applies, rejecting an effort the
167
+ * row does not advertise. The row is the picker's source of truth, so a
168
+ * stale directory cannot smuggle an unsupported effort into the next step
169
+ * (the request pipeline would reject it before network I/O regardless). The
170
+ * empty string is the picker's "provider default" sentinel — an explicit
171
+ * choice to leave the effort to the model's own default, exactly like an
172
+ * absent effort.
173
+ * @param row - the picked model row.
174
+ * @param effortId - the chosen advertised effort, '' or undefined for the model default.
175
+ * @returns the selection the runner records for the next assembled step.
176
+ */
177
+ export function buildModelSelection(row: ModelRow, effortId?: string): ModelSelection {
178
+ const selected = effortId === undefined || effortId === '' ? undefined : effortId
179
+ if (selected !== undefined
180
+ && (row.reasoning === undefined || !row.reasoning.efforts.some(effort => effort.id === selected))) {
181
+ throw new Error(`model ${row.provider}/${row.model} does not support reasoning effort "${selected}"`)
182
+ }
183
+ return {
184
+ provider: row.provider,
185
+ model: row.model,
186
+ ...selected === undefined ? {} : { reasoningEffort: ReasoningEffortId(selected) },
187
+ }
188
+ }
189
+
190
+ /** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
191
+ export function modelSelectionLabel(selection: ModelSelection): string {
192
+ return selection.reasoningEffort === undefined
193
+ ? `${selection.provider}/${selection.model}`
194
+ : `${selection.provider}/${selection.model}@${selection.reasoningEffort}`
195
+ }
196
+
197
+ /**
198
+ * Apply one model selection onto a resolved request config — the exact
199
+ * semantics of the kernel's `installModelSelection` request listener,
200
+ * extracted so the TUI can mirror it for subagent-origin requests: children
201
+ * spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
202
+ * which a mid-session /model switch never touches, so delegated work would
203
+ * otherwise keep running on the launch-time route. An absent effort strips
204
+ * any inherited effort (restoring the selected model's provider default),
205
+ * matching the kernel listener field-for-field.
206
+ * @param resolved - the config the inner chain produced.
207
+ * @param selection - the selection to enforce.
208
+ * @returns the overridden config.
209
+ */
210
+ export function applyModelSelectionToConfig(resolved: LlmCallConfig, selection: ModelSelection): LlmCallConfig {
211
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
212
+ return {
213
+ ...withoutInheritedEffort,
214
+ provider: selection.provider,
215
+ model: selection.model,
216
+ ...selection.reasoningEffort === undefined
217
+ ? {}
218
+ : { reasoningEffort: selection.reasoningEffort },
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Load the selectable model directory from the live `ctx.llm` registry.
224
+ * Providers are listed synchronously; each provider's models are discovered
225
+ * with a bounded parallel fan-out whose failures degrade to that provider
226
+ * contributing no rows (mirrors the web catalog's per-provider failures).
227
+ * Each row's reasoning levels are resolved per exact model like the web
228
+ * catalog (`buildModelCatalog`); a single model's capability lookup failure
229
+ * degrades to that row having no effort picker rather than hiding the model,
230
+ * and the failure rides `reasoningFailures` so the caller can tell "no
231
+ * advertised reasoning" from "capability lookup failed".
232
+ * @param ctx - context carrying the `llm` service.
233
+ * @returns the resolved directory; empty rows when `llm` is unavailable.
234
+ */
235
+ export async function loadModelDirectory(ctx: Context): Promise<ModelDirectory> {
236
+ const llm = ctx.get('llm')
237
+ if (llm === undefined) return { rows: [], failures: [], reasoningFailures: [] }
238
+ // Call resolveModelInfo AS A METHOD (llm.resolveModelInfo(...)): destructured
239
+ // off the service it loses `this` — this.registration() then throws on the
240
+ // first provider, the catch swallows it, and every row lands in
241
+ // reasoningFailures ("capability lookup failed") even though the adapter
242
+ // itself never fails.
243
+ const llmResolve = llm as { resolveModelInfo?: (provider: string, model: string) => Promise<LlmResolvedModelInfo> }
244
+ const providers = llm.listProviders()
245
+ const listed = await Promise.all(providers.map(async (provider) => {
246
+ try {
247
+ const models: readonly LlmModelInfo[] = await llm.listModels(provider.id)
248
+ const reasoningFailures: string[] = []
249
+ const rows = await Promise.all(models.map(async (model): Promise<ModelRow> => {
250
+ const row: ModelRow = {
251
+ provider: provider.id,
200
252
  providerName: provider.name,
201
253
  model: model.id,
202
254
  modelName: model.name,
203
255
  ...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
204
256
  }
205
- if (llmResolve.resolveModelInfo === undefined) return row
206
- try {
207
- const resolved = await llmResolve.resolveModelInfo(provider.id, model.id)
257
+ if (llmResolve.resolveModelInfo === undefined) return row
258
+ try {
259
+ const resolved = await llmResolve.resolveModelInfo(provider.id, model.id)
208
260
  return {
209
261
  ...row,
210
262
  ...resolved.inputModalities === undefined ? {} : { inputModalities: [...resolved.inputModalities] },
211
263
  ...resolved.reasoning === undefined ? {} : { reasoning: mapReasoning(resolved.reasoning) },
212
264
  }
213
- } catch {
214
- reasoningFailures.push(`${provider.id}/${model.id}`)
215
- return row
216
- }
217
- }))
218
- return {
219
- provider: provider.id,
220
- providerName: provider.name,
221
- models: rows,
222
- reasoningFailures,
223
- }
224
- } catch {
225
- return { provider: provider.id, providerName: provider.name, models: [] as ModelRow[], failed: true }
226
- }
227
- }))
228
- const rows = listed.flatMap(entry => entry.models)
229
- const failures = listed.filter(entry => 'failed' in entry && entry.failed === true).map(entry => entry.provider)
230
- const reasoningFailures = listed.flatMap(entry => 'failed' in entry ? [] : entry.reasoningFailures)
231
- return { rows, failures, reasoningFailures }
232
- }
265
+ } catch {
266
+ reasoningFailures.push(`${provider.id}/${model.id}`)
267
+ return row
268
+ }
269
+ }))
270
+ return {
271
+ provider: provider.id,
272
+ providerName: provider.name,
273
+ models: rows,
274
+ reasoningFailures,
275
+ }
276
+ } catch {
277
+ return { provider: provider.id, providerName: provider.name, models: [] as ModelRow[], failed: true }
278
+ }
279
+ }))
280
+ const rows = listed.flatMap(entry => entry.models)
281
+ const failures = listed.filter(entry => 'failed' in entry && entry.failed === true).map(entry => entry.provider)
282
+ const reasoningFailures = listed.flatMap(entry => 'failed' in entry ? [] : entry.reasoningFailures)
283
+ return { rows, failures, reasoningFailures }
284
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Stable per-panel accents for the prismatic theme: every bordered panel
3
+ * claims the next slot in a first-seen sequence keyed by its stable id, so a
4
+ * screen with several open panels shows several ring colors while each panel
5
+ * keeps one learnable color. Deliberately NOT a React hook — several panels
6
+ * render through direct function calls inside conditionals, where hooks are
7
+ * illegal — and identity is by panel id, not by mount. Outside prismatic the
8
+ * helper is an identity: callers pass their usual border/title colors
9
+ * through unchanged and dark/light render pixel-identically.
10
+ *
11
+ * @module @deepseek-ai/dsh-tui/panel-accent
12
+ */
13
+
14
+ import { surfaceAccent, type RgbTriple } from './theme.ts'
15
+
16
+ /** First-seen slot per panel id; stable for the process lifetime. */
17
+ const panelSlots = new Map<string, number>()
18
+
19
+ /** One panel's resolved accent pair (border and title share the ring color in prismatic). */
20
+ export interface PanelAccent {
21
+ readonly border: RgbTriple
22
+ readonly title: RgbTriple
23
+ }
24
+
25
+ /**
26
+ * The accent pair for one panel: in prismatic both entries are the panel's
27
+ * ring color (border and title match, per the design); in every other theme
28
+ * they are exactly the colors passed in.
29
+ * @param id - stable panel identity ('todos', 'model', 'kernel-list', …).
30
+ * @param border - the border color used outside prismatic.
31
+ * @param title - the title color used outside prismatic (defaults to border).
32
+ * @returns the resolved accent pair for this panel.
33
+ */
34
+ export function panelAccent(id: string, border: RgbTriple, title: RgbTriple = border): PanelAccent {
35
+ let slot = panelSlots.get(id)
36
+ if (slot === undefined) {
37
+ slot = panelSlots.size
38
+ panelSlots.set(id, slot)
39
+ }
40
+ return { border: surfaceAccent(slot, border), title: surfaceAccent(slot, title) }
41
+ }
package/src/presets.ts CHANGED
@@ -38,7 +38,7 @@ export function resolvePreset(session: Pick<Session, 'header' | 'snapshotEvents'
38
38
  for (let index = events.length - 1; index >= 0; index -= 1) {
39
39
  const event = events[index] as unknown as { type: string; data?: { agentPreset?: string } }
40
40
  if (event.type === 'agent-preset/selected' && event.data?.agentPreset !== undefined) {
41
- return normalizePresetId(event.data.agentPreset) as string
41
+ return normalizePresetId(event.data.agentPreset)
42
42
  }
43
43
  }
44
44
  return normalizePresetId(session.header.agentPreset) ?? 'standard'
@@ -722,7 +722,7 @@ export async function discoverProviderModels(
722
722
  const draft = hasUrl
723
723
  ? {
724
724
  ...(oneShotKey !== undefined && oneShotKey !== '' ? { apiKey: oneShotKey } : {}),
725
- baseURL: baseURL!,
725
+ baseURL: baseURL,
726
726
  ...target.configuration.api === undefined ? {} : { api: target.configuration.api },
727
727
  }
728
728
  : {