@adecore/agent-contracts 0.39.0 → 0.40.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/dist/chat.js CHANGED
@@ -107,9 +107,9 @@ export const ChatUiAnswerSchema = ChatUiChoiceOriginSchema.extend({
107
107
  queued: z.boolean(),
108
108
  turnId: z.string().min(1)
109
109
  });
110
- // A message typed while a turn was running; the daemon sends it when that turn settles.
111
110
  /* The reply a quoted passage was taken from, so a client can lead the reader back to it. */
112
111
  export const ChatQuoteSourceSchema = z.object({ itemId: z.string().min(1).max(256) });
112
+ // A message typed while a turn was running; the daemon sends it when that turn settles.
113
113
  export const ChatQueuedMessageSchema = z.object({
114
114
  id: z.string().min(1),
115
115
  // Older saved queues have no reserved turn yet; draining them still mints one.
@@ -430,13 +430,13 @@ export const ChatSubagentUsageSchema = z.object({
430
430
  toolUses: z.number().int().nonnegative(),
431
431
  durationMs: z.number().int().nonnegative()
432
432
  });
433
+ // The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
434
+ export const HOST_TASK_ORIGIN = 'ruimte';
433
435
  /*
434
436
  * One agent the agent delegated to, foreground or background. Its own work stays in the thread as
435
437
  * ordinary items that carry `parentToolUseId`, so streaming keeps working; the timeline gathers
436
438
  * them under this row. `result` is the report it ended with, as markdown.
437
439
  */
438
- // The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
439
- export const HOST_TASK_ORIGIN = 'ruimte';
440
440
  export const ChatSubagentItemSchema = z.object({
441
441
  ...base,
442
442
  kind: z.literal('subagent'),
package/dist/text.js CHANGED
@@ -1,4 +1,4 @@
1
- const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
1
+ let graphemes;
2
2
  /*
3
3
  * The longest start of `text` that fits in `max` UTF-16 units, the measure a schema's `max` counts in,
4
4
  * cut only between characters as a person sees them. A cut between the halves of a surrogate pair
@@ -8,6 +8,7 @@ export function clipText(text, max) {
8
8
  if (text.length <= max) {
9
9
  return text;
10
10
  }
11
+ graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
11
12
  let end = 0;
12
13
  for (const { segment } of graphemes.segment(text)) {
13
14
  if (end + segment.length > max) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agent-contracts",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
4
4
  "description": "Schemas and port contracts for agent chats, provider accounts, models and usage.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -86,11 +86,11 @@
86
86
  "test": "bun --conditions=source test"
87
87
  },
88
88
  "dependencies": {
89
- "@adecore/intelligent-ui": "0.39.0",
90
- "zod": "^4.6.5"
89
+ "@adecore/intelligent-ui": "0.40.1",
90
+ "zod": "^4.6.7"
91
91
  },
92
92
  "devDependencies": {
93
- "@types/bun": "^1.4.2",
93
+ "@types/bun": "^1.4.4",
94
94
  "typescript": "^7.0.2"
95
95
  },
96
96
  "license": "FSL-1.1-MIT",
package/src/agent.ts CHANGED
@@ -17,6 +17,7 @@ export const AgentKindSchema = z
17
17
  .min(1)
18
18
  .max(AGENT_KIND_MAX_LENGTH)
19
19
  .regex(/^[a-z][a-z0-9-]*$/);
20
+
20
21
  export type AgentKind = KnownAgentKind | (string & {});
21
22
 
22
23
  export function isKnownAgentKind(kind: string): kind is KnownAgentKind {
package/src/chat.ts CHANGED
@@ -60,18 +60,23 @@ const IMAGE_MIME_BY_EXTENSION = new Map([
60
60
  ['webp', 'image/webp'],
61
61
  ['gif', 'image/gif']
62
62
  ]);
63
+
63
64
  const IMAGE_MIME_TYPES = new Set(IMAGE_MIME_BY_EXTENSION.values());
64
65
 
65
66
  // A generic MIME type from Finder may still name a picture; a declared PDF must stay a PDF.
66
67
  export function attachmentImageMime({ name, mime }: { name: string; mime: string }): string | null {
67
68
  const type = mime.split(';')[0]!.trim().toLowerCase();
69
+
68
70
  if (IMAGE_MIME_TYPES.has(type)) {
69
71
  return type;
70
72
  }
73
+
71
74
  if (type !== '' && type !== 'application/octet-stream' && type !== 'binary/octet-stream') {
72
75
  return null;
73
76
  }
77
+
74
78
  const dot = name.lastIndexOf('.');
79
+
75
80
  return dot < 0 ? null : (IMAGE_MIME_BY_EXTENSION.get(name.slice(dot + 1).toLowerCase()) ?? null);
76
81
  }
77
82
 
@@ -129,11 +134,11 @@ export const ChatUiAnswerSchema = ChatUiChoiceOriginSchema.extend({
129
134
  });
130
135
  export type ChatUiAnswer = z.infer<typeof ChatUiAnswerSchema>;
131
136
 
132
- // A message typed while a turn was running; the daemon sends it when that turn settles.
133
137
  /* The reply a quoted passage was taken from, so a client can lead the reader back to it. */
134
138
  export const ChatQuoteSourceSchema = z.object({ itemId: z.string().min(1).max(256) });
135
139
  export type ChatQuoteSource = z.infer<typeof ChatQuoteSourceSchema>;
136
140
 
141
+ // A message typed while a turn was running; the daemon sends it when that turn settles.
137
142
  export const ChatQueuedMessageSchema = z.object({
138
143
  id: z.string().min(1),
139
144
  // Older saved queues have no reserved turn yet; draining them still mints one.
@@ -518,14 +523,14 @@ export const ChatSubagentUsageSchema = z.object({
518
523
  });
519
524
  export type ChatSubagentUsage = z.infer<typeof ChatSubagentUsageSchema>;
520
525
 
526
+ // The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
527
+ export const HOST_TASK_ORIGIN = 'ruimte';
528
+
521
529
  /*
522
530
  * One agent the agent delegated to, foreground or background. Its own work stays in the thread as
523
531
  * ordinary items that carry `parentToolUseId`, so streaming keeps working; the timeline gathers
524
532
  * them under this row. `result` is the report it ended with, as markdown.
525
533
  */
526
- // The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
527
- export const HOST_TASK_ORIGIN = 'ruimte';
528
-
529
534
  export const ChatSubagentItemSchema = z.object({
530
535
  ...base,
531
536
  kind: z.literal('subagent'),
@@ -733,6 +738,7 @@ export function activeSubagents(items: readonly ChatItem[]): ChatSubagentItem[]
733
738
  const lastMessageAt = lastMessage < 0 ? null : items[lastMessage]!.createdAt;
734
739
  const rows = items.flatMap((item, index) => (item.kind === 'subagent' ? [{ item, index }] : []));
735
740
  const turns = new Set(rows.flatMap(({ item }) => (item.status === 'running' && item.turnId !== null ? [item.turnId] : [])));
741
+
736
742
  return rows
737
743
  .filter(
738
744
  ({ item, index }) =>
@@ -824,14 +830,17 @@ export const ChatAttachPayloadSchema = ChatTargetPayloadSchema.extend({
824
830
  // The last seq this client saw; honored when the daemon still holds everything after it.
825
831
  since: z.number().int().nonnegative().optional()
826
832
  });
833
+
827
834
  export const ChatHistoryPayloadSchema = ChatTargetPayloadSchema.extend({
828
835
  cursor: z.string().min(1).max(128),
829
836
  limit: z.number().int().min(1).max(100).optional()
830
837
  });
838
+
831
839
  export const ChatHistoryPageSchema = z.object({
832
840
  start: z.number().int().nonnegative(),
833
841
  cursor: z.string().nullable()
834
842
  });
843
+
835
844
  export const ChatHistoryResultSchema = z.object({ items: z.array(ChatItemSchema), history: ChatHistoryPageSchema });
836
845
  export type ChatHistoryResult = z.infer<typeof ChatHistoryResultSchema>;
837
846
 
@@ -1159,6 +1168,8 @@ export function generatedImageAttachment(item: ChatItem): ChatAttachment | null
1159
1168
  if (item.kind !== 'tool' || item.name !== 'ImageGeneration') {
1160
1169
  return null;
1161
1170
  }
1171
+
1162
1172
  const parsed = GeneratedImageInputSchema.safeParse(item.input);
1173
+
1163
1174
  return parsed.success ? (parsed.data.attachment ?? null) : null;
1164
1175
  }
package/src/envelope.ts CHANGED
@@ -47,6 +47,7 @@ function toResult<T>(parsed: z.ZodSafeParseResult<T>): ParseResult<T> {
47
47
  if (parsed.success) {
48
48
  return { ok: true, value: parsed.data };
49
49
  }
50
+
50
51
  return { ok: false, message: z.prettifyError(parsed.error) };
51
52
  }
52
53
 
package/src/model.ts CHANGED
@@ -155,6 +155,7 @@ export const ProviderInstallSchema = z.object({
155
155
  signIn: z.array(z.unknown()).transform((ways) =>
156
156
  ways.flatMap((way) => {
157
157
  const parsed = ProviderSignInSchema.safeParse(way);
158
+
158
159
  return parsed.success ? [parsed.data] : [];
159
160
  })
160
161
  )
@@ -204,7 +205,9 @@ export function pickerGroupsOf<T extends { kind: AgentKind; name: string }>(prov
204
205
  nameCollator ??= new Intl.Collator(undefined, { sensitivity: 'base' });
205
206
  const collator = nameCollator;
206
207
  const sorted = [...providers].sort((a, b) => collator.compare(a.name, b.name));
208
+
207
209
  const inGroup = (group: ProviderPickerGroup): T[] => sorted.filter((provider) => providerGroupOf(provider.kind) === group);
210
+
208
211
  return { frontier: inGroup('frontier'), others: inGroup('others'), api: inGroup('api') };
209
212
  }
210
213
 
@@ -220,6 +223,7 @@ export type ProviderListResult = z.infer<typeof ProviderListResultSchema>;
220
223
  */
221
224
  export function resumeCommandFor(template: string, agentSessionId: string, flags: string[] = []): string {
222
225
  const id = `'${agentSessionId.replaceAll("'", `'\\''`)}'`;
226
+
223
227
  return template
224
228
  .split(' ')
225
229
  .flatMap((word) => (word === '{flags}' ? flags : [word.replace('{id}', id)]))
package/src/text.ts CHANGED
@@ -1,4 +1,4 @@
1
- const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
1
+ let graphemes: Intl.Segmenter | undefined;
2
2
 
3
3
  /*
4
4
  * The longest start of `text` that fits in `max` UTF-16 units, the measure a schema's `max` counts in,
@@ -9,13 +9,18 @@ export function clipText(text: string, max: number): string {
9
9
  if (text.length <= max) {
10
10
  return text;
11
11
  }
12
+
13
+ graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
12
14
  let end = 0;
15
+
13
16
  for (const { segment } of graphemes.segment(text)) {
14
17
  if (end + segment.length > max) {
15
18
  break;
16
19
  }
20
+
17
21
  end += segment.length;
18
22
  }
23
+
19
24
  return text.slice(0, end);
20
25
  }
21
26
 
@@ -26,12 +31,16 @@ export const TOOL_OUTPUT_TAIL = 64 * 1024;
26
31
  export function appendToolOutput(output: string | null, text: string): string {
27
32
  const whole = (output ?? '') + text;
28
33
  let start = whole.length - TOOL_OUTPUT_TAIL;
34
+
29
35
  if (start <= 0) {
30
36
  return whole;
31
37
  }
38
+
32
39
  const unit = whole.charCodeAt(start);
40
+
33
41
  if (unit >= 0xdc00 && unit <= 0xdfff) {
34
42
  start += 1;
35
43
  }
44
+
36
45
  return whole.slice(start);
37
46
  }
package/src/usage.ts CHANGED
@@ -4,6 +4,7 @@ import { ProviderAccountIdSchema } from './provider-accounts.ts';
4
4
  /* The providers whose usage the daemon reads: the CLIs' own records, and the log a host keeps of what it calls itself. A subset of `AgentKindSchema`: it grows with the readers. */
5
5
  export const UsageProviderSchema = z.enum(['claude', 'codex', 'nanogpt', 'grok', 'copilot', 'anthropic-claude-api']);
6
6
  export type UsageProvider = z.infer<typeof UsageProviderSchema>;
7
+
7
8
  export const USAGE_PROVIDERS = UsageProviderSchema.options;
8
9
 
9
10
  export const UsageResolutionSchema = z.enum(['hour', 'day']);
@@ -194,8 +195,10 @@ export function evenPace(window: UsageWindow, now: number): EvenPace | null {
194
195
  if (window.resetsAt === null || window.durationMs === null || window.durationMs <= 0) {
195
196
  return null;
196
197
  }
198
+
197
199
  const percent = Math.round(Math.min(1, Math.max(0, 1 - (window.resetsAt - now) / window.durationMs)) * 100);
198
200
  const used = Math.round(window.used * 100);
201
+
199
202
  return { percent, standing: used > percent ? 'ahead' : used < percent ? 'behind' : 'even' };
200
203
  }
201
204
 
package/src/visual.ts CHANGED
@@ -63,17 +63,21 @@ function clampHeight(height: number): number {
63
63
  export function visualFrameHeight(visual: Pick<ChatVisual, 'maxHeight' | 'heights'>, width: number): number | undefined {
64
64
  let below: VisualHeight | undefined;
65
65
  let above: VisualHeight | undefined;
66
+
66
67
  for (const measured of visual.heights ?? []) {
67
68
  if (measured[0] <= width && (below === undefined || measured[0] > below[0])) {
68
69
  below = measured;
69
70
  }
71
+
70
72
  if (measured[0] >= width && (above === undefined || measured[0] < above[0])) {
71
73
  above = measured;
72
74
  }
73
75
  }
76
+
74
77
  if (below === undefined && above === undefined) {
75
78
  return undefined;
76
79
  }
80
+
77
81
  return clampHeight(Math.min(Math.max(below?.[1] ?? 0, above?.[1] ?? 0), visual.maxHeight));
78
82
  }
79
83
 
@@ -174,8 +178,10 @@ function webUrl(value: unknown): string | undefined {
174
178
  if (typeof value !== 'string') {
175
179
  return undefined;
176
180
  }
181
+
177
182
  try {
178
183
  const url = new (globalThis as unknown as { URL: UrlParser }).URL(value);
184
+
179
185
  return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : undefined;
180
186
  } catch {
181
187
  return undefined;
@@ -190,7 +196,9 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
190
196
  if (!isRecord(data) || data.jsonrpc !== '2.0' || typeof data.method !== 'string') {
191
197
  return undefined;
192
198
  }
199
+
193
200
  const params = isRecord(data.params) ? data.params : {};
201
+
194
202
  switch (data.method) {
195
203
  case VISUAL_BRIDGE_METHODS.sandboxProxyReady:
196
204
  return { method: data.method };
@@ -200,15 +208,19 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
200
208
  if (params.theme !== 'light' && params.theme !== 'dark') {
201
209
  return undefined;
202
210
  }
211
+
203
212
  const styles = isRecord(params.styles) ? params.styles : {};
213
+
204
214
  return { method: data.method, theme: { appearance: params.theme, variables: cleanVisualVariables(styles.variables) } };
205
215
  }
206
216
  case VISUAL_BRIDGE_METHODS.sizeChanged: {
207
217
  const height = sizeOf(params.height);
208
218
  const width = sizeOf(params.width);
219
+
209
220
  if (height === undefined) {
210
221
  return undefined;
211
222
  }
223
+
212
224
  return width === undefined ? { method: data.method, height } : { method: data.method, width, height };
213
225
  }
214
226
  case VISUAL_BRIDGE_METHODS.viewportReady:
@@ -217,26 +229,33 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
217
229
  if (data.params === null) {
218
230
  return { method: data.method, viewport: null };
219
231
  }
232
+
220
233
  const top = sizeOf(params.top);
221
234
  const height = sizeOf(params.height);
235
+
222
236
  return top !== undefined && height !== undefined && height > 0 ? { method: data.method, viewport: { top, height } } : undefined;
223
237
  }
224
238
  case VISUAL_BRIDGE_METHODS.scrollRequest: {
225
239
  if (typeof params.by === 'number' && Number.isFinite(params.by)) {
226
240
  return { method: data.method, request: { by: Math.max(-100_000, Math.min(params.by, 100_000)) } };
227
241
  }
242
+
228
243
  const top = sizeOf(params.to);
244
+
229
245
  if (top !== undefined) {
230
246
  return { method: data.method, request: { to: top } };
231
247
  }
248
+
232
249
  return params.edge === 'start' || params.edge === 'end' ? { method: data.method, request: { edge: params.edge } } : undefined;
233
250
  }
234
251
  case VISUAL_BRIDGE_METHODS.openLink: {
235
252
  const url = webUrl(params.url);
236
253
  const id = data.id;
254
+
237
255
  if (url === undefined || !(typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id)))) {
238
256
  return undefined;
239
257
  }
258
+
240
259
  return { method: data.method, id, url };
241
260
  }
242
261
  default:
@@ -377,14 +396,17 @@ const MAX_VALUE_LENGTH = 512;
377
396
  /* The variables of a theme a page may be given, from anything: the names it may hold and the values that cannot break out of their rule. */
378
397
  export function cleanVisualVariables(input: unknown): Record<string, string> {
379
398
  const clean: Record<string, string> = {};
399
+
380
400
  if (!isRecord(input)) {
381
401
  return clean;
382
402
  }
403
+
383
404
  for (const [name, value] of Object.entries(input)) {
384
405
  if (VARIABLE_NAME.test(name) && typeof value === 'string' && value.length <= MAX_VALUE_LENGTH && !UNSAFE_VALUE.test(value) && value.trim() !== '') {
385
406
  clean[name] = value.trim();
386
407
  }
387
408
  }
409
+
388
410
  return clean;
389
411
  }
390
412
 
@@ -421,6 +443,7 @@ const BASE_CSS = [
421
443
 
422
444
  function rootRule(theme: VisualTheme): string {
423
445
  const declarations = Object.entries(theme.variables).map(([name, value]) => `${name}:${value};`);
446
+
424
447
  return `:root{color-scheme:${theme.appearance};\n${declarations.join('\n')}\n}`;
425
448
  }
426
449
 
@@ -445,6 +468,7 @@ function bootstrapScript(): string {
445
468
  sizeChanged: VISUAL_BRIDGE_METHODS.sizeChanged,
446
469
  openLink: VISUAL_BRIDGE_METHODS.openLink
447
470
  };
471
+
448
472
  return `(function () {
449
473
  var script = document.currentScript;
450
474
  var style = script && script.previousElementSibling;
@@ -540,6 +564,7 @@ if (framed && typeof ResizeObserver === 'function') {
540
564
 
541
565
  function bootstrapMarkup(viewport: boolean): string {
542
566
  const theme = `${rootRule(VISUAL_DARK_THEME)}\n@media (prefers-color-scheme: light){\n${rootRule(VISUAL_LIGHT_THEME)}\n}\n${BASE_CSS}`;
567
+
543
568
  return [
544
569
  // Always first: the stored file is UTF-8 whatever the page declares, and the first charset wins.
545
570
  '<meta charset="utf-8">',
@@ -564,41 +589,56 @@ function commentEnd(html: string, from: number): number {
564
589
  if (html.startsWith('>', from)) {
565
590
  return from + 1;
566
591
  }
592
+
567
593
  if (html.startsWith('->', from)) {
568
594
  return from + 2;
569
595
  }
596
+
570
597
  const ends = [html.indexOf('-->', from), html.indexOf('--!>', from)].filter((index) => index >= 0);
598
+
571
599
  if (ends.length === 0) {
572
600
  return html.length;
573
601
  }
602
+
574
603
  const end = Math.min(...ends);
604
+
575
605
  return end + (html.startsWith('-->', end) ? 3 : 4);
576
606
  }
577
607
 
578
608
  // Past the `>` of a tag, skipping a quoted attribute value, which may hold one.
579
609
  function tagEnd(html: string, from: number): number {
580
610
  let at = from;
611
+
581
612
  while (at < html.length) {
582
613
  const char = html[at];
614
+
583
615
  if (char === '>') {
584
616
  return at + 1;
585
617
  }
618
+
586
619
  at++;
620
+
587
621
  if (char !== '=') {
588
622
  continue;
589
623
  }
624
+
590
625
  while (at < html.length && /\s/.test(html[at]!)) {
591
626
  at++;
592
627
  }
628
+
593
629
  const quote = html[at];
630
+
594
631
  if (quote === '"' || quote === "'") {
595
632
  const close = html.indexOf(quote, at + 1);
633
+
596
634
  if (close < 0) {
597
635
  return html.length;
598
636
  }
637
+
599
638
  at = close + 1;
600
639
  }
601
640
  }
641
+
602
642
  return html.length;
603
643
  }
604
644
 
@@ -606,9 +646,11 @@ function rawTextEnd(html: string, name: string, from: number): number {
606
646
  if (name === 'plaintext') {
607
647
  return html.length;
608
648
  }
649
+
609
650
  const close = new RegExp(`</${name}[\\s/>]`, 'gi');
610
651
  close.lastIndex = from;
611
652
  const found = close.exec(html);
653
+
612
654
  return found === null ? html.length : tagEnd(html, found.index + 2);
613
655
  }
614
656
 
@@ -618,15 +660,20 @@ function rawTextEnd(html: string, name: string, from: number): number {
618
660
  */
619
661
  function* tokens(html: string): Generator<Token> {
620
662
  let at = 0;
663
+
621
664
  while (at < html.length) {
622
665
  const open = html.indexOf('<', at);
666
+
623
667
  if (open < 0) {
624
668
  yield { kind: 'text', name: '', start: at, end: html.length };
669
+
625
670
  return;
626
671
  }
672
+
627
673
  const next = html[open + 1] ?? '';
628
674
  let token: Token | null = null;
629
675
  let resume = 0;
676
+
630
677
  if (html.startsWith('<!--', open)) {
631
678
  token = { kind: 'comment', name: '', start: open, end: commentEnd(html, open + 4) };
632
679
  } else if (next === '!' || next === '?') {
@@ -639,21 +686,26 @@ function* tokens(html: string): Generator<Token> {
639
686
  } else if (/[a-z]/i.test(next)) {
640
687
  const name = /^<([a-z][^\s/>]*)/i.exec(html.slice(open, open + 64))![1]!.toLowerCase();
641
688
  token = { kind: 'start', name, start: open, end: tagEnd(html, open + 1 + name.length) };
689
+
642
690
  if (RAW_TEXT.has(name)) {
643
691
  resume = rawTextEnd(html, name, token.end);
644
692
  }
645
693
  }
694
+
646
695
  if (token === null) {
647
696
  // A `<` that opens nothing is text, which goes on to the next `<`.
648
697
  const following = html.indexOf('<', open + 1);
649
698
  const end = following < 0 ? html.length : following;
650
699
  yield { kind: 'text', name: '', start: at, end };
651
700
  at = end;
701
+
652
702
  continue;
653
703
  }
704
+
654
705
  if (open > at) {
655
706
  yield { kind: 'text', name: '', start: at, end: open };
656
707
  }
708
+
657
709
  yield token;
658
710
  at = Math.max(token.end, resume);
659
711
  }
@@ -672,20 +724,25 @@ function bootstrapPlace(html: string): { at: number; head: boolean } {
672
724
  if (token.kind === 'comment' || token.kind === 'doctype' || (token.kind === 'text' && BLANK.test(html.slice(token.start, token.end)))) {
673
725
  continue;
674
726
  }
727
+
675
728
  if (token.kind === 'start' && token.name === 'html') {
676
729
  continue;
677
730
  }
731
+
678
732
  if (token.kind === 'start' && token.name === 'head') {
679
733
  return { at: token.end, head: true };
680
734
  }
735
+
681
736
  return { at: token.start, head: false };
682
737
  }
738
+
683
739
  return { at: html.length, head: false };
684
740
  }
685
741
 
686
742
  // Whether the page sets its own viewport, outside a template.
687
743
  function hasViewport(html: string): boolean {
688
744
  let templates = 0;
745
+
689
746
  for (const token of tokens(html)) {
690
747
  if (token.name === 'template') {
691
748
  templates = Math.max(0, templates + (token.kind === 'start' ? 1 : token.kind === 'end' ? -1 : 0));
@@ -695,6 +752,7 @@ function hasViewport(html: string): boolean {
695
752
  }
696
753
  }
697
754
  }
755
+
698
756
  return false;
699
757
  }
700
758
 
@@ -706,11 +764,13 @@ function hasViewport(html: string): boolean {
706
764
  export function injectVisualBootstrap(html: string): string {
707
765
  const place = bootstrapPlace(html);
708
766
  const bootstrap = bootstrapMarkup(!hasViewport(html));
767
+
709
768
  return html.slice(0, place.at) + (place.head ? bootstrap : `<head>${bootstrap}</head>`) + html.slice(place.at);
710
769
  }
711
770
 
712
771
  function hostPageScript(): string {
713
772
  const methods = { ready: VISUAL_BRIDGE_METHODS.sandboxProxyReady, resource: VISUAL_BRIDGE_METHODS.sandboxResourceReady };
773
+
714
774
  return `(function () {
715
775
  if (window.parent === window) { return; }
716
776
  var methods = ${inlineJson(methods)};