@neta-art/cohub 5.6.0 → 5.8.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.
Files changed (59) hide show
  1. package/README.md +18 -5
  2. package/dist/board/codec.js +15 -0
  3. package/dist/board/core/palette.d.ts +3 -2
  4. package/dist/board/core/shape-types.d.ts +8 -2
  5. package/dist/board/core/shape-types.js +3 -7
  6. package/dist/board/core/tool-styles.d.ts +2 -1
  7. package/dist/board/image-key.js +3 -5
  8. package/dist/board/index.d.ts +10 -5
  9. package/dist/board/index.js +7 -3
  10. package/dist/board/media-playback.d.ts +22 -0
  11. package/dist/board/media-playback.js +67 -0
  12. package/dist/board/media.d.ts +7 -0
  13. package/dist/board/media.js +70 -0
  14. package/dist/board/nodes.d.ts +113 -0
  15. package/dist/board/nodes.js +154 -0
  16. package/dist/board/render/audio-waveform.d.ts +12 -0
  17. package/dist/board/render/audio-waveform.js +36 -0
  18. package/dist/board/render/index.d.ts +4 -1
  19. package/dist/board/render/index.js +4 -1
  20. package/dist/board/render/media-interaction.d.ts +19 -0
  21. package/dist/board/render/media-interaction.js +26 -0
  22. package/dist/board/render/renderers/audio-card-renderer.d.ts +5 -0
  23. package/dist/board/render/renderers/audio-card-renderer.js +120 -0
  24. package/dist/board/render/renderers/board-renderer-registry.js +4 -2
  25. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  26. package/dist/board/render/renderers/file-card-renderer.js +1 -1
  27. package/dist/board/render/renderers/frame-card-renderer.js +1 -1
  28. package/dist/board/render/renderers/geo-card-renderer.js +1 -1
  29. package/dist/board/render/renderers/image-card-renderer.js +1 -1
  30. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  31. package/dist/board/render/renderers/task-card-renderer.js +21 -53
  32. package/dist/board/render/renderers/text-card-renderer.js +1 -1
  33. package/dist/board/render/renderers/unknown-card-renderer.js +1 -1
  34. package/dist/board/render/renderers/video-card-renderer.js +1 -1
  35. package/dist/board/render/video-thumbnail.d.ts +16 -0
  36. package/dist/board/render/video-thumbnail.js +86 -0
  37. package/dist/board/task.d.ts +10 -5
  38. package/dist/board/task.js +159 -103
  39. package/dist/chunks/environment.d.ts +6 -6
  40. package/dist/chunks/environment.js +6 -6
  41. package/dist/chunks/http.d.ts +71 -5
  42. package/dist/chunks/http.js +1535 -167
  43. package/dist/chunks/transport.js +8 -1
  44. package/dist/chunks/websocket.d.ts +139 -3
  45. package/dist/http.d.ts +3 -3
  46. package/dist/index.d.ts +245 -4
  47. package/dist/index.js +438 -788
  48. package/dist/protocol/dist/board-document.d.ts +271 -53
  49. package/dist/protocol/dist/board-document.js +54 -22
  50. package/dist/protocol/dist/board-node.d.ts +18 -0
  51. package/dist/protocol/dist/board-node.js +239 -0
  52. package/dist/protocol/dist/board-url.d.ts +12 -0
  53. package/dist/protocol/dist/board-url.js +83 -0
  54. package/dist/protocol/dist/board.d.ts +5 -0
  55. package/dist/protocol/dist/index.d.ts +2 -1
  56. package/dist/protocol/dist/index.js +2 -1
  57. package/dist/protocol/dist/provenance.js +1 -0
  58. package/docs/work-runtime-guide.md +7 -7
  59. package/package.json +1 -1
