@openchamber/sdk 1.24.2 → 2.0.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.
@@ -10,7 +10,24 @@ export type PanelContribution = {
10
10
  name: string;
11
11
  icon: string;
12
12
  entry?: string;
13
+ /**
14
+ * Beside a shared surface (`service.surface`), the panel page is docked to
15
+ * one edge of the host-drawn picture: a toolbar above it, an inspector
16
+ * below, a tool column beside it. `dock` is the edge (default
17
+ * `GUEST_SURFACE_DOCK_DEFAULT`) and `size` the page's thickness in CSS
18
+ * pixels across that edge (default `GUEST_SURFACE_DOCK_SIZE_DEFAULT`).
19
+ * Both only make sense with `entry` and `service.surface` together.
20
+ */
21
+ dock?: GuestSurfaceDock;
22
+ size?: number;
13
23
  };
24
+ export declare const GUEST_SURFACE_DOCKS: readonly ["top", "bottom", "left", "right"];
25
+ export type GuestSurfaceDock = (typeof GUEST_SURFACE_DOCKS)[number];
26
+ export declare const GUEST_SURFACE_DOCK_DEFAULT: GuestSurfaceDock;
27
+ /** Thickness in CSS px of a docked `panel.entry` when the manifest names none. */
28
+ export declare const GUEST_SURFACE_DOCK_SIZE_DEFAULT = 40;
29
+ export declare const GUEST_SURFACE_DOCK_SIZE_MIN = 24;
30
+ export declare const GUEST_SURFACE_DOCK_SIZE_MAX = 480;
14
31
  /** Sandboxed HTML loaded on demand for background actions and slash commands. */
