@agent-native/core 0.70.0 → 0.70.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +61 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/cli/skills.ts +2 -2
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +86 -2
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/core-routes-plugin.ts +78 -0
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/styles.css +70 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/skills.js +2 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +86 -2
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +63 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- 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-
|
|
66
|
-
|
|
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:
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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 &&
|
|
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`,
|
|
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.
|
|
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));
|