@neta-art/cohub 10.0.0 → 11.1.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 (155) hide show
  1. package/README.md +42 -7
  2. package/dist/board/editor/animation.d.ts +3 -0
  3. package/dist/board/editor/animation.js +16 -1
  4. package/dist/board/editor/clipboard.js +7 -0
  5. package/dist/board/editor/copy.js +23 -18
  6. package/dist/board/editor/creation.d.ts +7 -3
  7. package/dist/board/editor/creation.js +5 -4
  8. package/dist/board/editor/deletion.js +3 -3
  9. package/dist/board/editor/document.d.ts +8 -2
  10. package/dist/board/editor/document.js +54 -7
  11. package/dist/board/editor/editor.d.ts +22 -7
  12. package/dist/board/editor/editor.js +12 -2
  13. package/dist/board/editor/index.d.ts +2 -2
  14. package/dist/board/editor/interaction.d.ts +8 -2
  15. package/dist/board/editor/interaction.js +34 -1
  16. package/dist/board/editor/query.d.ts +5 -5
  17. package/dist/board/editor/query.js +14 -12
  18. package/dist/board/editor/selection.js +2 -1
  19. package/dist/board/editor/snapshots.d.ts +0 -2
  20. package/dist/board/editor/snapshots.js +4 -20
  21. package/dist/board/export/index.d.ts +25 -4
  22. package/dist/board/export/index.js +45 -12
  23. package/dist/board/export/node/index.d.ts +5 -4
  24. package/dist/board/export/node/index.js +16 -11
  25. package/dist/board/export/node/scripts.d.ts +16 -0
  26. package/dist/board/export/node/scripts.js +258 -0
  27. package/dist/board/export/scene.d.ts +2 -1
  28. package/dist/board/export/scene.js +24 -7
  29. package/dist/board/model/animation.d.ts +4 -1
  30. package/dist/board/model/animation.js +34 -7
  31. package/dist/board/model/authoring.d.ts +20 -0
  32. package/dist/board/model/authoring.js +185 -0
  33. package/dist/board/model/export-plan.js +1 -0
  34. package/dist/board/model/file-preview.js +1 -1
  35. package/dist/board/model/file-snapshot.js +3 -2
  36. package/dist/board/model/image-key.js +7 -3
  37. package/dist/board/model/index.d.ts +15 -7
  38. package/dist/board/model/index.js +16 -9
  39. package/dist/board/model/items/builtin.js +2 -2
  40. package/dist/board/model/items/create.d.ts +19 -4
  41. package/dist/board/model/items/create.js +130 -9
  42. package/dist/board/model/items/index.d.ts +2 -2
  43. package/dist/board/model/items/index.js +2 -2
  44. package/dist/board/model/media-playback.d.ts +2 -1
  45. package/dist/board/model/media-playback.js +15 -5
  46. package/dist/board/model/media.js +7 -1
  47. package/dist/board/model/overlay-placement.d.ts +25 -0
  48. package/dist/board/model/overlay-placement.js +35 -0
  49. package/dist/board/model/replay.d.ts +1 -1
  50. package/dist/board/model/replay.js +1 -0
  51. package/dist/board/model/sampling.d.ts +19 -0
  52. package/dist/board/model/sampling.js +96 -0
  53. package/dist/board/model/script-example.d.ts +4 -0
  54. package/dist/board/model/script-example.js +21 -0
  55. package/dist/board/model/script-hit.d.ts +27 -0
  56. package/dist/board/model/script-hit.js +109 -0
  57. package/dist/board/model/script-index.d.ts +7 -0
  58. package/dist/board/model/script-index.js +15 -0
  59. package/dist/board/model/script-observers.d.ts +18 -0
  60. package/dist/board/model/script-observers.js +92 -0
  61. package/dist/board/model/script-scene.d.ts +24 -0
  62. package/dist/board/model/script-scene.js +294 -0
  63. package/dist/board/model/script.d.ts +149 -0
  64. package/dist/board/model/script.js +1 -0
  65. package/dist/board/render/board-background.js +5 -4
  66. package/dist/board/render/css-color.d.ts +6 -1
  67. package/dist/board/render/css-color.js +34 -20
  68. package/dist/board/render/diagnostics.d.ts +20 -0
  69. package/dist/board/render/diagnostics.js +117 -0
  70. package/dist/board/render/index.d.ts +9 -6
  71. package/dist/board/render/index.js +10 -7
  72. package/dist/board/render/media-interaction.js +1 -1
  73. package/dist/board/render/palette.d.ts +2 -1
  74. package/dist/board/render/palette.js +6 -2
  75. package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
  76. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  77. package/dist/board/render/renderers/board-renderer-registry.d.ts +25 -5
  78. package/dist/board/render/renderers/board-renderer-registry.js +11 -4
  79. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  80. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +18 -16
  81. package/dist/board/render/renderers/draw-card-renderer.js +3 -2
  82. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  83. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  84. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  85. package/dist/board/render/renderers/shape-card-renderer.js +10 -27
  86. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  87. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  88. package/dist/board/render/renderers/text-card-renderer.js +5 -3
  89. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  90. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  91. package/dist/board/render/sampling.d.ts +22 -0
  92. package/dist/board/render/sampling.js +21 -0
  93. package/dist/board/render/script-export.d.ts +11 -0
  94. package/dist/board/render/script-export.js +86 -0
  95. package/dist/board/render/script-host.d.ts +31 -0
  96. package/dist/board/render/script-host.js +446 -0
  97. package/dist/board/render/script-modules.d.ts +2 -0
  98. package/dist/board/render/script-modules.js +2 -0
  99. package/dist/board/render/script-runtime.d.ts +29 -0
  100. package/dist/board/render/script-runtime.js +223 -0
  101. package/dist/board/render/script-sandbox.d.ts +4 -0
  102. package/dist/board/render/script-sandbox.js +173 -0
  103. package/dist/board/render/script-texture.d.ts +11 -0
  104. package/dist/board/render/script-texture.js +33 -0
  105. package/dist/board/render/stroke.d.ts +2 -1
  106. package/dist/board/render/stroke.js +22 -1
  107. package/dist/board/render/text-measurement.js +1 -1
  108. package/dist/board/stage/assets.js +7 -2
  109. package/dist/board/stage/maintenance.d.ts +5 -0
  110. package/dist/board/stage/maintenance.js +11 -0
  111. package/dist/board/stage/scene.d.ts +2 -0
  112. package/dist/board/stage/scene.js +21 -2
  113. package/dist/board/stage/script-pointer.d.ts +20 -0
  114. package/dist/board/stage/script-pointer.js +41 -0
  115. package/dist/board/stage/stage.d.ts +21 -3
  116. package/dist/board/stage/stage.js +350 -47
  117. package/dist/board/stage/texture-pool.js +2 -3
  118. package/dist/chunks/environment.d.ts +305 -27
  119. package/dist/chunks/environment.js +12 -4
  120. package/dist/chunks/host-bridge.d.ts +2 -2
  121. package/dist/chunks/http.d.ts +90 -7
  122. package/dist/chunks/http.js +157 -64
  123. package/dist/chunks/transport.js +4 -2
  124. package/dist/chunks/websocket.d.ts +9 -1
  125. package/dist/chunks/websocket.js +16 -2
  126. package/dist/host-bridge.js +2 -1
  127. package/dist/http.d.ts +3 -3
  128. package/dist/index.d.ts +182 -4
  129. package/dist/index.js +1559 -299
  130. package/dist/protocol/dist/board-assets.d.ts +1 -0
  131. package/dist/protocol/dist/board-constants.js +2 -2
  132. package/dist/protocol/dist/board-model.d.ts +236 -11
  133. package/dist/protocol/dist/board-model.js +46 -17
  134. package/dist/protocol/dist/board-patch.js +2 -1
  135. package/dist/protocol/dist/board-schema.d.ts +4 -2
  136. package/dist/protocol/dist/board-schema.js +18 -4
  137. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  138. package/dist/protocol/dist/board-script-modules.js +127 -0
  139. package/dist/protocol/dist/board-task.d.ts +22 -0
  140. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  141. package/dist/protocol/dist/display/index.d.ts +1 -0
  142. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  143. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  144. package/dist/protocol/dist/index.d.ts +7 -2
  145. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  146. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  147. package/dist/protocol/dist/task/index.d.ts +4 -0
  148. package/package.json +2 -1
  149. package/dist/board/export/node/sketch.d.ts +0 -19
  150. package/dist/board/export/node/sketch.js +0 -146
  151. package/dist/board/model/task.d.ts +0 -11
  152. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  153. package/dist/board/render/sketch-host.d.ts +0 -14
  154. package/dist/board/render/sketch-host.js +0 -216
  155. package/dist/types.d.ts +0 -36
