@neta-art/cohub-cli 10.10.0 → 10.11.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.
@@ -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) => {
@@ -9389,7 +9416,9 @@ function imageAssetKey(item) {
9389
9416
  const url = taskArtifactPreviewUrl(featuredTaskArtifact(item.props.snapshot.artifacts));
9390
9417
  return url ? `url:${url}` : null;
9391
9418
  }
9392
- return null;
9419
+ const cover = boardItemPreview(item)?.cover;
9420
+ const source = cover ? parseBoardMediaSource$1(cover) : null;
9421
+ return source ? `${source.kind}:${source.value}` : null;
9393
9422
  }
9394
9423
  const STILL_MEDIA = ["video", "model"];
9395
9424
  function stillKey(media, src, mtimeMs) {
@@ -9447,6 +9476,12 @@ function boardAssetKeySource(key) {
9447
9476
  media: "image",
9448
9477
  value: base.slice(4)
9449
9478
  };
9479
+ if (base.startsWith("cover-url:")) return {
9480
+ kind: "url",
9481
+ media: "image",
9482
+ value: base.slice(10),
9483
+ fit: "cover"
9484
+ };
9450
9485
  return null;
9451
9486
  }
9452
9487
  function boardTexturePixels(item, scale) {
@@ -10074,7 +10109,7 @@ const BOARD_THEME_DATA = {
10074
10109
  surface: 2039072,
10075
10110
  hover: 1842202,
10076
10111
  border: 1842202,
10077
- brand: 888041,
10112
+ brand: 3900150,
10078
10113
  text: 15920612,
10079
10114
  muted: 9999759,
10080
10115
  rare: 9235170,
@@ -10128,7 +10163,10 @@ const BOARD_THEME_DATA = {
10128
10163
  label: 15920612
10129
10164
  }
10130
10165
  },
10131
- style: {}
10166
+ style: {
10167
+ selection: { outset: 0 },
10168
+ frameLabelSize: 16
10169
+ }
10132
10170
  },
10133
10171
  "neta-pixel": {
10134
10172
  colorScheme: "light",
@@ -10200,9 +10238,9 @@ const BOARD_THEME_DATA = {
10200
10238
  y: 0
10201
10239
  },
10202
10240
  selection: {
10203
- width: 2,
10204
- outset: 8,
10205
- dash: 4,
10241
+ width: 1.5,
10242
+ outset: 0,
10243
+ dash: 0,
10206
10244
  handle: "square",
10207
10245
  handleSize: 8
10208
10246
  },
@@ -10210,7 +10248,8 @@ const BOARD_THEME_DATA = {
10210
10248
  progress: "mosaic",
10211
10249
  icons: "pixel",
10212
10250
  motion: "stepped",
10213
- font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif"
10251
+ font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif",
10252
+ frameLabelSize: 16
10214
10253
  }
10215
10254
  },
10216
10255
  "neta-pixel-dark": {
@@ -10283,9 +10322,9 @@ const BOARD_THEME_DATA = {
10283
10322
  y: 0
10284
10323
  },
10285
10324
  selection: {
10286
- width: 2,
10287
- outset: 8,
10288
- dash: 4,
10325
+ width: 1.5,
10326
+ outset: 0,
10327
+ dash: 0,
10289
10328
  handle: "square",
10290
10329
  handleSize: 8
10291
10330
  },
@@ -10293,7 +10332,8 @@ const BOARD_THEME_DATA = {
10293
10332
  progress: "mosaic",
10294
10333
  icons: "pixel",
10295
10334
  motion: "stepped",
10296
- font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif"
10335
+ font: "\"Figtree Variable\", \"PingFang SC\", \"Noto Sans CJK SC\", \"Microsoft YaHei\", system-ui, sans-serif",
10336
+ frameLabelSize: 16
10297
10337
  }
10298
10338
  }
10299
10339
  };
@@ -10358,6 +10398,13 @@ function layoutShapeLabel(props, width) {
10358
10398
  width: Math.max(1, width - 16)
10359
10399
  });
10360
10400
  }
