@neta-art/cohub-cli 10.10.0 → 10.11.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.
@@ -3,5 +3,4 @@ export declare const BOARD_CREATE_INPUT_MAX_BYTES: number;
3
3
  export declare const BOARD_DOMAIN_INPUT_MAX_BYTES: number;
4
4
  export declare function parseBoardJsonObject(text: string, source?: string): Record<string, unknown>;
5
5
  export declare function readBoardJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
6
- export declare function resolveBoardId(spaceId: string, target: string): Promise<string>;
7
6
  export declare function writeBoardOutput(path: string, bytes: Uint8Array, force?: boolean): Promise<void>;
@@ -2,8 +2,6 @@ import { randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { access, link, open, rename, unlink } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
- import { isBoardPath, parseBoardManifest, } from "@neta-art/cohub/board";
6
- import { createClient } from "./client.js";
7
5
  export const BOARD_TRANSACTION_INPUT_MAX_BYTES = 16 * 1024 * 1024;
8
6
  export const BOARD_CREATE_INPUT_MAX_BYTES = 32 * 1024 * 1024;
9
7
  export const BOARD_DOMAIN_INPUT_MAX_BYTES = 1024 * 1024;
@@ -62,15 +60,6 @@ export async function readBoardJsonObject(source, maxBytes = BOARD_DOMAIN_INPUT_
62
60
  await handle.close();
63
61
  }
64
62
  }
