@agent-native/core 0.70.0 → 0.70.2

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 (172) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +67 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/cli/skills.ts +2 -2
  10. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +275 -0
  11. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  12. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  13. package/corpus/core/src/client/client-surface.ts +41 -0
  14. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  15. package/corpus/core/src/client/feedback-context.ts +4 -0
  16. package/corpus/core/src/client/index.ts +1 -0
  17. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  18. package/corpus/core/src/mcp/build-server.ts +57 -1
  19. package/corpus/core/src/mcp/embed-app.ts +86 -2
  20. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  21. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  22. package/corpus/core/src/server/onboarding-html.ts +35 -0
  23. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  24. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  25. package/corpus/templates/analytics/AGENTS.md +8 -1
  26. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  27. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  28. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  29. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  30. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  31. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  32. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  33. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  35. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  36. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  37. package/corpus/templates/assets/app/global.css +17 -1
  38. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  39. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  41. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  42. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  43. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  44. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  45. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  51. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  52. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  53. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  54. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  55. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  57. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  59. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  61. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  62. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  63. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  64. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  65. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  66. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  67. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  68. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  69. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  70. package/corpus/templates/content/AGENTS.md +20 -3
  71. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  72. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  73. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  74. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  75. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  76. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  77. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  78. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  79. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  80. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  81. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  82. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  84. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  86. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  87. package/corpus/templates/content/app/global.css +9 -0
  88. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  89. package/corpus/templates/content/server/db/schema.ts +30 -0
  90. package/corpus/templates/content/server/plugins/db.ts +87 -0
  91. package/corpus/templates/content/shared/api.ts +7 -0
  92. package/corpus/templates/content/shared/properties.ts +109 -0
  93. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  94. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  95. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  96. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  97. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  99. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  100. package/corpus/templates/forms/server/db/schema.ts +7 -0
  101. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  102. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  103. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  104. package/corpus/templates/forms/shared/types.ts +12 -0
  105. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  106. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  107. package/dist/action.js +87 -0
  108. package/dist/action.js.map +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts +1 -1
  110. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  111. package/dist/agent/model-config.d.ts +2 -2
  112. package/dist/agent/model-config.d.ts.map +1 -1
  113. package/dist/agent/model-config.js +0 -2
  114. package/dist/agent/model-config.js.map +1 -1
  115. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  116. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  117. package/dist/agent/run-loop-with-resume.js +44 -0
  118. package/dist/agent/run-loop-with-resume.js.map +1 -1
  119. package/dist/cli/skills.js +2 -2
  120. package/dist/cli/skills.js.map +1 -1
  121. package/dist/cli/sync-builder-starter-manifest.d.ts +32 -0
  122. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -0
  123. package/dist/cli/sync-builder-starter-manifest.js +179 -0
  124. package/dist/cli/sync-builder-starter-manifest.js.map +1 -0
  125. package/dist/client/AgentPanel.d.ts.map +1 -1
  126. package/dist/client/AgentPanel.js +5 -3
  127. package/dist/client/AgentPanel.js.map +1 -1
  128. package/dist/client/AssistantChat.d.ts +6 -1
  129. package/dist/client/AssistantChat.d.ts.map +1 -1
  130. package/dist/client/AssistantChat.js +2 -2
  131. package/dist/client/AssistantChat.js.map +1 -1
  132. package/dist/client/client-surface.d.ts +17 -0
  133. package/dist/client/client-surface.d.ts.map +1 -0
  134. package/dist/client/client-surface.js +24 -0
  135. package/dist/client/client-surface.js.map +1 -0
  136. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  137. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  138. package/dist/client/composer/TiptapComposer.js +27 -5
  139. package/dist/client/composer/TiptapComposer.js.map +1 -1
  140. package/dist/client/feedback-context.d.ts +3 -0
  141. package/dist/client/feedback-context.d.ts.map +1 -1
  142. package/dist/client/feedback-context.js +2 -0
  143. package/dist/client/feedback-context.js.map +1 -1
  144. package/dist/client/index.d.ts +1 -0
  145. package/dist/client/index.d.ts.map +1 -1
  146. package/dist/client/index.js +1 -0
  147. package/dist/client/index.js.map +1 -1
  148. package/dist/client/sse-event-processor.d.ts.map +1 -1
  149. package/dist/client/sse-event-processor.js +7 -1
  150. package/dist/client/sse-event-processor.js.map +1 -1
  151. package/dist/mcp/build-server.d.ts +21 -0
  152. package/dist/mcp/build-server.d.ts.map +1 -1
  153. package/dist/mcp/build-server.js +43 -1
  154. package/dist/mcp/build-server.js.map +1 -1
  155. package/dist/mcp/embed-app.d.ts.map +1 -1
  156. package/dist/mcp/embed-app.js +86 -2
  157. package/dist/mcp/embed-app.js.map +1 -1
  158. package/dist/mcp/oauth-route.d.ts.map +1 -1
  159. package/dist/mcp/oauth-route.js +95 -12
  160. package/dist/mcp/oauth-route.js.map +1 -1
  161. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  162. package/dist/server/core-routes-plugin.js +63 -0
  163. package/dist/server/core-routes-plugin.js.map +1 -1
  164. package/dist/server/onboarding-html.d.ts.map +1 -1
  165. package/dist/server/onboarding-html.js +35 -0
  166. package/dist/server/onboarding-html.js.map +1 -1
  167. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  168. package/docs/design/durable-agent-runs.md +217 -0
  169. package/package.json +1 -1
  170. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  171. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  172. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -73,6 +73,101 @@ import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