@@ -0,0 +1,154 @@
1
+ import "../protocol/dist/board-constants.js";
2
+ import { validateBoardNodeInput } from "../protocol/dist/board-node.js";
3
+ import "../protocol/dist/index.js";
4
+ import { computeDrawBounds } from "./core/draw-geometry.js";
5
+ //#region src/board/nodes.ts
6
+ var BoardInputError = class extends Error {
7
+ code = "INVALID_BOARD_NODE";
8
+ diagnostics;
9
+ body;
10
+ constructor(diagnostics) {
11
+ const message = diagnostics[0]?.message ?? "Invalid Board node";
12
+ super(message);
13
+ this.name = "BoardInputError";
14
+ this.diagnostics = diagnostics;
15
+ this.body = {
16
+ code: this.code,
17
+ message,
18
+ diagnostics
19
+ };
20
+ }
21
+ };
22
+ function baseNode(spec, frame) {
23
+ return {
24
+ nodeId: spec.id,
25
+ type: spec.type,
26
+ parentId: spec.parentId ?? null,
27
+ orderKey: spec.orderKey ?? null,
28
+ x: frame.x,
29
+ y: frame.y,
30
+ width: frame.width,
31
+ height: frame.height,
32
+ rotation: frame.rotation ?? 0,
33
+ refKind: null,
34
+ refPath: null,
35
+ refUrl: null,
36
+ view: {},
37
+ style: spec.style ?? {},
38
+ data: {}
39
+ };
40
+ }
41
+ function arrowFrame(start, end) {
42
+ const padding = 16;
43
+ return {
44
+ x: Math.min(start.x, end.x) - padding,
45
+ y: Math.min(start.y, end.y) - padding,
46
+ width: Math.max(1, Math.abs(end.x - start.x) + padding * 2),
47
+ height: Math.max(1, Math.abs(end.y - start.y) + padding * 2)
48
+ };
49
+ }
50
+ /**
51
+ * Create one validated wire node from semantic Board input.
52
+ *
53
+ * Box nodes take an explicit world-space frame. Draw samples and arrow endpoints
54
+ * take world coordinates; the builder derives their frame and local storage form.
55
+ */
56
+ function createBoardNode(spec) {
57
+ let node;
58
+ if (spec.type === "draw") {
59
+ const size = spec.size ?? 4;
60
+ const worldPoints = spec.points.map((point) => ({
61
+ x: point.x,
62
+ y: point.y,
63
+ p: point.p ?? .5
64
+ }));
65
+ const bounds = computeDrawBounds(worldPoints, size);
66
+ node = baseNode(spec, bounds);
67
+ node.data = {
68
+ points: worldPoints.map((point) => ({
69
+ x: point.x - bounds.x,
70
+ y: point.y - bounds.y,
71
+ p: point.p
72
+ })),
73
+ color: spec.color ?? "brand",
74
+ size
75
+ };
76
+ } else if (spec.type === "arrow") {
77
+ node = baseNode(spec, arrowFrame(spec.start, spec.end));
78
+ node.data = {
79
+ start: spec.start,
80
+ end: spec.end,
81
+ bend: spec.bend ?? 0,
82
+ color: spec.color ?? "brand",
83
+ size: spec.size ?? 2.5,
84
+ arrowStart: spec.arrowStart ?? false,
85
+ arrowEnd: spec.arrowEnd ?? true,
86
+ label: spec.label ?? ""
87
+ };
88
+ } else {
89
+ node = baseNode(spec, spec.frame);
90
+ switch (spec.type) {
91
+ case "text":
92
+ node.data = {
93
+ text: spec.text ?? "",
94
+ color: spec.color ?? "neutral",
95
+ fontSize: spec.fontSize ?? 24
96
+ };
97
+ break;
98
+ case "geo":
99
+ node.data = {
100
+ geo: spec.geo ?? "rectangle",
101
+ text: spec.text ?? "",
102
+ color: spec.color ?? "brand",
103
+ fillOpacity: spec.fillOpacity ?? 0
104
+ };
105
+ break;
106
+ case "frame":
107
+ node.data = {
108
+ label: spec.label ?? "Frame",
109
+ color: spec.color ?? "neutral"
110
+ };
111
+ break;
112
+ case "image":
113
+ node.refKind = "space_file";
114
+ node.refPath = spec.path;
115
+ node.view = spec.snapshot ?? {};
116
+ node.data = spec.crop ? { crop: spec.crop } : {};
117
+ break;
118
+ case "video":
119
+ case "audio":
120
+ node.refKind = "space_file";
121
+ node.refPath = spec.path;
122
+ node.view = spec.snapshot ?? {};
123
+ break;
124
+ case "file":
125
+ node.refKind = "space_file";
126
+ node.refPath = spec.path;
127
+ node.view = spec.snapshot ?? {};
128
+ break;
129
+ case "task":
130
+ node.view = spec.snapshot;
131
+ node.data = { taskRunId: spec.taskRunId };
132
+ break;
133
+ }
134
+ }
135
+ assertBoardNodes([node]);
136
+ return node;
137
+ }
138
+ function validateBoardNodes(nodes, path = "nodes") {
139
+ return nodes.flatMap((node, index) => validateBoardNodeInput(node, `${path}.${index}`));
140
+ }
141
+ function assertBoardNodes(nodes, path = "nodes") {
142
+ const diagnostics = validateBoardNodes(nodes, path);
143
+ if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
144
+ }
145
+ function assertBoardTransactionNodeCreates(operations) {
146
+ const diagnostics = operations.flatMap((operation, index) => {
147
+ if (operation.type !== "node.create") return [];
148
+ const payload = operation.payload;
149
+ return payload.node ? validateBoardNodeInput(payload.node, `operations.${index}.payload.node`) : [];
150
+ });
151
+ if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
152
+ }
153
+ //#endregion
154
+ export { BoardInputError, assertBoardNodes, assertBoardTransactionNodeCreates, createBoardNode, validateBoardNodes };
@@ -0,0 +1,12 @@
1
+ import { Graphics } from "pixi.js";
2
+ //#region src/board/render/audio-waveform.d.ts
3
+ /** Stable placeholder amplitudes without reading or decoding the audio file. */
4
+ declare function audioWaveformBars(seed: string, count: number): number[];
5
+ declare function drawAudioWaveform(graphics: Graphics, seed: string, rect: {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }, color: number, alpha?: number): void;
11
+ //#endregion
12
+ export { audioWaveformBars, drawAudioWaveform };
@@ -0,0 +1,36 @@
1
+ //#region src/board/render/audio-waveform.ts
2
+ const DEFAULT_BARS = 44;
3
+ const BAR_GAP = 2;
4
+ /** Stable placeholder amplitudes without reading or decoding the audio file. */
5
+ function audioWaveformBars(seed, count) {
6
+ let hash = 2166136261;
7
+ for (let index = 0; index < seed.length; index += 1) {
8
+ hash ^= seed.charCodeAt(index);
9
+ hash = Math.imul(hash, 16777619);
10
+ }
11
+ const bars = [];
12
+ for (let index = 0; index < count; index += 1) {
13
+ hash ^= hash << 13;
14
+ hash ^= hash >>> 17;
15
+ hash ^= hash << 5;
16
+ bars.push(.2 + (hash >>> 0) / 4294967295 * .8);
17
+ }
18
+ return bars;
19
+ }
20
+ function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
21
+ const count = Math.max(8, Math.min(DEFAULT_BARS, Math.floor(rect.width / 4)));
22
+ const step = rect.width / count;
23
+ const barWidth = Math.max(1.5, step - BAR_GAP);
24
+ const centerY = rect.y + rect.height / 2;
25
+ const maxHeight = Math.max(2, rect.height * .72);
26
+ const bars = audioWaveformBars(seed, count);
27
+ for (let index = 0; index < count; index += 1) {
28
+ const height = Math.max(2, (bars[index] ?? .4) * maxHeight);
29
+ graphics.roundRect(rect.x + index * step, centerY - height / 2, barWidth, height, barWidth / 2).fill({
30
+ color,
31
+ alpha
32
+ });
33
+ }
34
+ }
35
+ //#endregion
36
+ export { audioWaveformBars, drawAudioWaveform };
@@ -1,7 +1,10 @@
1
1
  import { ConnectionLayer, ConnectionRenderInput, createConnectionLayer, framesFromItems } from "./connection-layer.js";