@@ -1,6 +1,6 @@
1
1
  import { positionShell } from "./base-card-renderer.js";
2
2
  import { drawFarPlate } from "./far-plate.js";
3
- import { Container, Graphics, Sprite, Texture } from "pixi.js";
3
+ import { Container, Graphics, Rectangle, Sprite, Texture } from "pixi.js";
4
4
  //#region src/board/render/renderers/image-card-renderer.ts
5
5
  const partsByContainer = /* @__PURE__ */ new WeakMap();
6
6
  const RADIUS = 2;
@@ -15,6 +15,39 @@ function layoutContain(sprite, width, height) {
15
15
  sprite.x = (width - sprite.width) / 2;
16
16
  sprite.y = (height - sprite.height) / 2;
17
17
  }
18
+ function cropRect(crop) {
19
+ if (!crop) return null;
20
+ const w = Math.min(crop.w, 1 - crop.x);
21
+ const h = Math.min(crop.h, 1 - crop.y);
22
+ return w > 0 && h > 0 ? {
23
+ x: crop.x,
24
+ y: crop.y,
25
+ w,
26
+ h
27
+ } : null;
28
+ }
29
+ function showTexture(parts, texture, crop) {
30
+ const rect = texture ? cropRect(crop) : null;
31
+ const key = rect ? `${rect.x}|${rect.y}|${rect.w}|${rect.h}` : "";
32
+ const previous = parts.cropped;
33
+ const reusable = previous && parts.cropSource === texture && parts.cropKey === key;
34
+ if (texture && rect && !reusable) {
35
+ const { frame } = texture;
36
+ const width = Math.max(1, frame.width * rect.w);
37
+ const height = Math.max(1, frame.height * rect.h);
38
+ const x = frame.x + Math.min(frame.width * rect.x, frame.width - width);
39
+ const y = frame.y + Math.min(frame.height * rect.y, frame.height - height);
40
+ parts.cropped = new Texture({
41
+ source: texture.source,
42
+ frame: new Rectangle(x, y, width, height)
43
+ });
44
+ } else if (!rect) parts.cropped = null;
45
+ parts.cropSource = rect ? texture : null;
46
+ parts.cropKey = key;
47
+ const display = parts.cropped ?? texture ?? Texture.EMPTY;
48
+ if (parts.sprite.texture !== display) parts.sprite.texture = display;
49
+ if (previous && previous !== parts.cropped) previous.destroy(false);
50
+ }
18
51
  function sync(container, item, context) {
19
52
  const parts = partsByContainer.get(container);
20
53
  if (!parts) return;
@@ -33,12 +66,13 @@ function sync(container, item, context) {
33
66
  selected,
34
67
  hovered,
35
68
  texId,
69
+ JSON.stringify(item.props.crop ?? null),
36
70
  failed,
37
71
  context.colorScheme
38
72
  ].join("|");
39
73
  if (sig === parts.sig) return;
40
74
  parts.sig = sig;
41
- if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
75
+ showTexture(parts, texture, item.props.crop);
42
76
  parts.sprite.visible = Boolean(texture);
43
77
  if (texture) layoutContain(parts.sprite, width, height);
44
78
  parts.placeholder.clear();
@@ -98,7 +132,10 @@ const imageCardRenderer = {
98
132
  root,
99
133
  sprite,
100
134
  placeholder,
101
- sig: ""
135
+ sig: "",
136
+ cropped: null,
137
+ cropSource: null,
138
+ cropKey: ""
102
139
  });
