@neta-art/cohub 8.21.1 → 8.23.0

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/index.js CHANGED
@@ -14,15 +14,28 @@ const appAuthorizationTargetSchema = z.discriminatedUnion("kind", [
14
14
  }),
15
15
  z.object({ kind: z.literal("pick-space") })
16
16
  ]);
17
- const APP_SILENT_SHELL_SCOPE_SET = /* @__PURE__ */ new Set([
17
+ /**
18
+ * Read-only scopes the Shell may renew without a dialog for any App on the
19
+ * Space it shows. The API still checks the viewer's own permissions.
20
+ */
21
+ const APP_SILENT_SHELL_SCOPES = [
18
22
  "space.view",
19
23
  "file.view",
20
24
  "file.view.filtered",
21
25
  "session.view",
22
26
  "taskrun.view",
23
27
  "checkpoint.view"
24
- ]);
28
+ ];
29
+ const APP_SILENT_SHELL_SCOPE_SET = new Set(APP_SILENT_SHELL_SCOPES);
25
30
  const isAppSilentShellScope = (scope) => APP_SILENT_SHELL_SCOPE_SET.has(scope);
31
+ /**
32
+ * The API-enforced ceiling for Host consent (`consent: "host"`), given to Apps
33
+ * the Shell Space published or installed. Trust is decided by the Shell, the
34
+ * only holder of the viewer's token.
35
+ */
36
+ const APP_HOST_CONSENT_SCOPES = [...APP_SILENT_SHELL_SCOPES, "file.edit"];
37
+ const APP_HOST_CONSENT_SCOPE_SET = new Set(APP_HOST_CONSENT_SCOPES);
38
+ const isAppHostConsentScope = (scope) => APP_HOST_CONSENT_SCOPE_SET.has(scope);
26
39
  const appAuthorizationRequestSchema = z.object({
27
40
  target: appAuthorizationTargetSchema,
28
41
  scopes: z.array(z.string().min(1)).min(1).max(128),
@@ -281,8 +294,8 @@ const BUILTIN_VERSIONS = new Set(BOARD_BUILTIN_CAPABILITIES.map((c) => `${c.kind
281
294
  function isBuiltinBoardCapability(kind, id, version) {
282
295
  return BUILTIN_VERSIONS.has(`${kind}:${id}@${version}`);
283
296
  }
284
- const record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
285
- const finite = (value) => typeof value === "number" && Number.isFinite(value);
297
+ const record$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
298
+ const finite$1 = (value) => typeof value === "number" && Number.isFinite(value);
286
299
  function validateBuiltinBoardClip(clip, path = "clip") {
287
300
  if (!CLIPS.has(clip.kind)) return [];
288
301
  const errors = [];
@@ -300,13 +313,13 @@ function validateBuiltinBoardClip(clip, path = "clip") {
300
313
  if (clip.kind === "motion.path") {
301
314
  const points = clip.params.points;
302
315
  if (!Array.isArray(points) || points.length < 2 || points.length > 1e4) error("motion path must contain 2 to 10000 world-offset points", "params.points", "world-offset");
303
- else for (const [index, point] of points.entries()) if (!record(point) || !finite(point.x) || !finite(point.y)) error("motion path point must contain finite x and y", `params.points.${index}`, "world-offset");
316
+ else for (const [index, point] of points.entries()) if (!record$1(point) || !finite$1(point.x) || !finite$1(point.y)) error("motion path point must contain finite x and y", `params.points.${index}`, "world-offset");
304
317
  }
305
318
  if (clip.kind === "effects.particles") {
306
319
  const count = clip.params.count;
307
320
  if (!Number.isSafeInteger(count) || count < 1 || count > DEFAULT_BOARD_RENDER_LIMITS.particles) error(`particle count must be an integer from 1 to ${DEFAULT_BOARD_RENDER_LIMITS.particles}`, "params.count", void 0, "INVALID_PARTICLE_COUNT");
308
321
  const bounds = clip.params.bounds;
309
- if (!record(bounds) || !finite(bounds.x) || !finite(bounds.y) || !finite(bounds.width) || !finite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) error("particles require positive finite world bounds", "params.bounds", "world", "PARTICLE_BOUNDS_REQUIRED");
322
+ if (!record$1(bounds) || !finite$1(bounds.x) || !finite$1(bounds.y) || !finite$1(bounds.width) || !finite$1(bounds.height) || bounds.width <= 0 || bounds.height <= 0) error("particles require positive finite world bounds", "params.bounds", "world", "PARTICLE_BOUNDS_REQUIRED");
310
323
  }
311
324
  if (clip.kind === "camera.focus") {
312
325
  const parsed = BoardCameraFocusParamsSchema.safeParse(clip.params);
@@ -1036,14 +1049,14 @@ var GenerationPolicyError = class extends Error {
1036
1049
  this.name = "GenerationPolicyError";
1037
1050
  }
1038
1051
  };
1039
- function isRecord$4(value) {
1052
+ function isRecord$5(value) {
1040
1053
  return !!value && typeof value === "object" && !Array.isArray(value);
1041
1054
  }
1042
1055
  function isPrimitiveEnumValue(value) {
1043
1056
  return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1044
1057
  }
1045
1058
  function normalizeConstraint(value) {
1046
- if (!isRecord$4(value) || typeof value.kind !== "string") return null;
1059
+ if (!isRecord$5(value) || typeof value.kind !== "string") return null;
1047
1060
  if (value.kind === "enum") {
1048
1061
  if (!Array.isArray(value.values) || value.values.length === 0 || !value.values.every(isPrimitiveEnumValue)) return null;
1049
1062
  return {
@@ -1073,7 +1086,7 @@ function normalizeConstraint(value) {
1073
1086
  return null;
1074
1087
  }
1075
1088
  function normalizeGenerationPolicy(value) {
1076
- if (!isRecord$4(value) || value.version !== 1) return null;
1089
+ if (!isRecord$5(value) || value.version !== 1) return null;
1077
1090
  if (value.mode === "auto") return {
1078
1091
  version: 1,
1079
1092
  mode: "auto"
@@ -1081,10 +1094,10 @@ function normalizeGenerationPolicy(value) {
1081
1094
  if (value.mode !== "limited" || !Array.isArray(value.models) || value.models.length === 0) return null;
1082
1095
  const models = [];
1083
1096
  for (const item of value.models) {
1084
- if (!isRecord$4(item) || typeof item.model !== "string" || !item.model.trim()) return null;
1097
+ if (!isRecord$5(item) || typeof item.model !== "string" || !item.model.trim()) return null;
1085
1098
  const modelPolicy = { model: item.model.trim() };
1086
1099
  if (item.parameters !== void 0) {
1087
- if (!isRecord$4(item.parameters)) return null;
1100
+ if (!isRecord$5(item.parameters)) return null;
1088
1101
  const parameters = {};
1089
1102
  for (const [key, rawConstraint] of Object.entries(item.parameters)) {
1090
1103
  if (!key.trim()) return null;
@@ -1372,8 +1385,7 @@ const usage = z.object({
1372
1385
  }).nullable().optional()
1373
1386
  });
1374
1387
  const nativeOrigin = z.enum(["local_import"]);
1375
- /** One native execution maps to one Cohub Turn. No message-level fork anchors. */
1376
- const nativeTurnStartSchema = z.object({
1388
+ z.object({
1377
1389
  turnId: id$1,
1378
1390
  sessionId: id$1.nullable(),
1379
1391
  parentTurnId: id$1.nullable(),
@@ -1383,7 +1395,8 @@ const nativeTurnStartSchema = z.object({
1383
1395
  userContent: content$1,
1384
1396
  startedAt: z.iso.datetime(),
1385
1397
  sessionStartedAt: z.iso.datetime().optional(),
1386
- origin: nativeOrigin.optional()
1398
+ origin: nativeOrigin.optional(),
1399
+ controllable: z.boolean().optional()
1387
1400
  }).strict().refine((value) => value.sessionId !== null || value.parentTurnId === null, "A parent Turn requires a Session");
1388
1401
  const nativeTurnMessageSchema = z.object({
1389
1402
  content: content$1,
@@ -1393,9 +1406,10 @@ const nativeTurnMessageSchema = z.object({
1393
1406
  stopReason: z.string().max(64).nullable().optional(),
1394
1407
  errorMessage: z.string().max(16384).nullable().optional()
1395
1408
  }).strict();
1396
- /** Ephemeral display snapshot within a Turn; never a fork or persistence boundary. */
1409
+ /** Ephemeral display state within a Turn; never a fork or persistence boundary. */
1397
1410
  const nativeTurnProgressSchema = z.object({
1398
1411
  revision: z.number().int().safe().positive(),
1412
+ from: z.number().int().nonnegative().max(1e4).optional(),
1399
1413
  messages: z.array(nativeTurnMessageSchema).max(1e4)
1400
1414
  }).strict();
1401
1415
  /** Final, immutable Turn receipt. Native bytes are archived separately, without lossy normalization. */
@@ -1408,10 +1422,41 @@ const nativeTurnCompleteSchema = z.object({
1408
1422
  ]),
1409
1423
  completedAt: z.iso.datetime()
1410
1424
  }).strict();
1425
+ /** Durable ingest of one transcript's Turns. */
1426
+ const nativeIngestTurnSchema = z.object({
1427
+ turnId: id$1,
1428
+ parentTurnId: id$1.nullable(),
1429
+ userContent: content$1,
1430
+ startedAt: z.iso.datetime(),
1431
+ origin: nativeOrigin.optional(),
1432
+ result: nativeTurnCompleteSchema.nullable(),
1433
+ controllable: z.boolean().optional()
1434
+ }).strict();
1435
+ /** Turn states that end a Turn; a native Turn in one of them is settled on both sides. */
1436
+ const SETTLED_TURN_STATUSES = [
1437
+ "completed",
1438
+ "failed",
1439
+ "interrupted",
1440
+ "cancelled",
1441
+ "merged"
1442
+ ];
1443
+ /** How many Turns one ingest may carry; a larger backlog is sent as consecutive batches. */
1444
+ const NATIVE_INGEST_MAX_TURNS = 500;
1445
+ const nativeIngestSchema = z.object({
1446
+ harness: z.enum(["pi", "codex"]),
1447
+ nativeSessionId: z.string().min(1).max(255),
1448
+ turns: z.array(nativeIngestTurnSchema).min(1).max(500)
1449
+ }).strict();
1450
+ /** Which of these Turns the Space already recorded; lets a restart resume without local receipts. */
1451
+ const nativeKnownSchema = z.object({ turnIds: z.array(id$1).min(1).max(500) }).strict();
1411
1452
  const nativeRuntimeEventSchema = z.discriminatedUnion("type", [
1412
1453
  z.object({
1413
- type: z.literal("start"),
1414
- input: nativeTurnStartSchema
1454
+ type: z.literal("ingest"),
1455
+ input: nativeIngestSchema
1456
+ }).strict(),
1457
+ z.object({
1458
+ type: z.literal("known"),
1459
+ input: nativeKnownSchema
1415
1460
  }).strict(),
1416
1461
  z.object({
1417
1462
  type: z.literal("progress"),
@@ -1420,17 +1465,19 @@ const nativeRuntimeEventSchema = z.discriminatedUnion("type", [
1420
1465
  progress: nativeTurnProgressSchema
1421
1466
  }).strict(),
1422
1467
  z.object({
1423
- type: z.literal("complete"),
1468
+ type: z.literal("status"),
1424
1469
  sessionId: id$1,
1425
1470
  turnId: id$1,
1426
- result: nativeTurnCompleteSchema
1427
- }).strict(),
1428
- z.object({
1429
- type: z.literal("heartbeat"),
1430
- sessionId: id$1,
1431
- turnId: id$1
1471
+ controllable: z.boolean().optional()
1432
1472
  }).strict()
1433
1473
  ]);
1474
+ /** Pushed to the Runtime: stop a running native Turn, as the web's stop button asks. */
1475
+ const runtimeNativeStopSchema = z.object({
1476
+ type: z.literal("runtime.native.stop"),
1477
+ spaceId: id$1,
1478
+ sessionId: id$1,
1479
+ turnId: id$1
1480
+ }).strict();
1434
1481
  const NATIVE_SYNC_SOURCE = "native_client";
1435
1482
  const isNativeClientTurn = (meta) => {
1436
1483
  if (!meta || typeof meta !== "object") return false;
@@ -2344,8 +2391,8 @@ const APP_SURFACE_REQUEST_TIMEOUT_MS = 15e3;
2344
2391
  const APP_COMPOSER_CHIP_KEY_MAX_LENGTH = 80;
2345
2392
  const APP_COMPOSER_CHIP_LABEL_MAX_LENGTH = 120;
2346
2393
  const APP_COMPOSER_CHIP_CONTENT_MAX_BYTES = 32768;
2347
- const isRecord$3 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
2348
- const isSurfaceEnvelope = (value) => isRecord$3(value) && value.protocol === "cohub.app.surface" && value.version === 1;
2394
+ const isRecord$4 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
2395
+ const isSurfaceEnvelope = (value) => isRecord$4(value) && value.protocol === "cohub.app.surface" && value.version === 1;
2349
2396
  const parseAppSurfaceReady = (value) => {
2350
2397
  if (!isSurfaceEnvelope(value) || value.type !== "ready") return null;
2351
2398
  const methods = Array.isArray(value.methods) ? value.methods.filter((method) => typeof method === "string" && Boolean(method)) : [];
@@ -2359,7 +2406,7 @@ const parseAppSurfaceReady = (value) => {
2359
2406
  const parseAppSurfaceResponse = (value) => {
2360
2407
  if (!isSurfaceEnvelope(value) || value.type !== "response") return null;
2361
2408
  if (typeof value.requestId !== "string" || !value.requestId) return null;
2362
- const error = isRecord$3(value.error) ? {
2409
+ const error = isRecord$4(value.error) ? {
2363
2410
  code: typeof value.error.code === "string" && value.error.code ? value.error.code : "surface_error",
2364
2411
  message: typeof value.error.message === "string" ? value.error.message : "App surface call failed"
2365
2412
  } : void 0;
@@ -2379,7 +2426,7 @@ const parseComposerChipKey = (value) => {
2379
2426
  return key;
2380
2427
  };
2381
2428
  const parseAppComposerChipSet = (value) => {
2382
- if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.set" || !isRecord$3(value.chip)) return null;
2429
+ if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.set" || !isRecord$4(value.chip)) return null;
2383
2430
  const key = parseComposerChipKey(value.chip.key);
2384
2431
  if (!key || typeof value.chip.label !== "string" || typeof value.chip.content !== "string") return null;
2385
2432
  const label = value.chip.label.trim();
@@ -2456,6 +2503,7 @@ const buildAppComposerChipClear = (key) => ({
2456
2503
  //#endregion
2457
2504
  //#region ../protocol/dist/app-runtime.js
2458
2505
  const APP_RUNTIME_PROTOCOL = "cohub.app.runtime";
2506
+ const isRecord$3 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
2459
2507
  const buildAppRuntimeReady = () => ({
2460
2508
  protocol: APP_RUNTIME_PROTOCOL,
2461
2509
  version: 1,
@@ -2466,6 +2514,7 @@ const buildAppRuntimeCloseRequest = () => ({
2466
2514
  version: 1,
2467
2515
  type: "close.request"
2468
2516
  });
2517
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
2469
2518
  const buildAppRuntimeConfigureRequest = (input) => ({
2470
2519
  protocol: APP_RUNTIME_PROTOCOL,
2471
2520
  version: 1,
@@ -2480,6 +2529,154 @@ const buildAppRuntimePointer = (input) => ({
2480
2529
  y: input.y,
2481
2530
  down: input.down
2482
2531
  });
2532
+ const isRuntimeMessage = (value, type) => isRecord$3(value) && value.protocol === "cohub.app.runtime" && value.version === 1 && value.type === type;
2533
+ const envelope$1 = (type) => ({
2534
+ protocol: APP_RUNTIME_PROTOCOL,
2535
+ version: 1,
2536
+ type
2537
+ });
2538
+ const boundedText = (value, max) => {
2539
+ if (typeof value !== "string") return null;
2540
+ const text = value.replace(/\s+/g, " ").trim();
2541
+ return text ? text.slice(0, max) : null;
2542
+ };
2543
+ const buildAppRuntimeAnnounce = () => envelope$1("announce");
2544
+ const parseAppRuntimeAnnounce = (value) => isRuntimeMessage(value, "announce") ? buildAppRuntimeAnnounce() : null;
2545
+ const buildAppRuntimeWindowState = (state) => ({
2546
+ ...envelope$1("window.state"),
2547
+ title: state.title,
2548
+ status: state.status,
2549
+ dirty: state.dirty
2550
+ });
2551
+ const requestIdOf = (value) => typeof value.requestId === "string" && value.requestId && value.requestId.length <= 128 ? value.requestId : null;
2552
+ const buildAppRuntimeClosePrepare = (requestId) => ({
2553
+ ...envelope$1("close.prepare"),
2554
+ requestId
2555
+ });
2556
+ const parseAppRuntimeClosePrepare = (value) => {
2557
+ if (!isRuntimeMessage(value, "close.prepare")) return null;
2558
+ const requestId = requestIdOf(value);
2559
+ return requestId ? buildAppRuntimeClosePrepare(requestId) : null;
2560
+ };
2561
+ const buildAppRuntimeClosePrepared = (input) => ({
2562
+ ...envelope$1("close.prepared"),
2563
+ requestId: input.requestId,
2564
+ ok: input.ok
2565
+ });
2566
+ /** Select all, copy, paste, cut, undo, redo: the browser's, and the App's. */
2567
+ const EDITING_CHORD_KEYS = /* @__PURE__ */ new Set([
2568
+ "a",
2569
+ "c",
2570
+ "v",
2571
+ "x",
2572
+ "y",
2573
+ "z"
2574
+ ]);
2575
+ /** Whether a Ctrl / Meta chord may reach the host; both sides apply this rule. */
2576
+ const isAppRuntimeHostChord = (input) => (input.ctrlKey || input.metaKey) && !EDITING_CHORD_KEYS.has(input.key.toLowerCase());
2577
+ const buildAppRuntimeKey = (input) => ({
2578
+ ...envelope$1("key"),
2579
+ key: input.key,
2580
+ code: input.code,
2581
+ altKey: input.altKey,
2582
+ ctrlKey: input.ctrlKey,
2583
+ metaKey: input.metaKey,
2584
+ shiftKey: input.shiftKey
2585
+ });
2586
+ /** Resource kinds the Cohub host can drop into an App. */
2587
+ const APP_DROP_RESOURCE_TYPES = [
2588
+ "file",
2589
+ "task",
2590
+ "app",
2591
+ "session",
2592
+ "checkpoint"
2593
+ ];
2594
+ const isDropResourceType = (value) => APP_DROP_RESOURCE_TYPES.includes(value);
2595
+ const MAX_DROP_RESOURCES = 100;
2596
+ const parseDropResource = (value) => {
2597
+ if (!isRecord$3(value) || !isDropResourceType(value.type)) return null;
2598
+ const ref = boundedText(value.ref, 4096);
2599
+ if (!ref) return null;
2600
+ const resource = {
2601
+ type: value.type,
2602
+ ref
2603
+ };
2604
+ for (const key of [
2605
+ "title",
2606
+ "path",
2607
+ "mimeType",
2608
+ "appId",
2609
+ "href",
2610
+ "taskRunId"
2611
+ ]) {
2612
+ const text = boundedText(value[key], 4096);
2613
+ if (text) resource[key] = text;
2614
+ }
2615
+ if (finite(value.size) && value.size >= 0) resource.size = value.size;
2616
+ if (finite(value.mtimeMs)) resource.mtimeMs = value.mtimeMs;
2617
+ if (isRecord$3(value.snapshot)) resource.snapshot = value.snapshot;
2618
+ return resource;
2619
+ };
2620
+ const parseDropTypes = (value) => Array.isArray(value) ? [...new Set(value.filter(isDropResourceType))] : [];
2621
+ const buildAppRuntimeDropConfig = (accept) => ({
2622
+ ...envelope$1("drop.config"),
2623
+ accept: [...accept]
2624
+ });
2625
+ const buildAppRuntimeDrag = (input) => ({
2626
+ ...envelope$1("drag"),
2627
+ phase: input.phase,
2628
+ x: input.x,
2629
+ y: input.y,
2630
+ types: [...input.types],
2631
+ ...input.phase === "drop" ? { resources: input.resources ?? [] } : {}
2632
+ });
2633
+ const parseAppRuntimeDrag = (value) => {
2634
+ if (!isRuntimeMessage(value, "drag")) return null;
2635
+ if (value.phase !== "over" && value.phase !== "leave" && value.phase !== "drop") return null;
2636
+ if (!finite(value.x) || !finite(value.y)) return null;
2637
+ const resources = value.phase === "drop" && Array.isArray(value.resources) ? value.resources.slice(0, MAX_DROP_RESOURCES).map(parseDropResource).filter((resource) => resource !== null) : void 0;
2638
+ return buildAppRuntimeDrag({
2639
+ phase: value.phase,
2640
+ x: value.x,
2641
+ y: value.y,
2642
+ types: parseDropTypes(value.types),
2643
+ ...resources ? { resources } : {}
2644
+ });
2645
+ };
2646
+ /** Public design tokens, sent as resolved CSS values. */
2647
+ const APP_APPEARANCE_TOKENS = [
2648
+ "bg-primary",
2649
+ "bg-content",
2650
+ "bg-surface",
2651
+ "bg-elevated",
2652
+ "bg-input",
2653
+ "bg-hover",
2654
+ "bg-active",
2655
+ "text-primary",
2656
+ "text-secondary",
2657
+ "text-tertiary",
2658
+ "text-placeholder",
2659
+ "text-disabled",
2660
+ "border-primary",
2661
+ "border-subtle",
2662
+ "brand",
2663
+ "brand-hover",
2664
+ "brand-soft",
2665
+ "brand-muted",
2666
+ "brand-border",
2667
+ "brand-ring",
2668
+ "brand-contrast-fg",
2669
+ "error-fg",
2670
+ "selection-bg",
2671
+ "overlay-scrim",
2672
+ "shadow-subtle",
2673
+ "shadow-medium",
2674
+ "shadow-strong",
2675
+ "font-sans",
2676
+ "font-mono"
2677
+ ];
2678
+ /** CSS custom property an App reads a shared token from, e.g. `--cohub-bg-primary`. */
2679
+ const appAppearanceVar = (token) => `--cohub-${token}`;
2483
2680
  //#endregion
2484
2681
  //#region ../protocol/dist/app-navigation.js
2485
2682
  const APP_NAVIGATION_PROTOCOL = "cohub.app.navigation";
@@ -3104,6 +3301,8 @@ var AppSurfaceApi = class {
3104
3301
  listening = false;
3105
3302
  allowedOrigins = null;
3106
3303
  trustedOrigin;
3304
+ /** The chip on show, so it can be announced again. */
3305
+ chip = null;
3107
3306
  allowHostOrigins(origins) {
3108
3307
  this.allowedOrigins = origins.map((origin) => origin.trim()).filter(Boolean).map((origin) => {
3109
3308
  try {
@@ -3134,11 +3333,14 @@ var AppSurfaceApi = class {
3134
3333
  setComposerChip(chip) {
3135
3334
  const message = parseAppComposerChipSet(buildAppComposerChipSet(chip));
3136
3335
  if (!message) throw new Error("Invalid App composer chip");
3336
+ this.chip = message.chip;
3337
+ this.start();
3137
3338
  this.post(message);
3138
3339
  }
3139
3340
  clearComposerChip(key) {
3140
3341
  const message = parseAppComposerChipClear(buildAppComposerChipClear(key));
3141
3342
  if (!message) throw new Error("Invalid App composer chip key");
3343
+ if (this.chip?.key === message.key) this.chip = null;
3142
3344
  this.post(message);
3143
3345
  }
3144
3346
  announce() {
@@ -3163,6 +3365,11 @@ var AppSurfaceApi = class {
3163
3365
  onMessage = (event) => {
3164
3366
  if (typeof window === "undefined" || event.source !== window.parent) return;
3165
3367
  if (!this.isTrusted(event.origin)) return;
3368
+ if (parseAppRuntimeAnnounce(event.data)) {
3369
+ if (this.handlers.size > 0) this.announce();
3370
+ if (this.chip) this.post(buildAppComposerChipSet(this.chip));
3371
+ return;
3372
+ }
3166
3373
  const request = parseAppSurfaceRequest(event.data);
3167
3374
  if (!request) return;
3168
3375
  const commandId = request.commandId;
@@ -3267,6 +3474,171 @@ function attachAppEmbed(frame, options) {
3267
3474
  };
3268
3475
  }
3269
3476
  //#endregion
3477
+ //#region src/app-window.ts
3478
+ /** One document listener for all clients, so a chord reaches the host once. */
3479
+ const keyTransports = /* @__PURE__ */ new Set();
3480
+ function forwardKey(event) {
3481
+ if (event.isComposing || !isAppRuntimeHostChord(event)) return;
3482
+ setTimeout(() => {
3483
+ if (event.defaultPrevented) return;
3484
+ [...keyTransports].find((candidate) => candidate.isHostConnected?.())?.notify?.(buildAppRuntimeKey({
3485
+ key: event.key,
3486
+ code: event.code,
3487
+ altKey: event.altKey,
3488
+ ctrlKey: event.ctrlKey,
3489
+ metaKey: event.metaKey,
3490
+ shiftKey: event.shiftKey
3491
+ }));
3492
+ }, 0);
3493
+ }
3494
+ function attachKeys(transport) {
3495
+ if (keyTransports.size === 0) window.addEventListener("keydown", forwardKey);
3496
+ keyTransports.add(transport);
3497
+ }
3498
+ function detachKeys(transport) {
3499
+ if (!keyTransports.delete(transport)) return;
3500
+ if (keyTransports.size === 0) window.removeEventListener("keydown", forwardKey);
3501
+ }
3502
+ /** The App's side of its host window; a no-op outside a Cohub host. */
3503
+ var AppWindowApi = class {
3504
+ transport;
3505
+ runtime;
3506
+ state = {
3507
+ title: null,
3508
+ status: "idle",
3509
+ dirty: false
3510
+ };
3511
+ /** Only a client that reported state re-announces it; others must not reset it. */
3512
+ stateReported = false;
3513
+ beforeClose = null;
3514
+ dropHandler = null;
3515
+ hostUnsubscribe = null;
3516
+ constructor(transport, runtime) {
3517
+ this.transport = transport;
3518
+ this.runtime = runtime;
3519
+ if (typeof window === "undefined" || !transport.notify) return;
3520
+ if (window.parent === window || typeof window.addEventListener !== "function") return;
3521
+ attachKeys(transport);
3522
+ }
3523
+ /** Report the tab title, save status, and whether work is still unsaved. */
3524
+ setState(patch) {
3525
+ this.state = {
3526
+ ...this.state,
3527
+ ...patch
3528
+ };
3529
+ this.stateReported = true;
3530
+ this.listen();
3531
+ this.transport.notify?.(buildAppRuntimeWindowState(this.state));
3532
+ }
3533
+ /** Persist work before a `dirty` window closes; `false` or throw keeps it open. */
3534
+ onBeforeClose(handler) {
3535
+ this.beforeClose = handler;
3536
+ this.listen();
3537
+ return () => {
3538
+ if (this.beforeClose === handler) this.beforeClose = null;
3539
+ };
3540
+ }
3541
+ /** Accept Cohub resources dragged from the host onto this App. */
3542
+ onDrop(handler) {
3543
+ this.dropHandler = handler;
3544
+ this.listen();
3545
+ this.transport.notify?.(buildAppRuntimeDropConfig(handler.accept));
3546
+ return () => {
3547
+ if (this.dropHandler !== handler) return;
3548
+ this.dropHandler = null;
3549
+ this.transport.notify?.(buildAppRuntimeDropConfig([]));
3550
+ };
3551
+ }
3552
+ /** The window's file: on open, on reopen, and after a rename or move. */
3553
+ onLaunch(listener) {
3554
+ let last = null;
3555
+ const handle = (context) => {
3556
+ const invocation = context?.invocation;
3557
+ if (!invocation?.file || !invocation.spaceId) return;
3558
+ const key = `${invocation.id ?? ""}:${invocation.spaceId}:${invocation.file.path}`;
3559
+ if (key === last) return;
3560
+ last = key;
3561
+ listener({ file: {
3562
+ spaceId: invocation.spaceId,
3563
+ path: invocation.file.path
3564
+ } });
3565
+ };
3566
+ const unsubscribe = this.runtime.onContextChanged(handle);
3567
+ this.runtime.context().then(handle, () => {});
3568
+ return unsubscribe;
3569
+ }
3570
+ dispose() {
3571
+ this.hostUnsubscribe?.();
3572
+ this.hostUnsubscribe = null;
3573
+ detachKeys(this.transport);
3574
+ }
3575
+ listen() {
3576
+ if (this.hostUnsubscribe || !this.transport.subscribeHostMessages) return;
3577
+ this.hostUnsubscribe = this.transport.subscribeHostMessages((data) => {
3578
+ if (parseAppRuntimeAnnounce(data)) {
3579
+ this.announce();
3580
+ return;
3581
+ }
3582
+ if (!this.transport.isHostConnected?.()) return;
3583
+ const prepare = parseAppRuntimeClosePrepare(data);
3584
+ if (prepare) {
3585
+ this.prepareClose(prepare.requestId);
3586
+ return;
3587
+ }
3588
+ const drag = parseAppRuntimeDrag(data);
3589
+ if (drag) this.dispatchDrag(drag);
3590
+ });
3591
+ this.runtime.context().catch(() => {});
3592
+ }
3593
+ /** A reloaded frame forgot the tab state and drops; report them again. */
3594
+ announce() {
3595
+ if (this.stateReported) this.transport.notify?.(buildAppRuntimeWindowState(this.state));
3596
+ if (this.dropHandler) this.transport.notify?.(buildAppRuntimeDropConfig(this.dropHandler.accept));
3597
+ }
3598
+ async prepareClose(requestId) {
3599
+ let ok;
3600
+ try {
3601
+ ok = this.beforeClose ? await this.beforeClose() !== false : !this.state.dirty;
3602
+ } catch {
3603
+ ok = false;
3604
+ }
3605
+ this.transport.notify?.(buildAppRuntimeClosePrepared({
3606
+ requestId,
3607
+ ok
3608
+ }));
3609
+ }
3610
+ dispatchDrag(drag) {
3611
+ const handler = this.dropHandler;
3612
+ if (!handler) return;
3613
+ const point = {
3614
+ x: drag.x,
3615
+ y: drag.y,
3616
+ types: drag.types
3617
+ };
3618
+ if (drag.phase === "over") handler.over?.(point);
3619
+ else if (drag.phase === "leave") handler.leave?.();
3620
+ else handler.drop({
3621
+ ...point,
3622
+ resources: drag.resources ?? []
3623
+ });
3624
+ }
3625
+ };
3626
+ /** Writes host tokens (`--cohub-*`), color scheme, and `lang` onto `root`. */
3627
+ function applyAppAppearance(root, context) {
3628
+ const appearance = context?.appearance;
3629
+ for (const token of APP_APPEARANCE_TOKENS) {
3630
+ const value = appearance?.tokens[token];
3631
+ if (value) root.style.setProperty(appAppearanceVar(token), value);
3632
+ else root.style.removeProperty(appAppearanceVar(token));
3633
+ }
3634
+ if (appearance) {
3635
+ root.style.colorScheme = appearance.colorScheme;
3636
+ root.dataset.cohubColorScheme = appearance.colorScheme;
3637
+ root.dataset.cohubReducedMotion = String(appearance.reducedMotion);
3638
+ }
3639
+ if (context?.locale) root.lang = context.locale;
3640
+ }
3641
+ //#endregion
3270
3642
  //#region src/app-runtime.ts
3271
3643
  var AppRuntimeError = class extends Error {
3272
3644
  code;
@@ -3306,6 +3678,36 @@ var ParentBridgeTransport = class {
3306
3678
  contextListener = null;
3307
3679
  diagnosticListeners = /* @__PURE__ */ new Set();
3308
3680
  diagnosticListener = null;
3681
+ hostListeners = /* @__PURE__ */ new Set();
3682
+ hostListener = null;
3683
+ announceListener = null;
3684
+ isHostConnected() {
3685
+ return this.trustedParentOrigin !== null;
3686
+ }
3687
+ isFromParent(event) {
3688
+ if (event.source !== window.parent) return false;
3689
+ const parentOrigin = this.trustedParentOrigin ?? getParentOrigin();
3690
+ return !parentOrigin || event.origin === parentOrigin;
3691
+ }
3692
+ subscribeHostMessages(listener) {
3693
+ if (!hasParent() || typeof window.addEventListener !== "function") return () => {};
3694
+ this.hostListeners.add(listener);
3695
+ this.announceReady();
3696
+ if (!this.hostListener) {
3697
+ this.hostListener = (event) => {
3698
+ if (!this.isFromParent(event)) return;
3699
+ for (const current of this.hostListeners) current(event.data);
3700
+ };
3701
+ window.addEventListener("message", this.hostListener);
3702
+ }
3703
+ return () => {
3704
+ this.hostListeners.delete(listener);
3705
+ if (this.hostListeners.size === 0 && this.hostListener) {
3706
+ window.removeEventListener("message", this.hostListener);
3707
+ this.hostListener = null;
3708
+ }
3709
+ };
3710
+ }
3309
3711
  /**
3310
3712
  * Tells the host the runtime is ready once, before any subscription. Hosts
3311
3713
  * gate unsolicited messages (context pushes, diagnostics) on this handshake,
@@ -3319,8 +3721,19 @@ var ParentBridgeTransport = class {
3319
3721
  try {
3320
3722
  window.parent.postMessage(buildAppRuntimeReady(), parentOrigin);
3321
3723
  this.readyAnnounced = true;
3724
+ this.answerAnnounceRequests();
3322
3725
  } catch {}
3323
3726
  }
3727
+ /** Says ready again when a reloaded host asks. */
3728
+ answerAnnounceRequests() {
3729
+ if (this.announceListener || typeof window.addEventListener !== "function") return;
3730
+ this.announceListener = (event) => {
3731
+ if (!this.isFromParent(event) || !parseAppRuntimeAnnounce(event.data)) return;
3732
+ this.readyAnnounced = false;
3733
+ this.announceReady();
3734
+ };
3735
+ window.addEventListener("message", this.announceListener);
3736
+ }
3324
3737
  subscribeDiagnostics(listener) {
3325
3738
  if (!hasParent() || typeof window.addEventListener !== "function") return () => {};
3326
3739
  this.diagnosticListeners.add(listener);
@@ -4299,6 +4712,7 @@ var CohubClient = class {
4299
4712
  transport;
4300
4713
  websocketClient;
4301
4714
  appRuntime;
4715
+ appWindow;
4302
4716
  constructor(options = {}) {
4303
4717
  const apiBaseUrl = resolveApiBaseUrl(options);
4304
4718
  const appRuntime = options.app ?? options.work;
@@ -4314,6 +4728,10 @@ var CohubClient = class {
4314
4728
  fetch: options.fetch
4315
4729
  }), resolveWebBaseUrl({ env: options.env }));
4316
4730
  this.appRuntime = createAppRuntime(appTransport, appRuntime?.appId, appIdResolver);
4731
+ this.appWindow = new AppWindowApi(appTransport, {
4732
+ context: () => this.appRuntime.context(),
4733
+ onContextChanged: (listener) => this.appRuntime.onContextChanged(listener)
4734
+ });
4317
4735
  const executionToken = resolveExecutionToken();
4318
4736
  const getAccessToken = options.getAccessToken ?? (executionToken ? () => executionToken : (tokenOptions) => this.appRuntime.getAccessToken(tokenOptions));
4319
4737
  const resolvedOptions = {
@@ -4369,6 +4787,7 @@ var CohubClient = class {
4369
4787
  /** Releases listeners this client registered. Safe to call more than once. */
4370
4788
  dispose() {
4371
4789
  this.appRuntime.dispose();
4790
+ this.appWindow.dispose();
4372
4791
  }
4373
4792
  auth = {
4374
4793
  authorize: (input) => this.appRuntime.authorize(input),
@@ -4390,6 +4809,26 @@ var CohubClient = class {
4390
4809
  /** Expose callable methods from inside a published app. */
4391
4810
  surface: new AppSurfaceApi(),
4392
4811
  onContextChanged: (listener) => this.appRuntime.onContextChanged(listener),
4812
+ /** Files the host opens with this App — the first one and every later open. */
4813
+ onLaunch: (listener) => this.appWindow.onLaunch(listener),
4814
+ /** Accept Cohub resources the viewer drags from the host onto this App. */
4815
+ onDrop: (handler) => this.appWindow.onDrop(handler),
4816
+ window: {
4817
+ /** Report the tab title, save status, and whether work is still unsaved. */
4818
+ setState: (patch) => this.appWindow.setState(patch),
4819
+ /** Persist pending work before the host closes a `dirty` window. */
4820
+ onBeforeClose: (handler) => this.appWindow.onBeforeClose(handler)
4821
+ },
4822
+ appearance: {
4823
+ /** Keep `root` (default `<html>`) in sync with the host theme and locale. */
4824
+ sync: (root) => {
4825
+ const target = root ?? (typeof document === "undefined" ? null : document.documentElement);
4826
+ if (!target) return () => {};
4827
+ const apply = (context) => applyAppAppearance(target, context);
4828
+ const stop = this.appRuntime.onContextChanged(apply);
4829
+ this.appRuntime.context().then(apply, () => {});
4830
+ return stop;
4831
+ } },
4393
4832
  /** Ask the host to close this App's surface. */
4394
4833
  requestClose: () => this.appRuntime.requestClose(),
4395
4834
  /**
@@ -4759,7 +5198,17 @@ function cloneInvocation(invocation) {
4759
5198
  ...invocation.embedder ? { embedder: {
4760
5199
  appId: invocation.embedder.appId,
4761
5200
  slug: invocation.embedder.slug
4762
- } } : {}
5201
+ } } : {},
5202
+ ...invocation.file ? { file: { path: invocation.file.path } } : {}
5203
+ };
5204
+ }
5205
+ /** Host appearance can be reactive state too; copy it into plain data. */
5206
+ function cloneAppearance(appearance) {
5207
+ return {
5208
+ colorScheme: appearance.colorScheme,
5209
+ theme: appearance.theme,
5210
+ tokens: { ...appearance.tokens },
5211
+ reducedMotion: appearance.reducedMotion
4763
5212
  };
4764
5213
  }
4765
5214
  /**
@@ -5012,6 +5461,9 @@ function createAppBridgeCore(config) {
5012
5461
  async function getContext() {
5013
5462
  const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
5014
5463
  const shell = config.getShell?.() ?? config.shell;
5464
+ const locale = config.getLocale?.();
5465
+ const appearance = config.getAppearance?.();
5466
+ const windowState = config.getWindow?.();
5015
5467
  const appScopes = clonePermissionScopes(app.appScopes);
5016
5468
  const viewerUuid = await getViewerUuid();
5017
5469
  synchronizeViewer(viewerUuid);
@@ -5040,6 +5492,9 @@ function createAppBridgeCore(config) {
5040
5492
  session: shell.session ? { ...shell.session } : null,
5041
5493
  turn: shell.turn ? { ...shell.turn } : null
5042
5494
  } } : {},
5495
+ ...locale ? { locale } : {},
5496
+ ...appearance ? { appearance: cloneAppearance(appearance) } : {},
5497
+ ...windowState ? { window: { visible: windowState.visible } } : {},
5043
5498
  permissions: {
5044
5499
  scopes: normalizePermissionScopes([...appScopes, ...viewerGrants.flatMap((grant) => grant.scopes)]),
5045
5500
  appScopes,
@@ -5217,8 +5672,7 @@ function createAppBridgeCore(config) {
5217
5672
  /**
5218
5673
  * Calls the authorize endpoint. `silent` marks a background refresh of a
5219
5674
  * previous consent: the server then only renews a live grant and never
5220
- * creates or revives one, so a revoked grant cannot come back without a
5221
- * fresh dialog.
5675
+ * creates or revives one. `consent: "host"` may also create or widen one.
5222
5676
  */
5223
5677
  async function authorize(scopes, spaceId, options) {
5224
5678
  const incremental = Boolean(state.pendingAuth && authorizationRequests.has(state.pendingAuth.requestId));
@@ -5242,6 +5696,7 @@ function createAppBridgeCore(config) {
5242
5696
  scopes,
5243
5697
  ...spaceId ? { spaceId } : {},
5244
5698
  ...options?.silent ? { silent: true } : {},
5699
+ ...options?.consent ? { consent: options.consent } : {},
5245
5700
  ...incremental ? { scopeMode: "extend" } : {}
5246
5701
  })
5247
5702
  });
@@ -5288,17 +5743,15 @@ function createAppBridgeCore(config) {
5288
5743
  }
5289
5744
  }
5290
5745
  /**
5291
- * Silently renews the approved read-only scopes on the current Shell Space.
5746
+ * Grants scopes on the current Shell Space without opening the dialog.
5292
5747
  *
5293
5748
  * Deciding that a request needs no dialog is the Host's job: it compares the
5294
- * target against the Space it is actually showing. That check only decides
5295
- * *whether to ask*; the server still decides *what may be granted* through
5296
- * the silent path, so a revoked consent can never come back on its own.
5749
+ * target and trust; the server still caps what may be granted.
5297
5750
  */
5298
5751
  const shellAuthorizationInFlight = /* @__PURE__ */ new Map();
5299
5752
  const shellAuthorizationCache = /* @__PURE__ */ new Map();
5300
5753
  const SHELL_AUTH_CACHE_MS = 3e4;
5301
- async function authorizeShellSilently(scopes, spaceId) {
5754
+ async function authorizeShellSilently(scopes, spaceId, consent) {
5302
5755
  const shellContext = config.getShell?.() ?? config.shell;
5303
5756
  const shell = shellContext?.space ?? null;
5304
5757
  if (!shellContext || authorizationContext.surface === "broker" || !shell || shell.id !== spaceId) throw new AppAuthorizationError("Shell authorization is unavailable for this surface.", 403, "consent_required");
@@ -5307,13 +5760,16 @@ function createAppBridgeCore(config) {
5307
5760
  await startSignIn();
5308
5761
  throw new AppLoginRedirect();
5309
5762
  }
5310
- const cacheKey = `${viewerUuid}:${app.id}:${spaceId}:${[...scopes].sort().join(",")}`;
5763
+ const cacheKey = `${viewerUuid}:${app.id}:${spaceId}:${consent}:${[...scopes].sort().join(",")}`;
5311
5764
  const cached = shellAuthorizationCache.get(cacheKey);
5312
5765
  if (cached && cached.expiresAt > Date.now()) return cached.result;
5313
5766
  if (cached) shellAuthorizationCache.delete(cacheKey);
5314
5767
  const pending = shellAuthorizationInFlight.get(cacheKey);
5315
5768
  if (pending) return pending;
5316
- const request = authorize(scopes, spaceId, { silent: true });
5769
+ const request = authorize(scopes, spaceId, consent === "host" ? {
5770
+ silent: true,
5771
+ consent: "host"
5772
+ } : { silent: true });
5317
5773
  shellAuthorizationInFlight.set(cacheKey, request);
5318
5774
  try {
5319
5775
  const result = await request;
@@ -5326,6 +5782,20 @@ function createAppBridgeCore(config) {
5326
5782
  shellAuthorizationInFlight.delete(cacheKey);
5327
5783
  }
5328
5784
  }
5785
+ /** Trusted Apps get Host consent; others may only renew read-only grants. */
5786
+ async function shellConsentFor(scopes, spaceId) {
5787
+ if (!scopes.every(isAppHostConsentScope)) return null;
5788
+ if (await isTrustedIn(spaceId)) return "host";
5789
+ return scopes.every(isAppSilentShellScope) ? "renew" : null;
5790
+ }
5791
+ async function isTrustedIn(spaceId) {
5792
+ if (app.spaceId === spaceId) return true;
5793
+ try {
5794
+ return await config.isInstalledIn?.(spaceId) === true;
5795
+ } catch {
5796
+ return false;
5797
+ }
5798
+ }
5329
5799
  /**
5330
5800
  * Reads the Shell Space supplied by the current Host. Invocation context is
5331
5801
  * deliberately excluded: an App may be opened from one Space while the Shell
@@ -5789,9 +6259,11 @@ function createAppBridgeCore(config) {
5789
6259
  return;
5790
6260
  }
5791
6261
  const shellSpace = resolveShellSpace();
5792
- const shellTargetId = spaceId ?? shellSpace?.id;
5793
- if (!alwaysAsk && !selectSpace && Boolean(shellTargetId && (!spaceId || shellSpace?.id === spaceId)) && scopes.every(isAppSilentShellScope) && shellTargetId) try {
5794
- const result = await authorizeShellSilently(scopes, shellTargetId);
6262
+ const shellTargetId = !alwaysAsk && !selectSpace && shellSpace && (!spaceId || spaceId === shellSpace.id) ? shellSpace.id : null;
6263
+ const shellConsent = shellTargetId ? await shellConsentFor(scopes, shellTargetId) : null;
6264
+ if (state.pendingAuth !== reserved) return;
6265
+ if (shellConsent && shellTargetId) try {
6266
+ const result = await authorizeShellSilently(scopes, shellTargetId, shellConsent);
5795
6267
  if (state.pendingAuth !== reserved) return;
5796
6268
  setGrantedAppScopes(await getViewerUuid(), app.id, result.scopes, result.spaceId);
5797
6269
  replyPendingAuth(reserved, authorizeResult(result.token, result.spaceId, shellSpace?.name ?? null));
@@ -6044,6 +6516,213 @@ function createAppBridgeCore(config) {
6044
6516
  };
6045
6517
  }
6046
6518
  //#endregion
6519
+ //#region src/generation-blocks.ts
6520
+ /** Tolerant readers for generation payloads, shared by every task projection. */
6521
+ const EXCERPT_LIMIT = 240;
6522
+ function record(value) {
6523
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6524
+ }
6525
+ function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
6526
+ if (typeof value !== "string") return void 0;
6527
+ const clean = value.replace(/\s+/g, " ").trim();
6528
+ if (!clean) return void 0;
6529
+ return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
6530
+ }
6531
+ function blockText(block) {
6532
+ return cleanExcerpt(block.text ?? block.content ?? block.value);
6533
+ }
6534
+ function blockUrl(block) {
6535
+ const source = record(block.source);
6536
+ return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
6537
+ }
6538
+ function blockMimeType(block) {
6539
+ const source = record(block.source);
6540
+ const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
6541
+ return typeof value === "string" ? value : void 0;
6542
+ }
6543
+ function positiveNumber(value) {
6544
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
6545
+ }
6546
+ function blockNaturalSize(block) {
6547
+ const source = record(block.source);
6548
+ const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
6549
+ const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
6550
+ return {
6551
+ ...naturalWidth ? { naturalWidth } : {},
6552
+ ...naturalHeight ? { naturalHeight } : {}
6553
+ };
6554
+ }
6555
+ function contentBlocks(value) {
6556
+ return Array.isArray(value) ? value.filter((item) => record(item)) : [];
6557
+ }
6558
+ function taskData(run) {
6559
+ const payload = record(run.payload);
6560
+ return record(payload?.data) ?? payload;
6561
+ }
6562
+ function generationOutput(run) {
6563
+ return contentBlocks(record(run.result)?.output);
6564
+ }
6565
+ function generationPrompt(run) {
6566
+ for (const block of contentBlocks(taskData(run)?.content)) {
6567
+ const text = blockText(block);
6568
+ if (text) return text;
6569
+ }
6570
+ }
6571
+ function blockMeta(block) {
6572
+ return record(block.meta);
6573
+ }
6574
+ function blockIdentity(block) {
6575
+ const meta = blockMeta(block);
6576
+ const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
6577
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
6578
+ return cleanExcerpt(String(value), 220);
6579
+ }
6580
+ function blockTitle(block) {
6581
+ return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
6582
+ }
6583
+ function blockDurationMs(block) {
6584
+ const source = record(block.source);
6585
+ const meta = blockMeta(block);
6586
+ const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
6587
+ if (milliseconds) return Math.round(milliseconds);
6588
+ const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
6589
+ return seconds ? Math.round(seconds * 1e3) : void 0;
6590
+ }
6591
+ function blockPreviewUrl(block) {
6592
+ const source = record(block.source);
6593
+ return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
6594
+ }
6595
+ /**
6596
+ * Image roles that depict playable media, by the media types they may cover;
6597
+ * `last_frame` stays a result of its own.
6598
+ */
6599
+ const COVER_ROLES = /* @__PURE__ */ new Map([
6600
+ ["first_frame", /* @__PURE__ */ new Set(["video"])],
6601
+ ["cover", /* @__PURE__ */ new Set(["video", "audio"])],
6602
+ ["poster", /* @__PURE__ */ new Set(["video", "audio"])],
6603
+ ["thumbnail", /* @__PURE__ */ new Set(["video", "audio"])]
6604
+ ]);
6605
+ function isPlayable(block) {
6606
+ return block.type === "video" || block.type === "audio";
6607
+ }
6608
+ /**
6609
+ * Pair playable media with its cover image, returning media index → cover
6610
+ * index. An image sharing the provider id wins (music tracks and their art,
6611
+ * shared by every media of that id); otherwise cover-role images pair in
6612
+ * order with remaining media their role fits (a video and its `first_frame`).
6613
+ * Covers need a remote URL, so an inline image is never folded away.
6614
+ */
6615
+ function generationCovers(blocks) {
6616
+ const covers = /* @__PURE__ */ new Map();
6617
+ const isCover = (block) => block.type === "image" && blockUrl(block) !== void 0 && blockMeta(block)?.role !== "last_frame";
6618
+ blocks.forEach((block, index) => {
6619
+ const id = blockIdentity(block);
6620
+ if (!isPlayable(block) || id === void 0) return;
6621
+ const cover = blocks.findIndex((candidate) => isCover(candidate) && blockIdentity(candidate) === id);
6622
+ if (cover >= 0) covers.set(index, cover);
6623
+ });
6624
+ const paired = new Set(covers.values());
6625
+ const roleCovers = blocks.flatMap((block, index) => {
6626
+ const role = blockMeta(block)?.role;
6627
+ const types = typeof role === "string" ? COVER_ROLES.get(role) : void 0;
6628
+ return isCover(block) && !paired.has(index) && types ? [{
6629
+ index,
6630
+ types
6631
+ }] : [];
6632
+ });
6633
+ blocks.forEach((block, index) => {
6634
+ if (!isPlayable(block) || covers.has(index)) return;
6635
+ const at = roleCovers.findIndex(({ types }) => types.has(String(block.type)));
6636
+ const [cover] = at < 0 ? [] : roleCovers.splice(at, 1);
6637
+ if (cover) covers.set(index, cover.index);
6638
+ });
6639
+ return covers;
6640
+ }
6641
+ //#endregion
6642
+ //#region src/generation-task.ts
6643
+ const INLINE_KEYS = [
6644
+ "data",
6645
+ "base64",
6646
+ "contentBase64"
6647
+ ];
6648
+ function isDeferred(block) {
6649
+ return block.deferredBase64 === true || record(block.source)?.deferredBase64 === true;
6650
+ }
6651
+ const MEDIA_TYPE_PATTERN = /^(image|video|audio)\/[\w.+-]+$/;
6652
+ const DEFAULT_MEDIA_TYPES = {
6653
+ image: "image/png",
6654
+ video: "video/mp4",
6655
+ audio: "audio/mpeg"
6656
+ };
6657
+ function outputType(value) {
6658
+ return value === "image" || value === "video" || value === "audio" || value === "text" ? value : null;
6659
+ }
6660
+ /** Covers fold into the media they depict instead of listing as images. */
6661
+ function projectOutputs(run) {
6662
+ const blocks = generationOutput(run);
6663
+ const covers = generationCovers(blocks);
6664
+ const coverIndexes = new Set(covers.values());
6665
+ const coverUrl = (index) => {
6666
+ const cover = covers.get(index);
6667
+ return cover === void 0 ? void 0 : blockUrl(blocks[cover] ?? {});
6668
+ };
6669
+ return blocks.flatMap((block, index) => {
6670
+ const type = outputType(block.type);
6671
+ if (!type || coverIndexes.has(index)) return [];
6672
+ const size = blockNaturalSize(block);
6673
+ const text = type === "text" ? blockText(block) ?? null : null;
6674
+ if (type === "text" && !text) return [];
6675
+ return [{
6676
+ index,
6677
+ type,
6678
+ url: type === "text" ? null : blockUrl(block) ?? null,
6679
+ previewUrl: blockPreviewUrl(block) ?? coverUrl(index) ?? null,
6680
+ title: blockTitle(block) ?? null,
6681
+ mimeType: blockMimeType(block) ?? null,
6682
+ text,
6683
+ width: size.naturalWidth ?? null,
6684
+ height: size.naturalHeight ?? null,
6685
+ durationMs: blockDurationMs(block) ?? null,
6686
+ deferred: type !== "text" && isDeferred(block)
6687
+ }];
6688
+ });
6689
+ }
6690
+ function toGenerationTaskView(run) {
6691
+ const data = taskData(run);
6692
+ const result = record(run.result);
6693
+ const model = data?.model ?? result?.model;
6694
+ return {
6695
+ id: run.id,
6696
+ status: run.status,
6697
+ spaceId: run.spaceId,
6698
+ sessionId: run.sessionId,
6699
+ turnId: run.turnId,
6700
+ model: typeof model === "string" && model ? model : null,
6701
+ prompt: generationPrompt(run) ?? null,
6702
+ errorMessage: run.errorMessage,
6703
+ outputs: run.status === "completed" ? projectOutputs(run) : [],
6704
+ createdAt: run.createdAt,
6705
+ startedAt: run.startedAt,
6706
+ finishedAt: run.finishedAt,
6707
+ updatedAt: run.updatedAt
6708
+ };
6709
+ }
6710
+ function isActiveGenerationTask(task) {
6711
+ return task.status === "pending" || task.status === "running";
6712
+ }
6713
+ /** Playable source of an output: its URL, or a data URL for inline payloads. */
6714
+ function generationOutputSource(result, outputIndex) {
6715
+ const block = contentBlocks(record(result)?.output)[outputIndex];
6716
+ if (!block) return null;
6717
+ const url = blockUrl(block);
6718
+ if (url) return url;
6719
+ const source = record(block.source);
6720
+ const data = INLINE_KEYS.map((key) => source?.[key] ?? block[key]).find((value) => typeof value === "string" && value !== "");
6721
+ if (!data) return null;
6722
+ const declared = blockMimeType(block);
6723
+ return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
6724
+ }
6725
+ //#endregion
6047
6726
  //#region src/board/animation.ts
6048
6727
  function track(input) {
6049
6728
  return BoardTrackSchema.parse(input);
@@ -6289,4 +6968,4 @@ function createBoardExtensionRegistry() {
6289
6968
  }
6290
6969
  const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
6291
6970
  //#endregion
6292
- export { APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, SpaceWebhooksApi, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeTurnCompleteSchema, nativeTurnProgressSchema, nativeTurnStartSchema, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeReadySchema, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, track, trimProjectionTurnsToCompaction, validateArchiveBoundary };
6971
+ export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, SpaceWebhooksApi, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, track, trimProjectionTurnsToCompaction, validateArchiveBoundary };