@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.1
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 +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +7 -3
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +248 -17
- package/server/provider/host-tools.ts +294 -26
- package/server/provider/omp-rpc.ts +499 -72
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +340 -1
- package/server/provider/session.ts +716 -249
- package/server/provider/subsessions.ts +25 -2
- package/server/provider/timeline-projector.ts +104 -44
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ProviderMcpServerConfig,
|
|
4
4
|
ProviderSessionConfig,
|
|
5
5
|
} from "@getpaseo/plugin/server/provider";
|
|
6
|
+
import { isValidImagePayload } from "./image";
|
|
6
7
|
import {
|
|
7
8
|
type ConnectedMcpClient,
|
|
8
9
|
type ConnectedMcpTool,
|
|
@@ -37,11 +38,19 @@ const MAX_HOST_TOOL_DESCRIPTION_BYTES = 64 * 1024;
|
|
|
37
38
|
const MAX_HOST_TOOL_SCHEMA_BYTES = 256 * 1024;
|
|
38
39
|
const MAX_HOST_TOOL_CATALOG_BYTES = 768 * 1024;
|
|
39
40
|
const MAX_HOST_TOOL_RESULT_BYTES = 12 * 1024 * 1024;
|
|
41
|
+
const MAX_HOST_TOOL_CONTENT_BLOCKS = 512;
|
|
42
|
+
const MAX_HOST_TOOL_TEXT_BYTES = 1024 * 1024;
|
|
43
|
+
const MAX_HOST_TOOL_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
|
|
40
44
|
const MAX_STRUCTURED_CONTENT_FALLBACK_BYTES = 1024 * 1024;
|
|
45
|
+
const CONTENT_TRUNCATED_NOTICE = "[MCP result content truncated: exceeded size limits]";
|
|
46
|
+
const DETAILS_OMITTED_NOTICE = "[MCP structured content omitted: exceeded size limits]";
|
|
47
|
+
const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
|
|
41
48
|
const MAX_PENDING_HOST_TOOL_CALLS = 64;
|
|
42
49
|
const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
|
|
43
50
|
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
|
|
44
51
|
const DEFAULT_MCP_CALL_LIFETIME_MS = 5 * 60 * 1000;
|
|
52
|
+
const MAX_DIRECT_BROWSER_CALLS = 4;
|
|
53
|
+
const BROWSER_CALL_TIMEOUT_MS = 20_000;
|
|
45
54
|
|
|
46
55
|
export type OmpMcpTool = ConnectedMcpTool;
|
|
47
56
|
export type OmpMcpToolPage = ConnectedMcpToolPage;
|
|
@@ -270,42 +279,171 @@ async function discoverMcpTools(
|
|
|
270
279
|
throw new OmpPublicError("MCP server tool-list pagination exceeds the supported limit");
|
|
271
280
|
}
|
|
272
281
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
type HostToolContentBlock = OmpHostToolResult["result"]["content"][number];
|
|
283
|
+
|
|
284
|
+
function hostToolContentIsBounded(content: readonly HostToolContentBlock[]): boolean {
|
|
285
|
+
return (
|
|
286
|
+
boundedJsonBytes(
|
|
287
|
+
content,
|
|
288
|
+
MAX_HOST_TOOL_RESULT_BYTES,
|
|
289
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS,
|
|
290
|
+
MAX_HOST_TOOL_IMAGE_DATA_BYTES,
|
|
291
|
+
4_096,
|
|
292
|
+
) !== Number.POSITIVE_INFINITY
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function noticeBlock(text: string): HostToolContentBlock {
|
|
297
|
+
return { type: "text", text };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function appendBoundedNotice(
|
|
301
|
+
content: readonly HostToolContentBlock[],
|
|
302
|
+
notice: string,
|
|
303
|
+
): HostToolContentBlock[] {
|
|
304
|
+
const trailingNotice = content.at(-1)?.text;
|
|
305
|
+
const contentWasTruncated =
|
|
306
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
307
|
+
const prefix = (contentWasTruncated ? content.slice(0, -1) : content).slice(
|
|
308
|
+
0,
|
|
309
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS - 1,
|
|
310
|
+
);
|
|
311
|
+
const markerText =
|
|
312
|
+
notice === DETAILS_OMITTED_NOTICE && contentWasTruncated ? RESULT_TRUNCATED_NOTICE : notice;
|
|
313
|
+
const marker = noticeBlock(markerText);
|
|
314
|
+
while (prefix.length > 0 && !hostToolContentIsBounded([...prefix, marker])) prefix.pop();
|
|
315
|
+
return [...prefix, marker];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function boundedTextPrefix(
|
|
319
|
+
prefix: readonly HostToolContentBlock[],
|
|
320
|
+
block: HostToolContentBlock,
|
|
321
|
+
): HostToolContentBlock | undefined {
|
|
322
|
+
if (block.type !== "text" || typeof block.text !== "string") return;
|
|
323
|
+
const marker = noticeBlock(CONTENT_TRUNCATED_NOTICE);
|
|
324
|
+
let low = 0;
|
|
325
|
+
let high = block.text.length;
|
|
326
|
+
let best = "";
|
|
327
|
+
while (low <= high) {
|
|
328
|
+
const middle = Math.floor((low + high) / 2);
|
|
329
|
+
let text = block.text.slice(0, middle);
|
|
330
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
331
|
+
const candidate = { type: "text", text } satisfies HostToolContentBlock;
|
|
332
|
+
if (hostToolContentIsBounded([...prefix, candidate, marker])) {
|
|
333
|
+
best = text;
|
|
334
|
+
low = middle + 1;
|
|
335
|
+
} else {
|
|
336
|
+
high = middle - 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return best ? { type: "text", text: best } : undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeContent(content: readonly unknown[]): HostToolContentBlock[] {
|
|
343
|
+
const normalized: HostToolContentBlock[] = [];
|
|
344
|
+
for (const value of content) {
|
|
345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
346
|
+
throw new Error("MCP tool returned invalid content");
|
|
347
|
+
}
|
|
348
|
+
const block = value as Record<string, unknown>;
|
|
349
|
+
if (typeof block.type !== "string" || !block.type || utf8Bytes(block.type) > 256) {
|
|
350
|
+
throw new Error("MCP tool returned invalid content");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let candidate: HostToolContentBlock;
|
|
354
|
+
let blockWasTruncated = false;
|
|
355
|
+
if (block.type === "text") {
|
|
356
|
+
if (typeof block.text !== "string") throw new Error("MCP tool returned invalid text content");
|
|
357
|
+
const text = truncateUtf8(block.text, MAX_HOST_TOOL_TEXT_BYTES);
|
|
358
|
+
candidate = { ...block, type: "text", text } as HostToolContentBlock;
|
|
359
|
+
blockWasTruncated = text !== block.text;
|
|
360
|
+
} else if (block.type === "image") {
|
|
361
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") {
|
|
362
|
+
throw new Error("MCP tool returned invalid image content");
|
|
363
|
+
}
|
|
364
|
+
if (block.data.length > MAX_HOST_TOOL_IMAGE_DATA_BYTES) {
|
|
365
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
366
|
+
}
|
|
367
|
+
if (!isValidImagePayload(block.data, block.mimeType, MAX_HOST_TOOL_IMAGE_DATA_BYTES)) {
|
|
368
|
+
throw new Error("MCP tool returned invalid image content");
|
|
369
|
+
}
|
|
370
|
+
candidate = {
|
|
371
|
+
...block,
|
|
372
|
+
type: "image",
|
|
373
|
+
data: block.data,
|
|
374
|
+
mimeType: block.mimeType,
|
|
375
|
+
} as HostToolContentBlock;
|
|
376
|
+
} else {
|
|
377
|
+
candidate = block as HostToolContentBlock;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (normalized.length >= MAX_HOST_TOOL_CONTENT_BLOCKS) {
|
|
381
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
382
|
+
}
|
|
383
|
+
if (!hostToolContentIsBounded([...normalized, candidate])) {
|
|
384
|
+
const prefix = boundedTextPrefix(normalized, candidate);
|
|
385
|
+
return appendBoundedNotice(
|
|
386
|
+
prefix ? [...normalized, prefix] : normalized,
|
|
387
|
+
CONTENT_TRUNCATED_NOTICE,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
normalized.push(candidate);
|
|
391
|
+
if (blockWasTruncated) return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
392
|
+
}
|
|
393
|
+
return normalized;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function structuredContentIsBounded(value: unknown): boolean {
|
|
397
|
+
return (
|
|
277
398
|
boundedJsonBytes(
|
|
278
|
-
|
|
399
|
+
value,
|
|
279
400
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
280
401
|
1_024,
|
|
281
402
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
282
403
|
4_096,
|
|
283
|
-
)
|
|
284
|
-
)
|
|
285
|
-
|
|
404
|
+
) !== Number.POSITIVE_INFINITY
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeResult(result: unknown): OmpHostToolResult["result"] {
|
|
409
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
410
|
+
throw new Error("MCP tool returned an invalid result");
|
|
286
411
|
}
|
|
287
412
|
const record = result as Record<string, unknown>;
|
|
288
413
|
if (Array.isArray(record.content)) {
|
|
289
414
|
const details = record.structuredContent;
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
415
|
+
const detailsAreBounded = details === undefined || structuredContentIsBounded(details);
|
|
416
|
+
let content = normalizeContent(record.content);
|
|
417
|
+
if (content.length === 0 && details !== undefined && detailsAreBounded) {
|
|
418
|
+
content = [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: truncateUtf8(JSON.stringify(details), MAX_STRUCTURED_CONTENT_FALLBACK_BYTES),
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
if (!detailsAreBounded) content = appendBoundedNotice(content, DETAILS_OMITTED_NOTICE);
|
|
426
|
+
|
|
427
|
+
let normalized = parseOmpHostToolAgentResult({
|
|
300
428
|
content,
|
|
301
|
-
...(details !== undefined ? { details } : {}),
|
|
429
|
+
...(details !== undefined && detailsAreBounded ? { details } : {}),
|
|
302
430
|
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
303
431
|
});
|
|
432
|
+
if (!structuredContentIsBounded(normalized)) {
|
|
433
|
+
normalized = parseOmpHostToolAgentResult({
|
|
434
|
+
content: appendBoundedNotice(content, DETAILS_OMITTED_NOTICE),
|
|
435
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return normalized;
|
|
304
439
|
}
|
|
305
440
|
if (Object.hasOwn(record, "toolResult")) {
|
|
441
|
+
const details = record.toolResult;
|
|
442
|
+
const detailsAreBounded = structuredContentIsBounded(details);
|
|
306
443
|
return parseOmpHostToolAgentResult({
|
|
307
|
-
content: [
|
|
308
|
-
details:
|
|
444
|
+
content: [noticeBlock(detailsAreBounded ? "MCP tool completed" : DETAILS_OMITTED_NOTICE)],
|
|
445
|
+
...(detailsAreBounded ? { details } : {}),
|
|
446
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
309
447
|
});
|
|
310
448
|
}
|
|
311
449
|
throw new Error("MCP tool returned an unsupported result");
|
|
@@ -331,6 +469,17 @@ async function settleCleanup(promises: readonly Promise<void>[]): Promise<void>
|
|
|
331
469
|
.map((result) => result.reason);
|
|
332
470
|
if (failures.length > 0) throw new AggregateError(failures, "OMP MCP cleanup failed");
|
|
333
471
|
}
|
|
472
|
+
function paseoBrowserFailure(error: unknown): OmpPublicError {
|
|
473
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
474
|
+
if (/browser_no_host/iu.test(detail)) {
|
|
475
|
+
return new OmpPublicError("No Paseo desktop browser host is connected");
|
|
476
|
+
}
|
|
477
|
+
if (/browser_disabled/iu.test(detail)) {
|
|
478
|
+
return new OmpPublicError("Paseo browser tools are disabled on this host");
|
|
479
|
+
}
|
|
480
|
+
return new OmpPublicError("Paseo could not open the MCP authorization browser tab");
|
|
481
|
+
}
|
|
482
|
+
|
|
334
483
|
export class OmpHostToolsBridge {
|
|
335
484
|
private runtime: OmpRuntimeSession | null = null;
|
|
336
485
|
private readonly pending = new Map<string, PendingCall>();
|
|
@@ -338,6 +487,7 @@ export class OmpHostToolsBridge {
|
|
|
338
487
|
private generation = 0;
|
|
339
488
|
private closePromise: Promise<void> | null = null;
|
|
340
489
|
private fatalHandler: ((error: Error) => void) | null = null;
|
|
490
|
+
private activeDirectBrowserCalls = 0;
|
|
341
491
|
|
|
342
492
|
readonly labels: ReadonlyMap<string, string>;
|
|
343
493
|
private constructor(
|
|
@@ -515,6 +665,63 @@ export class OmpHostToolsBridge {
|
|
|
515
665
|
onFatal(handler: (error: Error) => void): void {
|
|
516
666
|
this.fatalHandler = handler;
|
|
517
667
|
}
|
|
668
|
+
async openPaseoBrowser(url: string): Promise<void> {
|
|
669
|
+
let parsed: URL;
|
|
670
|
+
try {
|
|
671
|
+
parsed = new URL(url);
|
|
672
|
+
} catch {
|
|
673
|
+
throw new OmpPublicError("MCP authorization URL is invalid");
|
|
674
|
+
}
|
|
675
|
+
if (
|
|
676
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
677
|
+
parsed.username ||
|
|
678
|
+
parsed.password
|
|
679
|
+
) {
|
|
680
|
+
throw new OmpPublicError("MCP authorization URL is not safe to open");
|
|
681
|
+
}
|
|
682
|
+
if (!this.runtime || this.closePromise) {
|
|
683
|
+
throw new OmpPublicError("The OMP session is not ready for browser authorization");
|
|
684
|
+
}
|
|
685
|
+
const target = this.targets.get("browser_new_tab");
|
|
686
|
+
if (!target) {
|
|
687
|
+
throw new OmpPublicError("Paseo browser tools are unavailable in this OMP session");
|
|
688
|
+
}
|
|
689
|
+
if (this.activeDirectBrowserCalls >= MAX_DIRECT_BROWSER_CALLS) {
|
|
690
|
+
throw new OmpPublicError("Too many Paseo browser requests are already running");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
this.activeDirectBrowserCalls += 1;
|
|
694
|
+
const controller = new AbortController();
|
|
695
|
+
const deadline = this.callScheduler.set(
|
|
696
|
+
() => controller.abort(new Error("Paseo browser request timed out")),
|
|
697
|
+
BROWSER_CALL_TIMEOUT_MS,
|
|
698
|
+
);
|
|
699
|
+
try {
|
|
700
|
+
const result = normalizeResult(
|
|
701
|
+
await target.connection.callTool(
|
|
702
|
+
target.toolName,
|
|
703
|
+
{ i: "Opening MCP authorization", url },
|
|
704
|
+
{
|
|
705
|
+
signal: controller.signal,
|
|
706
|
+
maxTotalTimeoutMs: BROWSER_CALL_TIMEOUT_MS,
|
|
707
|
+
onProgress() {},
|
|
708
|
+
},
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
if (result.isError) {
|
|
712
|
+
const detail = result.content
|
|
713
|
+
.flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
|
|
714
|
+
.join("\n");
|
|
715
|
+
throw paseoBrowserFailure(new Error(detail));
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (error instanceof OmpPublicError) throw error;
|
|
719
|
+
throw paseoBrowserFailure(error);
|
|
720
|
+
} finally {
|
|
721
|
+
this.callScheduler.clear(deadline);
|
|
722
|
+
this.activeDirectBrowserCalls -= 1;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
518
725
|
|
|
519
726
|
handle(
|
|
520
727
|
event: OmpHostToolCall | { type: "host_tool_cancel"; id: string; targetId: string },
|
|
@@ -633,12 +840,73 @@ export class OmpHostToolsBridge {
|
|
|
633
840
|
result: OmpHostToolResult,
|
|
634
841
|
): OmpHostToolResult {
|
|
635
842
|
const limit = runtime.maxHostToolFrameBytes ?? 1024 * 1024;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
843
|
+
const fits = (candidate: OmpHostToolResult) => {
|
|
844
|
+
try {
|
|
845
|
+
return Buffer.byteLength(`${JSON.stringify(candidate)}\n`) <= limit;
|
|
846
|
+
} catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
if (fits(result)) return result;
|
|
851
|
+
|
|
852
|
+
const withoutDetails: OmpHostToolResult = {
|
|
853
|
+
...result,
|
|
854
|
+
result: {
|
|
855
|
+
content: appendBoundedNotice(result.result.content, DETAILS_OMITTED_NOTICE),
|
|
856
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
if (result.result.details !== undefined && fits(withoutDetails)) return withoutDetails;
|
|
860
|
+
|
|
861
|
+
const trailingNotice = result.result.content.at(-1)?.text;
|
|
862
|
+
const detailsWereOmitted =
|
|
863
|
+
trailingNotice === DETAILS_OMITTED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
864
|
+
const contentWasTruncated =
|
|
865
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
866
|
+
const marker = noticeBlock(
|
|
867
|
+
result.result.details !== undefined || detailsWereOmitted
|
|
868
|
+
? RESULT_TRUNCATED_NOTICE
|
|
869
|
+
: CONTENT_TRUNCATED_NOTICE,
|
|
870
|
+
);
|
|
871
|
+
const sourceContent =
|
|
872
|
+
detailsWereOmitted || contentWasTruncated
|
|
873
|
+
? result.result.content.slice(0, -1)
|
|
874
|
+
: result.result.content;
|
|
875
|
+
const boundedContent: HostToolContentBlock[] = [];
|
|
876
|
+
const terminalWith = (content: HostToolContentBlock[]): OmpHostToolResult => ({
|
|
877
|
+
...result,
|
|
878
|
+
result: {
|
|
879
|
+
content,
|
|
880
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
for (const block of sourceContent) {
|
|
884
|
+
const candidate = terminalWith([...boundedContent, block, marker]);
|
|
885
|
+
if (fits(candidate)) {
|
|
886
|
+
boundedContent.push(block);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
890
|
+
let low = 0;
|
|
891
|
+
let high = block.text.length;
|
|
892
|
+
let best = "";
|
|
893
|
+
while (low <= high) {
|
|
894
|
+
const middle = Math.floor((low + high) / 2);
|
|
895
|
+
let text = block.text.slice(0, middle);
|
|
896
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
897
|
+
if (fits(terminalWith([...boundedContent, { type: "text", text }, marker]))) {
|
|
898
|
+
best = text;
|
|
899
|
+
low = middle + 1;
|
|
900
|
+
} else {
|
|
901
|
+
high = middle - 1;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (best) boundedContent.push({ type: "text", text: best });
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
640
907
|
}
|
|
641
|
-
|
|
908
|
+
const bounded = terminalWith([...boundedContent, marker]);
|
|
909
|
+
return fits(bounded) ? bounded : errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
|
|
642
910
|
}
|
|
643
911
|
|
|
644
912
|
private sendTerminal(
|