@anionex/dsh-vision-toolkit 0.1.43 → 0.1.45
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 +503 -0
- package/lib/artifact-access.js.map +1 -1
- package/lib/client.js +39 -8
- package/lib/client.js.map +1 -1
- package/lib/exposure.js +36 -8
- package/lib/exposure.js.map +1 -1
- package/lib/image-input-variants.js +89 -4
- package/lib/image-input-variants.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 +4 -2
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +8 -0
- package/lib/types/image-input-variants.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 +39 -31
- package/src/artifact-access.ts +1 -1
- package/src/client/index.tsx +20 -3
- package/src/client/paste-images.tsx +53 -9
- package/src/exposure.ts +42 -8
- package/src/image-input-variants.ts +99 -4
- package/src/tools.ts +12 -1
package/lib/types/tools.d.ts
CHANGED
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
* model-visible semantics.
|
|
6
6
|
* @module dsh-vision-toolkit/tools
|
|
7
7
|
*/
|
|
8
|
-
import { defineTool
|
|
8
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import { VisionToolkitRuntime } from './runtime.ts';
|
|
10
|
+
/**
|
|
11
|
+
* A value that round-trips through JSON without loss, as the host transports it
|
|
12
|
+
* through `presentationMeta`. DSH 0.1.2 stopped exporting this type from
|
|
13
|
+
* `@deepseek-ai/dsh-tools`, and 0.1.5 keeps it in `@deepseek-ai/dsh-util-values`,
|
|
14
|
+
* a package that does not exist before 0.1.2-alpha.2, so no single specifier
|
|
15
|
+
* covers every release line this package declares. Both homes declare the
|
|
16
|
+
* identical structural union, so redeclaring it here costs nothing and lets old
|
|
17
|
+
* and new hosts compile against the same contract.
|
|
18
|
+
*/
|
|
19
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
20
|
+
[key: string]: JsonValue;
|
|
21
|
+
};
|
|
10
22
|
/** Canonical names shared by registration, bootstrap guidance, and tests. */
|
|
11
23
|
export declare const VISION_TOOL_NAMES: {
|
|
12
24
|
readonly glance: "vision_glance";
|
package/lib/types/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,UAAU,EAA6C,MAAM,wBAAwB,CAAA;AAC9F,OAAO,EACL,oBAAoB,EAWrB,MAAM,cAAc,CAAA;AAGrB;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAcrG,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB;;;;;;;;;;;CAWpB,CAAA;AAqHV,oFAAoF;AACpF,MAAM,MAAM,0BAA0B,GAAG,oBAAoB,GAAG,CAAC,MAAM,oBAAoB,CAAC,CAAA;AAE5F,kFAAkF;AAClF,MAAM,MAAM,kCAAkC,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,SAAS,CAAA;AAMhF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,0BAA0B,EAClC,mBAAmB,GAAE,kCAAyD,EAC9E,eAAe,CAAC,EAAE,WAAW,GAC5B,UAAU,CAAC,OAAO,UAAU,CAAC,EAAE,CAmZjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anionex/dsh-vision-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
4
4
|
"description": "DeepSeek Harness-native integration for agent-vision-toolkit: image Q&A, OCR, grounding, UI restoration, pixel diff, Artifacts, and Web UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"runtime",
|
|
56
56
|
"vendor",
|
|
57
57
|
"cordis.patch.yml",
|
|
58
|
+
"CHANGELOG.md",
|
|
58
59
|
"README.md",
|
|
59
60
|
"README.zh.md",
|
|
60
61
|
"LICENSE"
|
|
@@ -77,13 +78,15 @@
|
|
|
77
78
|
},
|
|
78
79
|
"client": {
|
|
79
80
|
"inject": [
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/dsh-client-
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
82
|
+
"@deepseek-ai/dsh-client-locale",
|
|
83
|
+
"@deepseek-ai/dsh-api-gateway",
|
|
82
84
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
85
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-settings-general",
|
|
83
87
|
"@deepseek-ai/dsh-client-ui-tool",
|
|
84
|
-
"@deepseek-ai/dsh-
|
|
85
|
-
"@deepseek-ai/dsh-client-ui-input-trigger"
|
|
86
|
-
"@deepseek-ai/dsh-client-locale"
|
|
88
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-input-trigger"
|
|
87
90
|
],
|
|
88
91
|
"platform": "web"
|
|
89
92
|
},
|
|
@@ -104,7 +107,10 @@
|
|
|
104
107
|
"0.1.2-alpha.5": "compatible",
|
|
105
108
|
"0.1.2-rc.1": "compatible",
|
|
106
109
|
"0.1.3-alpha.1": "unknown",
|
|
107
|
-
"0.1.3-alpha.2": "compatible"
|
|
110
|
+
"0.1.3-alpha.2": "compatible",
|
|
111
|
+
"0.1.5-alpha.1": "compatible",
|
|
112
|
+
"0.1.5-alpha.2": "compatible",
|
|
113
|
+
"0.1.5-rc.1": "compatible"
|
|
108
114
|
},
|
|
109
115
|
"profiles": [
|
|
110
116
|
"web",
|
|
@@ -119,26 +125,25 @@
|
|
|
119
125
|
"zod": "4.4.3"
|
|
120
126
|
},
|
|
121
127
|
"peerDependencies": {
|
|
122
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.
|
|
123
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.
|
|
124
|
-
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.
|
|
125
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
126
|
-
"@deepseek-ai/dsh-client-
|
|
127
|
-
"@deepseek-ai/dsh-client-ui-
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-
|
|
130
|
-
"@deepseek-ai/dsh-client-ui-
|
|
131
|
-
"@deepseek-ai/dsh-client-ui-
|
|
132
|
-
"@deepseek-ai/dsh-
|
|
133
|
-
"@deepseek-ai/dsh-
|
|
134
|
-
"@deepseek-ai/dsh-
|
|
135
|
-
"@deepseek-ai/dsh-
|
|
136
|
-
"@deepseek-ai/dsh-
|
|
137
|
-
"@deepseek-ai/dsh-
|
|
138
|
-
"@deepseek-ai/dsh-
|
|
139
|
-
"@deepseek-ai/dsh-
|
|
140
|
-
"@deepseek-ai/dsh-
|
|
141
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
128
|
+
"@deepseek-ai/dsh-agent": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
130
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
131
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
132
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
134
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
135
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
136
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
137
|
+
"@deepseek-ai/dsh-client-ui-tool": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
138
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
139
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
140
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
141
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
142
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
143
|
+
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
144
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
145
|
+
"@deepseek-ai/dsh-subprocess": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
146
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
142
147
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
143
148
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
144
149
|
"react": "^18.2.0"
|
|
@@ -149,10 +154,13 @@
|
|
|
149
154
|
}
|
|
150
155
|
},
|
|
151
156
|
"devDependencies": {
|
|
152
|
-
"@deepseek-ai/dsh-
|
|
153
|
-
"@deepseek-ai/dsh-
|
|
154
|
-
"@deepseek-ai/dsh-
|
|
155
|
-
"@deepseek-ai/dsh-
|
|
157
|
+
"@deepseek-ai/dsh-client-connection": "0.1.5-rc.1",
|
|
158
|
+
"@deepseek-ai/dsh-client-store": "0.1.5-rc.1",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.1",
|
|
160
|
+
"@deepseek-ai/dsh-scope": "0.1.5-rc.1",
|
|
161
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.5-rc.1",
|
|
162
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1",
|
|
163
|
+
"@deepseek-ai/dsh-tool-skill": "0.1.5-rc.1",
|
|
156
164
|
"@testing-library/react": "^16.3.2",
|
|
157
165
|
"@types/node": "^22.20.0",
|
|
158
166
|
"@types/react": "~18.3.1",
|
package/src/artifact-access.ts
CHANGED
|
@@ -12,10 +12,10 @@ import { chmod, lstat, mkdir, open, readFile, realpath, writeFile } from 'node:f
|
|
|
12
12
|
import type { FileHandle } from 'node:fs/promises'
|
|
13
13
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
14
14
|
import { basename, dirname, extname, isAbsolute, join, relative, sep } from 'node:path'
|
|
15
|
-
import type { JsonValue } from '@deepseek-ai/dsh-tools'
|
|
16
15
|
import type { ArtifactDescriptor, ArtifactKind } from './artifacts.ts'
|
|
17
16
|
import { assertSecureSharedStorageBase, assertSecureWorkspaceStorage, isWithin } from './paths.ts'
|
|
18
17
|
import { visionToolkitStateRoot } from './runtime-install.ts'
|
|
18
|
+
import type { JsonValue } from './tools.ts'
|
|
19
19
|
|
|
20
20
|
/** Prefix owned by the plugin's artifact capability route. */
|
|
21
21
|
export const ARTIFACT_ROUTE_PREFIX = '/_dsh/vision-toolkit/artifacts'
|
package/src/client/index.tsx
CHANGED
|
@@ -10,12 +10,24 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react'
|
|
12
12
|
import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
13
|
-
import type { ClientContext
|
|
13
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
14
|
+
/**
|
|
15
|
+
* `ToolCallBlock` is the one DSH type this bundle consumes by name. DSH 0.1.5
|
|
16
|
+
* publishes it from `@deepseek-ai/dsh-client-ui-conversation/client`; its
|
|
17
|
+
* earlier home, `@deepseek-ai/dsh-client-runtime/client`, stopped publishing
|
|
18
|
+
* after 0.1.1-rc.2 and has no member of the 0.1.5 family at all, so importing
|
|
19
|
+
* the old specifier would leave an unresolvable peer on a real host. The
|
|
20
|
+
* `paths` row in tsconfig.client.json and the peerDependency in package.json
|
|
21
|
+
* name the same package, and both must move together. A type-only import costs
|
|
22
|
+
* nothing at runtime and emits no `require()` in lib/client.js.
|
|
23
|
+
*/
|
|
24
|
+
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
14
25
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
26
|
+
import type {} from '@deepseek-ai/dsh-client-connection/client'
|
|
15
27
|
import type {} from '@deepseek-ai/dsh-credentials/types'
|
|
16
28
|
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
17
|
-
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
18
29
|
import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
30
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
19
31
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
20
32
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
21
33
|
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
@@ -1572,7 +1584,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
1572
1584
|
const disposers = typeof legacyRemote.$on === 'function'
|
|
1573
1585
|
? [
|
|
1574
1586
|
legacyRemote.$on('settings/document-updated', refreshSettings),
|
|
1575
|
-
|
|
1587
|
+
// DSH forwards `credentials/reference-updated` (payload: the credential
|
|
1588
|
+
// ref whose stored value committed) and never `credentials/updated`;
|
|
1589
|
+
// `$on` accepts unknown names silently, so the wrong key was a dead
|
|
1590
|
+
// subscription. Both the 0.1.2-alpha.1 and 0.1.5 allowlists carry this
|
|
1591
|
+
// exact name, so one key covers every supported host.
|
|
1592
|
+
legacyRemote.$on('credentials/reference-updated', refreshCredential),
|
|
1576
1593
|
]
|
|
1577
1594
|
: [
|
|
1578
1595
|
currentEvents.on('settings/changed', (namespace) => {
|
|
@@ -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
|
|
@@ -222,7 +264,7 @@ export class PasteImageController {
|
|
|
222
264
|
}
|
|
223
265
|
|
|
224
266
|
private inputFor(sessionId: string) {
|
|
225
|
-
const actx = this.ctx.
|
|
267
|
+
const actx = sessionRegistry(this.ctx).scope(sessionId as never)
|
|
226
268
|
if (actx === undefined) throw new Error('Open a live session before pasting images')
|
|
227
269
|
return this.ctx.conversation.input.for(actx)
|
|
228
270
|
}
|
|
@@ -482,7 +524,9 @@ export class PasteImageController {
|
|
|
482
524
|
* Auto-switch flow: switch the Session to the image-input variant, announce
|
|
483
525
|
* it, then replay the paste into the composer's native intake. A failed
|
|
484
526
|
* switch, or an environment that cannot replay clipboard bytes, degrades to
|
|
485
|
-
* 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.
|
|
486
530
|
* @param sessionId - the live Session id.
|
|
487
531
|
* @param target - the composer textarea the paste landed on.
|
|
488
532
|
* @param files - the captured image files.
|
|
@@ -511,10 +555,10 @@ export class PasteImageController {
|
|
|
511
555
|
// Replaying lets the composer's own intake run (thumbnail, limits,
|
|
512
556
|
// keyboard); if the environment cannot replay clipboard bytes, the
|
|
513
557
|
// images still land as workspace paths.
|
|
514
|
-
const
|
|
558
|
+
const attachmentsBefore = draftAttachmentCount(input.state.getSnapshot())
|
|
515
559
|
const replayed = this.replayPaste(target, files, text)
|
|
516
|
-
const
|
|
517
|
-
if (!replayed ||
|
|
560
|
+
const attachmentsAfter = draftAttachmentCount(input.state.getSnapshot())
|
|
561
|
+
if (!replayed || attachmentsAfter <= attachmentsBefore) {
|
|
518
562
|
this.takeoverPaste(sessionId, target, files, text)
|
|
519
563
|
}
|
|
520
564
|
}
|
|
@@ -558,7 +602,7 @@ export class PasteImageController {
|
|
|
558
602
|
const target = event.target
|
|
559
603
|
if (!(target instanceof HTMLTextAreaElement) || target.closest('[data-composer-card]') === null) return false
|
|
560
604
|
|
|
561
|
-
const sessionId = this.ctx.
|
|
605
|
+
const sessionId = sessionRegistry(this.ctx).list.getSnapshot().current
|
|
562
606
|
if (sessionId === undefined) return false
|
|
563
607
|
const modelLabel = currentModelLabel()
|
|
564
608
|
this.refreshVerdict(sessionId, modelLabel)
|
|
@@ -735,7 +779,7 @@ export function installPasteImages(ctx: ClientContext): void {
|
|
|
735
779
|
const listener = (event: ClipboardEvent): void => { controller.handlePaste(event) }
|
|
736
780
|
// A focus-time prefetch has the verdict ready before the first paste can land.
|
|
737
781
|
const onFocusIn = (): void => {
|
|
738
|
-
const sessionId = ctx.
|
|
782
|
+
const sessionId = sessionRegistry(ctx).list.getSnapshot().current
|
|
739
783
|
if (sessionId !== undefined) controller.refreshVerdict(String(sessionId), currentModelLabel())
|
|
740
784
|
}
|
|
741
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,12 +79,49 @@ 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
|
-
const events =
|
|
85
|
-
? (session as any).snapshotEvents()
|
|
86
|
-
: session.events ?? []
|
|
124
|
+
const events = sessionEvents(session)
|
|
87
125
|
for (const event of events) {
|
|
88
126
|
if (event.type === 'user/message') {
|
|
89
127
|
const source = event.data.source
|
|
@@ -106,11 +144,7 @@ function hasLoadedVisionSkill(session: Session): boolean {
|
|
|
106
144
|
&& containsBundledSkillContent(block.content)) return true
|
|
107
145
|
continue
|
|
108
146
|
}
|
|
109
|
-
if (event
|
|
110
|
-
&& event.data.name === 'skill'
|
|
111
|
-
&& event.data.isError === false
|
|
112
|
-
&& isVisionSkillArguments(event.data.arguments)
|
|
113
|
-
&& containsBundledSkillContent(event.data.content)) return true
|
|
147
|
+
if (isBundledSkillPtcDispatch(event)) return true
|
|
114
148
|
}
|
|
115
149
|
return false
|
|
116
150
|
}
|
|
@@ -14,7 +14,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
16
16
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
-
import LlmService, { LlmAdapter, contentHasImage } from '@deepseek-ai/dsh-llm'
|
|
17
|
+
import LlmService, { LlmAdapter, contentHasImage, freezeMessage } from '@deepseek-ai/dsh-llm'
|
|
18
18
|
import type {
|
|
19
19
|
ContentBlock,
|
|
20
20
|
GenerateOptions,
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
LlmProviderInfo,
|
|
24
24
|
LlmResolvedModelInfo,
|
|
25
25
|
Message,
|
|
26
|
+
ModelMessageSource,
|
|
26
27
|
ResolvedRetryPolicy,
|
|
27
28
|
StreamChunk,
|
|
28
29
|
} from '@deepseek-ai/dsh-llm'
|
|
@@ -508,6 +509,80 @@ export async function convertImagesToEvidence(
|
|
|
508
509
|
return converted
|
|
509
510
|
}
|
|
510
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Present one delegated request's assistant history under the upstream route,
|
|
514
|
+
* restoring the adapter-private replay state the host withholds from a facade.
|
|
515
|
+
*
|
|
516
|
+
* The host hands `replayState` to a target adapter only while that exact
|
|
517
|
+
* adapter instance owns both the historical provider and the target provider.
|
|
518
|
+
* A variant route is a different instance, so history produced under the
|
|
519
|
+
* upstream route arrives stripped, and history produced under the variant
|
|
520
|
+
* route is stripped again inside the delegation (the upstream adapter does not
|
|
521
|
+
* own the variant route either). Losing that state costs provider-native
|
|
522
|
+
* fidelity — thinking signatures, native effort binding, response ids — which
|
|
523
|
+
* makes an upstream that needs reasoning continuity answer without thinking
|
|
524
|
+
* while still being asked for it, and the host reports only a state it could
|
|
525
|
+
* not use, never one it withheld.
|
|
526
|
+
*
|
|
527
|
+
* The durable transcript is the authority for what was withheld: history
|
|
528
|
+
* messages of this wrapper's own routes are matched by id, and only the request
|
|
529
|
+
* copy is re-provenanced. The Session itself is never touched, so the durable
|
|
530
|
+
* log keeps the route the user actually selected.
|
|
531
|
+
* @param upstream - upstream route the delegated request is dispatched under.
|
|
532
|
+
* @param messages - messages about to be delegated to that route.
|
|
533
|
+
* @param transcript - reads the durable Session transcript on demand, if any.
|
|
534
|
+
* @returns the messages to delegate: the input array when nothing needed repair.
|
|
535
|
+
*/
|
|
536
|
+
function presentUnderUpstreamRoute(
|
|
537
|
+
upstream: string,
|
|
538
|
+
messages: Message[],
|
|
539
|
+
transcript: () => readonly Message[] | undefined,
|
|
540
|
+
): Message[] {
|
|
541
|
+
const variant = variantProviderId(upstream)
|
|
542
|
+
let durable: Map<string, Message> | undefined
|
|
543
|
+
let presented: Message[] | undefined
|
|
544
|
+
messages.forEach((message, index) => {
|
|
545
|
+
const source = message.source
|
|
546
|
+
if (message.role !== 'assistant' || source.kind !== 'model') return
|
|
547
|
+
if (source.provider !== upstream && source.provider !== variant) return
|
|
548
|
+
let replayState = source.replayState
|
|
549
|
+
if (replayState === undefined) {
|
|
550
|
+
durable ??= new Map(
|
|
551
|
+
(transcript() ?? [])
|
|
552
|
+
.filter(candidate => candidate.role === 'assistant')
|
|
553
|
+
.map(candidate => [String(candidate.id), candidate] as const),
|
|
554
|
+
)
|
|
555
|
+
const withheld = durable.get(String(message.id))
|
|
556
|
+
// Provenance and shape must still describe this very message: a Session
|
|
557
|
+
// that was rewritten under us (a fold, a rewind) must not have one
|
|
558
|
+
// message's state reattached to another's content.
|
|
559
|
+
if (withheld !== undefined && withheld.source.kind === 'model'
|
|
560
|
+
&& withheld.source.provider === source.provider
|
|
561
|
+
&& withheld.source.replayState !== undefined
|
|
562
|
+
&& sameBlockShape(withheld.content, message.content)) {
|
|
563
|
+
replayState = withheld.source.replayState
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Nothing withheld and no re-provenancing to do: leave the message alone.
|
|
567
|
+
if (source.provider === upstream && replayState === undefined) return
|
|
568
|
+
const replayed: ModelMessageSource = {
|
|
569
|
+
kind: 'model',
|
|
570
|
+
provider: upstream,
|
|
571
|
+
model: source.model,
|
|
572
|
+
...(replayState === undefined ? {} : { replayState }),
|
|
573
|
+
}
|
|
574
|
+
presented ??= [...messages]
|
|
575
|
+
presented[index] = freezeMessage({ ...message, source: replayed })
|
|
576
|
+
})
|
|
577
|
+
return presented ?? messages
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Whether two block lists carry the same block types in the same order. */
|
|
581
|
+
function sameBlockShape(left: readonly ContentBlock[], right: readonly ContentBlock[]): boolean {
|
|
582
|
+
return left.length === right.length
|
|
583
|
+
&& left.every((block, index) => block.type === right[index]?.type)
|
|
584
|
+
}
|
|
585
|
+
|
|
511
586
|
/**
|
|
512
587
|
* The adapter behind one variant route: model metadata declares image input,
|
|
513
588
|
* and every stream rewrites image blocks before delegating to the upstream
|
|
@@ -539,6 +614,18 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
539
614
|
return this.llm.providerRetryPolicy(this.upstream)
|
|
540
615
|
}
|
|
541
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Read the durable transcript of one Session, when the host still holds it.
|
|
619
|
+
* Only `presentUnderUpstreamRoute` consumes it, and only for messages whose
|
|
620
|
+
* replay state the host withheld from this facade route.
|
|
621
|
+
* @param sessionId - Session whose history is being delegated, if any.
|
|
622
|
+
* @returns the durable messages, or undefined without a live Session.
|
|
623
|
+
*/
|
|
624
|
+
private durableTranscript(sessionId: GenerateOptions['sessionId']): readonly Message[] | undefined {
|
|
625
|
+
if (sessionId === undefined) return undefined
|
|
626
|
+
return this.ctx.sessions.get(sessionId as never)?.deriveMessages()
|
|
627
|
+
}
|
|
628
|
+
|
|
542
629
|
override async listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
543
630
|
const models = await this.llm.listModels(this.upstream)
|
|
544
631
|
return models.filter(shouldWrapModel).map((model) => ({
|
|
@@ -605,9 +692,17 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
605
692
|
.digest('hex'),
|
|
606
693
|
storageDir,
|
|
607
694
|
)
|
|
608
|
-
// Delegate through the host service under the upstream route
|
|
609
|
-
//
|
|
610
|
-
|
|
695
|
+
// Delegate through the host service under the upstream route, so the
|
|
696
|
+
// upstream's own middleware, retry policy, and replay handling still apply.
|
|
697
|
+
// Replay state is host-owned and route-scoped, so the history this facade
|
|
698
|
+
// route cannot receive back is restored before that delegation: the durable
|
|
699
|
+
// log keeps the user's route, only the delegated copy is re-provenanced.
|
|
700
|
+
const delegated = presentUnderUpstreamRoute(
|
|
701
|
+
this.upstream,
|
|
702
|
+
messages,
|
|
703
|
+
() => this.durableTranscript(options.sessionId),
|
|
704
|
+
)
|
|
705
|
+
yield* this.llm.stream({ ...options, provider: this.upstream, messages: delegated })
|
|
611
706
|
}
|
|
612
707
|
}
|
|
613
708
|
|
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),
|