73
73
 
74
74
  const CONNECT_TIMEOUT_MS = 3000;
75
75
 
76
+ // ---- bitrate tuning for the loopback preview ----------------------------
77
+ //
78
+ // This is a SAME-MACHINE loopback (popover → bubble, both on 127.0.0.1),
79
+ // so there is effectively no bandwidth limit. Left untuned, WebKit's
80
+ // congestion controller still starts the encoder at libwebrtc's cautious
81
+ // ~300 kbps default and ramps up over several seconds, and with the
82
+ // default `balanced` degradation preference it downscales the picture
83
+ // while the bitrate is low. That produced the "bubble is blurry for the
84
+ // first ~10s, then sharpens" regression after the bubble stopped owning a
85
+ // direct local camera stream. We pin a high start/max bitrate and forbid
86
+ // resolution downscaling so the bubble is crisp from the first frame.
87
+ //
88
+ // 720p webcam is comfortable at a few Mbps; these are generous, not silly.
89
+ const BUBBLE_START_BITRATE_KBPS = 2500;
90
+ const BUBBLE_MIN_BITRATE_KBPS = 1200;
91
+ const BUBBLE_MAX_BITRATE_KBPS = 5000;
92
+
93
+ /**
94
+ * Pin the sender to a high, fixed bitrate and forbid resolution
95
+ * downscaling. Called after `setLocalDescription` so `encodings` is
96
+ * populated on every WebKit build. Best-effort — older WebViews may not
97
+ * support every field, so we swallow failures.
98
+ */
99
+ async function configureBubbleSender(
100
+ sender: RTCRtpSender | null,
101
+ ): Promise<void> {
102
+ if (!sender) return;
103
+ try {
104
+ const params = sender.getParameters();
105
+ if (!params.encodings || params.encodings.length === 0) {
106
+ // Some WebKit builds report empty encodings until the first
107
+ // negotiation — seed one so our settings have somewhere to land.
108
+ params.encodings = [{}];
109
+ }
110
+ params.encodings[0].maxBitrate = BUBBLE_MAX_BITRATE_KBPS * 1000;
111
+ params.encodings[0].maxFramerate = 30;
112
+ // `minBitrate` is non-standard but honored by libwebrtc-based stacks
113
+ // (WebKit, Chromium); harmless where unsupported.
114
+ (params.encodings[0] as { minBitrate?: number }).minBitrate =
115
+ BUBBLE_MIN_BITRATE_KBPS * 1000;
116
+ // Never trade resolution for framerate — a soft bubble looks broken;
117
+ // a marginally lower fps does not.
118
+ params.degradationPreference = "maintain-resolution";
119
+ await sender.setParameters(params);
120
+ } catch (err) {
121
+ console.warn("[clips-bubble-webrtc] setParameters failed", err);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Raise the encoder's START bitrate via SDP so it doesn't begin at
127
+ * libwebrtc's cautious default and slowly climb (the visible quality
128
+ * ramp). WebKit reads these codec params from our own local description
129
+ * and applies them to our encoder. Also stamps a high `b=AS`/`b=TIAS`
130
+ * ceiling on the video m-section.
131
+ */
132
+ function boostBubbleVideoBitrate(sdp: string | undefined): string {
133
+ if (!sdp) return sdp ?? "";
134
+ const eol = sdp.includes("\r\n") ? "\r\n" : "\n";
135
+ const lines = sdp.split(/\r\n|\n/);
136
+ const out: string[] = [];
137
+ let inVideo = false;
138
+ for (const line of lines) {
139
+ if (line.startsWith("m=")) {
140
+ inVideo = line.startsWith("m=video");
141
+ out.push(line);
142
+ continue;
143
+ }
144
+ if (inVideo && line.startsWith("c=")) {
145
+ out.push(line);
146
+ // b=AS is kbps; b=TIAS is bps. Both must follow the c= line.
147
+ out.push(`b=AS:${BUBBLE_MAX_BITRATE_KBPS}`);
148
+ out.push(`b=TIAS:${BUBBLE_MAX_BITRATE_KBPS * 1000}`);
149
+ continue;
150
+ }
151
+ if (
152
+ inVideo &&
153
+ line.startsWith("a=fmtp:") &&
154
+ // Skip RTX/FEC payloads (their fmtp carries `apt=`); only primary
155
+ // codecs honor the x-google-* hints.
156
+ !line.includes("apt=") &&
157
+ !line.includes("x-google-start-bitrate")
158
+ ) {
159
+ out.push(
160
+ `${line};x-google-start-bitrate=${BUBBLE_START_BITRATE_KBPS}` +
161
+ `;x-google-min-bitrate=${BUBBLE_MIN_BITRATE_KBPS}` +
162
+ `;x-google-max-bitrate=${BUBBLE_MAX_BITRATE_KBPS}`,
163
+ );
164
+ continue;
165
+ }
166
+ out.push(line);
167
+ }
168
+ return out.join(eol);
169
+ }
170
+
76
171
  export interface BubbleWebrtcHandle {
77
172
  /** Tear down the peer connection and unsubscribe listeners. */
78
173
  stop(): void;
@@ -243,11 +338,23 @@ export function startBubbleWebrtc(
243
338
  }
244
339
  };
245
340
 
341
+ // Hint the encoder to PRESERVE RESOLUTION. A raw camera track defaults
342
+ // to "motion" semantics — under any bitrate pressure WebKit downscales
343
+ // the picture to protect the frame rate, which on this loopback shows
344
+ // up as a soft/blurry bubble. "detail" flips the default toward keeping
345
+ // pixels, complementing the explicit `maintain-resolution` set below.
346
+ try {
347
+ videoTrack.contentHint = "detail";
348
+ } catch {
349
+ // contentHint is a harmless no-op on older WebViews.
350
+ }
351
+
246
352
  // Add the camera video track. WebRTC will renegotiate if the track
247
353
  // id changes, but we only add once per handshake so this is a
248
- // one-shot.
354
+ // one-shot. Capture the sender so we can pin its bitrate below.
355
+ let sender: RTCRtpSender | null = null;
249
356
  try {
250
- localPc.addTrack(videoTrack, stream);
357
+ sender = localPc.addTrack(videoTrack, stream);
251
358
  } catch (err) {
252
359
  console.warn("[clips-bubble-webrtc] addTrack failed:", err);
253
360
  onFailure("add-track-failed");
@@ -258,6 +365,10 @@ export function startBubbleWebrtc(
258
365
  let offer: RTCSessionDescriptionInit;
259
366
  try {
260
367
  offer = await localPc.createOffer();
368
+ // Raise the encoder's start/max bitrate before we commit the local
369
+ // description so the bubble is sharp immediately instead of ramping
370
+ // up over ~10s. See `boostBubbleVideoBitrate`.
371
+ offer = { ...offer, sdp: boostBubbleVideoBitrate(offer.sdp) };
261
372
  await localPc.setLocalDescription(offer);
262
373
  } catch (err) {
263
374
  console.warn(
@@ -270,6 +381,12 @@ export function startBubbleWebrtc(
270
381
  }
271
382
  if (stopped || myId !== handshakeId) return;
272
383
 
384
+ // Now that the local description is set, `getParameters().encodings`
385
+ // is populated on every WebKit build — pin the bitrate floor/ceiling
386
+ // and forbid resolution downscaling.
387
+ await configureBubbleSender(sender);
388
+ if (stopped || myId !== handshakeId) return;
389
+
273
390
  try {
274
391
  await emit("clips:webrtc-offer", {
275
392
  handshakeId: myId,
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { emit } from "@tauri-apps/api/event";
3
3
  import { getCurrentWindow } from "@tauri-apps/api/window";
4
+ import { IconPlayerSkipForwardFilled, IconX } from "@tabler/icons-react";
4
5
 
5
6
  const COUNTDOWN_STEP_MS = 1000;
6
7
 
@@ -62,8 +63,26 @@ export function Countdown() {
62
63
 
63
64
  return (
64
65
  <div className="countdown-root">
65
- <div className="countdown-number" key={n} aria-live="polite">
66
- {n > 0 ? n : ""}
66
+ <div className="countdown-controls">
67
+ <button
68
+ type="button"
69
+ className="countdown-control countdown-control-cancel"
70
+ aria-label="Cancel recording"
71
+ onClick={() => closeWithEvent("clips:countdown-cancel")}
72
+ >
73
+ <IconX size={30} stroke={2.4} />
74
+ </button>
75
+ <div className="countdown-number" key={n} aria-live="polite">
76
+ {n > 0 ? n : ""}
77
+ </div>
78
+ <button
79
+ type="button"
80
+ className="countdown-control countdown-control-skip"
81
+ aria-label="Skip countdown and start recording now"
82
+ onClick={() => closeWithEvent("clips:countdown-done")}
83
+ >
84
+ <IconPlayerSkipForwardFilled size={28} />
85
+ </button>
67
86
  </div>
68
87
  <div className="countdown-hint" aria-label="Countdown shortcuts">
69
88
  <span>
@@ -2037,6 +2037,70 @@ body[data-clips-route="recording-pill"] #root {
2037
2037
  pointer-events: none;
2038
2038
  }
2039
2039
 
2040
+ /*
2041
+ * Horizontal row of [cancel] [number] [skip]. The two circular buttons are
2042
+ * absolutely centered at exactly ±200px (logical) from the row center so the
2043
+ * native cursor-poll hit-rects in clips/mod.rs (CONTROL_OFFSET_X = 200.0,
2044
+ * CONTROL_DIAMETER = 64.0) line up with the rendered controls. Keep these
2045
+ * numbers in sync with that Rust code.
2046
+ */
2047
+ .countdown-controls {
2048
+ position: relative;
2049
+ display: flex;
2050
+ align-items: center;
2051
+ justify-content: center;
2052
+ }
2053
+
2054
+ .countdown-control {
2055
+ position: absolute;
2056
+ top: 50%;
2057
+ display: inline-flex;
2058
+ align-items: center;
2059
+ justify-content: center;
2060
+ width: 64px;
2061
+ height: 64px;
2062
+ margin: 0;
2063
+ padding: 0;
2064
+ border: 1px solid rgba(255, 255, 255, 0.22);
2065
+ border-radius: 50%;
2066
+ background: rgba(10, 12, 16, 0.42);
2067
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
2068
+ color: #ffffff;
2069
+ cursor: pointer;
2070
+ pointer-events: auto;
2071
+ backdrop-filter: blur(8px);
2072
+ transform: translateY(-50%);
2073
+ transition:
2074
+ background-color 0.15s ease,
2075
+ border-color 0.15s ease,
2076
+ transform 0.1s ease;
2077
+ }
2078
+
2079
+ .countdown-control-cancel {
2080
+ /* center at -200px from row center: -200 - (64 / 2) = -232px */
2081
+ left: -232px;
2082
+ }
2083
+
2084
+ .countdown-control-skip {
2085
+ /* center at +200px from row center: 200 - (64 / 2) = 168px */
2086
+ right: -232px;
2087
+ }
2088
+
2089
+ .countdown-control:hover {
2090
+ background: rgba(255, 255, 255, 0.92);
2091
+ border-color: rgba(255, 255, 255, 0.95);
2092
+ color: #0b0d12;
2093
+ }
2094
+
2095
+ .countdown-control:active {
2096
+ transform: translateY(-50%) scale(0.94);
2097
+ }
2098
+
2099
+ .countdown-control:focus-visible {
2100
+ outline: 2px solid rgba(255, 255, 255, 0.9);
2101
+ outline-offset: 2px;
2102
+ }
2103
+
2040
2104
  .countdown-number {
2041
2105
  font-size: 240px;
2042
2106
  font-weight: 800;
@@ -3848,11 +3912,15 @@ body[data-clips-route="recording-pill"] #root {
3848
3912
  }
3849
3913
 
3850
3914
  /* Collapsed (not expanded). Shrinks to dot + waveform; controls revealed via
3851
- CSS :hover which works at the WebKit level even when the app is unfocused. */
3915
+ CSS :hover which works at the WebKit level even when the app is unfocused.
3916
+ Top-anchored (not centered) inside the headroom-sized window so the hover
3917
+ reveal of the drag grip grows the capsule strictly DOWNWARD — centering it
3918
+ would split the growth and raise the top edge, sliding buttons up under the
3919
+ cursor and causing misclicks. */
3852
3920
  .pill-inner-compact {
3853
3921
  width: fit-content;
3854
3922
  height: fit-content;
3855
- align-self: center;
3923
+ align-self: flex-start;
3856
3924
  border-radius: 18px;
3857
3925
  }
3858
3926
 
@@ -4,6 +4,7 @@ use std::path::PathBuf;
4
4
  use std::process::Command;
5
5
  #[cfg(target_os = "macos")]
6
6
  use std::process::Stdio;
7
+ use std::sync::atomic::{AtomicBool, Ordering};
7
8
  use std::thread;
8
9
  use std::time::Duration;
9
10
  use tauri::{
@@ -26,6 +27,17 @@ use crate::util::{
26
27
  /// React bundle with a hash route that `main.tsx` uses to pick the component.
27
28
  const COUNTDOWN_LABEL: &str = "countdown";
28
29
  const TOOLBAR_LABEL: &str = "toolbar";
30
+ // Geometry of the two circular cancel/skip buttons that flank the countdown
31
+ // number. These MUST stay in sync with the CSS in
32
+ // `templates/clips/desktop/src/styles.css` (`.countdown-control` is 64px and
33
+ // its center sits ±200px logical from the window center). A few px of slop is
34
+ // added to each hit-rect so edge clicks register.
35
+ const COUNTDOWN_CONTROL_OFFSET_X: f64 = 200.0;
36
+ const COUNTDOWN_CONTROL_DIAMETER: f64 = 64.0;
37
+ const COUNTDOWN_CONTROL_HIT_PAD: f64 = 8.0;
38
+ // Guards the single cursor-poll loop that toggles click-through on the
39
+ // countdown overlay so only the button zones are interactive.
40
+ static COUNTDOWN_CONTROL_TRACKING: AtomicBool = AtomicBool::new(false);
29
41
  const BUBBLE_LABEL: &str = "bubble";
30
42
  const FINALIZING_LABEL: &str = "finalizing";
31
43
  const FLOW_BAR_LABEL: &str = "flow-bar";
@@ -304,6 +316,7 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
304
316
  dlog!("[clips-tray] show_countdown invoked");
305
317
  mark_popover_shown(&app);
306
318
  if let Some(existing) = app.get_webview_window(COUNTDOWN_LABEL) {
319
+ stop_countdown_control_tracking();
307
320
  let _ = app.emit("clips:countdown-shortcuts-active", false);
308
321
  let _ = existing.close();
309
322
  }
@@ -340,10 +353,68 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
340
353
  configure_overlay_behavior(&win);
341
354
  let _ = win.show();
342
355
  let _ = app.emit("clips:countdown-shortcuts-active", true);
356
+ start_countdown_control_tracking(&app);
343
357
  dlog!("[clips-tray] countdown shown");
344
358
  Ok(())
345
359
  }
346
360
 
361
+ /// True when the global cursor sits inside either circular cancel/skip button
362
+ /// zone of the countdown overlay. The controls row is centered in the window;
363
+ /// each button's center is `COUNTDOWN_CONTROL_OFFSET_X` logical px to the
364
+ /// left/right of the window center, vertically at the window center. Cursor,
365
+ /// window position, and window size all come from Tauri in physical px with a
366
+ /// desktop top-left origin, so we convert the logical button geometry using the
367
+ /// window's scale factor and do a plain point-in-rect test.
368
+ fn cursor_over_countdown_control(window: &WebviewWindow) -> bool {
369
+ let (Ok(c), Ok(p), Ok(s), Ok(scale)) = (
370
+ window.cursor_position(),
371
+ window.outer_position(),
372
+ window.outer_size(),
373
+ window.scale_factor(),
374
+ ) else {
375
+ return false;
376
+ };
377
+ let center_x = p.x as f64 + s.width as f64 / 2.0;
378
+ let center_y = p.y as f64 + s.height as f64 / 2.0;
379
+ let half = (COUNTDOWN_CONTROL_DIAMETER / 2.0 + COUNTDOWN_CONTROL_HIT_PAD) * scale;
380
+ let offset = COUNTDOWN_CONTROL_OFFSET_X * scale;
381
+ let in_button = |bx: f64| -> bool {
382
+ c.x >= bx - half && c.x <= bx + half && c.y >= center_y - half && c.y <= center_y + half
383
+ };
384
+ in_button(center_x - offset) || in_button(center_x + offset)
385
+ }
386
+
387
+ /// Poll the cursor against the two button zones while the countdown overlay is
388
+ /// alive, toggling `set_ignore_cursor_events` so the buttons are clickable only
389
+ /// when the cursor is over them and the rest of the screen stays click-through.
390
+ /// Idempotent; mirrors `start_pill_hover_tracking` in `recording_indicator.rs`.
391
+ fn start_countdown_control_tracking(app: &AppHandle) {
392
+ if COUNTDOWN_CONTROL_TRACKING.swap(true, Ordering::SeqCst) {
393
+ return;
394
+ }
395
+ let app = app.clone();
396
+ tauri::async_runtime::spawn(async move {
397
+ let mut prev_interactive = false;
398
+ while COUNTDOWN_CONTROL_TRACKING.load(Ordering::Relaxed) {
399
+ let Some(win) = app.get_webview_window(COUNTDOWN_LABEL) else {
400
+ break;
401
+ };
402
+ let over = cursor_over_countdown_control(&win);
403
+ if over != prev_interactive {
404
+ prev_interactive = over;
405
+ // ignore_cursor_events(false) => clicks land on the buttons.
406
+ let _ = win.set_ignore_cursor_events(!over);
407
+ }
408
+ tokio::time::sleep(Duration::from_millis(70)).await;
409
+ }
410
+ COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
411
+ });
412
+ }
413
+
414
+ fn stop_countdown_control_tracking() {
415
+ COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
416
+ }
417
+
347
418
  /// Full-screen transparent overlay that shows compact bottom-left progress
348
419
  /// while the recorder flushes its final chunks and awaits the server finalize.
349
420
  /// Rendered immediately after the user clicks Stop so they don't stare at a
@@ -800,6 +871,7 @@ pub async fn set_bubble_capture_excluded(app: AppHandle, excluded: bool) -> Resu
800
871
 
801
872
  #[tauri::command]
802
873
  pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
874
+ stop_countdown_control_tracking();
803
875
  let _ = app.emit("clips:countdown-shortcuts-active", false);
804
876
  for label in [
805
877
  COUNTDOWN_LABEL,
@@ -827,6 +899,7 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
827
899
  /// popover-close).
828
900
  #[tauri::command]
829
901
  pub async fn hide_recording_chrome(app: AppHandle) -> Result<(), String> {
902
+ stop_countdown_control_tracking();
830
903
  let _ = app.emit("clips:countdown-shortcuts-active", false);
831
904
  // The countdown + toolbar always tear down on recording stop. The region
832
905
  // guides only tear down when they aren't pinned on-screen via the always-on
@@ -41,6 +41,7 @@ import {
41
41
  setCookie,
42
42
  type H3Event,
43
43
  } from "h3";
44
+ import { eq } from "drizzle-orm";
44
45
  import { readAppState } from "@agent-native/core/application-state";
45
46
  import {
46
47
  createSsrfSafeDispatcher,
@@ -62,6 +63,7 @@ import {
62
63
  loomEmbedUrlForRecording,
63
64
  } from "../../../../shared/loom.js";
64
65
  import { verifySharePassword } from "../../../lib/share-password.js";
66
+ import { getDb, schema } from "../../../db/index.js";
65
67
 
66
68
  interface RecordingRow {
67
69
  expiresAt?: string | null;
@@ -301,12 +303,39 @@ export default defineEventHandler(async (event: H3Event) => {
301
303
  return runWithRequestContext(
302
304
  { userEmail: session?.email, orgId },
303
305
  async () => {
306
+ // Resolve via share grants first (owner / org / shared viewers). When
307
+ // there is no grant — e.g. an anonymous viewer on a public share/embed
308
+ // page — fall back to the public-visibility gate so public clips stay
309
+ // playable without signing in. This mirrors the visibility check in
310
+ // `/api/public-recording.get.ts` (which hands the player its videoUrl) so
311
+ // the metadata endpoint and this media endpoint never disagree about who
312
+ // can play a clip. Without this, anonymous viewers hit a 403 here and the
313
+ // player fails with "Could not start playback. Try again."
304
314
  const access = await resolveAccess("recording", recordingId);
305
- if (!access) {
306
- setResponseStatus(event, 403);
307
- return { error: "Forbidden" };
315
+ let recRow: RecordingRow | null =
316
+ (access?.resource as RecordingRow | undefined) ?? null;
317
+ let role: string | null = access?.role ?? null;
318
+
319
+ if (!recRow) {
320
+ const db = getDb();
321
+ const [row] = await db
322
+ .select()
323
+ .from(schema.recordings)
324
+ .where(eq(schema.recordings.id, recordingId))
325
+ .limit(1);
326
+ if (!row) {
327
+ setResponseStatus(event, 404);
328
+ return { error: "Not found" };
329
+ }
330
+ if (row.visibility !== "public") {
331
+ setResponseStatus(event, 403);
332
+ return { error: "Forbidden" };
333
+ }
334
+ recRow = row as RecordingRow;
335
+ role = "viewer";
308
336
  }
309
- const rec = access.resource as RecordingRow;
337
+
338
+ const rec = recRow;
310
339
 
311
340
  if (rec.expiresAt) {
312
341
  const expires = new Date(rec.expiresAt).getTime();
@@ -333,7 +362,7 @@ export default defineEventHandler(async (event: H3Event) => {
333
362
  password?: string;
334
363
  t?: string;
335
364
  };
336
- if (rec.password && access.role !== "owner") {
365
+ if (rec.password && role !== "owner") {
337
366
  const token = typeof q.t === "string" ? q.t : "";
338
367
  const cookieToken =
339
368
  getCookie(event, protectedMediaCookieName(recordingId)) ?? "";
@@ -122,7 +122,8 @@ cd templates/content && pnpm action <name> [args]
122
122
  | `configure-document-property` | `--documentId <id> [--id <propertyId>] --name <name> --type <type> [--visibility always_show\|hide_when_empty\|always_hide]` | Create or update a property definition |
123
123
  | `duplicate-document-property` | `--documentId <id> --propertyId <propertyId>` | Duplicate a property definition and its stored values |
124
124
  | `delete-document-property` | `--documentId <id> --propertyId <propertyId>` | Delete a property definition and its stored values |
125
- | `set-document-property` | `--documentId <id> --propertyId <propertyId> --value <json>` | Set a document property value |
125
+ | `set-document-property` | `--documentId <id> --propertyId <propertyId> --value <json>` | Set a document property value (for a `blocks` field, the value is its markdown content) |
126
+ | `reorder-document-property` | `--documentId <id> --propertyId <propertyId> --targetPropertyId <id> [--position before\|after]` | Reorder a property definition within its database (used to reorder Blocks fields on the page) |
126
127
  | `set-document-discoverability` | `--id <id> --hideFromSearch true\|false [--includeChildren true\|false]` | Hide/show an org-accessible document in Organization/search while keeping link access |
127
128
  | `move-document` | `--id <id> [--parentId] [--position]` | Move or reorder a document in the page tree |
128
129
  | `delete-document` | `--id <id>` | Delete with recursive children |
@@ -413,10 +414,26 @@ Document properties are SQL-backed, Notion-style structured metadata rather
413
414
  than YAML embedded in the markdown body. Database property definitions support
414
415
  `text`, `number`, `select`, `multi_select`, `status`, `date`, `person`,
415
416
  `place`, `files_media` (`Files & media`), `checkbox`, `url`, `email`,
416
- `phone`, plus computed `formula`, `id`, `created_time`, `created_by`, and
417
+ `phone`, `blocks` (Capacities-style rich-text body field), plus computed
418
+ `formula`, `id`, `created_time`, `created_by`, and
417
419
  `last_edited_time`, `last_edited_by`, plus property visibility (`always_show`,
418
420
  `hide_when_empty`, `always_hide`). The value table stores per-row-document JSON
419
- values. Formula properties store their expression in property options and support
421
+ values.
422
+
423
+ **Blocks fields.** A `blocks` field is independent rich-text content per row —
424
+ NOT YAML and NOT a pointer to the body. Every database is seeded with one
425
+ primary "Content" Blocks field whose content is backed by `documents.content`
426
+ (so it reuses the collaborative TipTap/Yjs body editor and existing data
427
+ migrates for free). Each additional Blocks field stores its own content in
428
+ `document_block_field_contents`, keyed by `(document_id, property_id)`, so no
429
+ two Blocks fields ever share content — adding a second Blocks field creates a
430
+ new, empty, independent field. On the page: one Blocks field renders chromeless
431
+ (no header, just the body); two or more each show their name as a header and are
432
+ collapsible and reorderable (the surviving lone field keeps its stored name).
433
+ In table views a Blocks column shows a word count (e.g. "412 words"), not the
434
+ body. A Blocks field can only be deleted from the database view's column menu
435
+ (not from the page body); deleting the last Blocks field warns that it removes
436
+ the body for every object of the type. Formula properties store their expression in property options and support
420
437
  `{Property name}` substitution plus simple numeric math such as `{MSV} * 2`.
421
438
  Database views support multiple named table, list, gallery, board, calendar, and timeline views saved in
422
439
  `content_databases.view_config_json`. Each view has its own stacked sorts,
@@ -123,6 +123,10 @@ export async function getContentDatabaseResponse(
123
123
 
124
124
  if (!database) throw new Error(`Database "${databaseId}" not found`);
125
125
 
126
+ // PURE read: the primary "Content" Blocks field is seeded at create time and
127
+ // by the one-time startup repair — never here. Reading a database (including a
128
+ // shared one a viewer is opening) must not mutate schema.
129
+
126
130
  const { limit, offset } = normalizeContentDatabasePageOptions(options);
127
131
  const [itemCount] = await db
128
132
  .select({ count: sql<number>`COUNT(*)` })
@@ -261,6 +265,11 @@ export async function deleteDatabaseDataForDocument(
261
265
  await db
262
266
  .delete(schema.documentPropertyValues)
263
267
  .where(eq(schema.documentPropertyValues.propertyId, definition.id));
268
+ // Independent Blocks-field content is keyed by property id; drop it so
269
+ // deleting a database leaves no orphaned document_block_field_contents.
270
+ await db
271
+ .delete(schema.documentBlockFieldContents)
272
+ .where(eq(schema.documentBlockFieldContents.propertyId, definition.id));
264
273
  }
265
274
  const sources = await db
266
275
  .select({ id: schema.contentDatabaseSources.id })
@@ -309,6 +318,12 @@ export async function deleteDatabaseDataForDocument(
309
318
  eq(schema.documentPropertyValues.ownerEmail, ownerEmail),
310
319
  ),
311
320
  );
321
+ // A deleted row document's independent Blocks-field content is keyed by
322
+ // document id; drop it so no document_block_field_contents rows are
323
+ // orphaned when the row is removed.
324
+ await db
325
+ .delete(schema.documentBlockFieldContents)
326
+ .where(eq(schema.documentBlockFieldContents.documentId, documentId));
312
327
  await db
313
328
  .delete(schema.contentDatabaseItems)
314
329
  .where(eq(schema.contentDatabaseItems.documentId, documentId));