103
140
  if (item.type === "image") sync(root, item, context);
104
141
  return root;
@@ -118,7 +155,9 @@ const imageCardRenderer = {
118
155
  });
119
156
  },
120
157
  destroy: (container) => {
121
- partsByContainer.get(container)?.root.destroy({ children: true });
158
+ const parts = partsByContainer.get(container);
159
+ parts?.root.destroy({ children: true });
160
+ parts?.cropped?.destroy(false);
122
161
  partsByContainer.delete(container);
123
162
  }
124
163
  };
@@ -1,10 +1,10 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { worldPoint } from "../../model/geometry.js";
3
+ import { itemAlpha, itemColor } from "../palette.js";
3
4
  import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
5
  import { positionShell } from "./base-card-renderer.js";
5
6
  import { drawFarPlate } from "./far-plate.js";
6
- import { itemColor } from "../palette.js";
7
- import { dashPattern, tracePolyline, trimPolyline } from "../stroke.js";
7
+ import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
8
8
  import { Container, Graphics, GraphicsPath, Matrix, Text } from "pixi.js";
9
9
  //#region src/board/render/renderers/shape-card-renderer.ts
10
10
  const DEFAULT_ROUNDED_RADIUS = 8;
@@ -86,27 +86,7 @@ function outlinePoints(geometry, width, height, radius) {
86
86
  p(0, height),
87
87
  p(width / 2, 0)
88
88
  ];
89
- default: {
90
- const r = Math.min(radius, width / 2, height / 2);
91
- if (r <= 0) return [
92
- p(0, 0),
93
- p(width, 0),
94
- p(width, height),
95
- p(0, height),
96
- p(0, 0)
97
- ];
98
- const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
99
- const angle = from + index / 8 * (Math.PI / 2);
100
- return p(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
101
- });
102
- return [
103
- ...corner(width - r, r, -Math.PI / 2),
104
- ...corner(width - r, height - r, 0),
105
- ...corner(r, height - r, Math.PI / 2),
106
- ...corner(r, r, Math.PI),
107
- p(width - r, 0)
108
- ];
109
- }
89
+ default: return roundRectPoints(width, height, radius);
110
90
  }
111
91
  }
