@agent-native/core 0.77.16 → 0.77.17
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +4 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +4 -0
- package/corpus/core/src/client/use-db-sync.ts +19 -2
- package/corpus/core/src/server/auth-marketing.ts +15 -0
- package/corpus/core/src/server/onboarding-html.ts +47 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +76 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +1 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +13 -2
- package/corpus/templates/analytics/app/i18n-data.ts +10 -0
- package/corpus/templates/analytics/app/lib/sql-query.ts +97 -21
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +22 -1
- package/corpus/templates/analytics/app/root.tsx +15 -6
- package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-no-longer-refresh-in-the-background-during-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-25-first-party-dashboards-use-indexed-event-dates-for-faster-da.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-25-retention-and-active-user-dashboard-panels-now-count-account.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +58 -58
- package/corpus/templates/analytics/server/db/schema.ts +2 -0
- package/corpus/templates/analytics/server/handlers/sql-query.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +16 -6
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +88 -56
- package/corpus/templates/analytics/server/plugins/db.ts +73 -0
- package/corpus/templates/clips/AGENTS.md +5 -0
- package/corpus/templates/clips/changelog/2026-06-25-github-issue-and-pull-request-pages-can-now-preview-playable.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +8 -2
- package/corpus/templates/clips/chrome-extension/public/manifest.json +14 -2
- package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +233 -0
- package/corpus/templates/clips/chrome-extension/src/github-preview.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/github-preview.ts +414 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/mail/actions/archive-email.ts +13 -3
- package/corpus/templates/mail/app/hooks/use-emails.ts +5 -2
- package/corpus/templates/mail/changelog/2026-06-25-archive-failures-now-explain-when-gmail-needs-reconnecting-p.md +6 -0
- package/corpus/templates/mail/shared/archive-errors.ts +137 -0
- package/corpus/templates/plan/changelog/2026-06-25-hosted-signup-now-shows-how-to-switch-visual-plan-to-local-files.md +6 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +3 -1
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +9 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +8 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/auth-marketing.d.ts +4 -0
- package/dist/server/auth-marketing.d.ts.map +1 -1
- package/dist/server/auth-marketing.js +9 -0
- package/dist/server/auth-marketing.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +46 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/package.json +1 -1
|
@@ -134,8 +134,10 @@ export default runMigrations(
|
|
|
134
134
|
event_name TEXT NOT NULL,
|
|
135
135
|
user_id TEXT,
|
|
136
136
|
anonymous_id TEXT,
|
|
137
|
+
user_key TEXT,
|
|
137
138
|
session_id TEXT,
|
|
138
139
|
timestamp TEXT NOT NULL,
|
|
140
|
+
event_date TEXT,
|
|
139
141
|
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
140
142
|
url TEXT,
|
|
141
143
|
path TEXT,
|
|
@@ -219,6 +221,77 @@ export default runMigrations(
|
|
|
219
221
|
version: 38,
|
|
220
222
|
sql: `SELECT 1`,
|
|
221
223
|
},
|
|
224
|
+
{
|
|
225
|
+
version: 39,
|
|
226
|
+
sql: `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS event_date TEXT`,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
version: 40,
|
|
230
|
+
sql: `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS user_key TEXT`,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
version: 41,
|
|
234
|
+
sql: {
|
|
235
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_event_date_idx; CREATE INDEX CONCURRENTLY analytics_events_org_event_date_idx ON analytics_events (org_id, event_date)`,
|
|
236
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_event_date_idx ON analytics_events (org_id, event_date)`,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
version: 42,
|
|
241
|
+
sql: {
|
|
242
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_event_name_date_idx; CREATE INDEX CONCURRENTLY analytics_events_org_event_name_date_idx ON analytics_events (org_id, event_name, event_date)`,
|
|
243
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_event_name_date_idx ON analytics_events (org_id, event_name, event_date)`,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
version: 43,
|
|
248
|
+
sql: {
|
|
249
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_date_user_idx; CREATE INDEX CONCURRENTLY analytics_events_org_date_user_idx ON analytics_events (org_id, event_date, user_key)`,
|
|
250
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_date_user_idx ON analytics_events (org_id, event_date, user_key)`,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
version: 44,
|
|
255
|
+
sql: {
|
|
256
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_date_template_idx; CREATE INDEX CONCURRENTLY analytics_events_org_date_template_idx ON analytics_events (org_id, event_date, template)`,
|
|
257
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_date_template_idx ON analytics_events (org_id, event_date, template)`,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
version: 45,
|
|
262
|
+
sql: {
|
|
263
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_event_date_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_event_date_idx ON analytics_events (owner_email, event_date) WHERE org_id IS NULL`,
|
|
264
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_event_date_idx ON analytics_events (owner_email, event_date) WHERE org_id IS NULL`,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
version: 46,
|
|
269
|
+
sql: {
|
|
270
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_event_name_date_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_event_name_date_idx ON analytics_events (owner_email, event_name, event_date) WHERE org_id IS NULL`,
|
|
271
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_event_name_date_idx ON analytics_events (owner_email, event_name, event_date) WHERE org_id IS NULL`,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
version: 47,
|
|
276
|
+
sql: {
|
|
277
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_date_user_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_date_user_idx ON analytics_events (owner_email, event_date, user_key) WHERE org_id IS NULL`,
|
|
278
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_date_user_idx ON analytics_events (owner_email, event_date, user_key) WHERE org_id IS NULL`,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
version: 48,
|
|
283
|
+
sql: {
|
|
284
|
+
postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_date_template_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_date_template_idx ON analytics_events (owner_email, event_date, template) WHERE org_id IS NULL`,
|
|
285
|
+
sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_date_template_idx ON analytics_events (owner_email, event_date, template) WHERE org_id IS NULL`,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
version: 49,
|
|
290
|
+
sql: {
|
|
291
|
+
postgres: `UPDATE analytics_events SET event_date = COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)), user_key = COALESCE(NULLIF(user_key, ''), NULLIF(user_id, ''), NULLIF(anonymous_id, '')) WHERE (event_date IS NULL OR event_date = '' OR user_key IS NULL OR user_key = '') AND timestamp >= to_char(CURRENT_DATE - INTERVAL '400 days', 'YYYY-MM-DD')`,
|
|
292
|
+
sqlite: `UPDATE analytics_events SET event_date = COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)), user_key = COALESCE(NULLIF(user_key, ''), NULLIF(user_id, ''), NULLIF(anonymous_id, '')) WHERE (event_date IS NULL OR event_date = '' OR user_key IS NULL OR user_key = '') AND timestamp >= date('now', '-400 days')`,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
222
295
|
],
|
|
223
296
|
{ table: "analytics_migrations" },
|
|
224
297
|
);
|
|
@@ -79,6 +79,11 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
|
|
|
79
79
|
the Chrome debugger API only on the tab the user launched from, only while a
|
|
80
80
|
recording is active, and returns the same redacted diagnostics shape saved by
|
|
81
81
|
`save-browser-diagnostics`.
|
|
82
|
+
- The Chrome extension also enhances GitHub issue and PR markdown: a narrow
|
|
83
|
+
`github.com` content script detects Clips `/r/`, `/share/`, and `/embed/`
|
|
84
|
+
links, then renders the existing `/embed/:id` player in an extension-owned
|
|
85
|
+
preview iframe so the video is playable without leaving GitHub. Keep this
|
|
86
|
+
scoped to GitHub unless there is a deliberate permission review.
|
|
82
87
|
- After mutations, rely on the app refresh/polling path; do not invent a second
|
|
83
88
|
sync mechanism.
|
|
84
89
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Permissions model
|
|
2
2
|
|
|
3
|
-
This extension ships **activeTab-only**: it deliberately
|
|
4
|
-
broad `<all_urls>` host permission
|
|
3
|
+
This extension ships **activeTab-only for recording overlays**: it deliberately
|
|
4
|
+
does **not** request the broad `<all_urls>` host permission for recording UI.
|
|
5
5
|
|
|
6
6
|
## Why
|
|
7
7
|
|
|
@@ -9,6 +9,11 @@ A declarative content script on `<all_urls>` (and the matching `<all_urls>` host
|
|
|
9
9
|
permission) triggers Chrome Web Store's **broad host permission in-depth review**,
|
|
10
10
|
which significantly delays publishing and updates. We avoid that.
|
|
11
11
|
|
|
12
|
+
The only declarative content script is scoped to `https://github.com/*`. It
|
|
13
|
+
looks for Clips links in GitHub issue/PR markdown and replaces them with a
|
|
14
|
+
playable preview iframe owned by the extension. That adds a narrow GitHub host
|
|
15
|
+
disclosure, but does not grant broad access to every page.
|
|
16
|
+
|
|
12
17
|
## How the overlay gets on the page
|
|
13
18
|
|
|
14
19
|
When the user clicks the extension and starts a recording, the background service
|
|
@@ -24,6 +29,7 @@ Declared permissions: `activeTab`, `debugger`, `offscreen`, `scripting`,
|
|
|
24
29
|
`storage`. Host permissions: only the configured Clips app + `forms.agent-native.com`
|
|
25
30
|
|
|
26
31
|
- `localhost`/`127.0.0.1`.
|
|
32
|
+
- `https://github.com/*` is content-script scoped for link previews only.
|
|
27
33
|
|
|
28
34
|
## What this costs
|
|
29
35
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent-Native Clips",
|
|
4
4
|
"short_name": "Clips",
|
|
5
|
-
"version": "0.1.
|
|
6
|
-
"description": "Start Clips recordings from Chrome
|
|
5
|
+
"version": "0.1.4",
|
|
6
|
+
"description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
|
|
7
7
|
"minimum_chrome_version": "116",
|
|
8
8
|
"action": {
|
|
9
9
|
"default_title": "Record with Clips",
|
|
@@ -25,10 +25,22 @@
|
|
|
25
25
|
"http://localhost/*",
|
|
26
26
|
"http://127.0.0.1/*"
|
|
27
27
|
],
|
|
28
|
+
"content_scripts": [
|
|
29
|
+
{
|
|
30
|
+
"matches": ["https://github.com/*"],
|
|
31
|
+
"js": ["assets/github-preview-content.js"],
|
|
32
|
+
"run_at": "document_idle",
|
|
33
|
+
"all_frames": false
|
|
34
|
+
}
|
|
35
|
+
],
|
|
28
36
|
"web_accessible_resources": [
|
|
29
37
|
{
|
|
30
38
|
"resources": ["src/overlay.html", "assets/*", "icons/*"],
|
|
31
39
|
"matches": ["<all_urls>"]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"resources": ["src/github-preview.html"],
|
|
43
|
+
"matches": ["https://github.com/*"]
|
|
32
44
|
}
|
|
33
45
|
],
|
|
34
46
|
"externally_connectable": {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// GitHub Clips link previews. This is a narrow declarative content script for
|
|
2
|
+
// github.com only; the recording overlay remains activeTab-injected.
|
|
3
|
+
(function clipsGithubPreviewContent() {
|
|
4
|
+
const ROOT_CLASS = "clips-github-preview";
|
|
5
|
+
const PROCESSED_ATTR = "data-clips-github-preview";
|
|
6
|
+
const SOURCE_ATTR = "data-clips-source-url";
|
|
7
|
+
const EXTENSION_ORIGIN = chrome.runtime.getURL("").replace(/\/$/, "");
|
|
8
|
+
const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
|
|
9
|
+
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
10
|
+
const ROUTE_SEGMENTS = new Set(["r", "share", "embed"]);
|
|
11
|
+
const MIN_HEIGHT = 120;
|
|
12
|
+
const MAX_HEIGHT = 720;
|
|
13
|
+
|
|
14
|
+
type ClipsLink = {
|
|
15
|
+
sourceUrl: string;
|
|
16
|
+
embedUrl: string;
|
|
17
|
+
metadataUrl: string;
|
|
18
|
+
clipId: string;
|
|
19
|
+
host: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let scanTimer: ReturnType<typeof setTimeout> | undefined;
|
|
23
|
+
let frameCounter = 0;
|
|
24
|
+
|
|
25
|
+
function isAllowedHost(url: URL): boolean {
|
|
26
|
+
if (CLIPS_HOSTS.has(url.hostname)) return true;
|
|
27
|
+
if (LOCAL_HOSTS.has(url.hostname)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeTimeParam(value: string | null): string | null {
|
|
32
|
+
const trimmed = value?.trim();
|
|
33
|
+
if (!trimmed || trimmed.length > 32) return null;
|
|
34
|
+
if (/^\d{1,6}(\.\d{1,3})?$/.test(trimmed)) return trimmed;
|
|
35
|
+
if (/^\d{1,2}:\d{1,2}(:\d{1,2})?$/.test(trimmed)) return trimmed;
|
|
36
|
+
if (/^(?:\d{1,4}h)?(?:\d{1,4}m)?(?:\d{1,4}s)?$/i.test(trimmed)) {
|
|
37
|
+
return /\d/.test(trimmed) ? trimmed : null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseClipsLink(raw: string): ClipsLink | null {
|
|
43
|
+
let parsed: URL;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (!isAllowedHost(parsed)) return null;
|
|
53
|
+
|
|
54
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
55
|
+
const routeIndex = segments.findIndex((segment) =>
|
|
56
|
+
ROUTE_SEGMENTS.has(segment),
|
|
57
|
+
);
|
|
58
|
+
if (routeIndex === -1) return null;
|
|
59
|
+
|
|
60
|
+
const route = segments[routeIndex];
|
|
61
|
+
const clipId = segments[routeIndex + 1];
|
|
62
|
+
if (!clipId || clipId === "meeting" || clipId.length > 240) return null;
|
|
63
|
+
if (route === "share" && clipId === "meeting") return null;
|
|
64
|
+
|
|
65
|
+
let decodedClipId = clipId;
|
|
66
|
+
try {
|
|
67
|
+
decodedClipId = decodeURIComponent(clipId);
|
|
68
|
+
} catch {
|
|
69
|
+
// Keep the raw segment; malformed escaping should not break the scanner.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const baseSegments = segments.slice(0, routeIndex);
|
|
73
|
+
const sourceUrl = new URL(parsed.pathname, parsed.origin);
|
|
74
|
+
const embedPath = [
|
|
75
|
+
"",
|
|
76
|
+
...baseSegments,
|
|
77
|
+
"embed",
|
|
78
|
+
encodeURIComponent(decodedClipId),
|
|
79
|
+
].join("/");
|
|
80
|
+
const embedUrl = new URL(embedPath, parsed.origin);
|
|
81
|
+
const metadataUrl = new URL(
|
|
82
|
+
["", ...baseSegments, "api", "public-recording"].join("/"),
|
|
83
|
+
parsed.origin,
|
|
84
|
+
);
|
|
85
|
+
metadataUrl.searchParams.set("id", decodedClipId);
|
|
86
|
+
|
|
87
|
+
const time = safeTimeParam(parsed.searchParams.get("t"));
|
|
88
|
+
if (time) {
|
|
89
|
+
sourceUrl.searchParams.set("t", time);
|
|
90
|
+
embedUrl.searchParams.set("t", time);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
sourceUrl: sourceUrl.toString(),
|
|
95
|
+
embedUrl: embedUrl.toString(),
|
|
96
|
+
metadataUrl: metadataUrl.toString(),
|
|
97
|
+
clipId,
|
|
98
|
+
host: parsed.host,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isGitHubMarkdownAnchor(anchor: HTMLAnchorElement): boolean {
|
|
103
|
+
if (anchor.closest(`.${ROOT_CLASS}`)) return false;
|
|
104
|
+
return Boolean(
|
|
105
|
+
anchor.closest(
|
|
106
|
+
[
|
|
107
|
+
".markdown-body",
|
|
108
|
+
".comment-body",
|
|
109
|
+
".js-comment-body",
|
|
110
|
+
".timeline-comment",
|
|
111
|
+
".js-issue-body",
|
|
112
|
+
".js-pull-request-body",
|
|
113
|
+
].join(","),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function previewInsertionTarget(anchor: HTMLAnchorElement): HTMLElement {
|
|
119
|
+
const block = anchor.closest(
|
|
120
|
+
[
|
|
121
|
+
"p",
|
|
122
|
+
"li",
|
|
123
|
+
"td",
|
|
124
|
+
"blockquote",
|
|
125
|
+
"pre",
|
|
126
|
+
".markdown-body > div",
|
|
127
|
+
".comment-body",
|
|
128
|
+
].join(","),
|
|
129
|
+
);
|
|
130
|
+
return block instanceof HTMLElement ? block : anchor;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function existingPreview(sourceUrl: string): HTMLElement | null {
|
|
134
|
+
for (const element of document.querySelectorAll<HTMLElement>(
|
|
135
|
+
`.${ROOT_CLASS}`,
|
|
136
|
+
)) {
|
|
137
|
+
if (element.getAttribute(SOURCE_ATTR) === sourceUrl) return element;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mountPreview(anchor: HTMLAnchorElement, link: ClipsLink): void {
|
|
143
|
+
if (existingPreview(link.sourceUrl)) {
|
|
144
|
+
anchor.setAttribute(PROCESSED_ATTR, "mounted");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const frameId = `clips-github-preview-${++frameCounter}`;
|
|
149
|
+
const wrapper = document.createElement("div");
|
|
150
|
+
wrapper.className = ROOT_CLASS;
|
|
151
|
+
wrapper.id = `${frameId}-wrapper`;
|
|
152
|
+
wrapper.setAttribute(SOURCE_ATTR, link.sourceUrl);
|
|
153
|
+
Object.assign(wrapper.style, {
|
|
154
|
+
display: "block",
|
|
155
|
+
width: "min(720px, 100%)",
|
|
156
|
+
maxWidth: "100%",
|
|
157
|
+
margin: "8px 0 16px",
|
|
158
|
+
clear: "both",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const frame = document.createElement("iframe");
|
|
162
|
+
frame.id = frameId;
|
|
163
|
+
frame.title = "Clips preview";
|
|
164
|
+
frame.allow = "autoplay; fullscreen; picture-in-picture";
|
|
165
|
+
frame.setAttribute("allowfullscreen", "true");
|
|
166
|
+
frame.setAttribute("scrolling", "no");
|
|
167
|
+
frame.referrerPolicy = "no-referrer";
|
|
168
|
+
Object.assign(frame.style, {
|
|
169
|
+
display: "block",
|
|
170
|
+
width: "100%",
|
|
171
|
+
height: "430px",
|
|
172
|
+
border: "0",
|
|
173
|
+
borderRadius: "8px",
|
|
174
|
+
background: "transparent",
|
|
175
|
+
overflow: "hidden",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const previewUrl = new URL(
|
|
179
|
+
chrome.runtime.getURL("src/github-preview.html"),
|
|
180
|
+
);
|
|
181
|
+
previewUrl.searchParams.set("frameId", frameId);
|
|
182
|
+
previewUrl.searchParams.set("embedUrl", link.embedUrl);
|
|
183
|
+
previewUrl.searchParams.set("metadataUrl", link.metadataUrl);
|
|
184
|
+
previewUrl.searchParams.set("sourceUrl", link.sourceUrl);
|
|
185
|
+
previewUrl.searchParams.set("clipId", link.clipId);
|
|
186
|
+
previewUrl.searchParams.set("host", link.host);
|
|
187
|
+
frame.src = previewUrl.toString();
|
|
188
|
+
|
|
189
|
+
wrapper.appendChild(frame);
|
|
190
|
+
previewInsertionTarget(anchor).after(wrapper);
|
|
191
|
+
anchor.setAttribute(PROCESSED_ATTR, "mounted");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scan(): void {
|
|
195
|
+
for (const anchor of document.querySelectorAll<HTMLAnchorElement>(
|
|
196
|
+
`a[href]:not([${PROCESSED_ATTR}])`,
|
|
197
|
+
)) {
|
|
198
|
+
if (!isGitHubMarkdownAnchor(anchor)) continue;
|
|
199
|
+
const link = parseClipsLink(anchor.href);
|
|
200
|
+
if (!link) continue;
|
|
201
|
+
mountPreview(anchor, link);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function scheduleScan(): void {
|
|
206
|
+
clearTimeout(scanTimer);
|
|
207
|
+
scanTimer = setTimeout(scan, 120);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
window.addEventListener("message", (event) => {
|
|
211
|
+
if (event.origin !== EXTENSION_ORIGIN) return;
|
|
212
|
+
const data = event.data as
|
|
213
|
+
| { source?: string; frameId?: string; height?: number }
|
|
214
|
+
| undefined;
|
|
215
|
+
if (data?.source !== "clips-github-preview") return;
|
|
216
|
+
if (!data.frameId || typeof data.height !== "number") return;
|
|
217
|
+
const frame = document.getElementById(data.frameId);
|
|
218
|
+
if (!(frame instanceof HTMLIFrameElement)) return;
|
|
219
|
+
const height = Math.max(
|
|
220
|
+
MIN_HEIGHT,
|
|
221
|
+
Math.min(MAX_HEIGHT, Math.ceil(data.height)),
|
|
222
|
+
);
|
|
223
|
+
frame.style.height = `${height}px`;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const observer = new MutationObserver(scheduleScan);
|
|
227
|
+
observer.observe(document.documentElement, {
|
|
228
|
+
childList: true,
|
|
229
|
+
subtree: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
scan();
|
|
233
|
+
})();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Clips preview</title>
|
|
7
|
+
<script type="module" src="./github-preview.ts"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main id="root"></main>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|