@xp266/dshtui 0.1.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 (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. package/src/vendor.ts +12 -0
@@ -0,0 +1,263 @@
1
+ import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm'
2
+ import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
3
+
4
+ export interface ConfiguredModel {
5
+ id: string
6
+ name: string
7
+ provider: string
8
+ providerName: string
9
+ }
10
+
11
+ export type ModelEffortKey = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
12
+
13
+ export interface ModelEntryConfig {
14
+ id: string
15
+ name?: string
16
+ contextWindow?: number
17
+ maxTokens?: number
18
+ input?: string[]
19
+ reasoningEfforts?: false | Partial<Record<ModelEffortKey, string | null>>
20
+ }
21
+
22
+ export interface DescribedModel {
23
+ name: string
24
+ contextWindow?: number
25
+ image: boolean
26
+ }
27
+
28
+ export interface CustomProviderForm {
29
+ providerId: string
30
+ displayName: string
31
+ apiUrl: string
32
+ apiProtocol: string
33
+ apiKey: string
34
+ }
35
+
36
+ export interface OfficialProvider {
37
+ provider: string
38
+ displayName: string
39
+ settingsNs: string
40
+ settingsPath: readonly string[]
41
+ declared?: boolean
42
+ }
43
+
44
+ export const API_PROTOCOLS = ['openai-completions', 'openai-responses', 'anthropic-messages']
45
+
46
+ export const PI_AI_SETTINGS_NS = 'llm-pi-ai'
47
+ export async function listConfiguredModels(llm: Pick<LlmRuntime, 'listProviders' | 'listModels'>): Promise<ConfiguredModel[]> {
48
+ const models: ConfiguredModel[] = []
49
+ for (const provider of llm.listProviders()) {
50
+ const list = await llm.listModels(provider.id)
51
+ for (const model of list) {
52
+ models.push({ id: model.id, name: model.name ?? model.id, provider: provider.id, providerName: provider.name })
53
+ }
54
+ }
55
+ return models
56
+ }
57
+
58
+ export function listProviderDirectory(llm: Pick<LlmRuntime, 'listConfigurableProviders'>): OfficialProvider[] {
59
+ return llm.listConfigurableProviders().map(entry => ({
60
+ provider: entry.provider,
61
+ displayName: entry.displayName,
62
+ settingsNs: entry.settingsNs,
63
+ settingsPath: entry.settingsPath,
64
+ ...(entry.declared === undefined ? {} : { declared: entry.declared }),
65
+ }))
66
+ }
67
+
68
+ export interface KeyStorer {
69
+ set(ref: string, value: string): Promise<void>
70
+ }
71
+
72
+ export interface ModelFetcher {
73
+ discoverModels(
74
+ ns: string,
75
+ request: { provider?: string; baseURL?: string; api?: string; apiKey?: string; signal?: AbortSignal },
76
+ ): Promise<LlmDiscoveredModel[]>
77
+ }
78
+
79
+ const DISCOVERY_TIMEOUT_MS = 15000
80
+
81
+ export async function fetchCustomModels(fetcher: ModelFetcher, form: CustomProviderForm): Promise<LlmDiscoveredModel[]> {
82
+ return fetcher.discoverModels(PI_AI_SETTINGS_NS, {
83
+ baseURL: form.apiUrl,
84
+ api: form.apiProtocol,
85
+ apiKey: form.apiKey,
86
+ signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
87
+ })
88
+ }
89
+
90
+ export async function fetchProviderModels(fetcher: ModelFetcher, settingsNs: string, providerId: string, apiKey: string): Promise<LlmDiscoveredModel[]> {
91
+ return fetcher.discoverModels(settingsNs, {
92
+ provider: providerId,
93
+ ...(apiKey.length > 0 ? { apiKey } : {}),
94
+ signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
95
+ })
96
+ }
97
+
98
+ export interface SettingsWriter {
99
+ update(ns: string, patch: unknown, expectedRevision?: number | null): Promise<unknown>
100
+ }
101
+
102
+ function storedModels(models: LlmDiscoveredModel[]): Array<Record<string, unknown>> {
103
+ return models.map(model => ({
104
+ id: model.id,
105
+ ...(model.name === undefined ? {} : { name: model.name }),
106
+ ...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }),
107
+ ...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
108
+ }))
109
+ }
110
+
111
+ export async function saveCustomProvider(
112
+ write: SettingsWriter,
113
+ store: KeyStorer,
114
+ form: CustomProviderForm,
115
+ models: LlmDiscoveredModel[],
116
+ ): Promise<void> {
117
+ const keyRef = providerKeyRef(form.providerId)
118
+ if (form.apiKey.length > 0) await store.set(keyRef, form.apiKey)
119
+ await write.update(PI_AI_SETTINGS_NS, {
120
+ providers: {
121
+ [form.providerId]: {
122
+ displayName: form.displayName || form.providerId,
123
+ api: form.apiProtocol,
124
+ baseURL: form.apiUrl,
125
+ ...(form.apiKey.length > 0 ? { apiKeyEnv: keyRef } : {}),
126
+ models: storedModels(models),
127
+ },
128
+ },
129
+ })
130
+ }
131
+
132
+ function patchAtPath(patch: object, path: readonly string[]): object {
133
+ const root: Record<string, unknown> = {}
134
+ let cursor = root
135
+ for (const segment of path) {
136
+ const next: Record<string, unknown> = {}
137
+ cursor[segment] = next
138
+ cursor = next
139
+ }
140
+ Object.assign(cursor, patch)
141
+ return root
142
+ }
143
+
144
+ export async function saveProviderKey(
145
+ write: SettingsWriter,
146
+ store: KeyStorer,
147
+ provider: OfficialProvider,
148
+ apiKey: string,
149
+ ): Promise<void> {
150
+ if (apiKey.trim() === '') throw new Error('API key must not be empty')
151
+ const keyRef = providerKeyRef(provider.provider)
152
+ await store.set(keyRef, apiKey)
153
+ await write.update(provider.settingsNs, patchAtPath({ apiKeyEnv: keyRef }, provider.settingsPath))
154
+ }
155
+
156
+ export async function saveProviderModels(
157
+ write: SettingsWriter,
158
+ provider: OfficialProvider,
159
+ models: LlmDiscoveredModel[],
160
+ ): Promise<void> {
161
+ await write.update(provider.settingsNs, patchAtPath({ models: storedModels(models) }, provider.settingsPath))
162
+ }
163
+
164
+ const KEY_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
165
+ const PROVIDER_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/
166
+
167
+ export function isProviderIdValid(providerId: string): boolean {
168
+ return PROVIDER_ID_PATTERN.test(providerId)
169
+ }
170
+
171
+ export function providerKeyRef(providerId: string): string {
172
+ const ref = `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`
173
+ if (!KEY_REF_PATTERN.test(ref)) {
174
+ throw new Error(`provider id "${providerId}" produces invalid credential ref "${ref}"; use a name starting with a letter`)
175
+ }
176
+ return ref
177
+ }
178
+
179
+ export interface SettingsReader {
180
+ get(ns: string): unknown
181
+ }
182
+
183
+ function providerSection(reader: SettingsReader, ns: string, provider: string): Record<string, unknown> | undefined {
184
+ const section = reader.get(ns)
185
+ if (section === undefined || typeof section !== 'object') return undefined
186
+ const providers = (section as { providers?: Record<string, unknown> }).providers
187
+ if (providers === undefined || typeof providers !== 'object') return undefined
188
+ const profile = providers[provider]
189
+ if (profile === undefined || typeof profile !== 'object') return undefined
190
+ return profile as Record<string, unknown>
191
+ }
192
+
193
+ export function readModelEntries(reader: SettingsReader, ns: string, provider: string): ModelEntryConfig[] {
194
+ const profile = providerSection(reader, ns, provider)
195
+ const models = profile?.models
196
+ if (!Array.isArray(models)) return []
197
+ return models.filter((entry): entry is ModelEntryConfig =>
198
+ typeof entry === 'object' && entry !== null && typeof (entry as ModelEntryConfig).id === 'string')
199
+ }
200
+
201
+ export async function saveModelEntry(
202
+ write: SettingsWriter,
203
+ reader: SettingsReader,
204
+ ns: string,
205
+ provider: string,
206
+ entry: ModelEntryConfig,
207
+ ): Promise<void> {
208
+ const models = readModelEntries(reader, ns, provider)
209
+ const index = models.findIndex(model => model.id === entry.id)
210
+ const next = [...models]
211
+ if (index >= 0) next[index] = entry
212
+ else next.push(entry)
213
+ await write.update(ns, { providers: { [provider]: { models: next } } })
214
+ }
215
+
216
+ export async function deleteModelEntry(
217
+ write: SettingsWriter,
218
+ reader: SettingsReader,
219
+ ns: string,
220
+ provider: string,
221
+ modelId: string,
222
+ ): Promise<void> {
223
+ const models = readModelEntries(reader, ns, provider)
224
+ const next = models.filter(model => model.id !== modelId)
225
+ if (next.length === models.length) return
226
+ await write.update(ns, { providers: { [provider]: { models: next } } })
227
+ }
228
+
229
+ export type SettingsPathOp =
230
+ | { op: 'set'; path: readonly string[]; value: unknown }
231
+ | { op: 'unset'; path: readonly string[] }
232
+
233
+ export interface SettingsMutator {
234
+ get(ns: string): unknown
235
+ mutate(ns: string, ops: readonly SettingsPathOp[]): Promise<void>
236
+ }
237
+
238
+ export interface KeyRemover {
239
+ unset(ref: string): Promise<void>
240
+ describe(ref: string): Promise<{ configured: boolean; writable: boolean }>
241
+ }
242
+
243
+ /**
244
+ * Delete one adapter-declared provider profile through the harness's official
245
+ * write path: path-addressed `unset` on the owning settings namespace, plus
246
+ * credential removal when the profile stores its key under the conventional
247
+ * managed reference. Mirrors the web bundle's removeProviderProfile.
248
+ */
249
+ export async function deleteProviderProfile(
250
+ settings: SettingsMutator,
251
+ store: KeyRemover,
252
+ provider: OfficialProvider,
253
+ ): Promise<void> {
254
+ if (provider.declared !== true) throw new Error('only custom providers can be deleted')
255
+ const managedRef = providerKeyRef(provider.provider)
256
+ const profile = providerSection(settings, provider.settingsNs, provider.provider)
257
+ const apiKeyEnv = typeof profile?.apiKeyEnv === 'string' ? profile.apiKeyEnv : undefined
258
+ if (apiKeyEnv === managedRef) {
259
+ const info = await store.describe(managedRef).catch(() => undefined)
260
+ if (info?.configured === true && info.writable) await store.unset(managedRef)
261
+ }
262
+ await settings.mutate(provider.settingsNs, [{ op: 'unset', path: [...provider.settingsPath] }])
263
+ }
@@ -0,0 +1,125 @@
1
+ export interface PartialField {
2
+ value: string
3
+ complete: boolean
4
+ }
5
+
6
+ export type PartialFields = Record<string, PartialField>
7
+
8
+ export function extractPartialJsonFields(raw: string): PartialFields {
9
+ const out: PartialFields = {}
10
+ const source = raw
11
+ const end = source.length
12
+ let i = 0
13
+ while (i < end && source[i] !== '{') i++
14
+ if (i >= end) return out
15
+ i++
16
+ while (i < end) {
17
+ while (i < end && (source[i] === ' ' || source[i] === '\n' || source[i] === '\r' || source[i] === '\t' || source[i] === ',')) i++
18
+ if (i < end && source[i] === '}') break
19
+ if (i >= end || source[i] !== '"') {
20
+ i++
21
+ continue
22
+ }
23
+ i++
24
+ const keyStart = i
25
+ while (i < end && source[i] !== '"') {
26
+ if (source[i] === '\\') i++
27
+ i++
28
+ }
29
+ if (i >= end) break
30
+ const key = unescapePartial(source.slice(keyStart, i))
31
+ i++
32
+ while (i < end && (source[i] === ' ' || source[i] === '\n' || source[i] === '\r' || source[i] === '\t')) i++
33
+ if (i >= end || source[i] !== ':') continue
34
+ i++
35
+ while (i < end && (source[i] === ' ' || source[i] === '\n' || source[i] === '\r' || source[i] === '\t')) i++
36
+ if (i >= end) {
37
+ out[key] = { value: '', complete: false }
38
+ break
39
+ }
40
+ if (source[i] === '"') {
41
+ i++
42
+ const valueStart = i
43
+ let closed = false
44
+ while (i < end) {
45
+ const ch = source[i]
46
+ if (ch === '\\') {
47
+ i += 2
48
+ continue
49
+ }
50
+ if (ch === '"') {
51
+ closed = true
52
+ break
53
+ }
54
+ i++
55
+ }
56
+ out[key] = { value: unescapePartial(source.slice(valueStart, Math.min(i, end))), complete: closed }
57
+ i++
58
+ continue
59
+ }
60
+ const valueStart = i
61
+ let depth = 0
62
+ while (i < end) {
63
+ const ch = source[i]
64
+ if (ch === '"') {
65
+ i++
66
+ while (i < end && source[i] !== '"') {
67
+ if (source[i] === '\\') i++
68
+ i++
69
+ }
70
+ i++
71
+ continue
72
+ }
73
+ if (ch === '{' || ch === '[') depth++
74
+ else if (ch === '}' || ch === ']') {
75
+ if (depth === 0) break
76
+ depth--
77
+ } else if (ch === ',' && depth === 0) break
78
+ i++
79
+ }
80
+ out[key] = { value: source.slice(valueStart, i).trim(), complete: i < end && (source[i] === ',' || source[i] === '}') }
81
+ }
82
+ return out
83
+ }
84
+
85
+ export function unescapePartial(text: string): string {
86
+ if (!text.includes('\\')) return text
87
+ let out = ''
88
+ for (let i = 0; i < text.length; i++) {
89
+ const ch = text[i]
90
+ if (ch !== '\\') {
91
+ out += ch
92
+ continue
93
+ }
94
+ const next = text[i + 1]
95
+ if (next === undefined) break
96
+ switch (next) {
97
+ case 'n': out += '\n'; i++; break
98
+ case 't': out += '\t'; i++; break
99
+ case 'r': out += '\r'; i++; break
100
+ case 'b': out += '\b'; i++; break
101
+ case 'f': out += '\f'; i++; break
102
+ case '/': out += '/'; i++; break
103
+ case '"': out += '"'; i++; break
104
+ case "'": out += "'"; i++; break
105
+ case '\\': out += '\\'; i++; break
106
+ case 'u': {
107
+ const hex = text.slice(i + 2, i + 6)
108
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
109
+ out += String.fromCharCode(Number.parseInt(hex, 16))
110
+ i += 5
111
+ } else if (/^[0-9a-fA-F]{0,3}$/.test(hex)) {
112
+ return out
113
+ } else {
114
+ out += next
115
+ i++
116
+ }
117
+ break
118
+ }
119
+ default:
120
+ out += next
121
+ i++
122
+ }
123
+ }
124
+ return out
125
+ }
@@ -0,0 +1,26 @@
1
+ import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets'
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
3
+
4
+ const BUILT_IN_NAMES: Readonly<Record<string, string>> = {
5
+ standard: 'Standard mode',
6
+ code: 'Code mode',
7
+ minimal: 'Minimal mode',
8
+ cordis: 'Creator mode',
9
+ }
10
+
11
+ export interface PresetSummary {
12
+ id: string
13
+ name: string
14
+ }
15
+
16
+ export function builtInPresetName(id: string): string | undefined {
17
+ return BUILT_IN_NAMES[id]
18
+ }
19
+
20
+ export function presetDisplayName(preset: AgentPreset): string {
21
+ return BUILT_IN_NAMES[preset.id] ?? preset.name ?? preset.id
22
+ }
23
+
24
+ export function isBlankSession(events: readonly SessionEvent[]): boolean {
25
+ return !events.some(event => event.type === 'turn/start')
26
+ }
@@ -0,0 +1,106 @@
1
+ import type { AskOptionLike, AskQuestionItemLike, AskUserQuestionAnswerItemLike } from './interactions.ts'
2
+
3
+ export const ASK_USER_TOOL_NAME = 'ask_user_question'
4
+
5
+ const ANSWER_INDENT = 3
6
+ const UNANSWERED_TEXT = '(Question not answered)'
7
+
8
+ export function answerSummary(
9
+ question: AskQuestionItemLike,
10
+ selected: readonly string[],
11
+ custom: string,
12
+ customChecked: boolean,
13
+ ): string {
14
+ if (customChecked && custom !== '') {
15
+ return question.multiSelect === true && selected.length > 0
16
+ ? [...selected, custom].join(', ')
17
+ : custom
18
+ }
19
+ return selected.length === 0 ? UNANSWERED_TEXT : selected.join(', ')
20
+ }
21
+
22
+ export function parseAskQuestions(raw: unknown): AskQuestionItemLike[] {
23
+ if (typeof raw !== 'object' || raw === null) return []
24
+ const questions = (raw as { questions?: unknown }).questions
25
+ if (!Array.isArray(questions)) return []
26
+ return questions.map(questionFromJson).filter(question => question !== undefined)
27
+ }
28
+
29
+ export function parseAskAnswers(resultText: string): AskUserQuestionAnswerItemLike[] {
30
+ let parsed: unknown
31
+ try {
32
+ parsed = JSON.parse(resultText)
33
+ } catch {
34
+ // A non-JSON result text simply carries no answers.
35
+ return []
36
+ }
37
+ if (typeof parsed !== 'object' || parsed === null) return []
38
+ const answers = (parsed as { answers?: unknown }).answers
39
+ if (!Array.isArray(answers)) return []
40
+ return answers.map(answerFromJson).filter(answer => answer !== undefined)
41
+ }
42
+
43
+ function answerTextOf(question: AskQuestionItemLike, answer: AskUserQuestionAnswerItemLike | undefined): string {
44
+ if (answer === undefined) return UNANSWERED_TEXT
45
+ const custom = answer.custom?.trim() ?? ''
46
+ return answerSummary(question, answer.selected, custom, custom !== '')
47
+ }
48
+
49
+ function questionFromJson(value: unknown): AskQuestionItemLike | undefined {
50
+ if (typeof value !== 'object' || value === null) return undefined
51
+ const record = value as Record<string, unknown>
52
+ if (typeof record.id !== 'string' || typeof record.question !== 'string') return undefined
53
+ const multiSelect = record.multiSelect === true || record.multi_select === true
54
+ return {
55
+ id: record.id,
56
+ question: record.question,
57
+ ...(typeof record.header === 'string' && record.header !== '' ? { header: record.header } : {}),
58
+ ...(typeof record.detail === 'string' && record.detail !== '' ? { detail: record.detail } : {}),
59
+ ...(Array.isArray(record.options) ? { options: record.options.map(optionFromJson).filter(option => option !== undefined) } : {}),
60
+ ...(multiSelect ? { multiSelect } : {}),
61
+ }
62
+ }
63
+
64
+ function optionFromJson(value: unknown): AskOptionLike | undefined {
65
+ if (typeof value !== 'object' || value === null) return undefined
66
+ const record = value as Record<string, unknown>
67
+ if (typeof record.label !== 'string') return undefined
68
+ return { label: record.label }
69
+ }
70
+
71
+ function answerFromJson(value: unknown): AskUserQuestionAnswerItemLike | undefined {
72
+ if (typeof value !== 'object' || value === null) return undefined
73
+ const record = value as Record<string, unknown>
74
+ if (typeof record.id !== 'string' || !Array.isArray(record.selected)) return undefined
75
+ const selected = record.selected.filter(label => typeof label === 'string')
76
+ return {
77
+ id: record.id,
78
+ selected,
79
+ ...(typeof record.custom === 'string' ? { custom: record.custom } : {}),
80
+ }
81
+ }
82
+
83
+ /** The ask questions rendered as numbered entries with their options, used
84
+ * as the card args area (the original question formatting without the
85
+ * tool-name title line). */
86
+ export function formatAskQuestions(raw: unknown): string {
87
+ const questions = parseAskQuestions(raw)
88
+ if (questions.length === 0) return ''
89
+ return questions.map((question, index) => {
90
+ const lines = [`${index + 1}. ${question.question}`]
91
+ if (question.detail !== undefined && question.detail !== '') lines.push(`${' '.repeat(ANSWER_INDENT)}${question.detail}`)
92
+ for (const option of question.options ?? []) lines.push(`${' '.repeat(ANSWER_INDENT)}${option.label}`)
93
+ return lines.join('\n')
94
+ }).join('\n')
95
+ }
96
+
97
+ /** The settled ask card: each question followed by the user's choice. */
98
+ export function formatAskAnswered(
99
+ questions: readonly AskQuestionItemLike[],
100
+ answers: readonly AskUserQuestionAnswerItemLike[],
101
+ ): string {
102
+ return questions.map((question, index) => {
103
+ const answer = answers.find(entry => entry.id === question.id)
104
+ return `${index + 1}. ${question.question}\n${' '.repeat(ANSWER_INDENT)}${answerTextOf(question, answer)}`
105
+ }).join('\n')
106
+ }
@@ -0,0 +1,33 @@
1
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
2
+
3
+ export interface RetryStatus {
4
+ attempt: number
5
+ maxRetries: number
6
+ untilTs: number
7
+ code: string
8
+ }
9
+
10
+ interface RetryEventData {
11
+ retry: number
12
+ maxRetries?: number
13
+ delayMs?: number
14
+ failure?: { code?: string }
15
+ }
16
+
17
+ export function nextRetryStatus(current: RetryStatus | undefined, event: SessionEvent): RetryStatus | undefined {
18
+ const type = event.type as string
19
+ if (type === 'llm/retry') {
20
+ const data = event.data as unknown as RetryEventData
21
+ return {
22
+ attempt: data.retry,
23
+ maxRetries: data.maxRetries ?? data.retry,
24
+ untilTs: Date.now() + Math.max(0, data.delayMs ?? 0),
25
+ code: data.failure?.code ?? 'UNKNOWN',
26
+ }
27
+ }
28
+ if (type === 'llm/retry-started') {
29
+ return current === undefined ? undefined : { ...current, untilTs: 0 }
30
+ }
31
+ if (event.type === 'assistant/chunk' || event.type === 'turn/end') return undefined
32
+ return current
33
+ }
@@ -0,0 +1,34 @@
1
+ import type { SessionSummary } from './session-list.ts'
2
+ import { sessionTime } from './session-list.ts'
3
+
4
+ export type SessionSectionKind = 'recent' | 'week' | 'older'
5
+
6
+ export interface SessionSection {
7
+ kind: SessionSectionKind
8
+ items: SessionSummary[]
9
+ }
10
+
11
+ const RECENT_LIMIT = 5
12
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000
13
+
14
+ function startOfDay(now: number): number {
15
+ const date = new Date(now)
16
+ date.setHours(0, 0, 0, 0)
17
+ return date.getTime()
18
+ }
19
+
20
+ export function groupSessions(items: readonly SessionSummary[], now: number = Date.now()): SessionSection[] {
21
+ const sorted = [...items].sort((a, b) => sessionTime(b) - sessionTime(a))
22
+ if (sorted.length === 0) return []
23
+ const sections: SessionSection[] = [{ kind: 'recent', items: sorted.slice(0, RECENT_LIMIT) }]
24
+ const weekStart = startOfDay(now) - WEEK_MS
25
+ const buckets: Record<Exclude<SessionSectionKind, 'recent'>, SessionSummary[]> = { week: [], older: [] }
26
+ for (const session of sorted) {
27
+ if (sessionTime(session) >= weekStart) buckets.week.push(session)
28
+ else buckets.older.push(session)
29
+ }
30
+ for (const kind of ['week', 'older'] as const) {
31
+ if (buckets[kind].length > 0) sections.push({ kind, items: buckets[kind] })
32
+ }
33
+ return sections
34
+ }