2
+ import { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
2
3
  import { BoardCardRenderer, BoardRenderContext, BoardRenderPalette, boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer } from "./renderers/board-renderer-registry.js";
3
4
  import { defaultBoardPalette } from "./palette.js";
5
+ import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
4
6
  import { ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
7
  import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
6
8
  import { BoardThemeContext, BoardThemeRenderer, getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
7
- export { BoardCardRenderer, BoardRenderContext, BoardRenderPalette, BoardThemeContext, BoardThemeRenderer, ConnectionLayer, ConnectionRenderInput, boardCardRenderersForTest, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
9
+ import { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
10
+ export { BoardCardRenderer, BoardMediaAction, BoardRenderContext, BoardRenderPalette, BoardThemeContext, BoardThemeRenderer, ConnectionLayer, ConnectionRenderInput, TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardRenderersForTest, boardMediaActionAt, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, loadVideoThumbnailTexture, mediaPlayBadgeHit, mediaPlayBadgeVisible, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, videoTextureNaturalSize, videoThumbnailSize };
@@ -1,7 +1,10 @@
1
1
  import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
2
2
  import { createConnectionLayer, framesFromItems } from "./connection-layer.js";
3
+ import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
4
+ import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
3
5
  import { defaultBoardPalette } from "./palette.js";
4
6
  import { ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
7
  import { boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer } from "./renderers/board-renderer-registry.js";
6
8
  import { getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
7
- export { boardCardRenderersForTest, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
9
+ import { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
10
+ export { TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, boardCardRenderersForTest, boardMediaActionAt, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, loadVideoThumbnailTexture, mediaPlayBadgeHit, mediaPlayBadgeVisible, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, videoTextureNaturalSize, videoThumbnailSize };
@@ -0,0 +1,19 @@
1
+ import { BoardItem } from "../../protocol/dist/board-document.js";
2
+ import { WorldPoint } from "../geometry.js";
3
+ //#region src/board/render/media-interaction.d.ts
4
+ type BoardMediaAction = {
5
+ action: "play-media";
6
+ itemId: string;
7
+ };
8
+ declare function mediaPlayBadgeVisible(item: BoardItem, zoom: number, options: {
9
+ materialized: boolean;
10
+ hasVideoPreview: boolean;
11
+ }): boolean;
12
+ /** The central badge is a fixed screen-space target, independent of Board zoom. */
13
+ declare function mediaPlayBadgeHit(item: BoardItem, point: WorldPoint, zoom: number): boolean;
14
+ declare function boardMediaActionAt(item: BoardItem, point: WorldPoint, zoom: number, options: {
15
+ materialized: boolean;
16
+ hasVideoPreview: boolean;
17
+ }): BoardMediaAction | null;
18
+ //#endregion
19
+ export { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
@@ -0,0 +1,26 @@
1
+ import { featuredTaskArtifact } from "../task.js";
2
+ import "./renderers/task-card-renderer.js";
3
+ //#region src/board/render/media-interaction.ts
4
+ function mediaPlayBadgeVisible(item, zoom, options) {
5
+ if (!options.materialized) return false;
6
+ if (item.type === "video" || item.type === "audio") return true;
7
+ if (item.type !== "task" || zoom < .55) return false;
8
+ const artifact = featuredTaskArtifact(item.snapshot.artifacts);
9
+ if (artifact?.type === "audio") return true;
10
+ return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
11
+ }
12
+ /** The central badge is a fixed screen-space target, independent of Board zoom. */
13
+ function mediaPlayBadgeHit(item, point, zoom) {
14
+ const radius = 28 / Math.max(zoom, .05);
15
+ const centerX = item.frame.x + item.frame.width / 2;
16
+ const centerY = item.frame.y + item.frame.height / 2;
17
+ return Math.hypot(point.x - centerX, point.y - centerY) <= radius;
18
+ }
19
+ function boardMediaActionAt(item, point, zoom, options) {
20
+ return mediaPlayBadgeVisible(item, zoom, options) && mediaPlayBadgeHit(item, point, zoom) ? {
21
+ action: "play-media",
22
+ itemId: item.id
23
+ } : null;
24
+ }
25
+ //#endregion
26
+ export { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
@@ -0,0 +1,5 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/audio-card-renderer.d.ts
3
+ declare const audioCardRenderer: BoardCardRenderer;
4
+ //#endregion
5
+ export { audioCardRenderer };
@@ -0,0 +1,120 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
3
+ import { drawAudioWaveform } from "../audio-waveform.js";
4
+ import { positionShell } from "./base-card-renderer.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
+ import { Container, Graphics, Text } from "pixi.js";
7
+ //#region src/board/render/renderers/audio-card-renderer.ts
8
+ const RADIUS = 4;
9
+ const PLAY_RADIUS = 18;
10
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
11
+ function sync(container, item, context) {
12
+ const parts = partsByContainer.get(container);
13
+ if (!parts) return;
14
+ positionShell(parts.root, item);
15
+ const { width, height } = item.frame;
16
+ const selected = context.selectedIds.has(item.id);
17
+ const hovered = context.hoveredId === item.id;
18
+ const title = item.snapshot?.title ?? item.ref.path.split("/").pop() ?? "Audio";
19
+ syncTextResolution(parts.label, parts, context.zoom);
20
+ if (parts.label.text !== title) parts.label.text = title;
21
+ syncTextWrapWidth(parts.label, parts, Math.max(1, width - 24), false);
22
+ parts.label.position.set(12, Math.max(8, height - parts.label.height - 8));
23
+ const sig = [
24
+ width,
25
+ height,
26
+ selected,
27
+ hovered,
28
+ title,
29
+ parts.label.height,
30
+ context.palette.surface,
31
+ context.palette.brand,
32
+ context.palette.border,
33
+ context.palette.muted,
34
+ context.palette.text
35
+ ].join("|");
36
+ if (sig === parts.sig) return;
37
+ parts.sig = sig;
38
+ parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
39
+ color: context.palette.surface,
40
+ alpha: .98
41
+ }).roundRect(0, 0, width, height, RADIUS).stroke({
42
+ color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
43
+ width: selected ? 2 : 1,
44
+ alpha: selected ? .96 : .84
45
+ });
46
+ const labelBand = Math.min(height * .38, parts.label.height + 16);
47
+ const waveformInset = Math.min(24, width * .08);
48
+ parts.waveform.clear();
49
+ drawAudioWaveform(parts.waveform, `${item.ref.path}:${item.snapshot?.mtimeMs ?? "unknown"}`, {
50
+ x: waveformInset,
51
+ y: 10,
52
+ width: Math.max(1, width - waveformInset * 2),
53
+ height: Math.max(8, height - labelBand - 16)
54
+ }, context.palette.brand);
55
+ const cx = width / 2;
56
+ const cy = Math.max(24, (height - labelBand) / 2);
57
+ parts.chrome.clear().circle(cx, cy, PLAY_RADIUS).fill({
58
+ color: selected ? context.palette.brand : context.palette.surface,
59
+ alpha: .92
60
+ });
61
+ const triangle = PLAY_RADIUS * .58;
62
+ parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .62).lineTo(cx - triangle * .35, cy + triangle * .62).lineTo(cx + triangle * .72, cy).closePath().fill({
63
+ color: context.palette.text,
64
+ alpha: .96
65
+ });
66
+ parts.label.style.fill = context.palette.text;
67
+ }
68
+ const audioCardRenderer = {
69
+ id: "audio-card",
70
+ canRender: (item) => item.type === "audio",
71
+ create: (item, context) => {
72
+ const root = new Container();
73
+ const plate = new Graphics();
74
+ const waveform = new Graphics();
75
+ const chrome = new Graphics();
76
+ const resolution = textResolutionForZoom(context.zoom);
77
+ const label = new Text({
78
+ text: "",
79
+ style: {
80
+ fill: context.palette.text,
81
+ fontFamily: BOARD_FONT_STACK,
82
+ fontSize: 12,
83
+ fontWeight: "500",
84
+ wordWrap: true
85
+ },
86
+ resolution,
87
+ roundPixels: true
88
+ });
89
+ root.addChild(plate, waveform, chrome, label);
90
+ partsByContainer.set(root, {
91
+ root,
92
+ plate,
93
+ waveform,
94
+ chrome,
95
+ label,
96
+ sig: "",
97
+ wrapWidth: 0,
98
+ resolution
99
+ });
100
+ if (item.type === "audio") sync(root, item, context);
101
+ return root;
102
+ },
103
+ update: (container, item, context) => {
104
+ if (item.type === "audio") sync(container, item, context);
105
+ },
106
+ renderFar: (graphics, item, context) => {
107
+ drawFarPlate(graphics, item.frame, {
108
+ fill: context.palette.surface,
109
+ fillAlpha: .96,
110
+ accent: context.palette.brand,
111
+ accentAlpha: .72
112
+ });
113
+ },
114
+ destroy: (container) => {
115
+ partsByContainer.get(container)?.root.destroy({ children: true });
116
+ partsByContainer.delete(container);
117
+ }
118
+ };
119
+ //#endregion
120
+ export { audioCardRenderer };
@@ -1,11 +1,12 @@
1
+ import { fileCardRenderer } from "./file-card-renderer.js";
2
+ import { taskCardRenderer } from "./task-card-renderer.js";
1
3
  import { ensureBoardTextMeasurement } from "../text-measurement.js";
2
4
  import { arrowCardRenderer } from "./arrow-card-renderer.js";
5
+ import { audioCardRenderer } from "./audio-card-renderer.js";
3
6
  import { drawCardRenderer } from "./draw-card-renderer.js";
4
- import { fileCardRenderer } from "./file-card-renderer.js";
5
7
  import { frameCardRenderer } from "./frame-card-renderer.js";
6
8
  import { geoCardRenderer } from "./geo-card-renderer.js";
7
9
  import { imageCardRenderer } from "./image-card-renderer.js";
8
- import { taskCardRenderer } from "./task-card-renderer.js";
9
10
  import { textCardRenderer } from "./text-card-renderer.js";
10
11
  import { unknownCardRenderer } from "./unknown-card-renderer.js";
11
12
  import { videoCardRenderer } from "./video-card-renderer.js";
@@ -14,6 +15,7 @@ const boardCardRenderers = [
14
15
  textCardRenderer,
15
16
  imageCardRenderer,
16
17
  videoCardRenderer,
18
+ audioCardRenderer,
17
19
  fileCardRenderer,
18
20
  taskCardRenderer,
19
21
  geoCardRenderer,
@@ -1,7 +1,7 @@
1
1
  import { buildStrokeOutline, computeDrawBounds } from "../../core/draw-geometry.js";
2
2
  import { pickBoardColor } from "../../core/palette.js";
3
- import { drawFarStroke } from "./far-plate.js";
4
3
  import { positionShell } from "./base-card-renderer.js";
4
+ import { drawFarStroke } from "./far-plate.js";
5
5
  import { Container, Graphics } from "pixi.js";
6
6
  //#region src/board/render/renderers/draw-card-renderer.ts
7
7
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -1,8 +1,8 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { fileBaseName, filePreviewKind } from "../../core/file-preview.js";
3
3
  import { syncTextResolution } from "../text-resolution.js";
4
- import { drawFarPlate } from "./far-plate.js";
5
4
  import { positionShell } from "./base-card-renderer.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
6
  import { CanvasTextMetrics, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
7
7
  //#region src/board/render/renderers/file-card-renderer.ts
8
8
  /**
@@ -1,8 +1,8 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { pickBoardColor } from "../../core/palette.js";
3
3
  import { syncTextResolution } from "../text-resolution.js";
4
- import { drawFarPlate } from "./far-plate.js";
5
4
  import { createLabel, positionShell } from "./base-card-renderer.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
6
  import { Container, Graphics } from "pixi.js";
7
7
  //#region src/board/render/renderers/frame-card-renderer.ts
8
8
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -1,8 +1,8 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { pickBoardColor } from "../../core/palette.js";
3
3
  import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
- import { drawFarPlate } from "./far-plate.js";
5
4
  import { positionShell } from "./base-card-renderer.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
6
  import { Container, Graphics, Text } from "pixi.js";
7
7
  //#region src/board/render/renderers/geo-card-renderer.ts
8
8
  const RADIUS = 8;
@@ -1,5 +1,5 @@
1
- import { drawFarPlate } from "./far-plate.js";
2
1
  import { positionShell } from "./base-card-renderer.js";
2
+ import { drawFarPlate } from "./far-plate.js";
3
3
  import { Container, Graphics, Sprite, Texture } from "pixi.js";
4
4
  //#region src/board/render/renderers/image-card-renderer.ts
5
5
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -1,5 +1,6 @@
1
1
  import { BoardCardRenderer } from "./board-renderer-registry.js";
2
2
  //#region src/board/render/renderers/task-card-renderer.d.ts
3
+ declare const TASK_CARD_FULL_DETAIL_ZOOM = 0.55;
3
4
  /** Scale a texture into a frame without cropping or distortion. */
4
5
  declare function containTaskPreviewRect(width: number, height: number, imageWidth: number, imageHeight: number): {
5
6
  x: number;
@@ -9,4 +10,4 @@ declare function containTaskPreviewRect(width: number, height: number, imageWidt
9
10
  };
10
11
  declare const taskCardRenderer: BoardCardRenderer;
11
12
  //#endregion
12
- export { containTaskPreviewRect, taskCardRenderer };
13
+ export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
@@ -1,24 +1,23 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { featuredTaskArtifact } from "../../task.js";
2
3
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
3
- import { drawFarPlate } from "./far-plate.js";
4
+ import { drawAudioWaveform } from "../audio-waveform.js";
4
5
  import { positionShell } from "./base-card-renderer.js";
6
+ import { drawFarPlate } from "./far-plate.js";
5
7
  import { fitTextToLines } from "./file-card-renderer.js";
6
8
  import { Container, Graphics, Sprite, Text } from "pixi.js";
7
9
  //#region src/board/render/renderers/task-card-renderer.ts
8
10
  const RADIUS = 4;
9
11
  const PADDING = 12;
10
12
  const META_HEIGHT = 28;
11
- const FULL_DETAIL_ZOOM = .55;
12
- const WAVEFORM_BARS = 44;
13
- const WAVEFORM_GAP = 2;
13
+ const TASK_CARD_FULL_DETAIL_ZOOM = .55;
14
14
  const PLAY_BADGE_RADIUS = 15;
15
15
  const partsByContainer = /* @__PURE__ */ new WeakMap();
16
- function previewKindFor(item) {
17
- const output = item.snapshot.primaryOutput;
18
- if (!output) return "empty";
19
- if (output.type === "image" || output.type === "video") return "texture";
20
- if (output.type === "audio") return "waveform";
21
- return output.textExcerpt ? "text" : "empty";
16
+ function previewKindFor(artifact) {
17
+ if (!artifact) return "empty";
18
+ if (artifact.type === "image" || artifact.type === "video") return "texture";
19
+ if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
20
+ return artifact.textExcerpt ? "text" : "empty";
22
21
  }
23
22
  function stateSurfaceFor(item, kind, hasTexture, previewFailed) {
24
23
  if (kind === "texture" && !hasTexture) {
@@ -47,7 +46,8 @@ function statusColor(item, context) {
47
46
  return context.colors.neutral.stroke;
48
47
  }
49
48
  function metadata(item) {
50
- const extra = item.snapshot.outputCount > 1 ? `+${item.snapshot.outputCount - 1}` : null;
49
+ const { artifactCount, artifacts } = item.snapshot;
50
+ const extra = artifactCount > artifacts.length ? `${artifacts.length}/${artifactCount}` : artifactCount > 1 ? `+${artifactCount - 1}` : null;
51
51
  return [item.snapshot.model, extra].filter(Boolean).join(" · ");
52
52
  }
53
53
  /** Scale a texture into a frame without cropping or distortion. */
@@ -68,40 +68,6 @@ function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
68
68
  height: renderedHeight
69
69
  };
70
70
  }
71
- function waveformBars(seed, count) {
72
- let hash = 2166136261;
73
- for (let index = 0; index < seed.length; index += 1) {
74
- hash ^= seed.charCodeAt(index);
75
- hash = Math.imul(hash, 16777619) >>> 0;
76
- }
77
- const bars = [];
78
- for (let index = 0; index < count; index += 1) {
79
- hash ^= hash << 13;
80
- hash ^= hash >>> 17;
81
- hash ^= hash << 5;
82
- hash >>>= 0;
83
- const unit = hash % 1e3 / 1e3;
84
- const envelope = Math.sin(Math.PI * (index + .5) / count);
85
- bars.push(.18 + unit * .82 * (.45 + envelope * .55));
86
- }
87
- return bars;
88
- }
89
- function drawWaveform(graphics, seed, rect, color) {
90
- const count = Math.max(8, Math.min(WAVEFORM_BARS, Math.floor(rect.width / 4)));
91
- const step = rect.width / count;
92
- const barWidth = Math.max(1.5, step - WAVEFORM_GAP);
93
- const centerY = rect.y + rect.height / 2;
94
- const maxHeight = Math.max(2, rect.height * .68);
95
- const bars = waveformBars(seed, count);
96
- for (let index = 0; index < count; index += 1) {
97
- const amplitude = (bars[index] ?? .4) * maxHeight;
98
- graphics.roundRect(rect.x + index * step + (step - barWidth) / 2, centerY - amplitude / 2, barWidth, amplitude, barWidth / 2);
99
- }
100
- graphics.fill({
101
- color,
102
- alpha: .68
103
- });
104
- }
105
71
  function drawPlayBadge(graphics, rect, context) {
106
72
  const radius = Math.min(PLAY_BADGE_RADIUS, Math.max(8, Math.min(rect.width, rect.height) * .16));
107
73
  const cx = rect.x + rect.width / 2;
@@ -194,7 +160,7 @@ function sync(container, item, context) {
194
160
  const { width, height } = item.frame;
195
161
  const selected = context.selectedIds.has(item.id);
196
162
  const hovered = context.hoveredId === item.id;
197
- const full = context.zoom >= FULL_DETAIL_ZOOM;
163
+ const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
198
164
  const color = statusColor(item, context);
199
165
  const key = context.assetKey(item);
200
166
  if (key !== parts.assetKey) {
@@ -204,7 +170,8 @@ function sync(container, item, context) {
204
170
  }
205
171
  const texture = key ? context.getTexture(key) : null;
206
172
  const previewFailed = Boolean(key && !texture && context.hasError(key));
207
- const kind = previewKindFor(item);
173
+ const artifact = featuredTaskArtifact(item.snapshot.artifacts);
174
+ const kind = previewKindFor(artifact);
208
175
  const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
209
176
  const frame = {
210
177
  x: 1,
@@ -212,7 +179,7 @@ function sync(container, item, context) {
212
179
  width: Math.max(1, width - 2),
213
180
  height: Math.max(1, height - 2)
214
181
  };
215
- const metaText = item.snapshot.primaryOutput ? metadata(item) : "";
182
+ const metaText = artifact ? metadata(item) : "";
216
183
  const showMeta = full && Boolean(metaText);
217
184
  const failedWithOutput = item.snapshot.status === "failed" && kind !== "empty";
218
185
  syncTextResolution(parts.body, parts, context.zoom);
@@ -228,7 +195,8 @@ function sync(container, item, context) {
228
195
  surface,
229
196
  showMeta,
230
197
  failedWithOutput,
231
- item.snapshot.primaryOutput?.type ?? "none",
198
+ artifact?.type ?? "none",
199
+ artifact?.id ?? "none",
232
200
  item.taskRunId,
233
201
  texture ? `${texture.width}x${texture.height}` : "none",
234
202
  context.palette.surface,
@@ -255,14 +223,14 @@ function sync(container, item, context) {
255
223
  parts.previewArt.clear();
256
224
  if (kind === "waveform") {
257
225
  const bottomInset = showMeta ? META_HEIGHT : 0;
258
- drawWaveform(parts.previewArt, item.taskRunId, {
226
+ drawAudioWaveform(parts.previewArt, item.taskRunId, {
259
227
  x: frame.x + PADDING,
260
228
  y: frame.y + PADDING,
261
229
  width: Math.max(1, frame.width - PADDING * 2),
262
230
  height: Math.max(1, frame.height - PADDING * 2 - bottomInset)
263
231
  }, context.colors.brand.stroke);
264
232
  }
265
- if (kind === "texture" && texture && item.snapshot.primaryOutput?.type === "video" && full) drawPlayBadge(parts.previewArt, frame, context);
233
+ if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
266
234
  if (surface) {
267
235
  const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
268
236
  drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
@@ -281,7 +249,7 @@ function sync(container, item, context) {
281
249
  parts.preview.width = fitted.width;
282
250
  parts.preview.height = fitted.height;
283
251
  }
284
- const bodyText = kind === "text" ? item.snapshot.primaryOutput?.textExcerpt ?? "" : stateLabel(surface);
252
+ const bodyText = kind === "text" && artifact?.type === "text" ? artifact.textExcerpt : stateLabel(surface);
285
253
  const textSig = [
286
254
  bodyText,
287
255
  metaText,
@@ -386,4 +354,4 @@ const taskCardRenderer = {
386
354
  }
387
355
  };
388
356
  //#endregion
389
- export { containTaskPreviewRect, taskCardRenderer };
357
+ export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
@@ -2,8 +2,8 @@ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { TEXT_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText } from "../../core/text-metrics.js";
3
3
  import { pickBoardColor } from "../../core/palette.js";
4
4
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
5
- import { drawFarPlate } from "./far-plate.js";
6
5
  import { positionShell } from "./base-card-renderer.js";
6
+ import { drawFarPlate } from "./far-plate.js";
7
7
  import { Container, Text } from "pixi.js";
8
8
  //#region src/board/render/renderers/text-card-renderer.ts
9
9
  const partsByContainer = /* @__PURE__ */ new WeakMap();