@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.
- package/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- 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
|
+
}
|