@anionex/dsh-vision-toolkit 0.1.40 → 0.1.44
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/lib/artifact-access.js.map +1 -1
- package/lib/client.js +50 -12
- package/lib/client.js.map +1 -1
- package/lib/exposure.js +37 -6
- package/lib/exposure.js.map +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types/artifact-access.d.ts +1 -1
- package/lib/types/artifact-access.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +12 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paste-images.d.ts +6 -2
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/tools.d.ts +13 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/package.json +55 -30
- package/src/artifact-access.ts +1 -1
- package/src/client/index.tsx +20 -3
- package/src/client/paste-images.tsx +67 -13
- package/src/exposure.ts +43 -6
- package/src/tools.ts +12 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Clipboard-only multi-image input for DSH Web. */
|
|
2
2
|
|
|
3
3
|
import { useSyncExternalStore, type ReactNode } from 'react'
|
|
4
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
5
|
-
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
4
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
5
|
+
import type { DraftAttachmentId } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
6
6
|
import type { InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
7
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
7
8
|
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
9
|
import { readDisplayConfig } from './display-config.ts'
|
|
9
10
|
|
|
@@ -17,6 +18,47 @@ const MAX_BATCH_BYTES = 80 * 1024 * 1024
|
|
|
17
18
|
/** A confirmed paste verdict older than this is unknown again, even while a refresh is in flight. */
|
|
18
19
|
const VERDICT_MAX_AGE_MS = 15000
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Count the draft attachment ids in an InputState snapshot across DSH
|
|
23
|
+
* generations. DSH 0.1.5 renamed the field to `attachmentIds` and widened its
|
|
24
|
+
* meaning from image-only ids to every draft attachment id; the older
|
|
25
|
+
* `imageIds` name survives only as a compatibility fallback for hosts that
|
|
26
|
+
* still publish it, and never crashes when it is absent.
|
|
27
|
+
* @param snapshot - the `input.state.getSnapshot()` value (either generation).
|
|
28
|
+
* @returns the draft attachment count, 0 when neither field is published.
|
|
29
|
+
*/
|
|
30
|
+
function draftAttachmentCount(snapshot: unknown): number {
|
|
31
|
+
if (snapshot === null || typeof snapshot !== 'object') return 0
|
|
32
|
+
const row = snapshot as {
|
|
33
|
+
attachmentIds?: readonly DraftAttachmentId[]
|
|
34
|
+
imageIds?: readonly string[]
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(row.attachmentIds)) return row.attachmentIds.length
|
|
37
|
+
return Array.isArray(row.imageIds) ? row.imageIds.length : 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The Client Session registry face this controller reads. DSH 0.1.5 publishes
|
|
42
|
+
* it as `Context.sessions` from `@deepseek-ai/dsh-api-session-controller/client`,
|
|
43
|
+
* whose peer set spans the whole host package graph and therefore cannot be a
|
|
44
|
+
* peer of a Web-only plugin; only these two members are used, so the face is
|
|
45
|
+
* declared structurally instead of importing that package.
|
|
46
|
+
*/
|
|
47
|
+
interface ClientSessionRegistry {
|
|
48
|
+
/** Resolve the Session-scoped Context; undefined once the Session has closed. */
|
|
49
|
+
scope(sessionId: never): ClientContext | undefined
|
|
50
|
+
/** Live Session list projection; `current` is the focused Session id. */
|
|
51
|
+
readonly list: { getSnapshot(): { readonly current?: string | undefined } }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param ctx - the browser plugin Context.
|
|
56
|
+
* @returns the host's Session registry through the structural face above.
|
|
57
|
+
*/
|
|
58
|
+
function sessionRegistry(ctx: ClientContext): ClientSessionRegistry {
|
|
59
|
+
return (ctx as unknown as { readonly sessions: ClientSessionRegistry }).sessions
|
|
60
|
+
}
|
|
61
|
+
|
|
20
62
|
interface PasteRecord {
|
|
21
63
|
ref: string
|
|
22
64
|
file: File
|
|
@@ -58,6 +100,8 @@ interface PasteOccurrence {
|
|
|
58
100
|
source: string
|
|
59
101
|
ref: string
|
|
60
102
|
offset: number
|
|
103
|
+
/** DSH rc.8+ stores the full @label text; older releases used one placeholder. */
|
|
104
|
+
length?: number
|
|
61
105
|
label: string
|
|
62
106
|
}
|
|
63
107
|
|
|
@@ -166,6 +210,10 @@ function pasteLabel(file: File, index: number): string {
|
|
|
166
210
|
return file.name.trim() || `clipboard-image-${index + 1}`
|
|
167
211
|
}
|
|
168
212
|
|
|
213
|
+
function occurrenceEnd(occurrence: PasteOccurrence): number {
|
|
214
|
+
return occurrence.offset + (occurrence.length ?? 1)
|
|
215
|
+
}
|
|
216
|
+
|
|
169
217
|
/** Owns browser File objects until DSH serializes the corresponding text references. */
|
|
170
218
|
export class PasteImageController {
|
|
171
219
|
private readonly records = new Map<string, PasteRecord>()
|
|
@@ -216,7 +264,7 @@ export class PasteImageController {
|
|
|
216
264
|
}
|
|
217
265
|
|
|
218
266
|
private inputFor(sessionId: string) {
|
|
219
|
-
const actx = this.ctx.
|
|
267
|
+
const actx = sessionRegistry(this.ctx).scope(sessionId as never)
|
|
220
268
|
if (actx === undefined) throw new Error('Open a live session before pasting images')
|
|
221
269
|
return this.ctx.conversation.input.for(actx)
|
|
222
270
|
}
|
|
@@ -241,6 +289,7 @@ export class PasteImageController {
|
|
|
241
289
|
if (before !== '' && !/\s$/u.test(before)) cursor = this.insertText(input, ' ', cursor)
|
|
242
290
|
for (const [index, file] of files.entries()) {
|
|
243
291
|
const ref = id()
|
|
292
|
+
const label = pasteLabel(file, index)
|
|
244
293
|
const record: PasteRecord = { ref, file, batch, status: 'ready' }
|
|
245
294
|
batch.records.push(record)
|
|
246
295
|
this.records.set(ref, record)
|
|
@@ -248,11 +297,14 @@ export class PasteImageController {
|
|
|
248
297
|
const accepted = input.insertReference({
|
|
249
298
|
source: SOURCE,
|
|
250
299
|
ref,
|
|
251
|
-
label
|
|
252
|
-
clipboardText: `[pasted image: ${
|
|
300
|
+
label,
|
|
301
|
+
clipboardText: `[pasted image: ${label}]`,
|
|
253
302
|
}, { start: cursor, end: cursor, draftRev: snapshot.draftRev })
|
|
254
303
|
if (!accepted) throw new Error('The composer changed before pasted images could be inserted')
|
|
255
|
-
|
|
304
|
+
const inserted = input.state.getSnapshot().occurrences.find(occurrence =>
|
|
305
|
+
occurrence.source === SOURCE && occurrence.ref === ref)
|
|
306
|
+
if (inserted === undefined) throw new Error('The pasted image reference was not present after insertion')
|
|
307
|
+
cursor = occurrenceEnd(inserted)
|
|
256
308
|
const hasNext = index + 1 < files.length
|
|
257
309
|
const suffix = input.state.getSnapshot().draft.slice(cursor)
|
|
258
310
|
if (hasNext || (suffix !== '' && !/^\s/u.test(suffix))) cursor = this.insertText(input, ' ', cursor)
|
|
@@ -472,7 +524,9 @@ export class PasteImageController {
|
|
|
472
524
|
* Auto-switch flow: switch the Session to the image-input variant, announce
|
|
473
525
|
* it, then replay the paste into the composer's native intake. A failed
|
|
474
526
|
* switch, or an environment that cannot replay clipboard bytes, degrades to
|
|
475
|
-
* the path takeover with the same files.
|
|
527
|
+
* the path takeover with the same files. The post-replay probe compares the
|
|
528
|
+
* draft attachment count before and after, so it observes any attachment the
|
|
529
|
+
* native intake admitted rather than images alone.
|
|
476
530
|
* @param sessionId - the live Session id.
|
|
477
531
|
* @param target - the composer textarea the paste landed on.
|
|
478
532
|
* @param files - the captured image files.
|
|
@@ -501,10 +555,10 @@ export class PasteImageController {
|
|
|
501
555
|
// Replaying lets the composer's own intake run (thumbnail, limits,
|
|
502
556
|
// keyboard); if the environment cannot replay clipboard bytes, the
|
|
503
557
|
// images still land as workspace paths.
|
|
504
|
-
const
|
|
558
|
+
const attachmentsBefore = draftAttachmentCount(input.state.getSnapshot())
|
|
505
559
|
const replayed = this.replayPaste(target, files, text)
|
|
506
|
-
const
|
|
507
|
-
if (!replayed ||
|
|
560
|
+
const attachmentsAfter = draftAttachmentCount(input.state.getSnapshot())
|
|
561
|
+
if (!replayed || attachmentsAfter <= attachmentsBefore) {
|
|
508
562
|
this.takeoverPaste(sessionId, target, files, text)
|
|
509
563
|
}
|
|
510
564
|
}
|
|
@@ -548,7 +602,7 @@ export class PasteImageController {
|
|
|
548
602
|
const target = event.target
|
|
549
603
|
if (!(target instanceof HTMLTextAreaElement) || target.closest('[data-composer-card]') === null) return false
|
|
550
604
|
|
|
551
|
-
const sessionId = this.ctx.
|
|
605
|
+
const sessionId = sessionRegistry(this.ctx).list.getSnapshot().current
|
|
552
606
|
if (sessionId === undefined) return false
|
|
553
607
|
const modelLabel = currentModelLabel()
|
|
554
608
|
this.refreshVerdict(sessionId, modelLabel)
|
|
@@ -594,7 +648,7 @@ export class PasteImageController {
|
|
|
594
648
|
insertText: (text: string, span: { start: number; end: number; draftRev: number }) => boolean
|
|
595
649
|
}).insertText('', {
|
|
596
650
|
start: current.offset,
|
|
597
|
-
end: current
|
|
651
|
+
end: occurrenceEnd(current),
|
|
598
652
|
draftRev: snapshot.draftRev,
|
|
599
653
|
})
|
|
600
654
|
if (!accepted) return
|
|
@@ -725,7 +779,7 @@ export function installPasteImages(ctx: ClientContext): void {
|
|
|
725
779
|
const listener = (event: ClipboardEvent): void => { controller.handlePaste(event) }
|
|
726
780
|
// A focus-time prefetch has the verdict ready before the first paste can land.
|
|
727
781
|
const onFocusIn = (): void => {
|
|
728
|
-
const sessionId = ctx.
|
|
782
|
+
const sessionId = sessionRegistry(ctx).list.getSnapshot().current
|
|
729
783
|
if (sessionId !== undefined) controller.refreshVerdict(String(sessionId), currentModelLabel())
|
|
730
784
|
}
|
|
731
785
|
document.addEventListener('paste', listener, true)
|
package/src/exposure.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
10
10
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
11
11
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
12
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
|
12
13
|
import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
13
14
|
import type { Context } from '@deepseek-ai/cordis'
|
|
14
15
|
import { VISION_SKILLS_CONTENT, VISION_SKILLS_NAME } from './skill.ts'
|
|
@@ -78,10 +79,50 @@ function isBundledSkillResult(value: unknown): boolean {
|
|
|
78
79
|
&& isBundledSkillContent(value.content)
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/** Event names for one settled programmatic (PTC) Tool dispatch. */
|
|
83
|
+
const PTC_DISPATCH_EVENT_TYPES: readonly string[] = ['tool/ptc-dispatch', 'tool/code-dispatch']
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether one PTC dispatch event records a successful load of the bundled
|
|
87
|
+
* Skill. The event is `tool/code-dispatch` through 0.1.2 and `tool/ptc-dispatch`
|
|
88
|
+
* from 0.1.5, so the name is compared outside the typed union and the payload is
|
|
89
|
+
* read structurally instead of narrowing on the installed host's literal.
|
|
90
|
+
* @param event - one durable Session event.
|
|
91
|
+
* @returns true when this dispatch ran the bundled Skill and returned its content.
|
|
92
|
+
*/
|
|
93
|
+
function isBundledSkillPtcDispatch(event: SessionEvent): boolean {
|
|
94
|
+
const type: string = event.type
|
|
95
|
+
if (!PTC_DISPATCH_EVENT_TYPES.includes(type)) return false
|
|
96
|
+
const data = (event as { data?: unknown }).data
|
|
97
|
+
return isRecord(data)
|
|
98
|
+
&& data.name === 'skill'
|
|
99
|
+
&& data.isError === false
|
|
100
|
+
&& isVisionSkillArguments(data.arguments)
|
|
101
|
+
&& containsBundledSkillContent(Array.isArray(data.content) ? data.content : [])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read a Session's durable event log across both published accessors: 0.1.1 and
|
|
106
|
+
* earlier expose the `events` getter, while 0.1.2 replaced it with
|
|
107
|
+
* `snapshotEvents()` and dropped the property from the type. Probing
|
|
108
|
+
* structurally keeps every release line this package declares working at
|
|
109
|
+
* runtime and still compiles against the installed 0.1.5 typings.
|
|
110
|
+
* @param session - the Session whose durable history is inspected.
|
|
111
|
+
* @returns the event log in log order, empty when neither accessor is present.
|
|
112
|
+
*/
|
|
113
|
+
function sessionEvents(session: Session): readonly SessionEvent[] {
|
|
114
|
+
const adapter = session as unknown as {
|
|
115
|
+
snapshotEvents?: () => readonly SessionEvent[]
|
|
116
|
+
events?: readonly SessionEvent[]
|
|
117
|
+
}
|
|
118
|
+
return typeof adapter.snapshotEvents === 'function' ? adapter.snapshotEvents() : adapter.events ?? []
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
/** Whether durable history proves that this Session loaded the bundled Skill. */
|
|
82
122
|
function hasLoadedVisionSkill(session: Session): boolean {
|
|
83
123
|
const nativeCalls = new Set<string>()
|
|
84
|
-
|
|
124
|
+
const events = sessionEvents(session)
|
|
125
|
+
for (const event of events) {
|
|
85
126
|
if (event.type === 'user/message') {
|
|
86
127
|
const source = event.data.source
|
|
87
128
|
if (source.kind === 'skill-invocation'
|
|
@@ -103,11 +144,7 @@ function hasLoadedVisionSkill(session: Session): boolean {
|
|
|
103
144
|
&& containsBundledSkillContent(block.content)) return true
|
|
104
145
|
continue
|
|
105
146
|
}
|
|
106
|
-
if (event
|
|
107
|
-
&& event.data.name === 'skill'
|
|
108
|
-
&& event.data.isError === false
|
|
109
|
-
&& isVisionSkillArguments(event.data.arguments)
|
|
110
|
-
&& containsBundledSkillContent(event.data.content)) return true
|
|
147
|
+
if (isBundledSkillPtcDispatch(event)) return true
|
|
111
148
|
}
|
|
112
149
|
return false
|
|
113
150
|
}
|
package/src/tools.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
10
|
-
import { defineTool, type
|
|
10
|
+
import { defineTool, type ToolRunContext, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
|
|
11
11
|
import {
|
|
12
12
|
VisionToolkitRuntime,
|
|
13
13
|
type CropRequest,
|
|
@@ -23,6 +23,17 @@ import {
|
|
|
23
23
|
} from './runtime.ts'
|
|
24
24
|
import { platformTempDirectory } from './paths.ts'
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* A value that round-trips through JSON without loss, as the host transports it
|
|
28
|
+
* through `presentationMeta`. DSH 0.1.2 stopped exporting this type from
|
|
29
|
+
* `@deepseek-ai/dsh-tools`, and 0.1.5 keeps it in `@deepseek-ai/dsh-util-values`,
|
|
30
|
+
* a package that does not exist before 0.1.2-alpha.2, so no single specifier
|
|
31
|
+
* covers every release line this package declares. Both homes declare the
|
|
32
|
+
* identical structural union, so redeclaring it here costs nothing and lets old
|
|
33
|
+
* and new hosts compile against the same contract.
|
|
34
|
+
*/
|
|
35
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
|
36
|
+
|
|
26
37
|
const renderJson = (_args: unknown, value: unknown): ContentBlock[] => [{
|
|
27
38
|
type: 'text',
|
|
28
39
|
text: JSON.stringify(value, null, 2),
|