112
92
  function sync(container, item, context) {
@@ -119,7 +99,8 @@ function sync(container, item, context) {
119
99
  const hovered = context.hoveredId === item.id;
120
100
  const stroke = itemColor(context, style.stroke ?? style.fill, "brand");
121
101
  const fill = itemColor(context, style.fill ?? style.stroke, "brand", "fill");
122
- const fillOpacity = style.fillOpacity ?? (style.fill ? 1 : 0);
102
+ const fillOpacity = (style.fillOpacity ?? (style.fill ? 1 : 0)) * itemAlpha(context, style.fill ?? style.stroke);
103
+ const strokeAlpha = itemAlpha(context, style.stroke ?? style.fill);
123
104
  const strokeWidth = style.strokeWidth ?? 1.75;
124
105
  syncTextResolution(parts.label, parts, context.zoom);
125
106
  const visualSig = [
@@ -132,6 +113,7 @@ function sync(container, item, context) {
132
113
  fill,
133
114
  fillOpacity,
134
115
  stroke,
116
+ strokeAlpha,
135
117
  strokeWidth,
136
118
  style.dash,
137
119
  style.radius,
@@ -159,12 +141,13 @@ function sync(container, item, context) {
159
141
  parts.shape.stroke({
160
142
  color: hovered && !selected ? context.palette.muted : stroke,
161
143
  width: lineWidth,
162
- alpha: selected ? 1 : hovered ? .95 : .85,
144
+ alpha: (selected ? 1 : hovered ? .95 : .85) * strokeAlpha,
163
145
  cap: partial || pattern ? "round" : "butt",
164
146
  join: "round"
165
147
  });
166
148
  }
167
149
  }
150
+ parts.label.alpha = itemAlpha(context, style.stroke);
168
151
  const label = itemColor(context, style.stroke, "neutral", "label");
169
152
  const textSig = [
170
153
  props.text,
@@ -226,9 +209,9 @@ const shapeCardRenderer = {
226
209
  if (item.type !== "shape") return;
227
210
  drawFarPlate(graphics, item.frame, {
228
211
  fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
229
- fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0),
212
+ fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0) * itemAlpha(context, item.style.fill ?? item.style.stroke),
230
213
  accent: itemColor(context, item.style.stroke, "brand"),
231
- accentAlpha: .9
214
+ accentAlpha: .9 * itemAlpha(context, item.style.stroke)
232
215
  });
233
216
  },
234
217
  destroy: (container) => {
@@ -1,6 +1,7 @@
1
1
  import { BoardCardRenderer } from "./board-renderer-registry.js";
2
2
  //#region src/board/render/renderers/task-card-renderer.d.ts
3
3
  declare const TASK_CARD_FULL_DETAIL_ZOOM = 0.55;
4
+ declare function formatTaskElapsed(ms: number): string;
4
5
  declare function containTaskPreviewRect(width: number, height: number, imageWidth: number, imageHeight: number): {
5
6
  x: number;
6
7
  y: 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 { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
13
+ export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, formatTaskElapsed, taskCardRenderer };
@@ -1,17 +1,21 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { featuredTaskArtifact } from "../../model/task.js";
2
+ import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
3
3
  import { drawAudioWaveform } from "../audio-waveform.js";
4
4
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
5
5
  import { positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate } from "./far-plate.js";
7
7
  import { fitTextToLines } from "./file-card-renderer.js";
8
- import { Container, Graphics, Sprite, Text } from "pixi.js";
8
+ import { Container, FillGradient, Graphics, Sprite, Text } from "pixi.js";
9
9
  //#region src/board/render/renderers/task-card-renderer.ts
10
10
  const RADIUS = 4;
11
11
  const PADDING = 12;
12
12
  const META_HEIGHT = 28;
13
13
  const TASK_CARD_FULL_DETAIL_ZOOM = .55;
14
14
  const PLAY_BADGE_RADIUS = 15;
15
+ const SHIMMER_PERIOD_MS = 1600;
16
+ const SHIMMER_FRAME_MS = 33;
17
+ const REVEAL_MS = 180;
18
+ const SECOND = 1e3;
15
19
  const partsByContainer = /* @__PURE__ */ new WeakMap();
16
20
  function previewKindFor(artifact) {
17
21
  if (!artifact) return "empty";
@@ -19,36 +23,51 @@ function previewKindFor(artifact) {
19
23
  if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
20
24
  return artifact.textExcerpt ? "text" : "empty";
21
25
  }
22
- function stateSurfaceFor(item, kind, hasTexture, previewFailed) {
26
+ function liveStatus(item, context) {
27
+ const { snapshot } = item.props;
28
+ const live = snapshot.status === "completed" ? null : context.taskRun(item.props.taskRunId);
29
+ const liveStart = Date.parse(live?.startedAt ?? "");
30
+ const working = live?.status === "pending" || live?.status === "running";
31
+ const adopt = snapshot.status === "failed" ? working && liveStart > Date.parse(snapshot.updatedAt ?? "") : live?.status === "running" || live?.status === "pending" && snapshot.status === "pending";
32
+ const status = adopt && live ? live.status : snapshot.status;
33
+ const started = adopt ? liveStart : Date.parse(snapshot.startedAt ?? "");
34
+ return {
35
+ status,
36
+ startedAt: Number.isFinite(started) ? started : null
37
+ };
38
+ }
39
+ function stateSurfaceFor(status, kind, hasTexture, previewFailed) {
23
40
  if (kind === "texture" && !hasTexture) {
24
- if (item.props.snapshot.status === "failed") return "failed";
41
+ if (status === "failed") return "failed";
25
42
  return previewFailed ? "unavailable" : "media-loading";
26
43
  }
27
44
  if (kind !== "empty") return null;
28
- if (item.props.snapshot.status === "failed") return "failed";
29
- if (item.props.snapshot.status === "running") return "generating";
30
- if (item.props.snapshot.status === "pending") return "queued";
45
+ if (status === "failed") return "failed";
46
+ if (status === "running") return "generating";
47
+ if (status === "pending") return "queued";
31
48
  return "empty";
32
49
  }
33
- function stateLabel(surface) {
50
+ function stateLabel(surface, context) {
34
51
  switch (surface) {
35
- case "generating": return "Generating";
36
- case "queued": return "Queued";
37
- case "failed": return "Failed";
38
- case "unavailable": return "Preview unavailable";
39
- case "empty": return "No preview";
52
+ case "generating": return context.labels.generating;
53
+ case "queued": return context.labels.queued;
54
+ case "failed": return context.labels.failed;
55
+ case "unavailable": return context.labels.previewUnavailable;
56
+ case "empty": return context.labels.noPreview;
40
57
  default: return "";
41
58
  }
42
59
  }
43
- function statusColor(item, context) {
44
- if (item.props.snapshot.status === "failed") return context.colors.rose.stroke;
45
- if (item.props.snapshot.status === "running") return context.colors.brand.stroke;
60
+ function statusColor(status, context) {
61
+ if (status === "failed") return context.colors.rose.stroke;
62
+ if (status === "running") return context.colors.brand.stroke;
46
63
  return context.colors.neutral.stroke;
47
64
  }
48
- function metadata(item) {
49
- const { artifactCount, artifacts } = item.props.snapshot;
50
- const extra = artifactCount > artifacts.length ? `${artifacts.length}/${artifactCount}` : artifactCount > 1 ? `+${artifactCount - 1}` : null;
51
- return [item.props.snapshot.model, extra].filter(Boolean).join(" · ");
65
+ function formatTaskElapsed(ms) {
66
+ const total = Math.max(0, Math.floor(ms / SECOND));
67
+ const hours = Math.floor(total / 3600);
68
+ const minutes = Math.floor(total % 3600 / 60);
69
+ const seconds = String(total % 60).padStart(2, "0");
70
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
52
71
  }
53
72
  function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
54
73
  if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
@@ -150,15 +169,83 @@ function drawFailedBadge(graphics, width, color, context) {
150
169
  cap: "round"
151
170
  });
152
171
  }
172
+ const shimmerGradients = /* @__PURE__ */ new Map();
173
+ function shimmerGradient(color) {
174
+ let gradient = shimmerGradients.get(color);
175
+ if (!gradient) {
176
+ const rgb = `${color >> 16 & 255}, ${color >> 8 & 255}, ${color & 255}`;
177
+ gradient = new FillGradient({
178
+ type: "linear",
179
+ start: {
180
+ x: 0,
181
+ y: 0
182
+ },
183
+ end: {
184
+ x: 1,
185
+ y: 0
186
+ },
187
+ textureSpace: "local",
188
+ colorStops: [
189
+ {
190
+ offset: 0,
191
+ color: `rgba(${rgb}, 0)`
192
+ },
193
+ {
194
+ offset: .5,
195
+ color: `rgba(${rgb}, 0.07)`
196
+ },
197
+ {
198
+ offset: 1,
199
+ color: `rgba(${rgb}, 0)`
200
+ }
201
+ ]
202
+ });
203
+ shimmerGradients.set(color, gradient);
204
+ }
205
+ return gradient;
206
+ }
207
+ function drawShimmerBand(graphics, width, height, color) {
208
+ const band = Math.max(48, width * .45);
209
+ graphics.clear().rect(-band, 0, band, height).fill(shimmerGradient(color));
210
+ }
211
+ function advance(parts, clock) {
212
+ const { motion } = parts;
213
+ let next = null;
214
+ if (motion.shimmer) {
215
+ const travel = motion.width + parts.shimmer.width;
216
+ parts.shimmer.x = clock % SHIMMER_PERIOD_MS / SHIMMER_PERIOD_MS * travel;
217
+ next = SHIMMER_FRAME_MS;
218
+ }
219
+ if (motion.startedAt !== null) {
220
+ const elapsedMs = Date.now() - motion.startedAt;
221
+ const elapsed = formatTaskElapsed(elapsedMs);
222
+ if (elapsed !== motion.elapsed) {
223
+ motion.elapsed = elapsed;
224
+ parts.body.text = `${motion.label} · ${elapsed}`;
225
+ }
226
+ const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
227
+ next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
228
+ }
229
+ if (motion.revealFrom !== null) {
230
+ const progress = Math.min(1, (clock - motion.revealFrom) / REVEAL_MS);
231
+ parts.preview.alpha = progress * (2 - progress);
232
+ if (progress >= 1) motion.revealFrom = null;
233
+ else next = 0;
234
+ }
235
+ return next;
236
+ }
153
237
  function sync(container, item, context) {
154
238
  const parts = partsByContainer.get(container);
155
239
  if (!parts) return;
156
240
  positionShell(parts.root, item);
157
241
  const { width, height } = item.frame;
242
+ const { snapshot } = item.props;
243
+ const { status, startedAt } = liveStatus(item, context);
158
244
  const selected = context.selectedIds.has(item.id);
159
245
  const hovered = context.hoveredId === item.id;
160
246
  const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
161
- const color = statusColor(item, context);
247
+ const working = status === "pending" || status === "running";
248
+ const color = statusColor(status, context);
162
249
  const key = context.assetKey(item);
163
250
  if (key !== parts.assetKey) {
164
251
  if (parts.assetKey) context.releaseTexture(parts.assetKey);
@@ -167,20 +254,58 @@ function sync(container, item, context) {
167
254
  }
168
255
  const texture = key ? context.getTexture(key) : null;
169
256
  const previewFailed = Boolean(key && !texture && context.hasError(key));
170
- const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
257
+ const artifact = featuredTaskArtifact(snapshot.artifacts);
171
258
  const kind = previewKindFor(artifact);
172
- const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
259
+ const surface = stateSurfaceFor(status, kind, Boolean(texture), previewFailed);
173
260
  const frame = {
174
261
  x: 1,
175
262
  y: 1,
176
263
  width: Math.max(1, width - 2),
177
264
  height: Math.max(1, height - 2)
178
265
  };
179
- const metaText = artifact ? metadata(item) : "";
266
+ const metaText = artifact || working ? snapshot.model ?? "" : "";
180
267
  const showMeta = full && Boolean(metaText);
181
- const failedWithOutput = item.props.snapshot.status === "failed" && kind !== "empty";
268
+ const failedWithOutput = status === "failed" && kind !== "empty";
269
+ const count = artifact && snapshot.artifactCount > 1 ? String(snapshot.artifactCount) : "";
270
+ const showBadge = full && Boolean(count);
271
+ const label = stateLabel(surface, context);
272
+ const counting = full && working && startedAt !== null && surface !== null;
273
+ const elapsed = counting ? formatTaskElapsed(Date.now() - startedAt) : "";
274
+ const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
275
+ const textArtifact = kind === "text" && artifact?.type === "text";
276
+ const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
182
277
  syncTextResolution(parts.body, parts, context.zoom);
278
+ syncTextResolution(parts.detail, parts, context.zoom);
183
279
  syncTextResolution(parts.meta, parts, context.zoom);
280
+ syncTextResolution(parts.badge, parts, context.zoom);
281
+ const textSig = [
282
+ counting ? label : bodyText,
283
+ detailText,
284
+ metaText,
285
+ count,
286
+ kind,
287
+ surface,
288
+ full,
289
+ width,
290
+ height,
291
+ color,
292
+ context.palette.text,
293
+ context.palette.muted
294
+ ].join("|");
295
+ if (textSig !== parts.textSig) {
296
+ parts.textSig = textSig;
297
+ const wrap = Math.max(24, width - 24);
298
+ parts.body.style.fill = surface === "failed" ? color : context.palette.text;
299
+ parts.body.anchor.set(textArtifact ? 0 : .5, 0);
300
+ fitTextToLines(parts.body, bodyText, textArtifact ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, wrap);
301
+ parts.detail.style.fill = context.palette.muted;
302
+ fitTextToLines(parts.detail, detailText, height - 24 > 120 ? 2 : 1, Math.min(wrap, 360));
303
+ parts.meta.style.fill = context.palette.text;
304
+ fitTextToLines(parts.meta, metaText, 1, wrap);
305
+ parts.badge.style.fill = context.palette.text;
306
+ if (parts.badge.text !== count) parts.badge.text = count;
307
+ }
308
+ const badgeWidth = Math.max(22, parts.badge.width + 12);
184
309
  const visualSig = [
185
310
  width,
186
311
  height,
@@ -191,6 +316,7 @@ function sync(container, item, context) {
191
316
  kind,
192
317
  surface,
193
318
  showMeta,
319
+ showBadge && badgeWidth,
194
320
  failedWithOutput,
195
321
  artifact?.type ?? "none",
196
322
  artifact?.id ?? "none",
@@ -199,7 +325,8 @@ function sync(container, item, context) {
199
325
  context.palette.surface,
200
326
  context.palette.hover,
201
327
  context.palette.border,
202
- context.palette.bg
328
+ context.palette.bg,
329
+ context.palette.text
203
330
  ].join("|");
204
331
  if (visualSig !== parts.visualSig) {
205
332
  parts.visualSig = visualSig;
@@ -217,6 +344,8 @@ function sync(container, item, context) {
217
344
  alpha: .5
218
345
  });
219
346
  parts.previewMask.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({ color: 16777215 });
347
+ drawShimmerBand(parts.shimmer, frame.width, frame.height, context.palette.text);
348
+ parts.shimmer.y = frame.y;
220
349
  parts.previewArt.clear();
221
350
  if (kind === "waveform") {
222
351
  const bottomInset = showMeta ? META_HEIGHT : 0;
@@ -229,47 +358,52 @@ function sync(container, item, context) {
229
358
  }
230
359
  if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
231
360
  if (surface) {
232
- const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
361
+ const markY = frame.y + frame.height / 2 - (full && label ? 18 : 0);
233
362
  drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
234
363
  }
235
364
  if (showMeta) parts.previewArt.rect(frame.x, frame.y + frame.height - META_HEIGHT, frame.width, META_HEIGHT).fill({
236
365
  color: context.palette.bg,
237
366
  alpha: .68
238
367
  });
368
+ if (showBadge) parts.previewArt.roundRect(width - badgeWidth - 8, 8, badgeWidth, 20, 10).fill({
369
+ color: context.palette.bg,
370
+ alpha: .72
371
+ });
239
372
  if (failedWithOutput) drawFailedBadge(parts.previewArt, width, color, context);
240
373
  }
241
- parts.preview.visible = kind === "texture" && Boolean(texture);
242
- if (parts.preview.visible && texture) {
374
+ const showPreview = kind === "texture" && Boolean(texture);
375
+ const revealKey = showPreview ? `${artifact?.id}:${texture?.uid}` : null;
376
+ const sameCard = parts.motion.itemId === item.id;
377
+ parts.motion.itemId = item.id;
378
+ if (revealKey !== parts.motion.revealKey || !sameCard) {
379
+ const appearing = sameCard && parts.motion.revealKey === null && revealKey !== null;
380
+ parts.motion.revealKey = revealKey;
381
+ parts.motion.revealFrom = appearing && !context.reducedMotion ? performance.now() : null;
382
+ parts.preview.alpha = parts.motion.revealFrom === null ? 1 : 0;
383
+ }
384
+ parts.preview.visible = showPreview;
385
+ if (showPreview && texture) {
243
386
  if (parts.preview.texture !== texture) parts.preview.texture = texture;
244
387
  const fitted = containTaskPreviewRect(frame.width, frame.height, texture.width, texture.height);
245
388
  parts.preview.position.set(frame.x + fitted.x, frame.y + fitted.y);
246
389
  parts.preview.width = fitted.width;
247
390
  parts.preview.height = fitted.height;
248
391
  }
249
- const bodyText = kind === "text" && artifact?.type === "text" ? artifact.textExcerpt : stateLabel(surface);
250
- const textSig = [
251
- bodyText,
252
- metaText,
253
- kind,
254
- surface,
255
- full,
256
- width,
257
- height,
258
- context.palette.text,
259
- context.palette.muted
260
- ].join("|");
261
- if (textSig !== parts.textSig) {
262
- parts.textSig = textSig;
263
- parts.body.style.fill = surface === "failed" ? color : context.palette.text;
264
- fitTextToLines(parts.body, bodyText, kind === "text" ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, Math.max(24, width - 24));
265
- parts.meta.style.fill = context.palette.text;
266
- fitTextToLines(parts.meta, metaText, 1, Math.max(24, width - 24));
267
- }
268
392
  parts.body.visible = full && Boolean(bodyText);
393
+ parts.detail.visible = full && Boolean(detailText);
269
394
  parts.meta.visible = showMeta;
270
- if (kind === "text") parts.body.position.set(PADDING, PADDING);
271
- else parts.body.position.set(Math.max(PADDING, (width - parts.body.width) / 2), height / 2 + 10);
395
+ parts.badge.visible = showBadge;
396
+ if (textArtifact) parts.body.position.set(PADDING, PADDING);
397
+ else parts.body.position.set(width / 2, height / 2 + 2);
398
+ parts.detail.position.set(width / 2, height / 2 + 2 + parts.body.height + 4);
272
399
  parts.meta.position.set(PADDING, height - META_HEIGHT + 7);
400
+ parts.badge.position.set(width - 8 - badgeWidth / 2, 18);
401
+ parts.motion.width = frame.width;
402
+ parts.motion.shimmer = full && working && kind !== "texture" && !context.reducedMotion;
403
+ parts.shimmer.visible = parts.motion.shimmer;
404
+ parts.motion.startedAt = counting ? startedAt : null;
405
+ parts.motion.label = label;
406
+ parts.motion.elapsed = elapsed;
273
407
  }
274
408
  const taskCardRenderer = {
275
409
  id: "task-card",
@@ -281,35 +415,32 @@ const taskCardRenderer = {
281
415
  const previewBg = new Graphics();
282
416
  const preview = new Sprite();
283
417
  const previewMask = new Graphics();
418
+ const shimmer = new Graphics();
284
419
  const previewArt = new Graphics();
285
420
  const resolution = textResolutionForZoom(context.zoom);
286
- const body = new Text({
287
- text: "",
288
- style: {
289
- fontFamily: BOARD_FONT_STACK,
290
- fontSize: 11.5,
291
- fontWeight: "500",
292
- wordWrap: true,
293
- breakWords: true
294
- },
295
- resolution,
296
- roundPixels: true
297
- });
298
- const meta = new Text({
421
+ const text = (fontFamily, fontSize, fontWeight) => new Text({
299
422
  text: "",
300
423
  style: {
301
- fontFamily: BOARD_MONO_FONT_STACK,
302
- fontSize: 10,
303
- fontWeight: "500",
424
+ fontFamily,
425
+ fontSize,
426
+ fontWeight,
304
427
  wordWrap: true,
305
428
  breakWords: true
306
429
  },
307
430
  resolution,
308
431
  roundPixels: true
309
432
  });
433
+ const body = text(BOARD_FONT_STACK, 11.5, "500");
434
+ const detail = text(BOARD_FONT_STACK, 10.5, "400");
435
+ const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
436
+ const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
437
+ badge.style.wordWrap = false;
438
+ badge.anchor.set(.5);
439
+ detail.anchor.set(.5, 0);
440
+ detail.style.align = "center";
310
441
  preview.mask = previewMask;
311
442
  root.mask = clip;
312
- root.addChild(plate, clip, previewBg, preview, previewMask, previewArt, body, meta);
443
+ root.addChild(plate, clip, previewBg, shimmer, preview, previewMask, previewArt, body, detail, meta, badge);
313
444
  partsByContainer.set(root, {
314
445
  root,
315
446
  plate,
@@ -317,13 +448,26 @@ const taskCardRenderer = {
317
448
  previewBg,
318
449
  preview,
319
450
  previewMask,
451
+ shimmer,
320
452
  previewArt,
453
+ badge,
321
454
  body,
455
+ detail,
322
456
  meta,
323
457
  assetKey: null,
324
458
  visualSig: "",
325
459
  textSig: "",
326
- resolution
460
+ resolution,
461
+ motion: {
462
+ itemId: "",
463
+ width: 0,
464
+ shimmer: false,
465
+ startedAt: null,
466
+ label: "",
467
+ elapsed: "",
468
+ revealFrom: null,
469
+ revealKey: null
470
+ }
327
471
  });
328
472
  if (item.type === "task") sync(root, item, context);
329
473
  return root;
@@ -331,14 +475,19 @@ const taskCardRenderer = {
331
475
  update: (container, item, context) => {
332
476
  if (item.type === "task") sync(container, item, context);
333
477
  },
478
+ tick: (container, clock) => {
479
+ const parts = partsByContainer.get(container);
480
+ return parts ? advance(parts, clock) : null;
481
+ },
334
482
  renderFar: (graphics, item, context) => {
335
483
  if (item.type !== "task") return;
336
- const active = item.props.snapshot.status === "failed" || item.props.snapshot.status === "running" || item.props.snapshot.status === "pending";
484
+ const { status } = liveStatus(item, context);
485
+ const active = status === "failed" || status === "running" || status === "pending";
337
486
  drawFarPlate(graphics, item.frame, {
338
487
  fill: context.palette.surface,
339
488
  fillAlpha: .82,
340
489
  ...active ? {
341
- accent: statusColor(item, context),
490
+ accent: statusColor(status, context),
342
491
  accentAlpha: .9
343
492
  } : {}
344
493
  });
@@ -351,4 +500,4 @@ const taskCardRenderer = {
351
500
  }
352
501
  };
353
502
  //#endregion
354
- export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
503
+ export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, formatTaskElapsed, taskCardRenderer };
@@ -1,10 +1,10 @@
1
1
  import { layoutBoardText } from "../../../protocol/dist/board-layout.js";
2
2
  import { boardFontFamily } from "../../model/text-metrics.js";
3
+ import { itemAlpha, itemColor } from "../palette.js";
4
+ import { BOARD_FONT_STACKS } from "../text-measurement.js";
3
5
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
6
  import { positionShell } from "./base-card-renderer.js";
5
7
  import { drawFarPlate } from "./far-plate.js";
6
- import { itemColor } from "../palette.js";
7
- import { BOARD_FONT_STACKS } from "../text-measurement.js";
8
8
  import { Container, Graphics, Text } from "pixi.js";
9
9
  //#region src/board/render/renderers/text-card-renderer.ts
10
10
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -33,6 +33,8 @@ function sync(container, item, context) {
33
33
  parts.body.anchor.set(anchorX, 0);
34
34
  parts.body.position.set(item.frame.width * anchorX, 0);
35
35
  syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
36
+ parts.body.alpha = itemAlpha(context, item.style.fill);
37
+ parts.caret.alpha = parts.body.alpha;
36
38
  const ink = itemColor(context, item.style.fill, "neutral");
37
39
  const text = revealed(props.text, props.reveal);
38
40
  const contentSig = [
@@ -93,7 +95,7 @@ const textCardRenderer = {
93
95
  if (item.type !== "text") return;
94
96
  drawFarPlate(graphics, item.frame, {
95
97
  fill: itemColor(context, item.style.fill, "neutral"),
96
- fillAlpha: .35
98
+ fillAlpha: .35 * itemAlpha(context, item.style.fill)
97
99
  });
98
100
  },
99
101
  destroy: (container) => {
@@ -54,6 +54,7 @@ const unknownCardRenderer = {
54
54
  create: (item, context) => {
55
55
  const root = new Container();
56
56
  const box = new Graphics();
57
+ box.context.batchMode = "batch";
57
58
  const resolution = textResolutionForZoom(context.zoom);
58
59
  const label = new Text({
59
60
  text: "",