feat(app,share): deep-link bundle shares into new-worker imports (#664)

* docs(share): add free-first and org hub sharing redesign flows

* feat(app,share): open bundle links into new-worker imports
This commit is contained in:
ben
2026-02-24 19:01:04 -08:00
committed by GitHub
parent 11c231f691
commit 28d1f4206c
19 changed files with 477 additions and 29 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,96 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenWork Share Flow Redesign</title>
<style>
:root {
color-scheme: only light;
--bg: #f7f5f0;
--card: #ffffff;
--line: #d7d1c5;
--ink: #1f1b16;
--muted: #6b6153;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Avenir Next", "Segoe UI", sans-serif;
color: var(--ink);
background: radial-gradient(circle at 5% -5%, #efe9dc 0, transparent 30%), var(--bg);
padding: 24px;
}
h1 {
margin: 0 0 8px;
font-size: 28px;
}
p {
margin: 0 0 20px;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04);
}
.card h2 {
margin: 0 0 8px;
font-size: 15px;
}
img {
width: 100%;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
}
@media (max-width: 980px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<h1>OpenWork Share Flow Redesign</h1>
<p>
Free/default sharing remains first-class. Organization is an optional hub for indexing and governed discovery of the same links.
</p>
<section class="grid">
<article class="card">
<h2>1) Free default share</h2>
<img src="01-free-default-share.png" alt="Free default share flow" />
</article>
<article class="card">
<h2>2) Open in app creates new worker</h2>
<img src="02-open-in-app-new-worker.png" alt="Open in app new worker flow" />
</article>
<article class="card">
<h2>3) Org hub overlays the same links</h2>
<img src="03-org-library-overlay.png" alt="Organization library overlay flow" />
</article>
<article class="card">
<h2>4) Remix and versioning loop</h2>
<img src="04-remix-and-versioning.png" alt="Remix and versioning flow" />
</article>
</section>
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -208,10 +208,24 @@ type SharedBundleV1 =
| SharedSkillsSetBundleV1
| SharedWorkspaceProfileBundleV1;
type SharedBundleImportIntent = "new_worker" | "import_current";
type SharedBundleDeepLink = {
bundleUrl: string;
intent: SharedBundleImportIntent;
source?: string;
orgId?: string;
label?: string;
};
function normalizeSharedBundleImportIntent(value: string | null | undefined): SharedBundleImportIntent {
const normalized = (value ?? "").trim().toLowerCase();
if (normalized === "new_worker" || normalized === "new-worker" || normalized === "newworker") {
return "new_worker";
}
return "import_current";
}
function readRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
@@ -419,7 +433,17 @@ function parseSharedBundleDeepLink(rawUrl: string): SharedBundleDeepLink | null
if (parsedBundleUrl.protocol !== "https:" && parsedBundleUrl.protocol !== "http:") {
return null;
}
return { bundleUrl: parsedBundleUrl.toString() };
const intent = normalizeSharedBundleImportIntent(url.searchParams.get("ow_intent") ?? url.searchParams.get("intent"));
const source = url.searchParams.get("ow_source")?.trim() ?? url.searchParams.get("source")?.trim() ?? "";
const orgId = url.searchParams.get("ow_org")?.trim() ?? "";
const label = url.searchParams.get("ow_label")?.trim() ?? "";
return {
bundleUrl: parsedBundleUrl.toString(),
intent,
source: source || undefined,
orgId: orgId || undefined,
label: label || undefined,
};
} catch {
return null;
}
@@ -434,7 +458,7 @@ function stripSharedBundleQuery(rawUrl: string): string | null {
}
let changed = false;
for (const key of ["ow_bundle", "bundleUrl", "source"]) {
for (const key of ["ow_bundle", "bundleUrl", "ow_intent", "intent", "ow_source", "source", "ow_org", "ow_label"]) {
if (url.searchParams.has(key)) {
url.searchParams.delete(key);
changed = true;
@@ -726,7 +750,13 @@ export default function App() {
}
if (bundleInvite?.bundleUrl) {
setPendingSharedBundleUrl(bundleInvite.bundleUrl);
setPendingSharedBundleInvite({
bundleUrl: bundleInvite.bundleUrl,
intent: normalizeSharedBundleImportIntent(bundleInvite.intent),
source: bundleInvite.source,
orgId: bundleInvite.orgId,
label: bundleInvite.label,
});
setSharedBundleNoticeShown(false);
}
@@ -2689,21 +2719,84 @@ export default function App() {
setOpenworkServerWorkspaceId(null);
});
createEffect(() => {
const bundleUrl = pendingSharedBundleUrl();
if (!bundleUrl || booting()) {
return;
const resolveSharedBundleWorkerTarget = () => {
const pref = startupPreference();
const hostInfo = openworkServerHostInfo();
const settings = openworkServerSettings();
const localHostUrl = normalizeOpenworkServerUrl(hostInfo?.baseUrl ?? "") ?? "";
const localToken = hostInfo?.clientToken?.trim() ?? "";
const serverHostUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
const serverToken = settings.token?.trim() ?? "";
if (pref === "server") {
return {
hostUrl: serverHostUrl || localHostUrl,
token: serverToken || localToken,
};
}
const client = openworkServerClient();
const workspaceId = openworkServerWorkspaceId();
const connected = openworkServerStatus() === "connected";
if (pref === "local") {
return {
hostUrl: localHostUrl || serverHostUrl,
token: localToken || serverToken,
};
}
if (!client || !workspaceId || !connected) {
if (!sharedBundleNoticeShown()) {
setSharedBundleNoticeShown(true);
setError("Share link detected. Connect to a writable OpenWork worker to import this bundle.");
if (localHostUrl) {
return {
hostUrl: localHostUrl,
token: localToken || serverToken,
};
}
return {
hostUrl: serverHostUrl,
token: serverToken || localToken,
};
};
const waitForSharedBundleImportTarget = async (timeoutMs = 20_000) => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const client = openworkServerClient();
const workspaceId = openworkServerWorkspaceId();
if (client && workspaceId && openworkServerStatus() === "connected") {
return { client, workspaceId };
}
await new Promise<void>((resolve) => {
window.setTimeout(resolve, 200);
});
}
throw new Error("OpenWork worker is not ready yet.");
};
const createWorkerForSharedBundle = async (request: SharedBundleDeepLink, bundle: SharedBundleV1) => {
const target = resolveSharedBundleWorkerTarget();
const hostUrl = target.hostUrl.trim();
const token = target.token.trim();
if (!hostUrl || !token) {
throw new Error("Share link detected. Configure an OpenWork worker host and token, then open the link again.");
}
const label = (request.label?.trim() || bundle.name?.trim() || "Shared setup").slice(0, 80);
const ok = await workspaceStore.createRemoteWorkspaceFlow({
openworkHostUrl: hostUrl,
openworkToken: token,
directory: null,
displayName: label,
manageBusy: false,
closeModal: false,
});
if (!ok) {
throw new Error("Failed to create a worker from this share link.");
}
};
createEffect(() => {
const request = pendingSharedBundleInvite();
if (!request || booting()) {
return;
}
@@ -2711,12 +2804,44 @@ export default function App() {
return;
}
if (request.intent === "import_current") {
const client = openworkServerClient();
const workspaceId = openworkServerWorkspaceId();
const connected = openworkServerStatus() === "connected";
if (!client || !workspaceId || !connected) {
if (!sharedBundleNoticeShown()) {
setSharedBundleNoticeShown(true);
setError("Share link detected. Connect to a writable OpenWork worker to import this bundle.");
}
return;
}
} else {
const target = resolveSharedBundleWorkerTarget();
if (!target.hostUrl.trim() || !target.token.trim()) {
if (!sharedBundleNoticeShown()) {
setSharedBundleNoticeShown(true);
setError("Share link detected. Configure an OpenWork host and token to create a new worker.");
}
return;
}
}
let cancelled = false;
setSharedBundleImportBusy(true);
void (async () => {
try {
const bundle = await fetchSharedBundle(bundleUrl);
const bundle = await fetchSharedBundle(request.bundleUrl);
if (cancelled) return;
if (request.intent === "new_worker") {
await createWorkerForSharedBundle(request, bundle);
if (cancelled) return;
}
const { client, workspaceId } = await waitForSharedBundleImportTarget();
if (cancelled) return;
const { payload, importedSkillsCount } = buildImportPayloadFromBundle(bundle);
await client.importWorkspace(workspaceId, payload);
await refreshSkills({ force: true });
@@ -2733,7 +2858,7 @@ export default function App() {
} finally {
if (!cancelled) {
setSharedBundleImportBusy(false);
setPendingSharedBundleUrl(null);
setPendingSharedBundleInvite(null);
setSharedBundleNoticeShown(false);
}
}
@@ -2882,7 +3007,7 @@ export default function App() {
const [editRemoteWorkspaceError, setEditRemoteWorkspaceError] = createSignal<string | null>(null);
const [deepLinkRemoteWorkspaceDefaults, setDeepLinkRemoteWorkspaceDefaults] = createSignal<RemoteWorkspaceDefaults | null>(null);
const [pendingRemoteConnectDeepLink, setPendingRemoteConnectDeepLink] = createSignal<RemoteWorkspaceDefaults | null>(null);
const [pendingSharedBundleUrl, setPendingSharedBundleUrl] = createSignal<string | null>(null);
const [pendingSharedBundleInvite, setPendingSharedBundleInvite] = createSignal<SharedBundleDeepLink | null>(null);
const [sharedBundleImportBusy, setSharedBundleImportBusy] = createSignal(false);
const [sharedBundleNoticeShown, setSharedBundleNoticeShown] = createSignal(false);
const [renameWorkspaceOpen, setRenameWorkspaceOpen] = createSignal(false);
@@ -2904,7 +3029,7 @@ export default function App() {
if (!parsed) {
return false;
}
setPendingSharedBundleUrl(parsed.bundleUrl);
setPendingSharedBundleInvite(parsed);
setSharedBundleNoticeShown(false);
return true;
};

View File

@@ -510,6 +510,10 @@ const OPENWORK_INVITE_PARAM_URL = "ow_url";
const OPENWORK_INVITE_PARAM_TOKEN = "ow_token";
const OPENWORK_INVITE_PARAM_STARTUP = "ow_startup";
const OPENWORK_INVITE_PARAM_BUNDLE = "ow_bundle";
const OPENWORK_INVITE_PARAM_BUNDLE_INTENT = "ow_intent";
const OPENWORK_INVITE_PARAM_BUNDLE_SOURCE = "ow_source";
const OPENWORK_INVITE_PARAM_BUNDLE_ORG = "ow_org";
const OPENWORK_INVITE_PARAM_BUNDLE_LABEL = "ow_label";
export type OpenworkConnectInvite = {
url: string;
@@ -517,10 +521,24 @@ export type OpenworkConnectInvite = {
startup?: "server";
};
export type OpenworkBundleInviteIntent = "new_worker" | "import_current";
export type OpenworkBundleInvite = {
bundleUrl: string;
intent: OpenworkBundleInviteIntent;
source?: string;
orgId?: string;
label?: string;
};
function normalizeOpenworkBundleInviteIntent(value: string | null | undefined): OpenworkBundleInviteIntent {
const normalized = (value ?? "").trim().toLowerCase();
if (normalized === "new_worker" || normalized === "new-worker" || normalized === "newworker") {
return "new_worker";
}
return "import_current";
}
export function buildOpenworkConnectInviteUrl(input: {
workspaceUrl: string;
token?: string | null;
@@ -583,6 +601,10 @@ export function readOpenworkConnectInviteFromSearch(input: string | URLSearchPar
export function buildOpenworkBundleInviteUrl(input: {
bundleUrl: string;
appUrl?: string | null;
intent?: OpenworkBundleInviteIntent;
source?: string | null;
orgId?: string | null;
label?: string | null;
}) {
const rawBundleUrl = input.bundleUrl?.trim() ?? "";
if (!rawBundleUrl) return "";
@@ -599,12 +621,48 @@ export function buildOpenworkBundleInviteUrl(input: {
try {
const url = new URL(base);
const search = new URLSearchParams(url.search);
const intent = normalizeOpenworkBundleInviteIntent(input.intent);
search.set(OPENWORK_INVITE_PARAM_BUNDLE, bundleUrl);
search.set(OPENWORK_INVITE_PARAM_BUNDLE_INTENT, intent);
const source = input.source?.trim() ?? "";
if (source) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_SOURCE, source);
}
const orgId = input.orgId?.trim() ?? "";
if (orgId) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_ORG, orgId);
}
const label = input.label?.trim() ?? "";
if (label) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_LABEL, label);
}
url.search = search.toString();
return url.toString();
} catch {
const search = new URLSearchParams();
const intent = normalizeOpenworkBundleInviteIntent(input.intent);
search.set(OPENWORK_INVITE_PARAM_BUNDLE, bundleUrl);
search.set(OPENWORK_INVITE_PARAM_BUNDLE_INTENT, intent);
const source = input.source?.trim() ?? "";
if (source) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_SOURCE, source);
}
const orgId = input.orgId?.trim() ?? "";
if (orgId) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_ORG, orgId);
}
const label = input.label?.trim() ?? "";
if (label) {
search.set(OPENWORK_INVITE_PARAM_BUNDLE_LABEL, label);
}
return `${DEFAULT_OPENWORK_CONNECT_APP_URL}?${search.toString()}`;
}
}
@@ -629,8 +687,17 @@ export function readOpenworkBundleInviteFromSearch(input: string | URLSearchPara
return null;
}
const intent = normalizeOpenworkBundleInviteIntent(search.get(OPENWORK_INVITE_PARAM_BUNDLE_INTENT));
const source = search.get(OPENWORK_INVITE_PARAM_BUNDLE_SOURCE)?.trim() ?? "";
const orgId = search.get(OPENWORK_INVITE_PARAM_BUNDLE_ORG)?.trim() ?? "";
const label = search.get(OPENWORK_INVITE_PARAM_BUNDLE_LABEL)?.trim() ?? "";
return {
bundleUrl,
intent,
source: source || undefined,
orgId: orgId || undefined,
label: label || undefined,
} satisfies OpenworkBundleInvite;
}
@@ -650,6 +717,10 @@ export function stripOpenworkBundleInviteFromUrl(input: string) {
try {
const url = new URL(input);
url.searchParams.delete(OPENWORK_INVITE_PARAM_BUNDLE);
url.searchParams.delete(OPENWORK_INVITE_PARAM_BUNDLE_INTENT);
url.searchParams.delete(OPENWORK_INVITE_PARAM_BUNDLE_SOURCE);
url.searchParams.delete(OPENWORK_INVITE_PARAM_BUNDLE_ORG);
url.searchParams.delete(OPENWORK_INVITE_PARAM_BUNDLE_LABEL);
return url.toString();
} catch {
return input;

View File

@@ -0,0 +1,12 @@
flowchart TD
A[OpenWork app\nWorkspace configured] --> B[Share modal]
B --> C[Create share link]
C --> D[Copy link and send]
D --> E[Recipient opens URL]
E --> F[Share service page]
F --> G[Open in OpenWork]
G --> H[Deep link with\now_intent=new_worker]
H --> I[OpenWork app intercepts]
I --> J[Create new worker prompt]
J --> K[Provision worker + import bundle]
K --> L[Open new session]

View File

@@ -0,0 +1,13 @@
flowchart TD
A[Share service page loaded] --> B{Open in OpenWork clicked}
B --> C[Build deep link:\now_bundle + ow_intent=new_worker]
C --> D[OS hands URL to OpenWork]
D --> E[Parse bundle + intent]
E --> F[Wizard: New worker from shared setup]
F --> G{Worker target}
G -->|Local host| H[Create local worker entry]
G -->|Remote host| I[Create remote worker entry]
H --> J[Import bundle]
I --> J
J --> K[Set new worker active]
K --> L[Open chat/session]

View File

@@ -0,0 +1,12 @@
flowchart TD
A[OpenWork app share modal] --> B{Add to organization library}
B -->|No| C[Direct link sharing only\nfree/default]
B -->|Yes| D[Publish metadata + tags\nto org hub]
D --> E[Org hub card points to same bundle]
E --> F[Teammate opens org hub]
F --> G[Open card details]
G --> H[Open in OpenWork]
H --> I[Deep link with org context\now_source=org_hub]
I --> J[OpenWork creates new worker\nand imports bundle]
C --> K[Recipient can still open\nshare URL directly]
K --> J

View File

@@ -0,0 +1,10 @@
flowchart TD
A[New worker created from shared bundle] --> B[User edits skills/config/commands]
B --> C{Share changes}
C -->|Fork as new setup| D[Publish new bundle ID]
C -->|Update org template| E[Publish next version\nsame template identity]
D --> F[New standalone share link]
E --> G[Org card now resolves\nto latest approved version]
F --> H[Others open as new worker]
G --> H
H --> I[More remixes and team reuse]

79
pr/share-flow-redesign.md Normal file
View File

@@ -0,0 +1,79 @@
# Seamless Share Flow Redesign (Free + Org)
## Product direction
- Free/default sharing remains the baseline experience.
- Organization is an optional index and permission layer on top of the exact same share links.
- One bundle format works everywhere (direct link, share service page, org hub card).
## Core UX rules
1. Every shared link must be usable directly, even without org membership.
2. `Open in OpenWork` from the share service should default to creating a new worker.
3. Importing into the current worker remains available as a secondary action.
4. Organization does not replace sharing, it organizes and governs the same assets.
## Deep-link contract
Proposed URL pattern used by share page and org hub:
```
openwork://import-bundle?ow_bundle=<encoded-share-url>&ow_intent=new_worker&ow_source=<share_service|org_hub>&ow_org=<optional-org-id>&ow_label=<optional-template-name>
```
Supported intents:
- `ow_intent=new_worker` (default): create a fresh worker and import bundle into it.
- `ow_intent=import_current`: import bundle into currently active worker.
## UI wiring
### OpenWork app: Share modal
- Primary action: `Create share link`
- Visibility chip:
- `Anyone with link` (default, free-friendly)
- `Add to organization` (optional)
- Destination controls:
- `Open in OpenWork -> New worker` (default recipient behavior)
- `Import into current worker` (secondary)
### Share service page
- Primary CTA: `Open in OpenWork (new worker)`
- Secondary CTA: `Import into current worker`
- Optional CTA: `Add to organization library` (visible only for signed-in org users)
### Organization hub (web app)
- Library is a central index of the same share artifacts:
- Search by team, tags, owner, recency.
- Open card -> same share detail model + Open in OpenWork deep link.
- Org ACL controls discovery and publishing, not bundle compatibility.
## Flowchart set
- `pr/diagrams/share-flow-redesign/01-free-default-share.mmd`
- `pr/diagrams/share-flow-redesign/02-open-in-app-new-worker.mmd`
- `pr/diagrams/share-flow-redesign/03-org-library-overlay.mmd`
- `pr/diagrams/share-flow-redesign/04-remix-and-versioning.mmd`
Rendered PNGs:
- `evidence/share-flow-redesign/01-free-default-share.png`
- `evidence/share-flow-redesign/02-open-in-app-new-worker.png`
- `evidence/share-flow-redesign/03-org-library-overlay.png`
- `evidence/share-flow-redesign/04-remix-and-versioning.png`
- `evidence/share-flow-redesign/share-flow-overview.png` (single sheet with all four flows)
Video walkthrough:
- `evidence/share-flow-redesign/share-flow-walkthrough.mp4`
## Acceptance checklist
- Direct link share works with no org context.
- Share page opens deep link with `ow_intent=new_worker` by default.
- OpenWork app deep-link handler can branch by intent.
- New worker provisioning path reuses existing remote/local worker creation flow.
- Org hub card launch and direct share URL launch land in equivalent app import UX.

View File

@@ -13,7 +13,11 @@ It is designed to be deployed on Vercel and backed by Vercel Blob.
- `GET /b/:id`
- Returns an HTML share page by default for browser requests.
- Includes an **Open in app** action that sends users to OpenWork app with `ow_bundle` query param.
- Includes an **Open in app** action that opens `openwork://import-bundle` with:
- `ow_bundle=<share-url>`
- `ow_intent=new_worker` (default import target)
- `ow_source=share_service`
- Also includes a web fallback action that opens `PUBLIC_OPENWORK_APP_URL` with the same query params.
- Returns raw JSON for API/programmatic requests:
- send `Accept: application/json`, or
- append `?format=json`.

View File

@@ -54,14 +54,34 @@ function normalizeAppUrl(input) {
return trimmed.replace(/\/+$/, "");
}
function buildOpenInAppUrl(shareUrl) {
function buildOpenInAppUrls(shareUrl, options = {}) {
const query = new URLSearchParams();
query.set("ow_bundle", shareUrl);
query.set("ow_intent", "new_worker");
query.set("ow_source", "share_service");
const label = String(options.label ?? "").trim();
if (label) {
query.set("ow_label", label.slice(0, 120));
}
const openInAppDeepLink = `openwork://import-bundle?${query.toString()}`;
const appUrl = normalizeAppUrl(OPENWORK_APP_URL) || "https://app.openwork.software";
try {
const url = new URL(appUrl);
url.searchParams.set("ow_bundle", shareUrl);
return url.toString();
for (const [key, value] of query.entries()) {
url.searchParams.set(key, value);
}
return {
openInAppDeepLink,
openInWebAppUrl: url.toString(),
};
} catch {
return `${"https://app.openwork.software"}?ow_bundle=${encodeURIComponent(shareUrl)}`;
return {
openInAppDeepLink,
openInWebAppUrl: `${"https://app.openwork.software"}?${query.toString()}`,
};
}
}
@@ -166,7 +186,9 @@ export function wantsDownload(req) {
export function renderBundlePage({ id, rawJson, req }) {
const bundle = parseBundle(rawJson);
const urls = buildBundleUrls(req, id);
const openInAppUrl = buildOpenInAppUrl(urls.shareUrl);
const { openInAppDeepLink, openInWebAppUrl } = buildOpenInAppUrls(urls.shareUrl, {
label: bundle.name || "Shared setup",
});
const prettyBundleJson = prettyJson(rawJson);
const schemaVersion = bundle.schemaVersion == null ? "unknown" : String(bundle.schemaVersion);
const typeLabel = humanizeType(bundle.type);
@@ -180,11 +202,11 @@ export function renderBundlePage({ id, rawJson, req }) {
const skillsSetCount = listCount(bundle.skills);
const installHint =
bundle.type === "skill"
? "Open OpenWork and import from this link to install this skill."
? "Use Open in app to create a new worker and install this skill."
: bundle.type === "skills-set"
? "Use Open in app to import this full skills set in one step."
? "Use Open in app to create a new worker and import this full skills set."
: bundle.type === "workspace-profile"
? "Use Open in app to import this full workspace profile (config, MCP, commands, and skills)."
? "Use Open in app to create a new worker from this full workspace profile (config, MCP, commands, and skills)."
: "Use the JSON endpoint if you want to import this bundle programmatically.";
const contentLabel = bundle.type === "skill" && bundle.content.trim() ? "Skill content" : "Bundle payload";
const contentPreview =
@@ -211,7 +233,7 @@ export function renderBundlePage({ id, rawJson, req }) {
<meta name="openwork:bundle-id" content="${escapeHtml(id)}" />
<meta name="openwork:bundle-type" content="${escapeHtml(bundle.type || "unknown")}" />
<meta name="openwork:schema-version" content="${escapeHtml(schemaVersion)}" />
<meta name="openwork:open-in-app-url" content="${escapeHtml(openInAppUrl)}" />
<meta name="openwork:open-in-app-url" content="${escapeHtml(openInAppDeepLink)}" />
<link rel="alternate" type="application/json" href="${escapeHtml(urls.jsonUrl)}" />
<style>
:root {
@@ -449,7 +471,8 @@ export function renderBundlePage({ id, rawJson, req }) {
<h1>${escapeHtml(title)}</h1>
<p>${escapeHtml(description)}</p>
<div class="actions">
<a class="action-link primary" href="${escapeHtml(openInAppUrl)}" target="_blank" rel="noreferrer">Open in app</a>
<a class="action-link primary" href="${escapeHtml(openInAppDeepLink)}">Open in app (new worker)</a>
<a class="action-link secondary" href="${escapeHtml(openInWebAppUrl)}" target="_blank" rel="noreferrer">Open in web app</a>
<button type="button" class="secondary" id="copy-link-button">Copy share link</button>
<button type="button" class="secondary" id="copy-json-button">Copy bundle JSON</button>
<a class="action-link secondary" href="${escapeHtml(urls.jsonUrl)}" target="_blank" rel="noreferrer">View raw JSON</a>

View File

@@ -59,7 +59,10 @@ test("renderBundlePage includes machine-readable metadata and escaped json scrip
assert.match(html, /data-openwork-bundle-type="skill"/);
assert.match(html, /meta name="openwork:bundle-id" content="01TEST"/);
assert.match(html, /\?format=json/);
assert.match(html, /openwork:\/\/import-bundle\?/);
assert.match(html, /ow_bundle=https%3A%2F%2Fshare\.openwork\.software%2Fb%2F01TEST/);
assert.match(html, /ow_intent=new_worker/);
assert.match(html, /ow_source=share_service/);
assert.match(html, /id="openwork-bundle-json" type="application\/json"/);
assert.match(html, /demo \\u003c\/script\\u003e skill/);
});