dsh-as-aistudio 0.2.13 → 0.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.
- package/CHANGELOG.md +31 -0
- package/README.md +5 -5
- package/package.json +17 -12
- package/src/client.js +3362 -225
- package/src/components.js +1 -1
- package/src/index.js +26 -1
- package/src/shell.js +30 -5
- package/src/vendor/host/_platform.js +132 -0
- package/src/vendor/host/dsh-delete-turn.js +661 -0
- package/src/vendor/host/dsh-delete-turn.logic.js +655 -0
- package/src/vendor/host/dsh-edit-turn.js +2813 -0
- package/src/vendor/host/dsh-edit-turn.patch.js +285 -0
- package/src/vendor/host/dsh-markdown-bubble.js +37 -0
- package/src/vendor/host/dsh-rerun-turn.js +2784 -0
- package/src/vendor/versions.json +6 -0
- package/docs/INTEROP.md +0 -230
- package/src/studio.js +0 -553
- package/src/vendor/dsh-delete-turn.js +0 -1174
- package/src/vendor/dsh-edit-turn.js +0 -2626
- package/src/vendor/dsh-markdown-bubble.js +0 -872
- package/src/vendor/dsh-rerun-turn.js +0 -1014
- package/tools/build.mjs +0 -87
- package/tools/bump.mjs +0 -82
- package/tools/head.txt +0 -13
- package/tools/tail.txt +0 -5
- package/tools/tail2.txt +0 -4
- package/tools/vendor.mjs +0 -125
- package/tools/verify-manifest.mjs +0 -77
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// dsh-edit-turn host half (dsh-edit-turn.patch.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
|
+
//
|
|
3
|
+
// Source: ../dsh-edit-turn/lib/patch.js (v0.5.4, sha256 5e1e8cce1d168f6c)
|
|
4
|
+
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
|
+
//
|
|
6
|
+
// src/shell.js imports the copy named after the component, which is why the
|
|
7
|
+
// studio installs with no runtime dependency on the component package.
|
|
8
|
+
// In-place reply edits through a plugin-owned session record and a message
|
|
9
|
+
// projection.
|
|
10
|
+
//
|
|
11
|
+
// The host refuses to let an `assistant/message` be a surface-replacement
|
|
12
|
+
// carrier (it may never carry `sourceEventSeqs`), so a corrected reply cannot be
|
|
13
|
+
// put back at its own position with a surface event. The official seam for that
|
|
14
|
+
// case is a message projection: append a log-only plugin record, then register a
|
|
15
|
+
// pure projection for its type that rewrites the model-visible message at the
|
|
16
|
+
// target seq. Nothing leaves the surface, so there is nothing to replay and the
|
|
17
|
+
// log grows by one small record per edit.
|
|
18
|
+
//
|
|
19
|
+
// `appendPluginRecord` writes the record with the `ignorable` envelope the
|
|
20
|
+
// persistence gate requires (DSH >= 0.2.1-rc.2). The projection is registered
|
|
21
|
+
// through `ctx.sessions.registerMessageProjection` and is fiber-owned: unloading
|
|
22
|
+
// the plugin removes it, and the record is then skipped, so the original text
|
|
23
|
+
// comes back. That is the deliberate trade against the surface path: prompt
|
|
24
|
+
// edits are official events that outlive the plugin, patch edits are an overlay
|
|
25
|
+
// that needs its owner.
|
|
26
|
+
|
|
27
|
+
/** The plugin record type. The grammar requires the `plugin:` namespace. */
|
|
28
|
+
export const PATCH_TYPE = 'plugin:dsh-edit-turn/patch'
|
|
29
|
+
|
|
30
|
+
/** Patch payload version. */
|
|
31
|
+
export const PATCH_VERSION = 1
|
|
32
|
+
|
|
33
|
+
/** Block kinds this plugin can edit, mapped to the recorded block types. */
|
|
34
|
+
const KIND_TO_TYPES = {
|
|
35
|
+
text: ['text'],
|
|
36
|
+
reasoning: ['reasoning', 'thinking'],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The editable kind of one content block.
|
|
41
|
+
* @param block - one message content block.
|
|
42
|
+
* @returns 'text' | 'reasoning' | null.
|
|
43
|
+
*/
|
|
44
|
+
export function kindOfBlock(block) {
|
|
45
|
+
return blockField(block) === null ? null : blockField(block).kind
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The editable text of one block, plus which field carries it.
|
|
50
|
+
*
|
|
51
|
+
* A platform is free to name the reasoning payload `text`, `reasoning` or
|
|
52
|
+
* `thinking`; the editor reads all three and writes back to the field it read,
|
|
53
|
+
* so a shape this build has not seen before is still editable.
|
|
54
|
+
*
|
|
55
|
+
* @param block - one content block.
|
|
56
|
+
* @returns `{ kind, field, text }`, or null when the block carries no text.
|
|
57
|
+
*/
|
|
58
|
+
export function blockField(block) {
|
|
59
|
+
if (!block || typeof block !== 'object') return null
|
|
60
|
+
if (block.type === 'text' && typeof block.text === 'string') return { kind: 'text', field: 'text', text: block.text }
|
|
61
|
+
if (block.type === 'reasoning' || block.type === 'thinking') {
|
|
62
|
+
for (const field of ['text', 'reasoning', 'thinking']) {
|
|
63
|
+
if (typeof block[field] === 'string') return { kind: 'reasoning', field, text: block[field] }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every block of a message an editor may target, in message order.
|
|
71
|
+
* @param message - a message payload with a `content` array.
|
|
72
|
+
* @returns `[{ index, kind, text }]`.
|
|
73
|
+
*/
|
|
74
|
+
export function editableBlocks(message) {
|
|
75
|
+
const content = message && Array.isArray(message.content) ? message.content : []
|
|
76
|
+
const out = []
|
|
77
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
78
|
+
const block = content[index]
|
|
79
|
+
const found = blockField(block)
|
|
80
|
+
if (found === null) continue
|
|
81
|
+
out.push({ index, kind: found.kind, text: found.text })
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A non-negative safe integer, matching the host's seq/blockIndex checks. */
|
|
87
|
+
function isIndex(value) {
|
|
88
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate one patch payload (shape and target grammar).
|
|
93
|
+
* @param data - the record payload.
|
|
94
|
+
* @returns the same payload when valid.
|
|
95
|
+
* @throws when malformed.
|
|
96
|
+
*/
|
|
97
|
+
export function validatePatchData(data) {
|
|
98
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
99
|
+
throw new Error(`${PATCH_TYPE}: data must be an object`)
|
|
100
|
+
}
|
|
101
|
+
if (data.version !== PATCH_VERSION) {
|
|
102
|
+
throw new Error(`${PATCH_TYPE}: unsupported version ${String(data.version)}`)
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(data.targets) || data.targets.length === 0) {
|
|
105
|
+
throw new Error(`${PATCH_TYPE}: targets must be a nonempty array`)
|
|
106
|
+
}
|
|
107
|
+
for (const target of data.targets) {
|
|
108
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
109
|
+
throw new Error(`${PATCH_TYPE}: each target must be an object`)
|
|
110
|
+
}
|
|
111
|
+
if (!isIndex(target.seq)) throw new Error(`${PATCH_TYPE}: target seq must be a non-negative safe integer`)
|
|
112
|
+
if (!isIndex(target.blockIndex)) throw new Error(`${PATCH_TYPE}: target blockIndex must be a non-negative safe integer`)
|
|
113
|
+
if (!(target.kind in KIND_TO_TYPES)) throw new Error(`${PATCH_TYPE}: target kind must be text or reasoning`)
|
|
114
|
+
if (typeof target.text !== 'string') throw new Error(`${PATCH_TYPE}: target text must be a string`)
|
|
115
|
+
}
|
|
116
|
+
return data
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a validated patch payload.
|
|
121
|
+
* @param targets - `[{ seq, blockIndex, kind, text }]`.
|
|
122
|
+
* @returns the payload for `appendPluginRecord`.
|
|
123
|
+
*/
|
|
124
|
+
export function buildPatchData(targets) {
|
|
125
|
+
const data = {
|
|
126
|
+
version: PATCH_VERSION,
|
|
127
|
+
targets: targets.map((target) => ({
|
|
128
|
+
seq: target.seq,
|
|
129
|
+
blockIndex: target.blockIndex,
|
|
130
|
+
kind: target.kind,
|
|
131
|
+
text: target.text,
|
|
132
|
+
})),
|
|
133
|
+
}
|
|
134
|
+
validatePatchData(data)
|
|
135
|
+
return data
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Apply the targets that belong to one seq to one message.
|
|
140
|
+
*
|
|
141
|
+
* Targets are grouped by the caller: a record may carry several blocks of the
|
|
142
|
+
* same message, and they all have to land in one projected message. A target
|
|
143
|
+
* whose block is gone (or changed kind) is skipped; the rest still apply.
|
|
144
|
+
*
|
|
145
|
+
* @param message - the base message payload.
|
|
146
|
+
* @param targets - validated targets, all for this message.
|
|
147
|
+
* @returns a patched message copy.
|
|
148
|
+
*/
|
|
149
|
+
export function applyTargetsToMessage(message, targets) {
|
|
150
|
+
const content = Array.isArray(message.content) ? message.content.slice() : []
|
|
151
|
+
for (const target of targets) {
|
|
152
|
+
const block = content[target.blockIndex]
|
|
153
|
+
const found = blockField(block)
|
|
154
|
+
if (found === null || found.kind !== target.kind) continue
|
|
155
|
+
const patched = { ...block, [found.field]: target.text }
|
|
156
|
+
if (found.kind === 'reasoning') {
|
|
157
|
+
// The signature signs the original text; keep it and some providers
|
|
158
|
+
// reject the whole request. The recorded block keeps its signature; the
|
|
159
|
+
// projected copy drops it, exactly like the disk path does.
|
|
160
|
+
delete patched.signature
|
|
161
|
+
delete patched.thinkingSignature
|
|
162
|
+
}
|
|
163
|
+
content[target.blockIndex] = patched
|
|
164
|
+
}
|
|
165
|
+
return { ...message, content }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The message projection for this plugin's record type.
|
|
170
|
+
*
|
|
171
|
+
* Pure and never throws: a projection error fails the whole session fold, which
|
|
172
|
+
* would make the session unreadable. A malformed record or a target that is no
|
|
173
|
+
* longer on the surface is simply skipped.
|
|
174
|
+
*
|
|
175
|
+
* Several records compose: the base for one target is the message the previous
|
|
176
|
+
* projections already produced (`context.messages`), falling back to the
|
|
177
|
+
* recorded message, so editing block A and later block B keeps both edits.
|
|
178
|
+
*
|
|
179
|
+
* @returns `{ type, project(event, context) }` for the host registry.
|
|
180
|
+
*/
|
|
181
|
+
export function patchProjection() {
|
|
182
|
+
return {
|
|
183
|
+
type: PATCH_TYPE,
|
|
184
|
+
project(event, context) {
|
|
185
|
+
const out = new Map()
|
|
186
|
+
let data
|
|
187
|
+
try {
|
|
188
|
+
data = validatePatchData(event && event.data)
|
|
189
|
+
} catch {
|
|
190
|
+
return out
|
|
191
|
+
}
|
|
192
|
+
const nodes = new Set(Array.isArray(context && context.nodes) ? context.nodes : [])
|
|
193
|
+
const events = Array.isArray(context && context.events) ? context.events : []
|
|
194
|
+
const baseSeq = typeof (context && context.baseSeq) === 'number' ? context.baseSeq : 0
|
|
195
|
+
const messages = context && context.messages
|
|
196
|
+
const groups = new Map()
|
|
197
|
+
for (const target of data.targets) {
|
|
198
|
+
// A retired target is not in the model context any more: editing it
|
|
199
|
+
// would be a no-op for the conversation.
|
|
200
|
+
if (!nodes.has(target.seq)) continue
|
|
201
|
+
const group = groups.get(target.seq)
|
|
202
|
+
if (group === undefined) groups.set(target.seq, [target])
|
|
203
|
+
else group.push(target)
|
|
204
|
+
}
|
|
205
|
+
for (const [seq, targets] of groups) {
|
|
206
|
+
const source = events[seq - baseSeq]
|
|
207
|
+
if (!source || source.type !== 'assistant/message') continue
|
|
208
|
+
const previous = messages && typeof messages.get === 'function' ? messages.get(seq) : undefined
|
|
209
|
+
const base = previous && typeof previous === 'object' ? previous : source.data && source.data.message
|
|
210
|
+
if (!base || !Array.isArray(base.content)) continue
|
|
211
|
+
try {
|
|
212
|
+
out.set(seq, applyTargetsToMessage(base, targets))
|
|
213
|
+
} catch {
|
|
214
|
+
// Keep the rest of the record; a broken target must not take the
|
|
215
|
+
// session's whole fold down.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read every landed patch from the log, latest per (seq, blockIndex).
|
|
225
|
+
* @param events - complete contiguous event log.
|
|
226
|
+
* @returns `Map<seq, Map<blockIndex, { kind, text }>>`.
|
|
227
|
+
*/
|
|
228
|
+
export function patchesOf(events) {
|
|
229
|
+
const out = new Map()
|
|
230
|
+
for (const event of events) {
|
|
231
|
+
if (!event || event.type !== PATCH_TYPE) continue
|
|
232
|
+
let data
|
|
233
|
+
try {
|
|
234
|
+
data = validatePatchData(event.data)
|
|
235
|
+
} catch {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
for (const target of data.targets) {
|
|
239
|
+
let byBlock = out.get(target.seq)
|
|
240
|
+
if (byBlock === undefined) {
|
|
241
|
+
byBlock = new Map()
|
|
242
|
+
out.set(target.seq, byBlock)
|
|
243
|
+
}
|
|
244
|
+
byBlock.set(target.blockIndex, { kind: target.kind, text: target.text })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The latest patch per block as a flat list for the browser half.
|
|
252
|
+
* @param patches - `patchesOf(events)`.
|
|
253
|
+
* @returns `[{ seq, blockIndex, kind, text }]`.
|
|
254
|
+
*/
|
|
255
|
+
export function patchList(patches) {
|
|
256
|
+
const out = []
|
|
257
|
+
for (const [seq, byBlock] of patches) {
|
|
258
|
+
for (const [blockIndex, patch] of byBlock) {
|
|
259
|
+
out.push({ seq, blockIndex, kind: patch.kind, text: patch.text })
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out.sort((a, b) => a.seq - b.seq || a.blockIndex - b.blockIndex)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* One message's blocks with the landed patch text applied.
|
|
267
|
+
* @param message - the recorded message payload.
|
|
268
|
+
* @param seq - the event seq that carries the message.
|
|
269
|
+
* @param patches - `patchesOf(events)`.
|
|
270
|
+
* @returns `[{ index, kind, text, original, patched }]`.
|
|
271
|
+
*/
|
|
272
|
+
export function projectedBlocks(message, seq, patches) {
|
|
273
|
+
const byBlock = patches.get(seq)
|
|
274
|
+
return editableBlocks(message).map((block) => {
|
|
275
|
+
const patch = byBlock && byBlock.get(block.index)
|
|
276
|
+
const applied = patch !== undefined && patch.kind === block.kind ? patch.text : block.text
|
|
277
|
+
return {
|
|
278
|
+
index: block.index,
|
|
279
|
+
kind: block.kind,
|
|
280
|
+
text: applied,
|
|
281
|
+
original: block.text,
|
|
282
|
+
patched: applied !== block.text,
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// dsh-markdown-bubble host half (dsh-markdown-bubble.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
|
+
//
|
|
3
|
+
// Source: ../dsh-markdown-bubble/src/index.js (v0.1.5, sha256 27114f0f8f55a7c6)
|
|
4
|
+
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
|
+
//
|
|
6
|
+
// src/shell.js imports the copy named after the component, which is why the
|
|
7
|
+
// studio installs with no runtime dependency on the component package.
|
|
8
|
+
/**
|
|
9
|
+
* dsh-markdown-bubble — host half.
|
|
10
|
+
*
|
|
11
|
+
* The plugin has no host-side behavior: a sent message's bubble is drawn by
|
|
12
|
+
* the browser's chat view, and how it renders is a client-side projection of
|
|
13
|
+
* the same text the drawer already receives. The host row exists so the
|
|
14
|
+
* package is mounted by the Loader — the client-module scanner scans Loader
|
|
15
|
+
* entries for packages declaring `dsh.client` — and so the bundle patch has a
|
|
16
|
+
* row id to insert.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-markdown-bubble
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Stable plugin id (matches the bundle patch's insert id and the client module id). */
|
|
22
|
+
export const PLUGIN_ID = 'dsh-markdown-bubble'
|
|
23
|
+
|
|
24
|
+
/** Keep in sync with package.json and src/client.js. */
|
|
25
|
+
export const PLUGIN_VERSION = '0.1.5'
|
|
26
|
+
|
|
27
|
+
/** Cordis plugin name; the bundle patch resolves the package by this row. */
|
|
28
|
+
export const name = PLUGIN_ID
|
|
29
|
+
|
|
30
|
+
/** Services the inert entry needs: none. */
|
|
31
|
+
export const inject = []
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Inert apply: the browser half owns every behavior.
|
|
35
|
+
* @param {import('@deepseek-ai/cordis').Context} _ctx - owning context.
|
|
36
|
+
*/
|
|
37
|
+
export function apply(_ctx) {}
|