eve-hub 0.14.6 → 0.14.9
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 +18 -0
- package/animated-images.mjs +185 -0
- package/hub-upload.d.ts +1 -1
- package/hub-upload.js +2 -2
- package/package.json +6 -4
- package/roster-model.mjs +2 -0
- package/tools/post_message.d.ts +1 -1
- package/tools/post_message.js +48 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.14.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `post_message` finds links with a markdown parser instead of a regex. A sandbox path in code (a fence or an inline span) stays as written, a label with brackets (nested or escaped) keeps its link, a `#…` fragment stays on the rewritten link, a `%20` in the path is decoded, and a reference definition's target is rewritten too. The 700-character cap and the `![]()` refusal read the same links, so an image written inside code is no longer refused.
|
|
8
|
+
|
|
9
|
+
## 0.14.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- `packKit` (the tarball the hub's tests and hosted workspaces' staging use) compiles the kit inside `packages/agent-kit/dist`, where a publish compiles it, instead of a temp dir. The compiler now resolves eve and the kit's other dependencies there too, so a declaration only a publish rejected (TS2883 in 0.14.5) fails the tests before merge.
|
|
14
|
+
|
|
15
|
+
## 0.14.7
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Bots see animated GIF motion as sampled frames.
|
|
20
|
+
|
|
3
21
|
## 0.14.6
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// A model sees only the first frame of an animated image: Anthropic, OpenAI
|
|
2
|
+
// and the rest read a GIF as one still. So every model call a bot makes swaps
|
|
3
|
+
// each inline animated image in its prompt (a message attachment, a read_file
|
|
4
|
+
// or view_image result, an image read_thread returns) for a header and up to
|
|
5
|
+
// MAX_FRAMES labeled stills sampled evenly across it, in time order.
|
|
6
|
+
//
|
|
7
|
+
// The swap happens on the Gateway request itself (installAnimatedFrames wraps
|
|
8
|
+
// global fetch, as model-timeout.mjs does), not by wrapping the model: a model
|
|
9
|
+
// object changes the id eve records for every step, while the request body is
|
|
10
|
+
// the AI SDK prompt verbatim. A body with no animated image goes through
|
|
11
|
+
// byte-identical.
|
|
12
|
+
//
|
|
13
|
+
// A decoder turns one media type into frames; video (mp4/mov) plugs in as a
|
|
14
|
+
// second entry in DECODERS.
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { crc32, deflateSync } from "node:zlib";
|
|
17
|
+
import omggif from "omggif";
|
|
18
|
+
|
|
19
|
+
export const MAX_FRAMES = 8;
|
|
20
|
+
|
|
21
|
+
const INSTALLED = Symbol.for("hub.animatedFrames");
|
|
22
|
+
const GATEWAY_HOST = "ai-gateway.vercel.sh";
|
|
23
|
+
|
|
24
|
+
function pngChunk(type, data) {
|
|
25
|
+
const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
|
|
26
|
+
const out = Buffer.alloc(body.length + 8);
|
|
27
|
+
out.writeUInt32BE(data.length, 0);
|
|
28
|
+
body.copy(out, 4);
|
|
29
|
+
out.writeUInt32BE(crc32(body), body.length + 4);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function encodePng(width, height, rgba) {
|
|
34
|
+
const ihdr = Buffer.alloc(13);
|
|
35
|
+
ihdr.writeUInt32BE(width, 0);
|
|
36
|
+
ihdr.writeUInt32BE(height, 4);
|
|
37
|
+
ihdr.set([8, 6, 0, 0, 0], 8);
|
|
38
|
+
const stride = width * 4;
|
|
39
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
40
|
+
for (let y = 0; y < height; y++) Buffer.from(rgba.buffer, rgba.byteOffset + y * stride, stride).copy(raw, y * (stride + 1) + 1);
|
|
41
|
+
return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), pngChunk("IHDR", ihdr), pngChunk("IDAT", deflateSync(raw)), pngChunk("IEND", Buffer.alloc(0))]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `count` indices spread evenly over `total` frames, first and last included.
|
|
45
|
+
export function sampleIndices(total, count = MAX_FRAMES) {
|
|
46
|
+
if (total <= count) return Array.from({ length: total }, (_, i) => i);
|
|
47
|
+
return Array.from({ length: count }, (_, i) => Math.round((i * (total - 1)) / (count - 1)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Browsers play a delay of 0 or 1 hundredths as 10.
|
|
51
|
+
const delayMs = (centis) => (centis <= 1 ? 10 : centis) * 10;
|
|
52
|
+
|
|
53
|
+
// Anthropic rejects a request with more than 20 images once any is over
|
|
54
|
+
// 2000 px on a side, and the decode below runs inside fetch, synchronously:
|
|
55
|
+
// a GIF bigger than either bound goes to the provider as it is.
|
|
56
|
+
export const MAX_SIDE = 2000;
|
|
57
|
+
export const MAX_DECODED_PIXELS = 32_000_000;
|
|
58
|
+
|
|
59
|
+
// Up to `max` composited frames of an animated GIF, or null for a still or
|
|
60
|
+
// one over the bounds.
|
|
61
|
+
function sampleGif(bytes, max) {
|
|
62
|
+
const gif = new omggif.GifReader(bytes);
|
|
63
|
+
const total = gif.numFrames();
|
|
64
|
+
const { width, height } = gif;
|
|
65
|
+
if (total < 2 || width > MAX_SIDE || height > MAX_SIDE || width * height * total > MAX_DECODED_PIXELS) return null;
|
|
66
|
+
const wanted = new Set(sampleIndices(total, max));
|
|
67
|
+
const canvas = new Uint8Array(width * height * 4);
|
|
68
|
+
const frames = [];
|
|
69
|
+
let timeMs = 0;
|
|
70
|
+
for (let i = 0; i < total; i++) {
|
|
71
|
+
const info = gif.frameInfo(i);
|
|
72
|
+
const previous = info.disposal === 3 ? canvas.slice() : null;
|
|
73
|
+
gif.decodeAndBlitFrameRGBA(i, canvas);
|
|
74
|
+
if (wanted.has(i)) frames.push({ index: i, timeMs, png: encodePng(width, height, canvas) });
|
|
75
|
+
timeMs += delayMs(info.delay);
|
|
76
|
+
if (info.disposal === 2) {
|
|
77
|
+
for (let y = info.y; y < info.y + info.height; y++) canvas.fill(0, (y * width + info.x) * 4, (y * width + info.x + info.width) * 4);
|
|
78
|
+
} else if (previous) canvas.set(previous);
|
|
79
|
+
}
|
|
80
|
+
return { frameCount: total, durationMs: timeMs, frames };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// media type -> (bytes, max frames) => { frameCount, durationMs, frames: [{ index, timeMs, png }] } | null
|
|
84
|
+
export const DECODERS = { "image/gif": sampleGif };
|
|
85
|
+
|
|
86
|
+
const seconds = (ms) => `${(ms / 1000).toFixed(2)}s`;
|
|
87
|
+
|
|
88
|
+
// An image stays in a session's history, so every later model call carries it
|
|
89
|
+
// again; decoding a 1 MB GIF takes about 400 ms.
|
|
90
|
+
const recent = new Map();
|
|
91
|
+
const RECENT_MAX = 16;
|
|
92
|
+
|
|
93
|
+
function expandImage(mediaType, base64) {
|
|
94
|
+
if (!DECODERS[mediaType]) return null;
|
|
95
|
+
const key = createHash("sha256").update(mediaType).update("\0").update(base64).digest("hex");
|
|
96
|
+
if (recent.has(key)) return recent.get(key);
|
|
97
|
+
const parts = decodeImage(mediaType, base64);
|
|
98
|
+
recent.set(key, parts);
|
|
99
|
+
if (recent.size > RECENT_MAX) recent.delete(recent.keys().next().value);
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The parts that replace one animated image: a header, then a label and a
|
|
104
|
+
// PNG per sampled frame. null when the image is a still or can't be decoded;
|
|
105
|
+
// then the provider gets the original and reads it as it always has.
|
|
106
|
+
function decodeImage(mediaType, base64) {
|
|
107
|
+
let sampled;
|
|
108
|
+
try {
|
|
109
|
+
sampled = DECODERS[mediaType](Buffer.from(base64, "base64"), MAX_FRAMES);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!sampled) return null;
|
|
114
|
+
const { frameCount, durationMs, frames } = sampled;
|
|
115
|
+
const header = `Animated image (${mediaType}), ${frameCount} frames over ${seconds(durationMs)}. The ${frames.length} images below are frames sampled from this one animation, in time order; motion between the sampled frames is not shown.`;
|
|
116
|
+
return [
|
|
117
|
+
{ type: "text", text: header },
|
|
118
|
+
...frames.flatMap(({ index, timeMs, png }) => [
|
|
119
|
+
{ type: "text", text: `Frame ${index + 1} of ${frameCount}, at ${seconds(timeMs)}` },
|
|
120
|
+
{ type: "file", mediaType: "image/png", data: { type: "data", data: png.toString("base64") } },
|
|
121
|
+
]),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `prompt` (AI SDK language model v4 messages) with each animated image in
|
|
126
|
+
// a user message or a tool result expanded, and how many were.
|
|
127
|
+
export function expandAnimatedImages(prompt) {
|
|
128
|
+
let expanded = 0;
|
|
129
|
+
const expandParts = (parts) =>
|
|
130
|
+
parts.flatMap((part) => {
|
|
131
|
+
const data = part?.type === "file" && part.data?.type === "data" && typeof part.data.data === "string" ? part.data.data : null;
|
|
132
|
+
const replacement = data === null ? null : expandImage(part.mediaType, data);
|
|
133
|
+
if (!replacement) return [part];
|
|
134
|
+
expanded++;
|
|
135
|
+
return replacement;
|
|
136
|
+
});
|
|
137
|
+
const out = prompt.map((message) => {
|
|
138
|
+
if (!Array.isArray(message?.content)) return message;
|
|
139
|
+
if (message.role === "user") return { ...message, content: expandParts(message.content) };
|
|
140
|
+
if (message.role !== "tool") return message;
|
|
141
|
+
return {
|
|
142
|
+
...message,
|
|
143
|
+
content: message.content.map((part) =>
|
|
144
|
+
part?.type === "tool-result" && part.output?.type === "content" && Array.isArray(part.output.value)
|
|
145
|
+
? { ...part, output: { ...part.output, value: expandParts(part.output.value) } }
|
|
146
|
+
: part,
|
|
147
|
+
),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
return { prompt: out, expanded };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A Gateway language-model request body with its animated images expanded,
|
|
154
|
+
// or the same string when it has none.
|
|
155
|
+
export function expandRequestBody(body) {
|
|
156
|
+
if (!Object.keys(DECODERS).some((type) => body.includes(`"${type}"`))) return body;
|
|
157
|
+
const parsed = JSON.parse(body);
|
|
158
|
+
if (!Array.isArray(parsed.prompt)) return body;
|
|
159
|
+
const { prompt, expanded } = expandAnimatedImages(parsed.prompt);
|
|
160
|
+
return expanded === 0 ? body : JSON.stringify({ ...parsed, prompt });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isLanguageModelCall(input) {
|
|
164
|
+
try {
|
|
165
|
+
const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
166
|
+
return url.hostname === GATEWAY_HOST && url.pathname.endsWith("/language-model");
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function withAnimatedFrames(baseFetch) {
|
|
173
|
+
return function fetchWithAnimatedFrames(input, init) {
|
|
174
|
+
if (typeof init?.body !== "string" || !isLanguageModelCall(input)) return baseFetch(input, init);
|
|
175
|
+
const body = expandRequestBody(init.body);
|
|
176
|
+
return baseFetch(input, body === init.body ? init : { ...init, body });
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function installAnimatedFrames() {
|
|
181
|
+
if (globalThis.fetch[INSTALLED]) return;
|
|
182
|
+
const wrapped = withAnimatedFrames(globalThis.fetch.bind(globalThis));
|
|
183
|
+
wrapped[INSTALLED] = true;
|
|
184
|
+
globalThis.fetch = wrapped;
|
|
185
|
+
}
|
package/hub-upload.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type Uploaded = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare const hostedHub: () => boolean;
|
|
13
13
|
export declare const IMAGE_FILE: RegExp;
|
|
14
|
-
/** A markdown link target that names a sandbox file: an absolute path, or one under the home directory. */
|
|
14
|
+
/** A markdown link target that names a sandbox file: an absolute path (not a `//host` one), or one under the home directory. */
|
|
15
15
|
export declare const isSandboxPath: (target: string) => boolean;
|
|
16
16
|
export declare function readSandboxFile(sandbox: SandboxFiles, path: string): Promise<Uint8Array>;
|
|
17
17
|
export declare function uploadBytes(bytes: Uint8Array, name: string, what: string): Promise<Uploaded>;
|
package/hub-upload.js
CHANGED
|
@@ -8,8 +8,8 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
import { registryFetch } from "./registry-fetch.mjs";
|
|
9
9
|
export const hostedHub = () => Boolean(process.env.VERCEL || process.env.HUB_TOKEN);
|
|
10
10
|
export const IMAGE_FILE = /\.(png|jpe?g|gif|webp|heic)$/i;
|
|
11
|
-
/** A markdown link target that names a sandbox file: an absolute path, or one under the home directory. */
|
|
12
|
-
export const isSandboxPath = (target) => /^(
|
|
11
|
+
/** A markdown link target that names a sandbox file: an absolute path (not a `//host` one), or one under the home directory. */
|
|
12
|
+
export const isSandboxPath = (target) => /^(\/(?!\/)|\$HOME\/|~\/)/.test(target);
|
|
13
13
|
// The sandbox resolves `~/`; `$HOME/` is the same place spelled as the shell does.
|
|
14
14
|
const sandboxPath = (path) => path.replace(/^\$HOME\//, "~/");
|
|
15
15
|
export async function readSandboxFile(sandbox, path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
|
|
6
6
|
"type": "module",
|
|
@@ -32,15 +32,17 @@
|
|
|
32
32
|
"./vercel": "./vercel.mjs"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"eve": "*",
|
|
36
|
-
"zod": "4.5.4",
|
|
37
35
|
"@vercel/connect": "^2.4.1",
|
|
38
|
-
"@vercel/oidc": "4.0.0"
|
|
36
|
+
"@vercel/oidc": "4.0.0",
|
|
37
|
+
"eve": "*",
|
|
38
|
+
"zod": "4.5.4"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"ai": "^7.0.137",
|
|
42
42
|
"jose": "^6.2.3",
|
|
43
43
|
"jsonc-parser": "3.3.1",
|
|
44
|
+
"mdast-util-from-markdown": "^2.1.0",
|
|
45
|
+
"omggif": "1.0.10",
|
|
44
46
|
"smol-toml": "^1.9.1",
|
|
45
47
|
"tree-sitter-bash": "0.25.1",
|
|
46
48
|
"web-tree-sitter": "0.27.1"
|
package/roster-model.mjs
CHANGED
|
@@ -18,8 +18,10 @@ import { CONTEXT_WINDOW, selectModel } from "./bot-model.mjs";
|
|
|
18
18
|
import { DEFAULT_SUBAGENT_CAP } from "./subagent-cap.mjs";
|
|
19
19
|
import { sessionLimits } from "./session-limits.mjs";
|
|
20
20
|
import { installModelCallTimeout } from "./model-timeout.mjs";
|
|
21
|
+
import { installAnimatedFrames } from "./animated-images.mjs";
|
|
21
22
|
import { findWorkspaceRoot, loadWorkspace, parseWorkspace } from "./workspace.mjs";
|
|
22
23
|
const real = process.env.ROSTER_MODELS !== "mock";
|
|
24
|
+
installAnimatedFrames();
|
|
23
25
|
installModelCallTimeout();
|
|
24
26
|
|
|
25
27
|
export function createRosterModel({ agents, subagentCap }) {
|
package/tools/post_message.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare const MAX_CHARS = 700;
|
|
|
3
3
|
export declare const postLength: (text: string) => number;
|
|
4
4
|
/** The error for a text the thread shouldn't carry, or null. */
|
|
5
5
|
export declare function textError(text: string): string | null;
|
|
6
|
-
/** The text with each sandbox-path link target replaced by where the human's app opens it. */
|
|
6
|
+
/** The text with each sandbox-path link target replaced by where the human's app opens it; a `#…` fragment stays on it. Each file is opened once. */
|
|
7
7
|
export declare function rewriteDocLinks(text: string, open: (path: string) => Promise<string>): Promise<string>;
|
|
8
8
|
declare const _default: import("eve/tools").ToolDefinition<{
|
|
9
9
|
threadId?: string | undefined;
|
package/tools/post_message.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "eve/tools";
|
|
2
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { documentUrl, hostedHub, isSandboxPath, uploadBytes, uploadFile } from "../hub-upload.js";
|
|
4
5
|
import { registryFetch } from "../registry-fetch.mjs";
|
|
@@ -23,30 +24,64 @@ import { registryFetch } from "../registry-fetch.mjs";
|
|
|
23
24
|
// move), the text is its summary, `metadata` its PR and sha. The hub keeps the
|
|
24
25
|
// latest one per bot and thread for the workspace state and the Dash.
|
|
25
26
|
export const MAX_CHARS = 700;
|
|
26
|
-
const LINK = /\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
27
|
-
const IMAGE_SYNTAX = /!\[[^\]]*\]\([^)]*\)/;
|
|
28
27
|
const SCREENSHOT = "screenshot";
|
|
28
|
+
// The markdown parser finds the links and where each destination starts, so a path in code is never read as a
|
|
29
|
+
// link and brackets in a label (nested or escaped) never cut it.
|
|
30
|
+
function destinations(text) {
|
|
31
|
+
const spans = new Map();
|
|
32
|
+
function enter(token) {
|
|
33
|
+
spans.set(this.stack.at(-1), token);
|
|
34
|
+
}
|
|
35
|
+
const tree = fromMarkdown(text, { mdastExtensions: [{ enter: { resource: enter, definitionDestination: enter } }] });
|
|
36
|
+
const found = [];
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
const span = spans.get(node);
|
|
39
|
+
if (span && node.url !== undefined && (node.type === "link" || node.type === "image" || node.type === "definition")) {
|
|
40
|
+
const to = node.type === "definition" ? node.position.end.offset : span.end.offset;
|
|
41
|
+
found.push({ kind: node.type, from: span.start.offset, to, url: node.url, title: node.title });
|
|
42
|
+
}
|
|
43
|
+
node.children?.forEach(visit);
|
|
44
|
+
};
|
|
45
|
+
visit(tree);
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
29
48
|
/** The characters the cap counts: the text without its links' targets. */
|
|
30
|
-
export const postLength = (text) => text.
|
|
49
|
+
export const postLength = (text) => destinations(text).reduce((n, d) => n - (d.to - d.from) + (d.kind === "definition" ? 0 : "()".length), text.length);
|
|
31
50
|
/** The error for a text the thread shouldn't carry, or null. */
|
|
32
51
|
export function textError(text) {
|
|
33
|
-
if (
|
|
52
|
+
if (destinations(text).some((d) => d.kind === "image"))
|
|
34
53
|
return "Images don't go in the text: pass the file's sandbox path in `images` and it shows inline";
|
|
35
54
|
if (postLength(text) > MAX_CHARS)
|
|
36
55
|
return "Too long: move the detail into a doc and link it";
|
|
37
56
|
return null;
|
|
38
57
|
}
|
|
39
|
-
|
|
58
|
+
const PATH_AND_FRAGMENT = /^([^?#]*)(?:\?[^#]*)?(#.*)?$/;
|
|
59
|
+
/** The text with each sandbox-path link target replaced by where the human's app opens it; a `#…` fragment stays on it. Each file is opened once. */
|
|
40
60
|
export async function rewriteDocLinks(text, open) {
|
|
41
|
-
const links =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
links.forEach((m, i) => {
|
|
45
|
-
const start = m.index + m[0].length - m[2].length - 1;
|
|
46
|
-
out += text.slice(at, start) + urls[i];
|
|
47
|
-
at = start + m[2].length;
|
|
61
|
+
const links = destinations(text).flatMap((d) => {
|
|
62
|
+
const [, path, fragment = ""] = PATH_AND_FRAGMENT.exec(d.url);
|
|
63
|
+
return d.kind !== "image" && isSandboxPath(path) ? [{ ...d, file: decode(path), fragment }] : [];
|
|
48
64
|
});
|
|
49
|
-
|
|
65
|
+
if (!links.length)
|
|
66
|
+
return text;
|
|
67
|
+
const files = [...new Set(links.map((l) => l.file))];
|
|
68
|
+
const opened = new Map(await Promise.all(files.map(async (file) => [file, await open(file)])));
|
|
69
|
+
let out = text;
|
|
70
|
+
for (const l of links.sort((a, b) => b.from - a.from)) {
|
|
71
|
+
const url = `${opened.get(l.file)}${l.fragment}`;
|
|
72
|
+
const target = /[\s()<>]/.test(url) ? `<${url}>` : url;
|
|
73
|
+
const title = l.title == null ? "" : ` "${l.title.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
74
|
+
out = `${out.slice(0, l.from)}${l.kind === "definition" ? `${target}${title}` : `(${target}${title})`}${out.slice(l.to)}`;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function decode(path) {
|
|
79
|
+
try {
|
|
80
|
+
return decodeURIComponent(path);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return path;
|
|
84
|
+
}
|
|
50
85
|
}
|
|
51
86
|
async function screenshot(sessionId) {
|
|
52
87
|
if (hostedHub())
|