65
- export async function resolveBoardId(spaceId, target) {
66
- if (!isBoardPath(target))
67
- return target;
68
- const file = await createClient().space(spaceId).files.read(target);
69
- if (!("content" in file) || typeof file.content !== "string") {
70
- throw new Error(`${target} is not a readable Board manifest`);
71
- }
72
- return parseBoardManifest(file.content).boardId;
73
- }
74
63
  export async function writeBoardOutput(path, bytes, force = false) {
75
64
  if (!force) {
76
65
  await access(path).then(() => { throw new Error(`Output already exists: ${path}; use --force to replace it`); }, () => undefined);
@@ -12,7 +12,6 @@ const execFile = promisify(execFileCallback);
12
12
  import { parseBoardMediaSource } from "@neta-art/cohub/board";
13
13
  import { imageVariantUrl, MEDIA_VARIANT_MAX, mediaVariantSize, videoFrameUrl } from "@neta-art/cohub/media";
14
14
  import { hasBoardScripts, boardAssetKey, boardAssetKeySource, boardDocumentAt, boardRenderTheme, boardTextureEdge, boardTexturePixels, createBoardRenderSampler, buildBoardScene, createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes, parseBoardDocument, planBoardExport, prepareBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
15
- import { resolveBoardId } from "./board-command-support.js";
16
15
  import { settleBoardDocument } from "./board-media.js";
17
16
  import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
18
17
  import { createClient } from "./client.js";
@@ -40,7 +39,7 @@ export function resolveBundledFonts() {
40
39
  });
41
40
  }
42
41
  export async function loadBoardDocument(spaceId, target) {
43
- const board = createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
42
+ const board = await createClient().space(spaceId).boards.open(target);
44
43
  let result = await board.get();
45
44
  const snapshot = result;
46
45
  const items = { ...result.items };
@@ -6127,10 +6127,17 @@ const BoardCustomItemSchema$1 = object({
6127
6127
  props: object({}).strict().default({})
6128
6128
  }).strict();
6129
6129
  const BOARD_EXTENSION_TYPE_PATTERN$1 = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
6130
+ const BoardItemPreviewSchema$1 = object({
6131
+ title: string().max(500).optional(),
6132
+ excerpt: string().max(2e3).optional(),
6133
+ cover: BoardMediaSourceSchema$1.optional().describe("An image for the card."),
6134
+ src: BoardMediaSourceSchema$1.optional().describe("The file or page the card stands for.")
6135
+ }).strict().describe("How a client without this type's renderer shows the card, and what it refers to. The type's own client keeps it current.");
6130
6136
  const BoardExtensionItemSchema$1 = object({
6131
6137
  ...boxed$1(BOARD_DEFAULT_ITEM_SIZE$1),
6132
6138
  type: string().regex(BOARD_EXTENSION_TYPE_PATTERN$1),
6133
- props: record(string(), unknown()).default({})
6139
+ props: record(string(), unknown()).default({}),
6140
+ preview: BoardItemPreviewSchema$1.optional()
6134
6141
  }).strict();
6135
6142
  const BOARD_ITEM_SCHEMAS$1 = {
6136
6143
  frame: BoardFrameItemSchema$1,
@@ -6150,6 +6157,12 @@ const BOARD_ITEM_TYPES$1 = Object.keys(BOARD_ITEM_SCHEMAS$1);
6150
6157
  function isBuiltinItemType(type) {
6151
6158
  return Object.hasOwn(BOARD_ITEM_SCHEMAS$1, type);
6152
6159
  }
6160
+ function isBuiltinItem(item) {
6161
+ return isBuiltinItemType(item.type);
6162
+ }
6163
+ function boardItemPreview(item) {
6164
+ return isBuiltinItem(item) ? null : item.preview ?? null;
6165
+ }
6153
6166
  function boardItemSchema(type) {
6154
6167
  if (isBuiltinItemType(type)) return BOARD_ITEM_SCHEMAS$1[type];
6155
6168
  return BOARD_EXTENSION_TYPE_PATTERN$1.test(type) ? BoardExtensionItemSchema$1 : null;
@@ -7687,6 +7700,16 @@ function unionRects(rects) {
7687
7700
  height: maxY - minY
7688
7701
  };
7689
7702
  }
7703
+ function rotatePointAround(point, center, angleRad) {
7704
+ const cos = Math.cos(angleRad);
7705
+ const sin = Math.sin(angleRad);
7706
+ const dx = point.x - center.x;
7707
+ const dy = point.y - center.y;
7708
+ return {
7709
+ x: center.x + dx * cos - dy * sin,
7710
+ y: center.y + dx * sin + dy * cos
7711
+ };
7712
+ }
7690
7713
  function itemBounds(frame) {
7691
7714
  const rotation = frame.rotation || 0;
7692
7715
  if (rotation === 0) return frameRect(frame);
@@ -9287,8 +9310,12 @@ const MEDIA_HOSTS = /* @__PURE__ */ new Map([
9287
9310
  transform: "oss",
9288
9311
  metaHeaders: true
9289
9312
  }],
9313
+ ["oss.talesofai.com", { transform: "oss" }],
9290
9314
  ["public.cohub.live", { transform: "oss" }],
9315
+ ["public.cohub.run", { transform: "oss" }],
9291
9316
  ["sessions.cohub.live", { transform: "oss" }],
9317
+ ["works.cohub.live", { transform: "oss" }],
9318
+ ["works.cohub.run", { transform: "oss" }],
9292
9319
  ["cca.neta.art", { transform: "cloudflare" }]
9293
9320
  ]);
9294
9321
  const parseHttpsUrl = (url) => {
@@ -9374,6 +9401,7 @@ function isBoardModelItem(item) {
9374
9401
  }
9375
9402
  //#endregion
9376
9403
  //#region ../sdk/src/board/model/asset-key.ts
9404
+ const COVER = "cover:";
9377
9405
  function imageAssetKey(item) {
9378
9406
  if (item.type === "image") {
9379
9407
  const source = parseBoardMediaSource$1(item.props.src);
@@ -9389,11 +9417,12 @@ function imageAssetKey(item) {
9389
9417
  const url = taskArtifactPreviewUrl(featuredTaskArtifact(item.props.snapshot.artifacts));
9390
9418
  return url ? `url:${url}` : null;
9391
9419
  }
9392
- return null;
9420
+ const cover = boardItemPreview(item)?.cover;
9421
+ const source = cover ? parseBoardMediaSource$1(cover) : null;
9422
+ return source ? `${source.kind}:${source.value}` : null;
9393
9423
  }
9394
9424
  const STILL_MEDIA = ["video", "model"];
9395
- function stillKey(media, src, mtimeMs) {
9396
- const source = parseBoardMediaSource$1(src);
9425
+ function stillKey(media, source, mtimeMs) {
9397
9426
  if (!source) return null;
9398
9427
  return source.kind === "url" ? `${media}-url:${encodeURIComponent(source.value)}` : `${media}:${encodeURIComponent(source.value)}:${mtimeMs ?? "unknown"}`;
9399
9428
  }
@@ -9407,8 +9436,8 @@ function taskPreviewKey(item) {
9407
9436
  }
9408
9437
  function boardAssetKey(item) {
9409
9438
  if (item.type === "task") return taskPreviewKey(item);
9410
- if (item.type === "video") return stillKey("video", item.props.src, item.props.snapshot?.mtimeMs);
9411
- if (item.type === "file" && isBoardModelItem(item)) return stillKey("model", item.props.src, item.props.snapshot?.mtimeMs);
9439
+ if (item.type === "video") return stillKey("video", parseBoardMediaSource$1(item.props.src), item.props.snapshot?.mtimeMs);
9440
+ if (item.type === "file" && isBoardModelItem(item)) return stillKey("model", parseBoardMediaSource$1(item.props.src), item.props.snapshot?.mtimeMs);
9412
9441
  return imageAssetKey(item);
9413
9442
  }
9414
9443
  function boardAssetKeySource(key) {
@@ -9418,6 +9447,14 @@ function boardAssetKeySource(key) {
9418
9447
  if (separator < 0) return null;
9419
9448
  base = key.slice(separator + 1);
9420
9449
  }
9450
+ if (!base.startsWith(COVER)) return sourceOf(base);
9451
+ const source = sourceOf(base.slice(6));
9452
+ return source?.media === "image" ? {
9453
+ ...source,
9454
+ fit: "cover"
9455
+ } : null;
9456
+ }
9457
+ function sourceOf(base) {
9421
9458
  try {
9422
9459
  for (const media of STILL_MEDIA) {
9423
9460
  if (base.startsWith(`${media}-url:`)) return {
@@ -10071,12 +10108,12 @@ const BOARD_THEME_DATA = {
10071
10108
  colorScheme: "dark",
10072
10109
  palette: {
10073
10110
  bg: 329221,
10074
- surface: 2039072,
10075
- hover: 1842202,
10111
+ surface: 1249814,
10112
+ hover: 1973788,
10076
10113
  border: 1842202,
10077
- brand: 888041,
10078
- text: 15920612,
10079
- muted: 9999759,
10114
+ brand: 3900150,
10115
+ text: 16314853,
10116
+ muted: 7630695,
10080
10117
  rare: 9235170,
10081
10118
  epic: 7266776,
10082
10119
  legendary: 15449187
@@ -10085,58 +10122,61 @@ const BOARD_THEME_DATA = {
10085
10122
  brand: {
10086
10123
  stroke: 15524306,
10087
10124
  fill: 15524306,
10088
- label: 15920612
10125
+ label: 16314853
10089
10126
  },
10090
10127
  neutral: {
10091
- stroke: 15920612,
10092
- fill: 15920612,
10093
- label: 15920612
10128
+ stroke: 15524306,
10129
+ fill: 15524306,
10130
+ label: 16314853
10094
10131
  },
10095
10132
  black: {
10096
10133
  stroke: 0,
10097
10134
  fill: 0,
10098
- label: 15920612
10135
+ label: 16314853
10099
10136
  },
10100
10137
  white: {
10101
10138
  stroke: 16777215,
10102
10139
  fill: 16777215,
10103
- label: 15920612
10140
+ label: 16314853
10104
10141
  },
10105
10142
  blue: {
10106
10143
  stroke: 9235170,
10107
10144
  fill: 9235170,
10108
- label: 15920612
10145
+ label: 16314853
10109
10146
  },
10110
10147
  green: {
10111
10148
  stroke: 8177036,
10112
10149
  fill: 8177036,
10113
- label: 15920612
10150
+ label: 16314853
10114
10151
  },
10115
10152
  amber: {
10116
10153
  stroke: 15449187,
10117
10154
  fill: 15449187,
10118
- label: 15920612
10155
+ label: 16314853
10119
10156
  },
10120
10157
  violet: {
10121
10158
  stroke: 11971327,
10122
10159
  fill: 11971327,
10123
- label: 15920612
10160
+ label: 16314853
10124
10161
  },
10125
10162
  rose: {
10126
10163
  stroke: 15562341,
10127
10164
  fill: 15562341,
10128
- label: 15920612
10165
+ label: 16314853
10129
10166
  }
10130
10167
  },
10131
- style: {}
10168
+ style: {
10169
+ selection: { outset: 0 },
10170
+ frameLabelSize: 16
10171
+ }
10132
10172
  },
10133
10173
  "neta-pixel": {
10134
10174
  colorScheme: "light",
10135
10175
  palette: {
10136
10176
  bg: 16185078,
10137
10177
  surface: 16777215,
10138
- hover: 15461355,
10139
- border: 14935011,
10178
+ hover: 16185078,
10179
+ border: 15527149,
10140
10180
  brand: 1824230,
10141
10181
  text: 1447704,
10142
10182
  muted: 7566452,
@@ -10200,9 +10240,9 @@ const BOARD_THEME_DATA = {
10200
10240
  y: 0
10201
10241
  },
10202
10242
  selection: {
10203
- width: 2,
10204
- outset: 8,
10205
- dash: 4,
10243
+ width: 1.5,
10244
+ outset: 0,
10245
+ dash: 0,
10206
10246
  handle: "square",
10207
10247
  handleSize: 8
10208
10248
  },
@@ -10210,19 +10250,20 @@ const BOARD_THEME_DATA = {
10210
10250
  progress: "mosaic",
10211
10251
  icons: "pixel",
10212
10252
  motion: "stepped",
10213
- font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif"
10253
+ font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif",
10254
+ frameLabelSize: 16
10214
10255
  }
10215
10256
  },
10216
10257
  "neta-pixel-dark": {
10217
10258
  colorScheme: "dark",
10218
10259
  palette: {
10219
- bg: 1447704,
10220
- surface: 2631979,
10221
- hover: 3224115,
10222
- border: 3618875,
10260
+ bg: 1118739,
10261
+ surface: 1842463,
10262
+ hover: 2237220,
10263
+ border: 2763565,
10223
10264
  brand: 1824230,
10224
- text: 15921907,
10225
- muted: 9408656,
10265
+ text: 16185078,
10266
+ muted: 9079436,
10226
10267
  rare: 1824230,
10227
10268
  epic: 1824230,
10228
10269
  legendary: 16766287
@@ -10231,47 +10272,47 @@ const BOARD_THEME_DATA = {
10231
10272
  brand: {
10232
10273
  stroke: 16185078,
10233
10274
  fill: 16185078,
10234
- label: 15921907
10275
+ label: 16185078
10235
10276
  },
10236
10277
  neutral: {
10237
- stroke: 9408656,
10238
- fill: 9408656,
10239
- label: 15921907
10278
+ stroke: 9079436,
10279
+ fill: 9079436,
10280
+ label: 16185078
10240
10281
  },
10241
10282
  black: {
10242
10283
  stroke: 0,
10243
10284
  fill: 0,
10244
- label: 15921907
10285
+ label: 16185078
10245
10286
  },
10246
10287
  white: {
10247
10288
  stroke: 16777215,
10248
10289
  fill: 16777215,
10249
- label: 15921907
10290
+ label: 16185078
10250
10291
  },
10251
10292
  blue: {
10252
10293
  stroke: 6010853,
10253
10294
  fill: 6010853,
10254
- label: 15921907
10295
+ label: 16185078
10255
10296
  },
10256
10297
  green: {
10257
10298
  stroke: 9683613,
10258
10299
  fill: 9683613,
10259
- label: 15921907
10300
+ label: 16185078
10260
10301
  },
10261
10302
  amber: {
10262
10303
  stroke: 14264410,
10263
10304
  fill: 14264410,
10264
- label: 15921907
10305
+ label: 16185078
10265
10306
  },
10266
10307
  violet: {
10267
10308
  stroke: 11441117,
10268
10309
  fill: 11441117,
10269
- label: 15921907
10310
+ label: 16185078
10270
10311
  },
10271
10312
  rose: {
10272
10313
  stroke: 15043210,
10273
10314
  fill: 15043210,
10274
- label: 15921907
10315
+ label: 16185078
10275
10316
  }
10276
10317
  },
10277
10318
  style: {
@@ -10283,9 +10324,9 @@ const BOARD_THEME_DATA = {
10283
10324
  y: 0
10284
10325
  },
10285
10326
  selection: {
10286
- width: 2,
10287
- outset: 8,
10288
- dash: 4,
10327
+ width: 1.5,
10328
+ outset: 0,
10329
+ dash: 0,
10289
10330
  handle: "square",
10290
10331
  handleSize: 8
10291
10332
  },
@@ -10293,7 +10334,8 @@ const BOARD_THEME_DATA = {
10293
10334
  progress: "mosaic",
10294
10335
  icons: "pixel",
10295
10336
  motion: "stepped",
10296
- font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif"
10337
+ font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif",
10338
+ frameLabelSize: 16
10297
10339
  }
10298
10340
  }
10299
10341
  };
@@ -10358,6 +10400,13 @@ function layoutShapeLabel(props, width) {
10358
10400
  width: Math.max(1, width - 16)
10359
10401
  });
10360
10402
  }
10403
+ const BOARD_FRAME_LABEL_LINE_HEIGHT = 1.25;
10404
+ function frameLabelOffset(size) {
10405
+ return {
10406
+ x: 4,
10407
+ y: -(size + 6)
10408
+ };
10409
+ }
10361
10410
  //#endregion
10362
10411
  //#region ../../node_modules/.pnpm/pixi.js@8.20.1/node_modules/pixi.js/lib/environment-browser/browserExt.mjs
10363
10412
  const browserExt = {
@@ -32769,8 +32818,8 @@ const measuredFonts = /* @__PURE__ */ new Set();
32769
32818
  const measuredFamilies = /* @__PURE__ */ new Set();
32770
32819
  const TRAILING_SPACE = /[ \t]+$/u;
32771
32820
  const unquote = (name) => name.trim().replace(/^["']|["']$/gu, "");
32772
- function prepareText(text, font) {
32773
- const key = `${font}\n${text}`;
32821
+ function prepareText(text, font, whiteSpace) {
32822
+ const key = `${whiteSpace}\n${font}\n${text}`;
32774
32823
  let value = prepared.get(key);
32775
32824
  if (value) {
32776
32825
  prepared.delete(key);
@@ -32781,30 +32830,75 @@ function prepareText(text, font) {
32781
32830
  measuredFonts.add(font);
32782
32831
  for (const name of /px\s+(.+)$/u.exec(font)?.[1]?.split(",") ?? []) measuredFamilies.add(unquote(name));
32783
32832
  }
32784
- value = prepareWithSegments(text, font, { whiteSpace: "pre-wrap" });
32833
+ value = prepareWithSegments(text, font, { whiteSpace });
32785
32834
  if (prepared.size >= PREPARED_LIMIT) prepared.delete(prepared.keys().next().value);
32786
32835
  prepared.set(key, value);
32787
32836
  return value;
32788
32837
  }
32789
- function layoutTextLines(text, font, maxWidth = Number.POSITIVE_INFINITY) {
32838
+ let measureContext;
32839
+ function measureTextWidth(text, font) {
32840
+ measureContext ??= DOMAdapter.get().createCanvas(1, 1).getContext("2d");
32841
+ if (!measureContext) return 0;
32842
+ measureContext.font = font;
32843
+ return measureContext.measureText(text).width;
32844
+ }
32845
+ function layoutTextLines(text, font, maxWidth = Number.POSITIVE_INFINITY, { whiteSpace = "pre-wrap", maxLines = Number.POSITIVE_INFINITY } = {}) {
32790
32846
  const lines = [];
32791
- let width = 0;
32792
- for (const paragraph of text.replace(/\r\n?/gu, "\n").replaceAll(" ", " ").split("\n")) {
32847
+ const widths = [];
32848
+ if (maxLines <= 0) return {
32849
+ lines,
32850
+ width: 0,
32851
+ truncated: text.length > 0
32852
+ };
32853
+ let source = whiteSpace === "normal" ? text.replace(/\s+/gu, " ").trim() : text.replace(/\r\n?/gu, "\n").replaceAll(" ", " ");
32854
+ const cap = Number.isFinite(maxLines) && Number.isFinite(maxWidth) ? maxLines * (Math.ceil(maxWidth) + 1) : Number.POSITIVE_INFINITY;
32855
+ let truncated = source.length > cap;
32856
+ if (truncated) source = source.slice(0, cap);
32857
+ const paragraphs = whiteSpace === "normal" ? source ? [source] : [] : source.split("\n");
32858
+ for (const paragraph of paragraphs) {
32859
+ if (lines.length > maxLines) break;
32793
32860
  if (!paragraph) {
32794
32861
  lines.push("");
32862
+ widths.push(0);
32795
32863
  continue;
32796
32864
  }
32797
- const wrapped = layoutWithLines(prepareText(paragraph, font), maxWidth, 1).lines;
32865
+ const wrapped = layoutWithLines(prepareText(paragraph, font, whiteSpace), maxWidth, 1).lines;
32798
32866
  for (const [index, line] of wrapped.entries()) {
32799
32867
  lines.push(index === wrapped.length - 1 ? line.text : line.text.replace(TRAILING_SPACE, ""));
32800
- width = Math.max(width, line.width);
32868
+ widths.push(line.width);
32801
32869
  }
32802
32870
  }
32871
+ if (lines.length > maxLines) {
32872
+ truncated = true;
32873
+ lines.length = maxLines;
32874
+ widths.length = maxLines;
32875
+ }
32876
+ if (truncated && lines.length > 0) {
32877
+ const last = lines.length - 1;
32878
+ lines[last] = fitLineWithEllipsis(lines[last], maxWidth, (value) => measureTextWidth(value, font));
32879
+ widths[last] = measureTextWidth(lines[last], font);
32880
+ }
32803
32881
  return {
32804
32882
  lines,
32805
- width
32883
+ width: widths.reduce((max, width) => Math.max(max, width), 0),
32884
+ truncated
32806
32885
  };
32807
32886
  }
32887
+ const graphemes = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
32888
+ function fitLineWithEllipsis(line, width, measureWidth) {
32889
+ const trimmed = line.replace(/\s+$/u, "");
32890
+ if (!trimmed) return "…";
32891
+ if (measureWidth(`${trimmed}…`) <= width) return `${trimmed}…`;
32892
+ const parts = graphemes ? Array.from(graphemes.segment(trimmed), ({ segment }) => segment) : Array.from(trimmed);
32893
+ let lo = 0;
32894
+ let hi = parts.length - 1;
32895
+ while (lo < hi) {
32896
+ const mid = lo + hi + 1 >> 1;
32897
+ if (measureWidth(`${parts.slice(0, mid).join("")}…`) <= width) lo = mid;
32898
+ else hi = mid - 1;
32899
+ }
32900
+ return `${parts.slice(0, lo).join("")}…`;
32901
+ }
32808
32902
  const baselineShifts = /* @__PURE__ */ new Map();
32809
32903
  function cssBaselineShift(style) {
32810
32904
  const font = fontStringFromTextStyle(style);
@@ -32996,40 +33090,10 @@ function boardTextSize(item) {
32996
33090
  }
32997
33091
  //#endregion
32998
33092
  //#region ../sdk/src/board/render/text-fit.ts
32999
- function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
33000
- const trimmed = line.replace(/\s+$/u, "");
33001
- if (!trimmed) return "…";
33002
- const full = `${trimmed}…`;
33003
- if (measureWidth(full) <= wrapWidth) return full;
33004
- let lo = 0;
33005
- let hi = trimmed.length;
33006
- let best = "…";
33007
- while (lo <= hi) {
33008
- const mid = lo + hi >> 1;
33009
- const candidate = mid > 0 ? `${trimmed.slice(0, mid)}…` : "…";
33010
- if (measureWidth(candidate) <= wrapWidth) {
33011
- best = candidate;
33012
- lo = mid + 1;
33013
- } else hi = mid - 1;
33014
- }
33015
- return best;
33016
- }
33017
33093
  function fitTextToLines(text, value, maxLines, wrapWidth) {
33018
- const width = Math.max(1, wrapWidth);
33019
33094
  text.style.wordWrap = false;
33020
- if (!value || maxLines <= 0) {
33021
- if (text.text !== "") text.text = "";
33022
- return;
33023
- }
33024
- const charCap = Math.max(maxLines * Math.ceil(width), maxLines * 4);
33025
- const sample = value.length > charCap ? value.slice(0, charCap) : value;
33026
- const { lines } = layoutTextLines(sample, fontStringFromTextStyle(text.style), width);
33027
- const kept = lines.slice(0, maxLines);
33028
- if (lines.length > maxLines || sample !== value) {
33029
- const lastIndex = kept.length - 1;
33030
- kept[lastIndex] = fitLineWithEllipsis(kept[lastIndex] ?? "", width, (candidate) => CanvasTextMetrics.measureText(candidate, text.style).width);
33031
- }
33032
- const fitted = kept.join("\n");
33095
+ const { lines } = layoutTextLines(value, fontStringFromTextStyle(text.style), Math.max(1, wrapWidth), { maxLines });
33096
+ const fitted = lines.join("\n");
33033
33097
  if (text.text !== fitted) text.text = fitted;
33034
33098
  }
33035
33099
  function fitTextToWidth(text, value, width) {
@@ -33644,6 +33708,10 @@ function traceLocal(graphics, frame, rect) {
33644
33708
  const d = corner(x, y + height);
33645
33709
  graphics.moveTo(a.x, a.y).lineTo(b.x, b.y).lineTo(c.x, c.y).lineTo(d.x, d.y).closePath();
33646
33710
  }
33711
+ function farPictureTone(item, context) {
33712
+ const key = context.assetKey(item);
33713
+ return (key ? context.assetTone?.(key) : null) ?? void 0;
33714
+ }
33647
33715
  function drawFarPlate(graphics, frame, style) {
33648
33716
  if (frame.width <= 0 || frame.height <= 0) return;
33649
33717
  traceLocal(graphics, frame, {
@@ -33983,7 +34051,7 @@ const modelCardRenderer = {
33983
34051
  },
33984
34052
  renderFar: (graphics, item, context) => {
33985
34053
  drawFarPlate(graphics, item.frame, {
33986
- fill: context.palette.surface,
34054
+ fill: farPictureTone(item, context) ?? context.palette.surface,
33987
34055
  fillAlpha: .96,
33988
34056
  accent: context.palette.legendary,
33989
34057
  accentAlpha: .45
@@ -33995,6 +34063,64 @@ const modelCardRenderer = {
33995
34063
  }
33996
34064
  };
33997
34065
  //#endregion
34066
+ //#region ../sdk/src/board/render/media-interaction.ts
34067
+ const BOARD_PLAY_MEDIA = "play-media";
34068
+ function mediaPlayBadgeVisible(item, zoom, options) {
34069
+ if (!options.materialized) return false;
34070
+ if (item.type === "video" || item.type === "audio") return true;
34071
+ if (isBoardModelItem(item)) return boardCardDetailed(zoom);
34072
+ if (item.type !== "task" || !boardCardDetailed(zoom)) return false;
34073
+ const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
34074
+ if (artifact?.type === "audio") return true;
34075
+ return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
34076
+ }
34077
+ function cornerKey(item) {
34078
+ const { width, height } = item.frame;
34079
+ if (item.type === "audio") return audioKeyCenter(width, height);
34080
+ return isBoardModelItem(item) ? modelKeyCenter(width, height) : null;
34081
+ }
34082
+ function mediaPlayKeyCenter(item) {
34083
+ const { x, y, width, height, rotation } = item.frame;
34084
+ const center = worldPoint(x + width / 2, y + height / 2);
34085
+ const key = cornerKey(item);
34086
+ if (!key) return center;
34087
+ return rotatePointAround(worldPoint(x + key.x, y + key.y), center, degToRad(rotation || 0));
34088
+ }
34089
+ function mediaPlayBadgeHit(item, point, zoom) {
34090
+ const scale = Math.max(zoom, .05);
34091
+ const radius = isBoardModelItem(item) ? Math.max(cornerKey(item)?.radius ?? 0, 16 / scale) : 28 / scale;
34092
+ const center = mediaPlayKeyCenter(item);
34093
+ return Math.hypot(point.x - center.x, point.y - center.y) <= radius;
34094
+ }
34095
+ function boardMediaActionAt(item, point, zoom, options) {
34096
+ return mediaPlayBadgeVisible(item, zoom, options) && mediaPlayBadgeHit(item, point, zoom) ? {
34097
+ action: BOARD_PLAY_MEDIA,
34098
+ itemId: item.id
34099
+ } : null;
34100
+ }
34101
+ const PLAY_HIT = {
34102
+ action: BOARD_PLAY_MEDIA,
34103
+ cursor: "pointer"
34104
+ };
34105
+ function mediaPlayHit(item, point, context) {
34106
+ const key = context.assetKey(item);
34107
+ return boardMediaActionAt(item, point, context.zoom, {
34108
+ materialized: true,
34109
+ hasVideoPreview: Boolean(key && context.getTexture(key))
34110
+ }) ? PLAY_HIT : null;
34111
+ }
34112
+ //#endregion
34113
+ //#region ../sdk/src/board/render/labels.ts
34114
+ const ARROW_LABEL = {
34115
+ fontWeight: "500",
34116
+ lineHeight: 1.25,
34117
+ padX: .5,
34118
+ padY: .25
34119
+ };
34120
+ function arrowLabelAnchor(arrow, style) {
34121
+ return style.trim === void 0 ? arrow.mid : pathPointAt(arrow.path, .5);
34122
+ }
34123
+ //#endregion
33998
34124
  //#region ../sdk/src/board/render/renderers/arrow-card-renderer.ts
33999
34125
  const partsByContainer$11 = /* @__PURE__ */ new WeakMap();
34000
34126
  function sync$11(container, item, context) {
@@ -34056,8 +34182,9 @@ function sync$11(container, item, context) {
34056
34182
  parts.label.text = props.label;
34057
34183
  parts.label.style.fill = context.palette.text;
34058
34184
  parts.label.style.fontSize = props.fontSize;
34059
- const padX = props.fontSize * .5;
34060
- const padY = props.fontSize * .25;
34185
+ parts.label.style.lineHeight = props.fontSize * ARROW_LABEL.lineHeight;
34186
+ const padX = props.fontSize * ARROW_LABEL.padX;
34187
+ const padY = props.fontSize * ARROW_LABEL.padY;
34061
34188
  const { width, height } = parts.label;
34062
34189
  const plate = {
34063
34190
  x: -width / 2 - padX,
@@ -34077,7 +34204,7 @@ function sync$11(container, item, context) {
34077
34204
  const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
34078
34205
  parts.label.visible = visible;
34079
34206
  parts.backdrop.visible = visible;
34080
- const mid = style.trim === void 0 ? resolved.mid : pathPointAt(resolved.path, .5);
34207
+ const mid = arrowLabelAnchor(resolved, style);
34081
34208
  parts.label.position.set(mid.x, mid.y);
34082
34209
  parts.backdrop.position.set(mid.x, mid.y);
34083
34210
  }
@@ -34093,7 +34220,7 @@ const arrowCardRenderer = {
34093
34220
  fill: 16777215,
34094
34221
  fontFamily: BOARD_FONT_STACK,
34095
34222
  fontSize: 14,
34096
- fontWeight: "500"
34223
+ fontWeight: ARROW_LABEL.fontWeight
34097
34224
  },
34098
34225
  resolution: context.textResolution,
34099
34226
  roundPixels: true
@@ -34481,8 +34608,34 @@ const partsByContainer$8 = /* @__PURE__ */ new WeakMap();
34481
34608
  function detailFor(zoom) {
34482
34609
  return boardCardDetailed(zoom) ? "full" : "title";
34483
34610
  }
34484
- function coverHeight(item, height) {
34485
- if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
34611
+ function viewOf(item) {
34612
+ if (item.type === "file") {
34613
+ const { src, snapshot } = item.props;
34614
+ return {
34615
+ src,
34616
+ title: snapshot?.title || fileStem(src),
34617
+ excerpt: snapshot?.excerpt ?? "",
34618
+ kind: filePreviewKind(snapshot),
34619
+ meta: fileMetaLine(src, snapshot?.size),
34620
+ mark: fileTypeLabel(src),
34621
+ category: fileCategory(src, snapshot?.mimeType)
34622
+ };
34623
+ }
34624
+ const preview = boardItemPreview(item);
34625
+ if (!preview) return null;
34626
+ const label = (item.type.split(".").pop() ?? item.type).toUpperCase();
34627
+ return {
34628
+ src: preview.src ?? null,
34629
+ title: preview.title || item.type,
34630
+ excerpt: preview.excerpt ?? "",
34631
+ kind: preview.cover ? "cover" : preview.excerpt ? "text" : "blank",
34632
+ meta: label,
34633
+ mark: label,
34634
+ category: preview.src ? fileCategory(preview.src) : "other"
34635
+ };
34636
+ }
34637
+ function coverHeight(view, height) {
34638
+ if (view.kind !== "cover") return 0;
34486
34639
  const ideal = Math.round(height * COVER_RATIO);
34487
34640
  return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
34488
34641
  }
@@ -34501,31 +34654,31 @@ function syncClip(parts, rect, style) {
34501
34654
  }
34502
34655
  function sync$8(container, item, context) {
34503
34656
  const parts = partsByContainer$8.get(container);
34504
- if (!parts) return;
34505
- syncContent(parts, item, context);
34657
+ const view = viewOf(item);
34658
+ if (!parts || !view) return;
34659
+ syncContent(parts, item, view, context);
34506
34660
  for (const text of [
34507
34661
  parts.title,
34508
34662
  parts.excerpt,
34509
34663
  parts.typeMark
34510
34664
  ]) syncTextResolution(text, context.textResolution);
34511
34665
  }
34512
- function syncContent(parts, item, context) {
34666
+ function syncContent(parts, item, view, context) {
34513
34667
  positionShell(parts.root, item);
34514
34668
  const { width, height } = item.frame;
34515
34669
  const detail = detailFor(context.zoom);
34516
34670
  const elevation = cardElevation(context, item.id);
34517
34671
  syncCardTag(parts.tag, {
34518
34672
  glyph: "file",
34519
- text: fileMetaLine(item.props.src, item.props.snapshot?.size),
34673
+ text: view.meta,
34520
34674
  width
34521
34675
  }, context);
34522
34676
  const key = context.assetKey(item);
34523
34677
  const texture = key ? context.getTexture(key) : null;
34524
34678
  const coverFailed = Boolean(key && !texture && context.hasError(key));
34525
- const fileState = context.fileState(item.props.src);
34526
- const band = coverHeight(item, height);
34527
- const kind = filePreviewKind(item.props.snapshot);
34528
- const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
34679
+ const fileState = view.src ? context.fileState(view.src) : "ok";
34680
+ const band = coverHeight(view, height);
34681
+ const { kind, category, title, excerpt, mark } = view;
34529
34682
  const accent = fileCategoryAccent(category, context.palette);
34530
34683
  const visualSig = [
34531
34684
  key ?? "",
@@ -34584,9 +34737,6 @@ function syncContent(parts, item, context) {
34584
34737
  parts.excerpt.visible = detail === "full";
34585
34738
  parts.typeMark.visible = kind === "blank";
34586
34739
  }
34587
- const title = item.props.snapshot?.title || fileStem(item.props.src);
34588
- const excerpt = item.props.snapshot?.excerpt ?? "";
34589
- const mark = fileTypeLabel(item.props.src);
34590
34740
  const innerWidth = Math.max(1, width - 20);
34591
34741
  const showTypeMark = kind === "blank";
34592
34742
  const textSig = [
@@ -34642,7 +34792,7 @@ function syncContent(parts, item, context) {
34642
34792
  }
34643
34793
  const fileCardRenderer = {
34644
34794
  id: "file-card",
34645
- canRender: (item) => item.type === "file",
34795
+ canRender: (item) => item.type === "file" || boardItemPreview(item) !== null,
34646
34796
  create: (item, context) => {
34647
34797
  const root = new Container();
34648
34798
  const plate = new Graphics();
@@ -34706,23 +34856,19 @@ const fileCardRenderer = {
34706
34856
  visualSig: "",
34707
34857
  textSig: ""
34708
34858
  });
34709
- if (item.type === "file") sync$8(root, item, context);
34859
+ sync$8(root, item, context);
34710
34860
  return root;
34711
34861
  },
34712
- update: (container, item, context) => {
34713
- if (item.type === "file") sync$8(container, item, context);
34714
- },
34715
- resolution: (container, item, context) => {
34716
- if (item.type === "file") sync$8(container, item, context);
34717
- },
34862
+ update: sync$8,
34863
+ resolution: sync$8,
34718
34864
  zoom: (container, zoom) => {
34719
34865
  const tag = partsByContainer$8.get(container)?.tag;
34720
34866
  if (tag) zoomCardTag(tag, zoom);
34721
34867
  },
34722
34868
  renderFar: (graphics, item, context) => {
34723
- const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
34869
+ const category = viewOf(item)?.category ?? "other";
34724
34870
  drawFarPlate(graphics, item.frame, {
34725
- fill: context.palette.surface,
34871
+ fill: farPictureTone(item, context) ?? context.palette.surface,
34726
34872
  fillAlpha: .96,
34727
34873
  accent: fileCategoryAccent(category, context.palette),
34728
34874
  accentAlpha: .45
@@ -34786,19 +34932,24 @@ function sync$7(container, item, context) {
34786
34932
  parts.label.visible = decorated;
34787
34933
  const title = locked ? `🔒 ${props.label}`.trim() : props.label;
34788
34934
  const labelColor = style.stroke ? baseStroke : context.palette.muted;
34935
+ const size = context.style.frameLabelSize;
34789
34936
  const textSig = [
34790
34937
  title,
34791
34938
  labelColor,
34792
- context.style.font
34939
+ context.style.font,
34940
+ size
34793
34941
  ].join("|");
34794
34942
  if (textSig !== parts.textSig) {
34795
34943
  parts.textSig = textSig;
34796
34944
  parts.label.text = title;
34797
34945
  parts.label.style.fill = labelColor;
34798
34946
  parts.label.style.fontFamily = context.style.font;
34947
+ parts.label.style.fontSize = size;
34948
+ parts.label.style.lineHeight = size * BOARD_FRAME_LABEL_LINE_HEIGHT;
34949
+ const offset = frameLabelOffset(size);
34950
+ parts.label.position.set(offset.x, offset.y);
34799
34951
  }
34800
34952
  syncTextResolution(parts.label, context.textResolution);
34801
- parts.label.position.set(4, -18);
34802
34953
  }
34803
34954
  const frameCardRenderer = {
34804
34955
  id: "frame-card",
@@ -34809,7 +34960,6 @@ const frameCardRenderer = {
34809
34960
  const label = createLabel("", {
34810
34961
  fill: context.palette.muted,
34811
34962
  fontFamily: BOARD_FONT_STACK,
34812
- fontSize: 12,
34813
34963
  fontWeight: "600"
34814
34964
  }, context.textResolution);
34815
34965
  root.addChild(box, label);
@@ -35284,7 +35434,7 @@ const imageCardRenderer = {
35284
35434
  lod: { kind: "media" },
35285
35435
  renderFar: (graphics, item, context) => {
35286
35436
  drawFarPlate(graphics, item.frame, {
35287
- fill: context.palette.hover,
35437
+ fill: farPictureTone(item, context) ?? context.palette.hover,
35288
35438
  fillAlpha: FAR_ALPHA
35289
35439
  });
35290
35440
  },
@@ -35797,7 +35947,7 @@ const taskCardRenderer = {
35797
35947
  const { status } = liveStatus(item, context);
35798
35948
  const active = status === "failed" || status === "running" || status === "pending";
35799
35949
  drawFarPlate(graphics, item.frame, {
35800
- fill: context.palette.surface,
35950
+ fill: farPictureTone(item, context) ?? context.palette.surface,
35801
35951
  fillAlpha: .82,
35802
35952
  ...active ? {
35803
35953
  accent: statusColor(status, context),
@@ -36170,7 +36320,7 @@ const videoCardRenderer = {
36170
36320
  lod: { kind: "media" },
36171
36321
  renderFar: (graphics, item, context) => {
36172
36322
  drawFarPlate(graphics, item.frame, {
36173
- fill: context.palette.surface,
36323
+ fill: farPictureTone(item, context) ?? context.palette.surface,
36174
36324
  fillAlpha: .96,
36175
36325
  accent: context.palette.brand,
36176
36326
  accentAlpha: .75
@@ -36190,14 +36340,18 @@ const DEFAULT_BOARD_RENDER_LABELS = {
36190
36340
  previewUnavailable: "Preview unavailable",
36191
36341
  noPreview: "No preview"
36192
36342
  };
36343
+ const playable = (renderer) => ({
36344
+ ...renderer,
36345
+ hitAt: mediaPlayHit
36346
+ });
36193
36347
  const builtinCardRenderers = [
36194
36348
  textCardRenderer,
36195
36349
  imageCardRenderer,
36196
- videoCardRenderer,
36197
- audioCardRenderer,
36198
- modelCardRenderer,
36350
+ playable(videoCardRenderer),
36351
+ playable(audioCardRenderer),
36352
+ playable(modelCardRenderer),
36199
36353
  fileCardRenderer,
36200
- taskCardRenderer,
36354
+ playable(taskCardRenderer),
36201
36355
  shapeCardRenderer,
36202
36356
  drawCardRenderer,
36203
36357
  arrowCardRenderer,
@@ -36690,10 +36844,17 @@ object({
36690
36844
  props: object({}).strict().default({})
36691
36845
  }).strict();
36692
36846
  const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
36847
+ const BoardItemPreviewSchema = object({
36848
+ title: string().max(500).optional(),
36849
+ excerpt: string().max(2e3).optional(),
36850
+ cover: BoardMediaSourceSchema.optional().describe("An image for the card."),
36851
+ src: BoardMediaSourceSchema.optional().describe("The file or page the card stands for.")
36852
+ }).strict().describe("How a client without this type's renderer shows the card, and what it refers to. The type's own client keeps it current.");
36693
36853
  object({
36694
36854
  ...boxed(BOARD_DEFAULT_ITEM_SIZE),
36695
36855
  type: string().regex(BOARD_EXTENSION_TYPE_PATTERN),
36696
- props: record(string(), unknown()).default({})
36856
+ props: record(string(), unknown()).default({}),
36857
+ preview: BoardItemPreviewSchema.optional()
36697
36858
  }).strict();
36698
36859
  const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
36699
36860
  "linear",
@@ -37143,7 +37304,8 @@ const BASE = {
37143
37304
  progress: "shimmer",
37144
37305
  icons: "line",
37145
37306
  motion: "smooth",
37146
- font: BOARD_FONT_STACK
37307
+ font: BOARD_FONT_STACK,
37308
+ frameLabelSize: 12
37147
37309
  };
37148
37310
  const SHADOW = {
37149
37311
  light: {
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, BOARD_SCRIPT_API, BOARD_THEME_IDS, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
6
- import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
6
+ import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, writeBoardOutput, } from "../board-command-support.js";
7
7
  import { BOARD_EXPORT_IMAGE_MEMORY_MIB } from "../board-export-resources.js";
8
8
  import { settleBoardPatch } from "../board-media.js";
9
9
  import { formatBoardTime, parseBoardTime, parseBoardTimes } from "../board-time.js";
@@ -55,7 +55,7 @@ function list(value) {
55
55
  }
56
56
  async function boardClient(boards, target, spaceId) {
57
57
  spaceId ??= await resolveSpace(boards);
58
- return createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
58
+ return createClient().space(spaceId).boards.open(target);
59
59
  }
60
60
  /** A patch from an inline argument, a file, or stdin (`-`). */
61
61
  async function readPatch(inline, input, maxBytes) {
@@ -538,9 +538,9 @@ Presets: ${BOARD_PRESET_NAMES.join(", ")}
538
538
  .action(async (target, options) => {
539
539
  try {
540
540
  const spaceId = await resolveSpace(boards);
541
- const boardId = await resolveBoardId(spaceId, target);
542
541
  const client = createRealtimeClient();
543
- const board = client.space(spaceId).board(boardId);
542
+ const board = await client.space(spaceId).boards.open(target);
543
+ const boardId = board.id;
544
544
  if (!jsonRequested(options))
545
545
  process.stderr.write(`Watching Board ${boardId}…\n`);
546
546
  const offConnection = client.onConnection((state) => {
@@ -90,6 +90,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
90
90
  };
91
91
  let closeInstance;
92
92
  let bridgeTask;
93
+ let retryDisconnectedBridge;
93
94
  let nativeStarted = false;
94
95
  let tokenInFlight = null;
95
96
  const token = (forceRefresh = false) => {
@@ -139,6 +140,9 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
139
140
  let probeTimer;
140
141
  let managed = false;
141
142
  let childStopped = false;
143
+ let bridgeDisconnected = false;
144
+ let terminating = false;
145
+ let recoveryTimer;
142
146
  let controlPipe = null;
143
147
  try {
144
148
  const initialToken = await token();
@@ -156,6 +160,8 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
156
160
  bridge.once("error", (error) => diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(error) }));
157
161
  bridge.once("exit", (code, exitSignal) => {
158
162
  childStopped = true;
163
+ clearTimeout(recoveryTimer);
164
+ recoveryTimer = undefined;
159
165
  if (!signal.aborted)
160
166
  diagnostics.log("warn", "sandboxd.process_exit", { code, signal: exitSignal });
161
167
  // Descendants may still hold inherited pipes. After draining buffered
@@ -198,8 +204,10 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
198
204
  managed = true;
199
205
  if (event.type === "connected" || event.type === "disconnected") {
200
206
  const connected = event.type === "connected";
207
+ bridgeDisconnected = !connected;
201
208
  update({ workspaceConnected: connected });
202
209
  diagnostics.log(connected ? "info" : "warn", `sandboxd.${event.type}`);
210
+ retryDisconnectedBridge?.();
203
211
  }
204
212
  }
205
213
  catch {
@@ -238,6 +246,11 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
238
246
  };
239
247
  probeTimer = setTimeout(() => void probe(), 1000);
240
248
  const terminate = () => {
249
+ if (terminating || childStopped)
250
+ return;
251
+ terminating = true;
252
+ clearTimeout(recoveryTimer);
253
+ recoveryTimer = undefined;
241
254
  // Windows has no SIGTERM; closing the managed stdin lets sandboxd stop gracefully.
242
255
  if (process.platform === "win32")
243
256
  bridge.stdin?.end();
@@ -246,6 +259,26 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
246
259
  const timeout = setTimeout(() => bridge.kill("SIGKILL"), 3000);
247
260
  void closed.finally(() => clearTimeout(timeout));
248
261
  };
262
+ // A managed bridge may be sleeping for five minutes after a lease
263
+ // rejection. Retry only after it reports a lost connection and the
264
+ // Harness has regained its lease; a connected bridge stays untouched.
265
+ retryDisconnectedBridge = () => {
266
+ if (signal.aborted || !status.harnessConnected || !bridgeDisconnected || terminating || childStopped) {
267
+ clearTimeout(recoveryTimer);
268
+ recoveryTimer = undefined;
269
+ }
270
+ else if (!recoveryTimer) {
271
+ // Give the bridge its normal retry and channel reattachment
272
+ // window before replacing a process stuck in config backoff.
273
+ recoveryTimer = setTimeout(() => {
274
+ recoveryTimer = undefined;
275
+ if (!signal.aborted && status.harnessConnected && bridgeDisconnected && !terminating && !childStopped) {
276
+ diagnostics.log("info", "sandboxd.lease_recovery");
277
+ terminate();
278
+ }
279
+ }, 30_000);
280
+ }
281
+ };
249
282
  signal.addEventListener("abort", terminate, { once: true });
250
283
  if (signal.aborted)
251
284
  terminate();
@@ -257,10 +290,12 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
257
290
  diagnostics.log("warn", "sandboxd.restart_failed", { error: serializeDiagnosticError(error) });
258
291
  }
259
292
  finally {
293
+ retryDisconnectedBridge = undefined;
260
294
  childStopped = true;
261
295
  controlPipe?.close();
262
296
  clearTimeout(refreshTimer);
263
297
  clearTimeout(probeTimer);
298
+ clearTimeout(recoveryTimer);
264
299
  update({ workspaceConnected: false });
265
300
  }
266
301
  if (Date.now() - startedAt > 60_000)
@@ -276,8 +311,8 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
276
311
  await serveRuntime({
277
312
  spaceId: config.spaceId, cwd: config.root, url: url.toString(), capabilities,
278
313
  runtimeId: status.runtimeId, diagnostics, token, signal, executor: native.executor,
279
- onReady: () => update({ harnessConnected: true }),
280
- onDisconnected: () => { native.connect(null); update({ harnessConnected: false }); },
314
+ onReady: () => { update({ harnessConnected: true }); retryDisconnectedBridge?.(); },
315
+ onDisconnected: () => { native.connect(null); update({ harnessConnected: false }); retryDisconnectedBridge?.(); },
281
316
  onNativeStop: (stop) => { native.stop(stop.sessionId, stop.turnId); },
282
317
  onNativeChannel: (send) => {
283
318
  native.connect(send);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "10.10.0",
3
+ "version": "10.11.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "12.2.0",
21
+ "@neta-art/cohub": "12.4.0",
22
22
  "@neta-art/generation": "^0.1.40",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",