10401
+ const BOARD_FRAME_LABEL_LINE_HEIGHT = 1.25;
10402
+ function frameLabelOffset(size) {
10403
+ return {
10404
+ x: 4,
10405
+ y: -(size + 6)
10406
+ };
10407
+ }
10361
10408
  //#endregion
10362
10409
  //#region ../../node_modules/.pnpm/pixi.js@8.20.1/node_modules/pixi.js/lib/environment-browser/browserExt.mjs
10363
10410
  const browserExt = {
@@ -32769,8 +32816,8 @@ const measuredFonts = /* @__PURE__ */ new Set();
32769
32816
  const measuredFamilies = /* @__PURE__ */ new Set();
32770
32817
  const TRAILING_SPACE = /[ \t]+$/u;
32771
32818
  const unquote = (name) => name.trim().replace(/^["']|["']$/gu, "");
32772
- function prepareText(text, font) {
32773
- const key = `${font}\n${text}`;
32819
+ function prepareText(text, font, whiteSpace) {
32820
+ const key = `${whiteSpace}\n${font}\n${text}`;
32774
32821
  let value = prepared.get(key);
32775
32822
  if (value) {
32776
32823
  prepared.delete(key);
@@ -32781,30 +32828,75 @@ function prepareText(text, font) {
32781
32828
  measuredFonts.add(font);
32782
32829
  for (const name of /px\s+(.+)$/u.exec(font)?.[1]?.split(",") ?? []) measuredFamilies.add(unquote(name));
32783
32830
  }
32784
- value = prepareWithSegments(text, font, { whiteSpace: "pre-wrap" });
32831
+ value = prepareWithSegments(text, font, { whiteSpace });
32785
32832
  if (prepared.size >= PREPARED_LIMIT) prepared.delete(prepared.keys().next().value);
32786
32833
  prepared.set(key, value);
32787
32834
  return value;
32788
32835
  }
32789
- function layoutTextLines(text, font, maxWidth = Number.POSITIVE_INFINITY) {
32836
+ let measureContext;
32837
+ function measureTextWidth(text, font) {
32838
+ measureContext ??= DOMAdapter.get().createCanvas(1, 1).getContext("2d");
32839
+ if (!measureContext) return 0;
32840
+ measureContext.font = font;
32841
+ return measureContext.measureText(text).width;
32842
+ }
32843
+ function layoutTextLines(text, font, maxWidth = Number.POSITIVE_INFINITY, { whiteSpace = "pre-wrap", maxLines = Number.POSITIVE_INFINITY } = {}) {
32790
32844
  const lines = [];
32791
- let width = 0;
32792
- for (const paragraph of text.replace(/\r\n?/gu, "\n").replaceAll(" ", " ").split("\n")) {
32845
+ const widths = [];
32846
+ if (maxLines <= 0) return {
32847
+ lines,
32848
+ width: 0,
32849
+ truncated: text.length > 0
32850
+ };
32851
+ let source = whiteSpace === "normal" ? text.replace(/\s+/gu, " ").trim() : text.replace(/\r\n?/gu, "\n").replaceAll(" ", " ");
32852
+ const cap = Number.isFinite(maxLines) && Number.isFinite(maxWidth) ? maxLines * (Math.ceil(maxWidth) + 1) : Number.POSITIVE_INFINITY;
32853
+ let truncated = source.length > cap;
32854
+ if (truncated) source = source.slice(0, cap);
32855
+ const paragraphs = whiteSpace === "normal" ? source ? [source] : [] : source.split("\n");
32856
+ for (const paragraph of paragraphs) {
32857
+ if (lines.length > maxLines) break;
32793
32858
  if (!paragraph) {
32794
32859
  lines.push("");
32860
+ widths.push(0);
32795
32861
  continue;
32796
32862
  }
32797
- const wrapped = layoutWithLines(prepareText(paragraph, font), maxWidth, 1).lines;
32863
+ const wrapped = layoutWithLines(prepareText(paragraph, font, whiteSpace), maxWidth, 1).lines;
32798
32864
  for (const [index, line] of wrapped.entries()) {
32799
32865
  lines.push(index === wrapped.length - 1 ? line.text : line.text.replace(TRAILING_SPACE, ""));
32800
- width = Math.max(width, line.width);
32866
+ widths.push(line.width);
32801
32867
  }
32802
32868
  }
32869
+ if (lines.length > maxLines) {
32870
+ truncated = true;
32871
+ lines.length = maxLines;
32872
+ widths.length = maxLines;
32873
+ }
32874
+ if (truncated && lines.length > 0) {
32875
+ const last = lines.length - 1;
32876
+ lines[last] = fitLineWithEllipsis(lines[last], maxWidth, (value) => measureTextWidth(value, font));
32877
+ widths[last] = measureTextWidth(lines[last], font);
32878
+ }
32803
32879
  return {
32804
32880
  lines,
32805
- width
32881
+ width: widths.reduce((max, width) => Math.max(max, width), 0),
32882
+ truncated
32806
32883
  };
32807
32884
  }
32885
+ const graphemes = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
32886
+ function fitLineWithEllipsis(line, width, measureWidth) {
32887
+ const trimmed = line.replace(/\s+$/u, "");
32888
+ if (!trimmed) return "…";
32889
+ if (measureWidth(`${trimmed}…`) <= width) return `${trimmed}…`;
32890
+ const parts = graphemes ? Array.from(graphemes.segment(trimmed), ({ segment }) => segment) : Array.from(trimmed);
32891
+ let lo = 0;
32892
+ let hi = parts.length - 1;
32893
+ while (lo < hi) {
32894
+ const mid = lo + hi + 1 >> 1;
32895
+ if (measureWidth(`${parts.slice(0, mid).join("")}…`) <= width) lo = mid;
32896
+ else hi = mid - 1;
32897
+ }
32898
+ return `${parts.slice(0, lo).join("")}…`;
32899
+ }
32808
32900
  const baselineShifts = /* @__PURE__ */ new Map();
32809
32901
  function cssBaselineShift(style) {
32810
32902
  const font = fontStringFromTextStyle(style);
@@ -32996,40 +33088,10 @@ function boardTextSize(item) {
32996
33088
  }
32997
33089
  //#endregion
32998
33090
  //#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
33091
  function fitTextToLines(text, value, maxLines, wrapWidth) {
33018
- const width = Math.max(1, wrapWidth);
33019
33092
  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");
33093
+ const { lines } = layoutTextLines(value, fontStringFromTextStyle(text.style), Math.max(1, wrapWidth), { maxLines });
33094
+ const fitted = lines.join("\n");
33033
33095
  if (text.text !== fitted) text.text = fitted;
33034
33096
  }
33035
33097
  function fitTextToWidth(text, value, width) {
@@ -33644,6 +33706,10 @@ function traceLocal(graphics, frame, rect) {
33644
33706
  const d = corner(x, y + height);
33645
33707
  graphics.moveTo(a.x, a.y).lineTo(b.x, b.y).lineTo(c.x, c.y).lineTo(d.x, d.y).closePath();
33646
33708
  }
33709
+ function farPictureTone(item, context) {
33710
+ const key = context.assetKey(item);
33711
+ return (key ? context.assetTone?.(key) : null) ?? void 0;
33712
+ }
33647
33713
  function drawFarPlate(graphics, frame, style) {
33648
33714
  if (frame.width <= 0 || frame.height <= 0) return;
33649
33715
  traceLocal(graphics, frame, {
@@ -33983,7 +34049,7 @@ const modelCardRenderer = {
33983
34049
  },
33984
34050
  renderFar: (graphics, item, context) => {
33985
34051
  drawFarPlate(graphics, item.frame, {
33986
- fill: context.palette.surface,
34052
+ fill: farPictureTone(item, context) ?? context.palette.surface,
33987
34053
  fillAlpha: .96,
33988
34054
  accent: context.palette.legendary,
33989
34055
  accentAlpha: .45
@@ -33995,6 +34061,64 @@ const modelCardRenderer = {
33995
34061
  }
33996
34062
  };
33997
34063
  //#endregion
34064
+ //#region ../sdk/src/board/render/media-interaction.ts
34065
+ const BOARD_PLAY_MEDIA = "play-media";
34066
+ function mediaPlayBadgeVisible(item, zoom, options) {
34067
+ if (!options.materialized) return false;
34068
+ if (item.type === "video" || item.type === "audio") return true;
34069
+ if (isBoardModelItem(item)) return boardCardDetailed(zoom);
34070
+ if (item.type !== "task" || !boardCardDetailed(zoom)) return false;
34071
+ const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
34072
+ if (artifact?.type === "audio") return true;
34073
+ return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
34074
+ }
34075
+ function cornerKey(item) {
34076
+ const { width, height } = item.frame;
34077
+ if (item.type === "audio") return audioKeyCenter(width, height);
34078
+ return isBoardModelItem(item) ? modelKeyCenter(width, height) : null;
34079
+ }
34080
+ function mediaPlayKeyCenter(item) {
34081
+ const { x, y, width, height, rotation } = item.frame;
34082
+ const center = worldPoint(x + width / 2, y + height / 2);
34083
+ const key = cornerKey(item);
34084
+ if (!key) return center;
34085
+ return rotatePointAround(worldPoint(x + key.x, y + key.y), center, degToRad(rotation || 0));
34086
+ }
34087
+ function mediaPlayBadgeHit(item, point, zoom) {
34088
+ const scale = Math.max(zoom, .05);
34089
+ const radius = isBoardModelItem(item) ? Math.max(cornerKey(item)?.radius ?? 0, 16 / scale) : 28 / scale;
34090
+ const center = mediaPlayKeyCenter(item);
34091
+ return Math.hypot(point.x - center.x, point.y - center.y) <= radius;
34092
+ }
34093
+ function boardMediaActionAt(item, point, zoom, options) {
34094
+ return mediaPlayBadgeVisible(item, zoom, options) && mediaPlayBadgeHit(item, point, zoom) ? {
34095
+ action: BOARD_PLAY_MEDIA,
34096
+ itemId: item.id
34097
+ } : null;
34098
+ }
34099
+ const PLAY_HIT = {
34100
+ action: BOARD_PLAY_MEDIA,
34101
+ cursor: "pointer"
34102
+ };
34103
+ function mediaPlayHit(item, point, context) {
34104
+ const key = context.assetKey(item);
34105
+ return boardMediaActionAt(item, point, context.zoom, {
34106
+ materialized: true,
34107
+ hasVideoPreview: Boolean(key && context.getTexture(key))
34108
+ }) ? PLAY_HIT : null;
34109
+ }
34110
+ //#endregion
34111
+ //#region ../sdk/src/board/render/labels.ts
34112
+ const ARROW_LABEL = {
34113
+ fontWeight: "500",
34114
+ lineHeight: 1.25,
34115
+ padX: .5,
34116
+ padY: .25
34117
+ };
34118
+ function arrowLabelAnchor(arrow, style) {
34119
+ return style.trim === void 0 ? arrow.mid : pathPointAt(arrow.path, .5);
34120
+ }
34121
+ //#endregion
33998
34122
  //#region ../sdk/src/board/render/renderers/arrow-card-renderer.ts
33999
34123
  const partsByContainer$11 = /* @__PURE__ */ new WeakMap();
34000
34124
  function sync$11(container, item, context) {
@@ -34056,8 +34180,9 @@ function sync$11(container, item, context) {
34056
34180
  parts.label.text = props.label;
34057
34181
  parts.label.style.fill = context.palette.text;
34058
34182
  parts.label.style.fontSize = props.fontSize;
34059
- const padX = props.fontSize * .5;
34060
- const padY = props.fontSize * .25;
34183
+ parts.label.style.lineHeight = props.fontSize * ARROW_LABEL.lineHeight;
34184
+ const padX = props.fontSize * ARROW_LABEL.padX;
34185
+ const padY = props.fontSize * ARROW_LABEL.padY;
34061
34186
  const { width, height } = parts.label;
34062
34187
  const plate = {
34063
34188
  x: -width / 2 - padX,
@@ -34077,7 +34202,7 @@ function sync$11(container, item, context) {
34077
34202
  const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
34078
34203
  parts.label.visible = visible;
34079
34204
  parts.backdrop.visible = visible;
34080
- const mid = style.trim === void 0 ? resolved.mid : pathPointAt(resolved.path, .5);
34205
+ const mid = arrowLabelAnchor(resolved, style);
34081
34206
  parts.label.position.set(mid.x, mid.y);
34082
34207
  parts.backdrop.position.set(mid.x, mid.y);
34083
34208
  }
@@ -34093,7 +34218,7 @@ const arrowCardRenderer = {
34093
34218
  fill: 16777215,
34094
34219
  fontFamily: BOARD_FONT_STACK,
34095
34220
  fontSize: 14,
34096
- fontWeight: "500"
34221
+ fontWeight: ARROW_LABEL.fontWeight
34097
34222
  },
34098
34223
  resolution: context.textResolution,
34099
34224
  roundPixels: true
@@ -34481,8 +34606,34 @@ const partsByContainer$8 = /* @__PURE__ */ new WeakMap();
34481
34606
  function detailFor(zoom) {
34482
34607
  return boardCardDetailed(zoom) ? "full" : "title";
34483
34608
  }
34484
- function coverHeight(item, height) {
34485
- if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
34609
+ function viewOf(item) {
34610
+ if (item.type === "file") {
34611
+ const { src, snapshot } = item.props;
34612
+ return {
34613
+ src,
34614
+ title: snapshot?.title || fileStem(src),
34615
+ excerpt: snapshot?.excerpt ?? "",
34616
+ kind: filePreviewKind(snapshot),
34617
+ meta: fileMetaLine(src, snapshot?.size),
34618
+ mark: fileTypeLabel(src),
34619
+ category: fileCategory(src, snapshot?.mimeType)
34620
+ };
34621
+ }
34622
+ const preview = boardItemPreview(item);
34623
+ if (!preview) return null;
34624
+ const label = (item.type.split(".").pop() ?? item.type).toUpperCase();
34625
+ return {
34626
+ src: preview.src ?? null,
34627
+ title: preview.title || item.type,
34628
+ excerpt: preview.excerpt ?? "",
34629
+ kind: preview.cover ? "cover" : preview.excerpt ? "text" : "blank",
34630
+ meta: label,
34631
+ mark: label,
34632
+ category: preview.src ? fileCategory(preview.src) : "other"
34633
+ };
34634
+ }
34635
+ function coverHeight(view, height) {
34636
+ if (view.kind !== "cover") return 0;
34486
34637
  const ideal = Math.round(height * COVER_RATIO);
34487
34638
  return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
34488
34639
  }
@@ -34501,31 +34652,31 @@ function syncClip(parts, rect, style) {
34501
34652
  }
34502
34653
  function sync$8(container, item, context) {
34503
34654
  const parts = partsByContainer$8.get(container);
34504
- if (!parts) return;
34505
- syncContent(parts, item, context);
34655
+ const view = viewOf(item);
34656
+ if (!parts || !view) return;
34657
+ syncContent(parts, item, view, context);
34506
34658
  for (const text of [
34507
34659
  parts.title,
34508
34660
  parts.excerpt,
34509
34661
  parts.typeMark
34510
34662
  ]) syncTextResolution(text, context.textResolution);
34511
34663
  }
34512
- function syncContent(parts, item, context) {
34664
+ function syncContent(parts, item, view, context) {
34513
34665
  positionShell(parts.root, item);
34514
34666
  const { width, height } = item.frame;
34515
34667
  const detail = detailFor(context.zoom);
34516
34668
  const elevation = cardElevation(context, item.id);
34517
34669
  syncCardTag(parts.tag, {
34518
34670
  glyph: "file",
34519
- text: fileMetaLine(item.props.src, item.props.snapshot?.size),
34671
+ text: view.meta,
34520
34672
  width
34521
34673
  }, context);
34522
34674
  const key = context.assetKey(item);
34523
34675
  const texture = key ? context.getTexture(key) : null;
34524
34676
  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);
34677
+ const fileState = view.src ? context.fileState(view.src) : "ok";
34678
+ const band = coverHeight(view, height);
34679
+ const { kind, category, title, excerpt, mark } = view;
34529
34680
  const accent = fileCategoryAccent(category, context.palette);
34530
34681
  const visualSig = [
34531
34682
  key ?? "",
@@ -34584,9 +34735,6 @@ function syncContent(parts, item, context) {
34584
34735
  parts.excerpt.visible = detail === "full";
34585
34736
  parts.typeMark.visible = kind === "blank";
34586
34737
  }
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
34738
  const innerWidth = Math.max(1, width - 20);
34591
34739
  const showTypeMark = kind === "blank";
34592
34740
  const textSig = [
@@ -34642,7 +34790,7 @@ function syncContent(parts, item, context) {
34642
34790
  }
34643
34791
  const fileCardRenderer = {
34644
34792
  id: "file-card",
34645
- canRender: (item) => item.type === "file",
34793
+ canRender: (item) => item.type === "file" || boardItemPreview(item) !== null,
34646
34794
  create: (item, context) => {
34647
34795
  const root = new Container();
34648
34796
  const plate = new Graphics();
@@ -34706,23 +34854,19 @@ const fileCardRenderer = {
34706
34854
  visualSig: "",
34707
34855
  textSig: ""
34708
34856
  });
34709
- if (item.type === "file") sync$8(root, item, context);
34857
+ sync$8(root, item, context);
34710
34858
  return root;
34711
34859
  },
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
- },
34860
+ update: sync$8,
34861
+ resolution: sync$8,
34718
34862
  zoom: (container, zoom) => {
34719
34863
  const tag = partsByContainer$8.get(container)?.tag;
34720
34864
  if (tag) zoomCardTag(tag, zoom);
34721
34865
  },
34722
34866
  renderFar: (graphics, item, context) => {
34723
- const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
34867
+ const category = viewOf(item)?.category ?? "other";
34724
34868
  drawFarPlate(graphics, item.frame, {
34725
- fill: context.palette.surface,
34869
+ fill: farPictureTone(item, context) ?? context.palette.surface,
34726
34870
  fillAlpha: .96,
34727
34871
  accent: fileCategoryAccent(category, context.palette),
34728
34872
  accentAlpha: .45
@@ -34786,19 +34930,24 @@ function sync$7(container, item, context) {
34786
34930
  parts.label.visible = decorated;
34787
34931
  const title = locked ? `🔒 ${props.label}`.trim() : props.label;
34788
34932
  const labelColor = style.stroke ? baseStroke : context.palette.muted;
34933
+ const size = context.style.frameLabelSize;
34789
34934
  const textSig = [
34790
34935
  title,
34791
34936
  labelColor,
34792
- context.style.font
34937
+ context.style.font,
34938
+ size
34793
34939
  ].join("|");
34794
34940
  if (textSig !== parts.textSig) {
34795
34941
  parts.textSig = textSig;
34796
34942
  parts.label.text = title;
34797
34943
  parts.label.style.fill = labelColor;
34798
34944
  parts.label.style.fontFamily = context.style.font;
34945
+ parts.label.style.fontSize = size;
34946
+ parts.label.style.lineHeight = size * BOARD_FRAME_LABEL_LINE_HEIGHT;
34947
+ const offset = frameLabelOffset(size);
34948
+ parts.label.position.set(offset.x, offset.y);
34799
34949
  }
34800
34950
  syncTextResolution(parts.label, context.textResolution);
34801
- parts.label.position.set(4, -18);
34802
34951
  }
34803
34952
  const frameCardRenderer = {
34804
34953
  id: "frame-card",
@@ -34809,7 +34958,6 @@ const frameCardRenderer = {
34809
34958
  const label = createLabel("", {
34810
34959
  fill: context.palette.muted,
34811
34960
  fontFamily: BOARD_FONT_STACK,
34812
- fontSize: 12,
34813
34961
  fontWeight: "600"
34814
34962
  }, context.textResolution);
34815
34963
  root.addChild(box, label);
@@ -35284,7 +35432,7 @@ const imageCardRenderer = {
35284
35432
  lod: { kind: "media" },
35285
35433
  renderFar: (graphics, item, context) => {
35286
35434
  drawFarPlate(graphics, item.frame, {
35287
- fill: context.palette.hover,
35435
+ fill: farPictureTone(item, context) ?? context.palette.hover,
35288
35436
  fillAlpha: FAR_ALPHA
35289
35437
  });
35290
35438
  },
@@ -35797,7 +35945,7 @@ const taskCardRenderer = {
35797
35945
  const { status } = liveStatus(item, context);
35798
35946
  const active = status === "failed" || status === "running" || status === "pending";
35799
35947
  drawFarPlate(graphics, item.frame, {
35800
- fill: context.palette.surface,
35948
+ fill: farPictureTone(item, context) ?? context.palette.surface,
35801
35949
  fillAlpha: .82,
35802
35950
  ...active ? {
35803
35951
  accent: statusColor(status, context),
@@ -36170,7 +36318,7 @@ const videoCardRenderer = {
36170
36318
  lod: { kind: "media" },
36171
36319
  renderFar: (graphics, item, context) => {
36172
36320
  drawFarPlate(graphics, item.frame, {
36173
- fill: context.palette.surface,
36321
+ fill: farPictureTone(item, context) ?? context.palette.surface,
36174
36322
  fillAlpha: .96,
36175
36323
  accent: context.palette.brand,
36176
36324
  accentAlpha: .75
@@ -36190,14 +36338,18 @@ const DEFAULT_BOARD_RENDER_LABELS = {
36190
36338
  previewUnavailable: "Preview unavailable",
36191
36339
  noPreview: "No preview"
36192
36340
  };
36341
+ const playable = (renderer) => ({
36342
+ ...renderer,
36343
+ hitAt: mediaPlayHit
36344
+ });
36193
36345
  const builtinCardRenderers = [
36194
36346
  textCardRenderer,
36195
36347
  imageCardRenderer,
36196
- videoCardRenderer,
36197
- audioCardRenderer,
36198
- modelCardRenderer,
36348
+ playable(videoCardRenderer),
36349
+ playable(audioCardRenderer),
36350
+ playable(modelCardRenderer),
36199
36351
  fileCardRenderer,
36200
- taskCardRenderer,
36352
+ playable(taskCardRenderer),
36201
36353
  shapeCardRenderer,
36202
36354
  drawCardRenderer,
36203
36355
  arrowCardRenderer,
@@ -36690,10 +36842,17 @@ object({
36690
36842
  props: object({}).strict().default({})
36691
36843
  }).strict();
36692
36844
  const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
36845
+ const BoardItemPreviewSchema = object({
36846
+ title: string().max(500).optional(),
36847
+ excerpt: string().max(2e3).optional(),
36848
+ cover: BoardMediaSourceSchema.optional().describe("An image for the card."),
36849
+ src: BoardMediaSourceSchema.optional().describe("The file or page the card stands for.")
36850
+ }).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
36851
  object({
36694
36852
  ...boxed(BOARD_DEFAULT_ITEM_SIZE),
36695
36853
  type: string().regex(BOARD_EXTENSION_TYPE_PATTERN),
36696
- props: record(string(), unknown()).default({})
36854
+ props: record(string(), unknown()).default({}),
36855
+ preview: BoardItemPreviewSchema.optional()
36697
36856
  }).strict();
36698
36857
  const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
36699
36858
  "linear",
@@ -37143,7 +37302,8 @@ const BASE = {
37143
37302
  progress: "shimmer",
37144
37303
  icons: "line",
37145
37304
  motion: "smooth",
37146
- font: BOARD_FONT_STACK
37305
+ font: BOARD_FONT_STACK,
37306
+ frameLabelSize: 12
37147
37307
  };
37148
37308
  const SHADOW = {
37149
37309
  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.0",
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.3.0",
22
22
  "@neta-art/generation": "^0.1.40",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",