@aexol/spectral 0.9.212 → 0.9.215
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/dist/backend/generated-images.d.ts +90 -0
- package/dist/backend/generated-images.d.ts.map +1 -0
- package/dist/backend/generated-images.js +214 -0
- package/dist/backend/machine-routing.d.ts +25 -0
- package/dist/backend/machine-routing.d.ts.map +1 -0
- package/dist/backend/machine-routing.js +35 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +20 -2
- package/dist/extensions/browser/browser-service.d.ts +75 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +600 -34
- package/dist/extensions/browser/index.d.ts +10 -4
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +44 -5
- package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -1
- package/dist/extensions/browser/stream/stream-tool.js +42 -14
- package/dist/extensions/browser/tools/core.d.ts.map +1 -1
- package/dist/extensions/browser/tools/core.js +63 -2
- package/dist/extensions/desktop-control/index.d.ts +55 -0
- package/dist/extensions/desktop-control/index.d.ts.map +1 -0
- package/dist/extensions/desktop-control/index.js +633 -0
- package/dist/extensions/desktop-control/keys.d.ts +33 -0
- package/dist/extensions/desktop-control/keys.d.ts.map +1 -0
- package/dist/extensions/desktop-control/keys.js +170 -0
- package/dist/extensions/desktop-control/linux.d.ts +26 -0
- package/dist/extensions/desktop-control/linux.d.ts.map +1 -0
- package/dist/extensions/desktop-control/linux.js +195 -0
- package/dist/extensions/desktop-control/macos.d.ts +53 -0
- package/dist/extensions/desktop-control/macos.d.ts.map +1 -0
- package/dist/extensions/desktop-control/macos.js +382 -0
- package/dist/extensions/desktop-control/safety.d.ts +89 -0
- package/dist/extensions/desktop-control/safety.d.ts.map +1 -0
- package/dist/extensions/desktop-control/safety.js +157 -0
- package/dist/extensions/desktop-control/types.d.ts +79 -0
- package/dist/extensions/desktop-control/types.d.ts.map +1 -0
- package/dist/extensions/desktop-control/types.js +10 -0
- package/dist/extensions/desktop-control/windows.d.ts +31 -0
- package/dist/extensions/desktop-control/windows.d.ts.map +1 -0
- package/dist/extensions/desktop-control/windows.js +276 -0
- package/dist/extensions/desktop-screenshot/index.d.ts +103 -0
- package/dist/extensions/desktop-screenshot/index.d.ts.map +1 -1
- package/dist/extensions/desktop-screenshot/index.js +363 -17
- package/dist/extensions/image-generation/index.d.ts.map +1 -1
- package/dist/extensions/image-generation/index.js +133 -34
- package/dist/extensions/seo/index.d.ts.map +1 -1
- package/dist/extensions/seo/index.js +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts.map +1 -0
- package/dist/extensions/seo/tools/backend-bridge.js +378 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +2 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +1 -1
- package/dist/extensions/seo/tools/report.d.ts +10 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +1 -1
- package/dist/extensions/spectral-vision-fallback.d.ts.map +1 -1
- package/dist/extensions/spectral-vision-fallback.js +11 -1
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +121 -4
- package/dist/generated/zeus/index.d.ts +527 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.js +5 -2
- package/dist/mcp/types.d.ts +5 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/relay/dispatcher.d.ts +12 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +44 -42
- package/dist/sdk/ai/providers/faux.js +1 -1
- package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.js +57 -16
- package/dist/sdk/ai/providers/openrouter-images.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openrouter-images.js +45 -9
- package/dist/sdk/ai/providers/simple-options.d.ts.map +1 -1
- package/dist/sdk/ai/providers/simple-options.js +2 -0
- package/dist/sdk/ai/types.d.ts +37 -1
- package/dist/sdk/ai/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/image-source.d.ts +121 -0
- package/dist/sdk/ai/utils/image-source.d.ts.map +1 -0
- package/dist/sdk/ai/utils/image-source.js +410 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +14 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +15 -9
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.js +16 -3
- package/dist/sdk/coding-agent/utils/image-resize.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/image-resize.js +7 -3
- package/dist/server/agent-bridge.d.ts +14 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +105 -26
- package/dist/server/handlers/queue.d.ts.map +1 -1
- package/dist/server/handlers/queue.js +11 -34
- package/dist/server/image-attachments.d.ts +109 -0
- package/dist/server/image-attachments.d.ts.map +1 -0
- package/dist/server/image-attachments.js +195 -0
- package/dist/server/session-stream.d.ts +67 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +222 -23
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +10 -31
- package/dist/server/ui/terminal-confirm.d.ts +145 -0
- package/dist/server/ui/terminal-confirm.d.ts.map +1 -0
- package/dist/server/ui/terminal-confirm.js +333 -0
- package/dist/server/wire.d.ts +30 -5
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image source resolution for provider serialization.
|
|
3
|
+
*
|
|
4
|
+
* `ImageContent` can now carry either inline base64 (`data`) or a hosted
|
|
5
|
+
* `url`. Providers (OpenRouter, AWS Bedrock, OpenAI, ...) can only fetch
|
|
6
|
+
* images from PUBLIC urls — a `http://localhost:9002/...` MinIO url in dev is
|
|
7
|
+
* unreachable for them. This module resolves an `ImageContent` block into
|
|
8
|
+
* something a provider can actually consume:
|
|
9
|
+
*
|
|
10
|
+
* - `data` present -> `data:<mimeType>;base64,<data>` (unchanged, legacy safe)
|
|
11
|
+
* - `url` public -> passed through as-is (no base64 in the context!)
|
|
12
|
+
* - `url` relative -> resolved against the backend base url first
|
|
13
|
+
* (`/generated-images/<id>` is what the backend returns)
|
|
14
|
+
* - `url` local/private -> fetched and inlined **at send time only**
|
|
15
|
+
*
|
|
16
|
+
* The inlining is deliberately scoped to a single request: the fetched bytes
|
|
17
|
+
* are written into a throw-away copy of the message list, never back into the
|
|
18
|
+
* session, so base64 never accumulates in persisted history.
|
|
19
|
+
*
|
|
20
|
+
* @module @spectral/ai/utils/image-source
|
|
21
|
+
*/
|
|
22
|
+
import type { Context, ImageContent } from "../types.js";
|
|
23
|
+
/** Don't inline absurd payloads — 25 MiB of base64 would blow the request. */
|
|
24
|
+
export declare const DEFAULT_MAX_INLINE_BYTES: number;
|
|
25
|
+
/** Timeout for fetching a local image that has to be inlined. */
|
|
26
|
+
export declare const DEFAULT_INLINE_FETCH_TIMEOUT_MS = 15000;
|
|
27
|
+
export interface ResolveImageOptions {
|
|
28
|
+
/** Abort the inline fetch (e.g. when the request is cancelled). */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
maxInlineBytes?: number;
|
|
32
|
+
/** Injectable for tests / alternative runtimes. */
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
/**
|
|
35
|
+
* Base url (usually the backend url) used to turn a relative image url
|
|
36
|
+
* (`/generated-images/<id>`) into an absolute one. May be a lazy getter so
|
|
37
|
+
* callers only pay the cost (e.g. reading config) when a relative url is
|
|
38
|
+
* actually present.
|
|
39
|
+
*/
|
|
40
|
+
baseUrl?: string | (() => string | undefined | Promise<string | undefined>);
|
|
41
|
+
onWarn?: (message: string) => void;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolvedImageSource {
|
|
44
|
+
/** Value to place in `image_url.url` (data URI or http(s) url). */
|
|
45
|
+
url: string;
|
|
46
|
+
/** True when the bytes were fetched and base64-inlined. */
|
|
47
|
+
inlined: boolean;
|
|
48
|
+
mimeType: string;
|
|
49
|
+
/**
|
|
50
|
+
* Raw base64 payload WITHOUT the `data:` prefix, matching the
|
|
51
|
+
* `ImageContent.data` contract. Only set when the bytes are known
|
|
52
|
+
* (inline `data` present, or fetched from a local url).
|
|
53
|
+
*/
|
|
54
|
+
data?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* True when `rawUrl` has no scheme, i.e. it is relative and therefore
|
|
58
|
+
* unusable for a provider until it is joined with a base url.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isRelativeImageUrl(rawUrl: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Join a relative image url (`/generated-images/<id>`) with the backend base
|
|
63
|
+
* url. Absolute urls and missing base urls are returned untouched.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveImageUrlAgainstBase(rawUrl: string, baseUrl?: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* True when a provider is unlikely to be able to fetch this URL itself and
|
|
68
|
+
* the image must therefore be inlined as base64 before sending.
|
|
69
|
+
*
|
|
70
|
+
* Covers: localhost, `*.local`, `*.localhost`, loopback IPs (v4 + v6),
|
|
71
|
+
* RFC1918 private ranges, link-local and CGNAT ranges, and non-http(s)
|
|
72
|
+
* schemes (except `data:`/`blob:` which need no fetch at all), plus
|
|
73
|
+
* container/metadata/reserved hosts and relative urls that still need to be
|
|
74
|
+
* resolved against a base url.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isLocalOrPrivateImageUrl(rawUrl: string): boolean;
|
|
77
|
+
export declare function hasInlineImageData(image: ImageContent): image is ImageContent & {
|
|
78
|
+
data: string;
|
|
79
|
+
};
|
|
80
|
+
/** Build a `data:` URI from base64 payload + mime type (idempotent). */
|
|
81
|
+
export declare function dataUriForImage(data: string, mimeType: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* `ImageContent.data` is base64 WITHOUT a `data:` prefix, but legacy
|
|
84
|
+
* sessions may carry a full data URI. Strip it so consumers doing
|
|
85
|
+
* `Buffer.from(data, "base64")` get real bytes.
|
|
86
|
+
*/
|
|
87
|
+
export declare function base64FromImageData(data: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Resolve an image block into a provider-sendable url.
|
|
90
|
+
*
|
|
91
|
+
* Returns `null` when the block carries neither data nor url (nothing sendable).
|
|
92
|
+
* Local/private urls are fetched and inlined; failures fall back to the raw
|
|
93
|
+
* url so the caller still emits *something* rather than dropping the image.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveImageForProvider(image: ImageContent, options?: ResolveImageOptions): Promise<ResolvedImageSource | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Pre-pass used right before a request is built: any image block that only has
|
|
98
|
+
* a local/private `url` gets its bytes fetched and attached as `data` on a
|
|
99
|
+
* throw-away copy of the context.
|
|
100
|
+
*
|
|
101
|
+
* The returned context is a shallow clone (messages copied, unchanged messages
|
|
102
|
+
* shared by reference) and is discarded after the single request completes, so
|
|
103
|
+
* base64 never lands in session history. When there is nothing to inline the
|
|
104
|
+
* original context object is returned unchanged.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveImagesForContext(context: Context, options?: ResolveImageOptions): Promise<Context>;
|
|
107
|
+
/**
|
|
108
|
+
* Same as `resolveImagesForContext` but for the images API input list.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveImagesForInput(input: Array<{
|
|
111
|
+
type: string;
|
|
112
|
+
}>, options?: ResolveImageOptions): Promise<Array<{
|
|
113
|
+
type: string;
|
|
114
|
+
}>>;
|
|
115
|
+
/**
|
|
116
|
+
* Synchronous serializer used inside the (sync) param builders: after the
|
|
117
|
+
* `resolveImagesForContext` pre-pass there should be nothing local left, so
|
|
118
|
+
* this only has to choose between an existing `data` payload and a public url.
|
|
119
|
+
*/
|
|
120
|
+
export declare function imageSourceUrlForProvider(image: ImageContent, baseUrl?: string): string | undefined;
|
|
121
|
+
//# sourceMappingURL=image-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-source.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/image-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAW,MAAM,aAAa,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,iEAAiE;AACjE,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IACnC,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IACnC,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAuCD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI1D;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAUnF;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BhE;AAcD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,KAAK,IAAI,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAEhG;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIxD;AAoBD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC5C,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,mBAAwB,GAC/B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAgErC;AAcD;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAC/B,OAAO,CAAC,OAAO,CAAC,CAoDlB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAC1C,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC9B,OAAO,GAAE,mBAAwB,GAC/B,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAsBlC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUnG"}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image source resolution for provider serialization.
|
|
3
|
+
*
|
|
4
|
+
* `ImageContent` can now carry either inline base64 (`data`) or a hosted
|
|
5
|
+
* `url`. Providers (OpenRouter, AWS Bedrock, OpenAI, ...) can only fetch
|
|
6
|
+
* images from PUBLIC urls — a `http://localhost:9002/...` MinIO url in dev is
|
|
7
|
+
* unreachable for them. This module resolves an `ImageContent` block into
|
|
8
|
+
* something a provider can actually consume:
|
|
9
|
+
*
|
|
10
|
+
* - `data` present -> `data:<mimeType>;base64,<data>` (unchanged, legacy safe)
|
|
11
|
+
* - `url` public -> passed through as-is (no base64 in the context!)
|
|
12
|
+
* - `url` relative -> resolved against the backend base url first
|
|
13
|
+
* (`/generated-images/<id>` is what the backend returns)
|
|
14
|
+
* - `url` local/private -> fetched and inlined **at send time only**
|
|
15
|
+
*
|
|
16
|
+
* The inlining is deliberately scoped to a single request: the fetched bytes
|
|
17
|
+
* are written into a throw-away copy of the message list, never back into the
|
|
18
|
+
* session, so base64 never accumulates in persisted history.
|
|
19
|
+
*
|
|
20
|
+
* @module @spectral/ai/utils/image-source
|
|
21
|
+
*/
|
|
22
|
+
/** Don't inline absurd payloads — 25 MiB of base64 would blow the request. */
|
|
23
|
+
export const DEFAULT_MAX_INLINE_BYTES = 25 * 1024 * 1024;
|
|
24
|
+
/** Timeout for fetching a local image that has to be inlined. */
|
|
25
|
+
export const DEFAULT_INLINE_FETCH_TIMEOUT_MS = 15_000;
|
|
26
|
+
function isDigitRun(value) {
|
|
27
|
+
return /^\d+$/.test(value);
|
|
28
|
+
}
|
|
29
|
+
function isPrivateIpv4(hostname) {
|
|
30
|
+
const parts = hostname.split(".");
|
|
31
|
+
if (parts.length !== 4 || !parts.every(isDigitRun))
|
|
32
|
+
return false;
|
|
33
|
+
const [a, b] = parts.map((p) => Number(p));
|
|
34
|
+
// 0.0.0.0/8, 10/8, 127/8 (loopback), 169.254/16 (link-local), 172.16/12, 192.168/16
|
|
35
|
+
if (a === 0 || a === 10 || a === 127)
|
|
36
|
+
return true;
|
|
37
|
+
if (a === 169 && b === 254)
|
|
38
|
+
return true;
|
|
39
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
40
|
+
return true;
|
|
41
|
+
if (a === 192 && b === 168)
|
|
42
|
+
return true;
|
|
43
|
+
// 100.64/10 CGNAT
|
|
44
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Host suffixes that never resolve publicly: container/dev hosts
|
|
50
|
+
* (`host.docker.internal`), cloud metadata (`metadata.google.internal`),
|
|
51
|
+
* home networks (`*.home.arpa`) and reserved TLDs (`*.test`, `*.example`,
|
|
52
|
+
* `*.invalid`). A provider can never fetch these -> inline them.
|
|
53
|
+
*/
|
|
54
|
+
const LOCAL_HOST_SUFFIXES = [
|
|
55
|
+
"docker.internal",
|
|
56
|
+
".internal",
|
|
57
|
+
".localhost",
|
|
58
|
+
".local",
|
|
59
|
+
".home.arpa",
|
|
60
|
+
".test",
|
|
61
|
+
".example",
|
|
62
|
+
".invalid",
|
|
63
|
+
];
|
|
64
|
+
const ABSOLUTE_URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
65
|
+
/**
|
|
66
|
+
* True when `rawUrl` has no scheme, i.e. it is relative and therefore
|
|
67
|
+
* unusable for a provider until it is joined with a base url.
|
|
68
|
+
*/
|
|
69
|
+
export function isRelativeImageUrl(rawUrl) {
|
|
70
|
+
const trimmed = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
return false;
|
|
73
|
+
return !ABSOLUTE_URL_SCHEME.test(trimmed);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Join a relative image url (`/generated-images/<id>`) with the backend base
|
|
77
|
+
* url. Absolute urls and missing base urls are returned untouched.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveImageUrlAgainstBase(rawUrl, baseUrl) {
|
|
80
|
+
const trimmed = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
81
|
+
if (!trimmed || !isRelativeImageUrl(trimmed))
|
|
82
|
+
return trimmed;
|
|
83
|
+
const base = typeof baseUrl === "string" ? baseUrl.trim() : "";
|
|
84
|
+
if (!base)
|
|
85
|
+
return trimmed;
|
|
86
|
+
try {
|
|
87
|
+
return new URL(trimmed, base).toString();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return trimmed;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Loopback / unique-local (fc00::/7) / link-local (fe80::/10) IPv6 are
|
|
95
|
+
* unreachable for a provider; every other literal (e.g. `2001:4860::8888`)
|
|
96
|
+
* is public and can be passed through.
|
|
97
|
+
*/
|
|
98
|
+
function isPrivateIpv6(hostname) {
|
|
99
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0:0:0:0:0:0:0:1")
|
|
100
|
+
return true;
|
|
101
|
+
const mapped = hostname.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
102
|
+
if (mapped)
|
|
103
|
+
return isPrivateIpv4(mapped[1]);
|
|
104
|
+
const head = hostname.split(":")[0] ?? "";
|
|
105
|
+
// fc00::/7 (fc00-fdff) unique local
|
|
106
|
+
if (/^f[cd][0-9a-f]{0,2}$/.test(head))
|
|
107
|
+
return true;
|
|
108
|
+
// fe80::/10 (fe80-febf) link local
|
|
109
|
+
if (/^fe[89ab][0-9a-f]?$/.test(head))
|
|
110
|
+
return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* True when a provider is unlikely to be able to fetch this URL itself and
|
|
115
|
+
* the image must therefore be inlined as base64 before sending.
|
|
116
|
+
*
|
|
117
|
+
* Covers: localhost, `*.local`, `*.localhost`, loopback IPs (v4 + v6),
|
|
118
|
+
* RFC1918 private ranges, link-local and CGNAT ranges, and non-http(s)
|
|
119
|
+
* schemes (except `data:`/`blob:` which need no fetch at all), plus
|
|
120
|
+
* container/metadata/reserved hosts and relative urls that still need to be
|
|
121
|
+
* resolved against a base url.
|
|
122
|
+
*/
|
|
123
|
+
export function isLocalOrPrivateImageUrl(rawUrl) {
|
|
124
|
+
const trimmed = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
125
|
+
if (!trimmed)
|
|
126
|
+
return false;
|
|
127
|
+
// Relative (`/generated-images/x`) or unparseable: the provider would only
|
|
128
|
+
// ever see a path fragment, so this needs resolution/inlining.
|
|
129
|
+
if (isRelativeImageUrl(trimmed))
|
|
130
|
+
return true;
|
|
131
|
+
let url;
|
|
132
|
+
try {
|
|
133
|
+
url = new URL(trimmed);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Unparseable -> treat like a relative url: unusable for the provider.
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
const protocol = url.protocol.toLowerCase();
|
|
140
|
+
if (protocol === "data:" || protocol === "blob:")
|
|
141
|
+
return false;
|
|
142
|
+
if (protocol !== "http:" && protocol !== "https:")
|
|
143
|
+
return true;
|
|
144
|
+
const hostname = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
|
|
145
|
+
if (hostname === "localhost" || hostname === "localhost.localdomain")
|
|
146
|
+
return true;
|
|
147
|
+
if (LOCAL_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)))
|
|
148
|
+
return true;
|
|
149
|
+
// IPv6 literal: explicit ranges only — a public literal must NOT be
|
|
150
|
+
// inlined (it has no dot, so the single-label rule below would misfire).
|
|
151
|
+
if (hostname.includes(":"))
|
|
152
|
+
return isPrivateIpv6(hostname);
|
|
153
|
+
if (isPrivateIpv4(hostname))
|
|
154
|
+
return true;
|
|
155
|
+
// Single-label hostnames (docker service names, "minio", ...) are not
|
|
156
|
+
// publicly resolvable -> inline them.
|
|
157
|
+
if (!hostname.includes("."))
|
|
158
|
+
return true;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
async function resolveBaseUrlOption(baseUrl) {
|
|
162
|
+
if (!baseUrl)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (typeof baseUrl === "string")
|
|
165
|
+
return baseUrl;
|
|
166
|
+
try {
|
|
167
|
+
return await baseUrl();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export function hasInlineImageData(image) {
|
|
174
|
+
return typeof image.data === "string" && image.data.length > 0;
|
|
175
|
+
}
|
|
176
|
+
/** Build a `data:` URI from base64 payload + mime type (idempotent). */
|
|
177
|
+
export function dataUriForImage(data, mimeType) {
|
|
178
|
+
if (data.startsWith("data:"))
|
|
179
|
+
return data;
|
|
180
|
+
return `data:${mimeType || "image/png"};base64,${data}`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `ImageContent.data` is base64 WITHOUT a `data:` prefix, but legacy
|
|
184
|
+
* sessions may carry a full data URI. Strip it so consumers doing
|
|
185
|
+
* `Buffer.from(data, "base64")` get real bytes.
|
|
186
|
+
*/
|
|
187
|
+
export function base64FromImageData(data) {
|
|
188
|
+
if (!data.startsWith("data:"))
|
|
189
|
+
return data;
|
|
190
|
+
const comma = data.indexOf(",");
|
|
191
|
+
return comma === -1 ? data : data.slice(comma + 1);
|
|
192
|
+
}
|
|
193
|
+
function bytesToBase64(bytes) {
|
|
194
|
+
const maybeBuffer = globalThis
|
|
195
|
+
.Buffer;
|
|
196
|
+
if (maybeBuffer)
|
|
197
|
+
return maybeBuffer.from(bytes).toString("base64");
|
|
198
|
+
let binary = "";
|
|
199
|
+
const CHUNK = 0x8000;
|
|
200
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
201
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
202
|
+
}
|
|
203
|
+
return btoa(binary);
|
|
204
|
+
}
|
|
205
|
+
function normalizeMimeType(value, fallback) {
|
|
206
|
+
if (!value)
|
|
207
|
+
return fallback || "image/png";
|
|
208
|
+
const base = value.split(";")[0].trim().toLowerCase();
|
|
209
|
+
return base || fallback || "image/png";
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolve an image block into a provider-sendable url.
|
|
213
|
+
*
|
|
214
|
+
* Returns `null` when the block carries neither data nor url (nothing sendable).
|
|
215
|
+
* Local/private urls are fetched and inlined; failures fall back to the raw
|
|
216
|
+
* url so the caller still emits *something* rather than dropping the image.
|
|
217
|
+
*/
|
|
218
|
+
export async function resolveImageForProvider(image, options = {}) {
|
|
219
|
+
if (hasInlineImageData(image)) {
|
|
220
|
+
// Keep the stored data URI intact (mime may differ from `mimeType`),
|
|
221
|
+
// but expose the raw base64 so copies stay contract-compliant.
|
|
222
|
+
const raw = base64FromImageData(image.data);
|
|
223
|
+
return {
|
|
224
|
+
url: image.data.startsWith("data:") ? image.data : dataUriForImage(raw, image.mimeType),
|
|
225
|
+
inlined: true,
|
|
226
|
+
mimeType: image.mimeType,
|
|
227
|
+
data: raw,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
let url = typeof image.url === "string" ? image.url.trim() : "";
|
|
231
|
+
if (!url)
|
|
232
|
+
return null;
|
|
233
|
+
// Relative urls (`/generated-images/<id>`) must be joined with the backend
|
|
234
|
+
// base url before anything else — otherwise the provider gets a path.
|
|
235
|
+
if (isRelativeImageUrl(url)) {
|
|
236
|
+
url = resolveImageUrlAgainstBase(url, await resolveBaseUrlOption(options.baseUrl));
|
|
237
|
+
}
|
|
238
|
+
if (!isLocalOrPrivateImageUrl(url)) {
|
|
239
|
+
return { url, inlined: false, mimeType: image.mimeType };
|
|
240
|
+
}
|
|
241
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
242
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_INLINE_FETCH_TIMEOUT_MS;
|
|
243
|
+
const maxInlineBytes = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
246
|
+
const onAbort = () => controller.abort();
|
|
247
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
248
|
+
try {
|
|
249
|
+
// An already-aborted signal never fires the listener above.
|
|
250
|
+
if (options.signal?.aborted)
|
|
251
|
+
throw new Error("aborted");
|
|
252
|
+
const response = await doFetch(url, { signal: controller.signal });
|
|
253
|
+
if (!response.ok)
|
|
254
|
+
throw new Error(`HTTP ${response.status}`);
|
|
255
|
+
// Reject oversized payloads from the declared length BEFORE reading
|
|
256
|
+
// the body — otherwise the whole payload lands in memory first.
|
|
257
|
+
const declaredLength = response.headers?.get?.("content-length");
|
|
258
|
+
if (declaredLength !== null && declaredLength !== undefined) {
|
|
259
|
+
const declared = Number(declaredLength);
|
|
260
|
+
if (Number.isFinite(declared) && declared > maxInlineBytes) {
|
|
261
|
+
throw new Error(`image too large to inline (${declared} bytes)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const buffer = await response.arrayBuffer();
|
|
265
|
+
if (buffer.byteLength > maxInlineBytes) {
|
|
266
|
+
throw new Error(`image too large to inline (${buffer.byteLength} bytes)`);
|
|
267
|
+
}
|
|
268
|
+
const mimeType = normalizeMimeType(response.headers?.get?.("content-type"), image.mimeType);
|
|
269
|
+
const data = bytesToBase64(new Uint8Array(buffer));
|
|
270
|
+
return {
|
|
271
|
+
url: dataUriForImage(data, mimeType),
|
|
272
|
+
inlined: true,
|
|
273
|
+
mimeType,
|
|
274
|
+
data,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
279
|
+
options.onWarn?.(`Could not inline local image ${url} (${reason}); sending url as-is`);
|
|
280
|
+
return { url, inlined: false, mimeType: image.mimeType };
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function inlineImageContentBlock(block, resolved) {
|
|
288
|
+
const image = block;
|
|
289
|
+
const patch = resolved.get(image);
|
|
290
|
+
if (patch === undefined)
|
|
291
|
+
return block;
|
|
292
|
+
// Copy-on-write: never mutate the block stored in the session.
|
|
293
|
+
const copy = { ...image, ...patch };
|
|
294
|
+
return copy;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Pre-pass used right before a request is built: any image block that only has
|
|
298
|
+
* a local/private `url` gets its bytes fetched and attached as `data` on a
|
|
299
|
+
* throw-away copy of the context.
|
|
300
|
+
*
|
|
301
|
+
* The returned context is a shallow clone (messages copied, unchanged messages
|
|
302
|
+
* shared by reference) and is discarded after the single request completes, so
|
|
303
|
+
* base64 never lands in session history. When there is nothing to inline the
|
|
304
|
+
* original context object is returned unchanged.
|
|
305
|
+
*/
|
|
306
|
+
export async function resolveImagesForContext(context, options = {}) {
|
|
307
|
+
const pending = [];
|
|
308
|
+
for (const message of context.messages) {
|
|
309
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
310
|
+
for (const block of message.content) {
|
|
311
|
+
if (block.type === "image" && !hasInlineImageData(block))
|
|
312
|
+
pending.push(block);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (message.role === "toolResult") {
|
|
316
|
+
for (const block of message.content) {
|
|
317
|
+
if (block.type === "image" && !hasInlineImageData(block))
|
|
318
|
+
pending.push(block);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (pending.length === 0)
|
|
323
|
+
return context;
|
|
324
|
+
const resolved = new Map();
|
|
325
|
+
await Promise.all(pending.map(async (image) => {
|
|
326
|
+
const source = await resolveImageForProvider(image, options);
|
|
327
|
+
if (!source)
|
|
328
|
+
return;
|
|
329
|
+
const originalUrl = typeof image.url === "string" ? image.url.trim() : "";
|
|
330
|
+
const patch = {};
|
|
331
|
+
if (source.inlined) {
|
|
332
|
+
// `data` must be raw base64 (no `data:` prefix) — see
|
|
333
|
+
// `ImageContent.data` and `base64FromImageData`.
|
|
334
|
+
if (source.data)
|
|
335
|
+
patch.data = source.data;
|
|
336
|
+
if (source.mimeType && source.mimeType !== image.mimeType)
|
|
337
|
+
patch.mimeType = source.mimeType;
|
|
338
|
+
}
|
|
339
|
+
else if (source.url !== originalUrl) {
|
|
340
|
+
// e.g. a relative url resolved against the backend base url.
|
|
341
|
+
patch.url = source.url;
|
|
342
|
+
}
|
|
343
|
+
if (Object.keys(patch).length > 0)
|
|
344
|
+
resolved.set(image, patch);
|
|
345
|
+
}));
|
|
346
|
+
if (resolved.size === 0)
|
|
347
|
+
return context;
|
|
348
|
+
const messages = context.messages.map((message) => {
|
|
349
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
350
|
+
return {
|
|
351
|
+
...message,
|
|
352
|
+
content: message.content.map((block) => inlineImageContentBlock(block, resolved)),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (message.role === "toolResult") {
|
|
356
|
+
return {
|
|
357
|
+
...message,
|
|
358
|
+
content: message.content.map((block) => inlineImageContentBlock(block, resolved)),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return message;
|
|
362
|
+
});
|
|
363
|
+
return { ...context, messages };
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Same as `resolveImagesForContext` but for the images API input list.
|
|
367
|
+
*/
|
|
368
|
+
export async function resolveImagesForInput(input, options = {}) {
|
|
369
|
+
const images = input.filter((item) => item.type === "image");
|
|
370
|
+
if (images.length === 0)
|
|
371
|
+
return input;
|
|
372
|
+
let changed = false;
|
|
373
|
+
const resolvedInputs = await Promise.all(input.map(async (item) => {
|
|
374
|
+
if (item.type !== "image")
|
|
375
|
+
return item;
|
|
376
|
+
const image = item;
|
|
377
|
+
const source = await resolveImageForProvider(image, options);
|
|
378
|
+
if (!source)
|
|
379
|
+
return item;
|
|
380
|
+
if (source.inlined && source.data) {
|
|
381
|
+
changed = true;
|
|
382
|
+
return { ...image, data: source.data, mimeType: source.mimeType };
|
|
383
|
+
}
|
|
384
|
+
if (!source.inlined && source.url !== (typeof image.url === "string" ? image.url.trim() : "")) {
|
|
385
|
+
changed = true;
|
|
386
|
+
return { ...image, url: source.url };
|
|
387
|
+
}
|
|
388
|
+
return item;
|
|
389
|
+
}));
|
|
390
|
+
return changed ? resolvedInputs : input;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Synchronous serializer used inside the (sync) param builders: after the
|
|
394
|
+
* `resolveImagesForContext` pre-pass there should be nothing local left, so
|
|
395
|
+
* this only has to choose between an existing `data` payload and a public url.
|
|
396
|
+
*/
|
|
397
|
+
export function imageSourceUrlForProvider(image, baseUrl) {
|
|
398
|
+
if (hasInlineImageData(image))
|
|
399
|
+
return dataUriForImage(image.data, image.mimeType);
|
|
400
|
+
const url = resolveImageUrlAgainstBase(image.url ?? "", baseUrl);
|
|
401
|
+
if (!url)
|
|
402
|
+
return undefined;
|
|
403
|
+
// Still relative after resolution (`/generated-images/<id>` with no
|
|
404
|
+
// backend base url configured): a provider would only receive a path
|
|
405
|
+
// fragment and answer HTTP 400, so drop the block instead of sending
|
|
406
|
+
// something it can never fetch.
|
|
407
|
+
if (isRelativeImageUrl(url))
|
|
408
|
+
return undefined;
|
|
409
|
+
return url;
|
|
410
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-extensions.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/extensions/native-extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;IAChD;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"native-extensions.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/extensions/native-extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;IAChD;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,uBAAuB,EAiKtD,CAAC;AAEF;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAI1G;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;IAClG,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC,CAUD"}
|
|
@@ -119,6 +119,20 @@ export const NATIVE_EXTENSIONS = [
|
|
|
119
119
|
defaultEnabled: true,
|
|
120
120
|
tags: ["screenshot", "desktop", "vision", "screen"],
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: "desktop-control",
|
|
124
|
+
label: "Desktop Control",
|
|
125
|
+
description: "Computer use with a safety layer: moves the mouse, clicks, drags, scrolls, types text, presses hotkeys and " +
|
|
126
|
+
"focuses windows via native OS APIs (macOS: JXA/CGEvent, Windows: PowerShell/SendInput, Linux: xdotool/wtype). " +
|
|
127
|
+
"Every mutating action returns before/after screenshots for verification. Enabling the extension is the " +
|
|
128
|
+
"explicit consent, so actions run without a confirmation prompt; set SPECTRAL_DESKTOP_REQUIRE_CONFIRM=1 to " +
|
|
129
|
+
"opt back into a per-session prompt. Supports SPECTRAL_DESKTOP_DRY_RUN and SPECTRAL_DESKTOP_ALLOWLIST. " +
|
|
130
|
+
"Off by default because it injects real input.",
|
|
131
|
+
category: "automation",
|
|
132
|
+
entryPath: "src/extensions/desktop-control/index.ts",
|
|
133
|
+
defaultEnabled: false,
|
|
134
|
+
tags: ["desktop", "computer-use", "mouse", "keyboard", "automation", "dangerous"],
|
|
135
|
+
},
|
|
122
136
|
{
|
|
123
137
|
id: "code-order",
|
|
124
138
|
label: "Code Order",
|
|
@@ -83,7 +83,16 @@ export interface ExtensionUIDialogOptions {
|
|
|
83
83
|
signal?: AbortSignal;
|
|
84
84
|
/** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
|
|
85
85
|
timeout?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Optional side-channel reporting how the dialog ended, so callers can
|
|
88
|
+
* distinguish an explicit human decision (`allow`/`deny`) from an auto-deny
|
|
89
|
+
* (`timeout`, `no_tty`, `cancelled`) where no user answered. Modes that
|
|
90
|
+
* cannot report outcomes simply never call it.
|
|
91
|
+
*/
|
|
92
|
+
onOutcome?: (outcome: ExtensionConfirmOutcome) => void;
|
|
86
93
|
}
|
|
94
|
+
/** How a confirm dialog ended (see ExtensionUIDialogOptions.onOutcome). */
|
|
95
|
+
export type ExtensionConfirmOutcome = "allow" | "deny" | "timeout" | "no_tty" | "cancelled";
|
|
87
96
|
/** Placement for extension widgets. */
|
|
88
97
|
export type WidgetPlacement = "aboveEditor" | "belowEditor";
|
|
89
98
|
/** Options for extension widgets. */
|