15
32
  export type BackgroundContribution = {
16
33
  entry: string;
@@ -31,6 +48,26 @@ export type PageContribution = true | {
31
48
  entry: string;
32
49
  title?: string;
33
50
  };
51
+ /**
52
+ * A section inside the chat's Work Status panel. `true` reuses `panel.entry`;
53
+ * the object form names its own package HTML, so an extension can ship only
54
+ * this section and no rail panel. `title` replaces `panel.name` on the section
55
+ * header; `height` is the starting frame height in CSS px before the guest
56
+ * reports its own through `setHeight`.
57
+ */
58
+ export type StatusSectionContribution = true | {
59
+ entry: string;
60
+ title?: string;
61
+ height?: number;
62
+ };
63
+ /** Characters in a status section title. */
64
+ export declare const GUEST_STATUS_SECTION_TITLE_MAX = 60;
65
+ /** Smallest frame height the host gives a status section, in CSS px. */
66
+ export declare const GUEST_STATUS_SECTION_HEIGHT_MIN = 24;
67
+ /** Tallest frame height the host gives a status section; taller content scrolls inside the frame. */
68
+ export declare const GUEST_STATUS_SECTION_HEIGHT_MAX = 320;
69
+ /** Frame height before the manifest or the guest says otherwise. */
70
+ export declare const GUEST_STATUS_SECTION_HEIGHT_DEFAULT = 120;
34
71
  export type GuestActionWhere = 'message' | 'session';
35
72
  export type GuestActionRole = 'user' | 'assistant';
36
73
  /** What a session action wants alongside the session id and title. */
@@ -199,18 +236,37 @@ export type PublicSocketBinding = {
199
236
  resolved: string | null;
200
237
  override: string | null;
201
238
  };
239
+ /**
240
+ * Host roles a service can stand in for. `browser` answers the agent's
241
+ * `browser.*` actions in place of the in-app browser view; the contract is in
242
+ * `service-providers.ts`. A service that provides a role needs no panel or
243
+ * background entry: the host starts it on the first action.
244
+ */
245
+ export declare const GUEST_SERVICE_PROVIDES: readonly ["browser"];
246
+ export type GuestServiceProvides = (typeof GUEST_SERVICE_PROVIDES)[number];
202
247
  export type ServiceContribution = {
203
248
  entry: string;
204
249
  runtime: 'host';
205
250
  permissions?: ServicePermissions;
251
+ provides?: GuestServiceProvides[];
252
+ /**
253
+ * The service shows a live surface (frames out, input in) that the host
254
+ * draws in this extension's rail panel; see `service-surface.ts`. With
255
+ * `panel.entry` too, that page is docked to one edge of the picture (see
256
+ * `PanelContribution.dock`); without it, the panel is the surface alone.
257
+ */
258
+ surface?: true;
206
259
  };
207
260
  /** Catalog card for a local service. Drops nothing secret; grant is host state. */
208
261
  export type PublicService = {
209
262
  runtime: 'host';
210
263
  permissions?: PublicServicePermissions;
211
264
  socketBindings?: PublicSocketBinding[];
265
+ provides?: GuestServiceProvides[];
266
+ surface?: true;
212
267
  granted: boolean;
213
268
  };
269
+ export declare const serviceProvides: (service: Pick<ServiceContribution, "provides"> | undefined, role: GuestServiceProvides) => boolean;
214
270
  /**
215
271
  * What a guest may do beyond drawing its own panel. The user approves the
216
272
  * full list once, when the package is installed; a later package that asks
@@ -240,6 +296,8 @@ export type OpenChamberContributes = {
240
296
  background?: BackgroundContribution;
241
297
  attach?: AttachContribution;
242
298
  page?: PageContribution;
299
+ /** A section in the chat's Work Status panel. */
300
+ statusSection?: StatusSectionContribution;
243
301
  capabilities?: DeclaredGuestCapability[];
244
302
  integration?: IntegrationContribution;
245
303
  service?: ServiceContribution;
@@ -274,6 +332,14 @@ export declare const resolveAttachMode: (attach: AttachContribution | undefined)
274
332
  */
275
333
  export declare const resolveAttachEntry: (contributes: Pick<OpenChamberContributes, "attach">) => string | null;
276
334
  export declare const resolvePageEntry: (contributes: Pick<OpenChamberContributes, "panel" | "page">) => string | null;
335
+ /**
336
+ * The HTML the Work Status section loads: `panel.entry` for `true`, the
337
+ * object's own `entry` otherwise, `null` when nothing is declared or `true`
338
+ * has no panel page to reuse.
339
+ */
340
+ export declare const resolveStatusSectionEntry: (contributes: Pick<OpenChamberContributes, "panel" | "statusSection">) => string | null;
341
+ /** Clamp a requested status section height to what the host allows. */
342
+ export declare const clampStatusSectionHeight: (height: number) => number;
277
343
  export type OpenChamberEngines = {
278
344
  openchamber: string;
279
345
  };
@@ -282,7 +348,7 @@ export type OpenChamberManifest = {
282
348
  engines?: OpenChamberEngines;
283
349
  contributes: OpenChamberContributes;
284
350
  };
285
- export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-background' | 'invalid-attach' | 'invalid-page' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools';
351
+ export type ParseManifestErrorCode = 'not-object' | 'missing-openchamber' | 'unsupported-api-version' | 'invalid-engines' | 'invalid-version' | 'missing-panel' | 'invalid-panel' | 'invalid-panel-id' | 'invalid-panel-name' | 'invalid-panel-icon' | 'invalid-panel-entry' | 'invalid-background' | 'invalid-attach' | 'invalid-page' | 'invalid-status-section' | 'invalid-capabilities' | 'invalid-integration' | 'invalid-service' | 'invalid-filesystem' | 'invalid-actions' | 'invalid-commands' | 'invalid-tools';
286
352
  export type ParseManifestFailure = {
287
353
  ok: false;
288
354
  code: ParseManifestErrorCode;
package/dist/manifest.js CHANGED
@@ -1,4 +1,18 @@
1
1
  export const PANEL_ID = /^[a-z][a-z0-9-]*$/;
2
+ export const GUEST_SURFACE_DOCKS = ['top', 'bottom', 'left', 'right'];
3
+ export const GUEST_SURFACE_DOCK_DEFAULT = 'top';
4
+ /** Thickness in CSS px of a docked `panel.entry` when the manifest names none. */
5
+ export const GUEST_SURFACE_DOCK_SIZE_DEFAULT = 40;
6
+ export const GUEST_SURFACE_DOCK_SIZE_MIN = 24;
7
+ export const GUEST_SURFACE_DOCK_SIZE_MAX = 480;
8
+ /** Characters in a status section title. */
9
+ export const GUEST_STATUS_SECTION_TITLE_MAX = 60;
10
+ /** Smallest frame height the host gives a status section, in CSS px. */
11
+ export const GUEST_STATUS_SECTION_HEIGHT_MIN = 24;
12
+ /** Tallest frame height the host gives a status section; taller content scrolls inside the frame. */
13
+ export const GUEST_STATUS_SECTION_HEIGHT_MAX = 320;
14
+ /** Frame height before the manifest or the guest says otherwise. */
15
+ export const GUEST_STATUS_SECTION_HEIGHT_DEFAULT = 120;
2
16
  /** How many `contributes.actions` entries a package may declare. */
3
17
  export const GUEST_ACTIONS_MAX = 8;
4
18
  /** Characters in an action label. */
@@ -71,6 +85,14 @@ export const resolveIntegrationApi = (integration) => {
71
85
  }
72
86
  return null;
73
87
  };
88
+ /**
89
+ * Host roles a service can stand in for. `browser` answers the agent's
90
+ * `browser.*` actions in place of the in-app browser view; the contract is in
91
+ * `service-providers.ts`. A service that provides a role needs no panel or
92
+ * background entry: the host starts it on the first action.
93
+ */
94
+ export const GUEST_SERVICE_PROVIDES = ['browser'];
95
+ export const serviceProvides = (service, role) => Boolean(service?.provides?.includes(role));
74
96
  /**
75
97
  * What a guest may do beyond drawing its own panel. The user approves the
76
98
  * full list once, when the package is installed; a later package that asks
@@ -152,6 +174,23 @@ export const resolvePageEntry = (contributes) => {
152
174
  return null;
153
175
  return contributes.page === true ? contributes.panel.entry : contributes.page.entry;
154
176
  };
177
+ /**
178
+ * The HTML the Work Status section loads: `panel.entry` for `true`, the
179
+ * object's own `entry` otherwise, `null` when nothing is declared or `true`
180
+ * has no panel page to reuse.
181
+ */
182
+ export const resolveStatusSectionEntry = (contributes) => {
183
+ const section = contributes.statusSection;
184
+ if (!section)
185
+ return null;
186
+ return section === true ? contributes.panel.entry ?? null : section.entry;
187
+ };
188
+ /** Clamp a requested status section height to what the host allows. */
189
+ export const clampStatusSectionHeight = (height) => {
190
+ if (!Number.isFinite(height))
191
+ return GUEST_STATUS_SECTION_HEIGHT_DEFAULT;
192
+ return Math.min(GUEST_STATUS_SECTION_HEIGHT_MAX, Math.max(GUEST_STATUS_SECTION_HEIGHT_MIN, Math.round(height)));
193
+ };
155
194
  export const isSafeAssetPath = (value) => {
156
195
  if (value.includes('\0') || value.includes('\\') || value.startsWith('/') || value.includes('://')) {
157
196
  return false;
@@ -217,6 +256,12 @@ export const toPublicService = (service, granted, socketBindings) => {
217
256
  override: binding.override,
218
257
  }));
219
258
  }
259
+ if (service.provides && service.provides.length > 0) {
260
+ next.provides = [...service.provides];
261
+ }
262
+ if (service.surface) {
263
+ next.surface = true;
264
+ }
220
265
  return next;
221
266
  };
222
267
  /** Guest package version: `1.2.3`, optional prerelease / build. */
package/dist/parse.d.ts CHANGED
@@ -11,6 +11,13 @@ export declare const openChamberManifestSchema: z.ZodObject<{
11
11
  name: z.ZodString;
12
12
  icon: z.ZodString;
13
13
  entry: z.ZodOptional<z.ZodString>;
14
+ dock: z.ZodOptional<z.ZodEnum<{
15
+ top: "top";
16
+ bottom: "bottom";
17
+ left: "left";
18
+ right: "right";
19
+ }>>;
20
+ size: z.ZodOptional<z.ZodNumber>;
14
21
  }, z.core.$strip>;
15
22
  background: z.ZodOptional<z.ZodObject<{
16
23
  entry: z.ZodString;
@@ -29,6 +36,11 @@ export declare const openChamberManifestSchema: z.ZodObject<{
29
36
  entry: z.ZodString;
30
37
  title: z.ZodOptional<z.ZodString>;
31
38
  }, z.core.$strip>]>>;
39
+ statusSection: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
40
+ entry: z.ZodString;
41
+ title: z.ZodOptional<z.ZodString>;
42
+ height: z.ZodOptional<z.ZodNumber>;
43
+ }, z.core.$strip>]>>;
32
44
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
33
45
  sessions: "sessions";
34
46
  prompt: "prompt";
@@ -97,6 +109,10 @@ export declare const openChamberManifestSchema: z.ZodObject<{
97
109
  sockets?: SocketBinding[] | undefined;
98
110
  exec?: string[] | undefined;
99
111
  }>>>;
112
+ provides: z.ZodOptional<z.ZodArray<z.ZodEnum<{
113
+ browser: "browser";
114
+ }>>>;
115
+ surface: z.ZodOptional<z.ZodLiteral<true>>;
100
116
  }, z.core.$strip>>;
101
117
  filesystem: z.ZodOptional<z.ZodArray<z.ZodString>>;
102
118
  actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -156,6 +172,13 @@ export declare const packageManifestSchema: z.ZodObject<{
156
172
  name: z.ZodString;
157
173
  icon: z.ZodString;
158
174
  entry: z.ZodOptional<z.ZodString>;
175
+ dock: z.ZodOptional<z.ZodEnum<{
176
+ top: "top";
177
+ bottom: "bottom";
178
+ left: "left";
179
+ right: "right";
180
+ }>>;
181
+ size: z.ZodOptional<z.ZodNumber>;
159
182
  }, z.core.$strip>;
160
183
  background: z.ZodOptional<z.ZodObject<{
161
184
  entry: z.ZodString;
@@ -174,6 +197,11 @@ export declare const packageManifestSchema: z.ZodObject<{
174
197
  entry: z.ZodString;
175
198
  title: z.ZodOptional<z.ZodString>;
176
199
  }, z.core.$strip>]>>;
200
+ statusSection: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
201
+ entry: z.ZodString;
202
+ title: z.ZodOptional<z.ZodString>;
203
+ height: z.ZodOptional<z.ZodNumber>;
204
+ }, z.core.$strip>]>>;
177
205
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
178
206
  sessions: "sessions";
179
207
  prompt: "prompt";
@@ -242,6 +270,10 @@ export declare const packageManifestSchema: z.ZodObject<{
242
270
  sockets?: SocketBinding[] | undefined;
243
271
  exec?: string[] | undefined;
244
272
  }>>>;
273
+ provides: z.ZodOptional<z.ZodArray<z.ZodEnum<{
274
+ browser: "browser";
275
+ }>>>;
276
+ surface: z.ZodOptional<z.ZodLiteral<true>>;
245
277
  }, z.core.$strip>>;
246
278
  filesystem: z.ZodOptional<z.ZodArray<z.ZodString>>;
247
279
  actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
package/dist/parse.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { OPENCHAMBER_SDK_MANIFEST_API_VERSIONS } from "./api-version.js";
3
3
  import { OPENCHAMBER_ENGINE_PATTERN } from "./host-version.js";
4
- import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, } from "./manifest.js";
4
+ import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_SERVICE_PROVIDES, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, } from "./manifest.js";
5
5
  const isPanelIcon = (value) => (PANEL_ID.test(value) || isGuestPackageSvgIcon(value));
6
6
  const isHttpsUrl = (value) => {
7
7
  try {
@@ -48,6 +48,8 @@ const panelSchema = z.object({
48
48
  name: z.string().trim().min(1),
49
49
  icon: z.string().trim().refine(isPanelIcon),
50
50
  entry: z.string().trim().refine(isSafeAssetPath).optional(),
51
+ dock: z.enum(GUEST_SURFACE_DOCKS).optional(),
52
+ size: z.number().int().min(GUEST_SURFACE_DOCK_SIZE_MIN).max(GUEST_SURFACE_DOCK_SIZE_MAX).optional(),
51
53
  });
52
54
  const integrationSettingSchema = z.object({
53
55
  id: z.string().trim().regex(PANEL_ID),
@@ -140,6 +142,10 @@ const serviceSchema = z.object({
140
142
  entry: z.string().trim().refine(isSafeAssetPath),
141
143
  runtime: z.literal('host'),
142
144
  permissions: servicePermissionsSchema.optional(),
145
+ provides: z.array(z.enum(GUEST_SERVICE_PROVIDES)).min(1).max(GUEST_SERVICE_PROVIDES.length)
146
+ .refine((roles) => new Set(roles).size === roles.length, { message: 'provides entries must be unique' })
147
+ .optional(),
148
+ surface: z.literal(true).optional(),
143
149
  });
144
150
  const uniqueBy = (items, key) => (new Set(items.map(key)).size === items.length);
145
151
  // `roles` only means something on a message action and `payload` only on a
@@ -187,6 +193,11 @@ const contributesSchema = z.object({
187
193
  entry: z.string().trim().refine(isSafeAssetPath),
188
194
  title: z.string().trim().min(1).max(200).optional(),
189
195
  })]).optional(),
196
+ statusSection: z.union([z.literal(true), z.object({
197
+ entry: z.string().trim().refine((value) => isSafeAssetPath(value) && value.toLowerCase().endsWith('.html')),
198
+ title: z.string().trim().min(1).max(GUEST_STATUS_SECTION_TITLE_MAX).optional(),
199
+ height: z.number().int().min(GUEST_STATUS_SECTION_HEIGHT_MIN).max(GUEST_STATUS_SECTION_HEIGHT_MAX).optional(),
200
+ })]).optional(),
190
201
  capabilities: z.array(z.enum(DECLARED_GUEST_CAPABILITIES)).max(8).optional(),
191
202
  integration: integrationSchema.optional(),
192
203
  service: serviceSchema.optional(),
@@ -208,7 +219,10 @@ const runtimeContributions = (contributes) => {
208
219
  declared.push('capabilities');
209
220
  if (contributes.integration !== undefined)
210
221
  declared.push('integration');
211
- if (contributes.service !== undefined)
222
+ // A service the host starts itself (it provides a role or a surface) needs
223
+ // no frame; one that only answers a panel's `serviceRequest` has no caller
224
+ // without one.
225
+ if (contributes.service !== undefined && !contributes.service.provides?.length && !contributes.service.surface)
212
226
  declared.push('service');
213
227
  if (contributes.filesystem !== undefined)
214
228
  declared.push('filesystem');
@@ -224,6 +238,23 @@ export const openChamberManifestSchema = z.object({
224
238
  openchamber: z.string().trim().regex(OPENCHAMBER_ENGINE_PATTERN),
225
239
  }).strict().optional(),
226
240
  contributes: contributesSchema.superRefine((contributes, ctx) => {
241
+ // Docking only means something for a page beside a shared surface.
242
+ const docked = contributes.panel.dock !== undefined || contributes.panel.size !== undefined;
243
+ if (docked && !(contributes.service?.surface && hasGuestPage(contributes))) {
244
+ ctx.addIssue({
245
+ code: 'custom', path: ['panel', 'dock'],
246
+ message: 'panel.dock and panel.size place panel.entry beside service.surface; they need both.',
247
+ });
248
+ return;
249
+ }
250
+ // `true` reuses the panel page; without one the section has nothing to load.
251
+ if (contributes.statusSection === true && !hasGuestPage(contributes)) {
252
+ ctx.addIssue({
253
+ code: 'custom', path: ['statusSection'],
254
+ message: 'contributes.statusSection: true reuses panel.entry; without a panel page, name its own { "entry": "<package .html>" }.',
255
+ });
256
+ return;
257
+ }
227
258
  if (hasGuestPage(contributes))
228
259
  return;
229
260
  if (contributes.background) {
@@ -241,6 +272,26 @@ export const openChamberManifestSchema = z.object({
241
272
  });
242
273
  return;
243
274
  }
275
+ if (contributes.statusSection) {
276
+ // The section frame runs code and may use what the package is granted,
277
+ // but it only hosts itself: things the host opens or invokes elsewhere
278
+ // need a panel or background entry.
279
+ const needsFrame = [];
280
+ if (contributes.page !== undefined)
281
+ needsFrame.push('page');
282
+ if (contributes.attach !== undefined && contributes.attach !== false)
283
+ needsFrame.push('attach');
284
+ if (contributes.actions !== undefined)
285
+ needsFrame.push('actions');
286
+ if (contributes.commands !== undefined)
287
+ needsFrame.push('commands');
288
+ if (needsFrame.length > 0)
289
+ ctx.addIssue({
290
+ code: 'custom', path: ['panel'],
291
+ message: `${needsFrame.map((key) => `contributes.${key}`).join(', ')} needs panel.entry or background.entry; a status section only hosts itself.`,
292
+ });
293
+ return;
294
+ }
244
295
  const needsRuntime = runtimeContributions(contributes);
245
296
  if (needsRuntime.length === 0)
246
297
  return;
@@ -301,6 +352,12 @@ const failureFromIssue = (issue) => {
301
352
  return fail('invalid-panel-icon', 'panel.icon must be a Remixicon name or a package .svg path.');
302
353
  case 'contributes.panel.entry':
303
354
  return fail('invalid-panel-entry', 'panel.entry must be a relative path inside the package.');
355
+ case 'contributes.panel.dock':
356
+ return fail('invalid-panel', issue.code === 'custom'
357
+ ? issue.message
358
+ : `panel.dock is one of ${GUEST_SURFACE_DOCKS.join(', ')}.`);
359
+ case 'contributes.panel.size':
360
+ return fail('invalid-panel', `panel.size is a whole number of CSS pixels from ${GUEST_SURFACE_DOCK_SIZE_MIN} to ${GUEST_SURFACE_DOCK_SIZE_MAX}.`);
304
361
  case 'contributes.capabilities':
305
362
  return fail('invalid-capabilities', 'contributes.capabilities may list "prompt", "sessions", and "files".');
306
363
  default:
@@ -309,6 +366,11 @@ const failureFromIssue = (issue) => {
309
366
  if (path === 'contributes.page' || path.startsWith('contributes.page.')) {
310
367
  return fail('invalid-page', 'contributes.page must be true or { entry: "<package HTML>", title?: "Page title" }.');
311
368
  }
369
+ if (path === 'contributes.statusSection' || path.startsWith('contributes.statusSection.')) {
370
+ return fail('invalid-status-section', issue.code === 'custom'
371
+ ? issue.message
372
+ : `contributes.statusSection must be true or { entry: "<package .html>", title?: "1 to ${GUEST_STATUS_SECTION_TITLE_MAX} characters", height?: ${GUEST_STATUS_SECTION_HEIGHT_MIN} to ${GUEST_STATUS_SECTION_HEIGHT_MAX} }.`);
373
+ }
312
374
  if (path === 'contributes.background' || path.startsWith('contributes.background.')) {
313
375
  return fail('invalid-background', 'contributes.background needs an entry ending in .html inside the package.');
314
376
  }
@@ -334,7 +396,7 @@ const failureFromIssue = (issue) => {
334
396
  return fail('invalid-integration', 'contributes.integration needs a name, description, and oauth, token, or host.');
335
397
  }
336
398
  if (path.startsWith('contributes.service')) {
337
- return fail('invalid-service', 'contributes.service needs entry, runtime "host", and optional permissions.');
399
+ return fail('invalid-service', 'contributes.service needs entry, runtime "host", optional permissions, optional provides ("browser"), and optional surface (true).');
338
400
  }
339
401
  return fail('missing-panel', 'contributes.panel is required.');
340
402
  };
@@ -144,6 +144,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
144
144
  dialog: "dialog";
145
145
  page: "page";
146
146
  background: "background";
147
+ status: "status";
147
148
  }>;
148
149
  connection: z.ZodObject<{
149
150
  connected: z.ZodBoolean;
@@ -633,7 +634,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
633
634
  id: string;
634
635
  ok: false;
635
636
  error: string;
636
- code: "HOST_UNAVAILABLE" | "HOST_TIMEOUT" | "HOST_REJECTED" | "DISCONNECTED" | "DISABLED" | "BAD_PATH" | "NO_INTEGRATION" | "NO_SERVICE" | "SERVICE_FAILED" | "NO_SESSION" | "SESSION_BUSY" | "NOT_GRANTED" | "NO_DIRECTORY" | "NOT_FOUND" | "FILE_TOO_LARGE" | "DENIED" | "NO_MODEL" | "MODEL_FAILED";
637
+ code: "HOST_UNAVAILABLE" | "HOST_TIMEOUT" | "HOST_REJECTED" | "DISCONNECTED" | "DISABLED" | "BAD_PATH" | "NO_INTEGRATION" | "NO_SERVICE" | "SERVICE_FAILED" | "NO_SESSION" | "SESSION_BUSY" | "NOT_GRANTED" | "NO_DIRECTORY" | "NOT_FOUND" | "FILE_TOO_LARGE" | "DENIED" | "NO_MODEL" | "MODEL_FAILED" | "UNSUPPORTED";
637
638
  payload?: undefined;
638
639
  }, {
639
640
  type: "result";
@@ -1114,6 +1115,22 @@ export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1114
1115
  }, z.core.$strip>;
1115
1116
  channel: z.ZodLiteral<"openchamber.sdk">;
1116
1117
  v: z.ZodLiteral<1>;
1118
+ }, z.core.$strip>, z.ZodObject<{
1119
+ type: z.ZodLiteral<"open-commit">;
1120
+ id: z.ZodString;
1121
+ payload: z.ZodObject<{
1122
+ sha: z.ZodString;
1123
+ }, z.core.$strip>;
1124
+ channel: z.ZodLiteral<"openchamber.sdk">;
1125
+ v: z.ZodLiteral<1>;
1126
+ }, z.core.$strip>, z.ZodObject<{
1127
+ type: z.ZodLiteral<"resize">;
1128
+ id: z.ZodString;
1129
+ payload: z.ZodObject<{
1130
+ height: z.ZodNumber;
1131
+ }, z.core.$strip>;
1132
+ channel: z.ZodLiteral<"openchamber.sdk">;
1133
+ v: z.ZodLiteral<1>;
1117
1134
  }, z.core.$strip>, z.ZodObject<{
1118
1135
  type: z.ZodLiteral<"resolve-result">;
1119
1136
  id: z.ZodString;
package/dist/protocol.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { guestSessionWorktreeSchema, guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceQuerySchema, guestWorkspaceSnapshotSchema } from "./workspace-schemas.js";
3
3
  import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from "./api-version.js";
4
- import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, } from "./contract.js";
4
+ import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_COMMIT_SHA, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, } from "./contract.js";
5
5
  const envelope = {
6
6
  channel: z.literal(OPENCHAMBER_SDK_CHANNEL),
7
7
  v: z.literal(OPENCHAMBER_SDK_API_VERSION),
@@ -146,7 +146,7 @@ const readyPayloadSchema = z.object({
146
146
  locale: z.string().min(1),
147
147
  directory: z.string().nullable(),
148
148
  session: sessionSnapshotSchema,
149
- surface: z.enum(['panel', 'dialog', 'page', 'background']),
149
+ surface: z.enum(['panel', 'dialog', 'page', 'background', 'status']),
150
150
  connection: guestConnectionSchema,
151
151
  settings: guestSettingsSchema,
152
152
  item: guestItemSchema,
@@ -443,6 +443,22 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
443
443
  count: z.number().int().min(0).max(GUEST_BADGE_MAX).nullable(),
444
444
  }),
445
445
  }),
446
+ z.object({
447
+ ...envelope,
448
+ type: z.literal('open-commit'),
449
+ id: z.string().min(1),
450
+ payload: z.object({
451
+ sha: z.string().regex(GUEST_COMMIT_SHA),
452
+ }),
453
+ }),
454
+ z.object({
455
+ ...envelope,
456
+ type: z.literal('resize'),
457
+ id: z.string().min(1),
458
+ payload: z.object({
459
+ height: z.number().int().min(0).max(GUEST_FRAME_HEIGHT_MAX),
460
+ }),
461
+ }),
446
462
  z.object({
447
463
  ...envelope,
448
464
  type: z.literal('resolve-result'),
package/dist/schemas.d.ts CHANGED
@@ -2,3 +2,6 @@ export { openChamberManifestSchema, PACKAGE_VERSION_PATTERN, packageManifestSche
2
2
  export type { ManifestDocument } from './parse.ts';
3
3
  export { guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceSnapshotSchema } from './workspace-schemas.ts';
4
4
  export { guestMessageSchema, hostMessageSchema, parseGuestMessage, parseHostMessage, } from './protocol.ts';
5
+ export { browserControlActionSchema, browserProviderResultSchema } from './service-provider-schemas.ts';
6
+ export { surfaceClipboardAnswerSchema, surfaceControllerSchema, surfaceHostMessageSchema, surfaceInputEventSchema, surfaceResizeAnswerSchema, surfaceViewerMessageSchema, } from './service-surface-schemas.ts';
7
+ export type { SurfaceHostMessage, SurfaceViewerMessage } from './service-surface-schemas.ts';
package/dist/schemas.js CHANGED
@@ -4,3 +4,5 @@
4
4
  export { openChamberManifestSchema, PACKAGE_VERSION_PATTERN, packageManifestSchema, parseManifest, parseManifestJson, } from "./parse.js";
5
5
  export { guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceSnapshotSchema } from "./workspace-schemas.js";
6
6
  export { guestMessageSchema, hostMessageSchema, parseGuestMessage, parseHostMessage, } from "./protocol.js";
7
+ export { browserControlActionSchema, browserProviderResultSchema } from "./service-provider-schemas.js";
8
+ export { surfaceClipboardAnswerSchema, surfaceControllerSchema, surfaceHostMessageSchema, surfaceInputEventSchema, surfaceResizeAnswerSchema, surfaceViewerMessageSchema, } from "./service-surface-schemas.js";
@@ -1,2 +1,10 @@
1
- /** Shared by served extension documents and the UI kit, including older guest bundles. */
2
- export declare const GUEST_SCROLLBAR_CSS = "\n:root {\n --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 40%, transparent);\n --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 65%, transparent);\n scrollbar-gutter: stable;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--oc-scrollbar-thumb) transparent;\n}\n/* Chromium's standard scrollbar properties otherwise override its pseudo-elements. */\n@supports selector(::-webkit-scrollbar) {\n * { scrollbar-width: auto; scrollbar-color: auto; }\n ::-webkit-scrollbar { width: 6px; height: 6px; background: transparent; }\n :root::-webkit-scrollbar, body::-webkit-scrollbar { background: var(--oc-bg, inherit); }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb {\n background: var(--oc-scrollbar-thumb);\n border-radius: 999px;\n min-width: 24px;\n min-height: 24px;\n }\n ::-webkit-scrollbar-thumb:hover { background: var(--oc-scrollbar-thumb-hover); }\n ::-webkit-scrollbar-corner { background: transparent; }\n ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }\n}\n@media (forced-colors: active) {\n * { scrollbar-color: auto; }\n ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background: CanvasText; }\n}\n";
1
+ export declare const GUEST_SCROLLBAR_CSS = "\n:root {\n --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 40%, transparent);\n --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 65%, transparent);\n scrollbar-gutter: stable;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: transparent transparent;\n}\n:hover, [data-oc-scrolling] {\n scrollbar-color: var(--oc-scrollbar-thumb) transparent;\n}\n/* Chromium's standard scrollbar properties otherwise override its pseudo-elements. */\n@supports selector(::-webkit-scrollbar) {\n *, :hover, [data-oc-scrolling] { scrollbar-width: auto; scrollbar-color: auto; }\n ::-webkit-scrollbar { width: 6px; height: 6px; background: transparent; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb {\n background: transparent;\n border-radius: 999px;\n min-width: 24px;\n min-height: 24px;\n }\n :hover::-webkit-scrollbar-thumb, [data-oc-scrolling]::-webkit-scrollbar-thumb { background: var(--oc-scrollbar-thumb); }\n ::-webkit-scrollbar-thumb:hover { background: var(--oc-scrollbar-thumb-hover); }\n ::-webkit-scrollbar-corner { background: transparent; }\n ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }\n}\n@media (forced-colors: active) {\n *, :hover, [data-oc-scrolling] { scrollbar-color: auto; }\n ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background: CanvasText; }\n}\n";
2
+ /**
3
+ * Marks whichever element scrolls (the root for document scrolling) with
4
+ * `GUEST_SCROLLING_ATTRIBUTE` and clears it once scrolling has been quiet for
5
+ * the hide delay. One capturing, passive listener per document; installing
6
+ * twice is a no-op. Self-contained on purpose: the server inlines its source.
7
+ */
8
+ export declare function installGuestScrollbarActivity(doc: Document): void;
9
+ /** Inline script form of `installGuestScrollbarActivity`, appended by the host to served guest HTML. */
10
+ export declare const GUEST_SCROLLBAR_SCRIPT: string;
@@ -1,4 +1,17 @@
1
- /** Shared by served extension documents and the UI kit, including older guest bundles. */
1
+ /**
2
+ * Scrollbars in extension documents behave like the app's own overlay
3
+ * scrollbars: invisible at rest, shown while the pointer is over the
4
+ * scrolling element or while it scrolls, then hidden again after
5
+ * one second (the app's `OverlayScrollbar` default).
6
+ *
7
+ * Hover works from CSS alone. "While it scrolls" needs the attribute set by
8
+ * `installGuestScrollbarActivity`; without it (an authored CSP that blocks the
9
+ * inline script), scrollbars still appear on hover.
10
+ *
11
+ * Shared by served extension documents and the UI kit, including older guest bundles.
12
+ */
13
+ /** Attribute present on an element for a moment after it scrolled. */
14
+ const GUEST_SCROLLING_ATTRIBUTE = 'data-oc-scrolling';
2
15
  export const GUEST_SCROLLBAR_CSS = `
3
16
  :root {
4
17
  --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 40%, transparent);
@@ -7,26 +20,58 @@ export const GUEST_SCROLLBAR_CSS = `
7
20
  }
8
21
  * {
9
22
  scrollbar-width: thin;
23
+ scrollbar-color: transparent transparent;
24
+ }
25
+ :hover, [${GUEST_SCROLLING_ATTRIBUTE}] {
10
26
  scrollbar-color: var(--oc-scrollbar-thumb) transparent;
11
27
  }
12
28
  /* Chromium's standard scrollbar properties otherwise override its pseudo-elements. */
13
29
  @supports selector(::-webkit-scrollbar) {
14
- * { scrollbar-width: auto; scrollbar-color: auto; }
30
+ *, :hover, [${GUEST_SCROLLING_ATTRIBUTE}] { scrollbar-width: auto; scrollbar-color: auto; }
15
31
  ::-webkit-scrollbar { width: 6px; height: 6px; background: transparent; }
16
- :root::-webkit-scrollbar, body::-webkit-scrollbar { background: var(--oc-bg, inherit); }
17
32
  ::-webkit-scrollbar-track { background: transparent; }
18
33
  ::-webkit-scrollbar-thumb {
19
- background: var(--oc-scrollbar-thumb);
34
+ background: transparent;
20
35
  border-radius: 999px;
21
36
  min-width: 24px;
22
37
  min-height: 24px;
23
38
  }
39
+ :hover::-webkit-scrollbar-thumb, [${GUEST_SCROLLING_ATTRIBUTE}]::-webkit-scrollbar-thumb { background: var(--oc-scrollbar-thumb); }
24
40
  ::-webkit-scrollbar-thumb:hover { background: var(--oc-scrollbar-thumb-hover); }
25
41
  ::-webkit-scrollbar-corner { background: transparent; }
26
42
  ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
27
43
  }
28
44
  @media (forced-colors: active) {
29
- * { scrollbar-color: auto; }
45
+ *, :hover, [${GUEST_SCROLLING_ATTRIBUTE}] { scrollbar-color: auto; }
30
46
  ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background: CanvasText; }
31
47
  }
32
48
  `;
49
+ /**
50
+ * Marks whichever element scrolls (the root for document scrolling) with
51
+ * `GUEST_SCROLLING_ATTRIBUTE` and clears it once scrolling has been quiet for
52
+ * the hide delay. One capturing, passive listener per document; installing
53
+ * twice is a no-op. Self-contained on purpose: the server inlines its source.
54
+ */
55
+ export function installGuestScrollbarActivity(doc) {
56
+ const root = doc.documentElement;
57
+ if (root.hasAttribute('data-oc-scrollbar-activity'))
58
+ return;
59
+ root.setAttribute('data-oc-scrollbar-activity', '');
60
+ const timers = new WeakMap();
61
+ doc.addEventListener('scroll', (event) => {
62
+ const target = event.target === doc ? root : event.target;
63
+ if (!(target instanceof Element))
64
+ return;
65
+ if (!target.hasAttribute('data-oc-scrolling'))
66
+ target.setAttribute('data-oc-scrolling', '');
67
+ const pending = timers.get(target);
68
+ if (pending !== undefined)
69
+ clearTimeout(pending);
70
+ timers.set(target, setTimeout(() => {
71
+ timers.delete(target);
72
+ target.removeAttribute('data-oc-scrolling');
73
+ }, 1000));
74
+ }, { capture: true, passive: true });
75
+ }
76
+ /** Inline script form of `installGuestScrollbarActivity`, appended by the host to served guest HTML. */
77
+ export const GUEST_SCROLLBAR_SCRIPT = `(${installGuestScrollbarActivity.toString()})(document);`;
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Host-side parse of what a browser provider answers. `data` stays whatever
4
+ * the service produced: the host hands it to the agent as is, and only
5
+ * `browser.capture` is read further (the image the host writes to disk).
6
+ */
7
+ export declare const browserProviderResultSchema: z.ZodUnion<readonly [z.ZodObject<{
8
+ ok: z.ZodLiteral<true>;
9
+ data: z.ZodObject<{}, z.core.$loose>;
10
+ }, z.core.$strip>, z.ZodObject<{
11
+ ok: z.ZodLiteral<false>;
12
+ error: z.ZodString;
13
+ }, z.core.$strip>]>;
14
+ export declare const browserControlActionSchema: z.ZodEnum<{
15
+ "browser.open": "browser.open";
16
+ "browser.snapshot": "browser.snapshot";
17
+ "browser.click": "browser.click";
18
+ "browser.type": "browser.type";
19
+ "browser.scroll": "browser.scroll";
20
+ "browser.back": "browser.back";
21
+ "browser.forward": "browser.forward";
22
+ "browser.inspect": "browser.inspect";
23
+ "browser.capture": "browser.capture";
24
+ "browser.resize": "browser.resize";
25
+ }>;
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import { BROWSER_CONTROL_ACTIONS } from "./service-providers.js";
3
+ /**
4
+ * Host-side parse of what a browser provider answers. `data` stays whatever
5
+ * the service produced: the host hands it to the agent as is, and only
6
+ * `browser.capture` is read further (the image the host writes to disk).
7
+ */
8
+ export const browserProviderResultSchema = z.union([
9
+ z.object({ ok: z.literal(true), data: z.object({}).passthrough() }),
10
+ z.object({ ok: z.literal(false), error: z.string().min(1).max(2_000) }),
11
+ ]);
12
+ export const browserControlActionSchema = z.enum(BROWSER_CONTROL_ACTIONS);