@dxos/plugin-magazine 0.0.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/{MagazineArticle-DEUTR23F.mjs → MagazineArticle-5T7RTIPS.mjs} +2 -2
- package/dist/lib/neutral/MagazinePlugin.mjs +1 -1
- package/dist/lib/neutral/MagazinePlugin.mjs.map +1 -1
- package/dist/lib/neutral/{PostArticle-RERHXPXC.mjs → PostArticle-BGIPU7XA.mjs} +2 -2
- package/dist/lib/neutral/capabilities/index.mjs +1 -1
- package/dist/lib/neutral/{chunk-MJ2AY2CK.mjs → chunk-LFSQMLPK.mjs} +2 -2
- package/dist/lib/neutral/containers/index.mjs +2 -2
- package/dist/lib/neutral/{curate-magazine-MA6OLK2X.mjs → curate-magazine-7RMKSJC5.mjs} +2 -2
- package/dist/lib/neutral/{fetch-article-content-CZO6D3QD.mjs → fetch-article-content-CXPKXIMR.mjs} +2 -2
- package/dist/lib/neutral/{load-post-content-XGL6VFF6.mjs → load-post-content-H3S4TKVM.mjs} +2 -2
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/operations/index.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +1 -1
- package/dist/lib/neutral/plugin.workerd.mjs +1 -1
- package/dist/lib/neutral/{routine-templates-A2XTJOZO.mjs → routine-templates-D5A2L4KL.mjs} +2 -2
- package/dist/lib/neutral/{sync-feed-UZX2XHSQ.mjs → sync-feed-SDXYF7HU.mjs} +2 -2
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +56 -56
- /package/dist/lib/neutral/{MagazineArticle-DEUTR23F.mjs.map → MagazineArticle-5T7RTIPS.mjs.map} +0 -0
- /package/dist/lib/neutral/{PostArticle-RERHXPXC.mjs.map → PostArticle-BGIPU7XA.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-MJ2AY2CK.mjs.map → chunk-LFSQMLPK.mjs.map} +0 -0
- /package/dist/lib/neutral/{curate-magazine-MA6OLK2X.mjs.map → curate-magazine-7RMKSJC5.mjs.map} +0 -0
- /package/dist/lib/neutral/{fetch-article-content-CZO6D3QD.mjs.map → fetch-article-content-CXPKXIMR.mjs.map} +0 -0
- /package/dist/lib/neutral/{load-post-content-XGL6VFF6.mjs.map → load-post-content-H3S4TKVM.mjs.map} +0 -0
- /package/dist/lib/neutral/{routine-templates-A2XTJOZO.mjs.map → routine-templates-D5A2L4KL.mjs.map} +0 -0
- /package/dist/lib/neutral/{sync-feed-UZX2XHSQ.mjs.map → sync-feed-SDXYF7HU.mjs.map} +0 -0
|
@@ -212,7 +212,7 @@ var useToolbar = ({ magazine }) => {
|
|
|
212
212
|
};
|
|
213
213
|
|
|
214
214
|
// src/containers/MagazineArticle/MagazineArticle.tsx
|
|
215
|
-
var __dxlog_file = "/
|
|
215
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/containers/MagazineArticle/MagazineArticle.tsx";
|
|
216
216
|
var MagazineArticle = ({ role, subject, attendableId }) => {
|
|
217
217
|
const { t } = useTranslation(meta2.profile.key);
|
|
218
218
|
const invoker = useOperationInvoker2();
|
|
@@ -322,4 +322,4 @@ var TileAdapter = ({ data }) => {
|
|
|
322
322
|
export {
|
|
323
323
|
MagazineArticle as default
|
|
324
324
|
};
|
|
325
|
-
//# sourceMappingURL=MagazineArticle-
|
|
325
|
+
//# sourceMappingURL=MagazineArticle-5T7RTIPS.mjs.map
|
|
@@ -12,7 +12,7 @@ import { meta } from "#meta";
|
|
|
12
12
|
import { translations } from "#translations";
|
|
13
13
|
import { Magazine, Subscription } from "#types";
|
|
14
14
|
|
|
15
|
-
// raw-loader:/
|
|
15
|
+
// raw-loader:/__w/dxos/dxos/packages/plugins/plugin-magazine/PLUGIN.mdl
|
|
16
16
|
var PLUGIN_default = "---\nid: org.dxos.plugin.magazine\nname: MagazinePlugin\nversion: 0.2.0\n---\n\nA Composer plugin for syndicated content. Today it supports RSS and ATproto\nFeeds whose Posts are synced into the space as ECHO objects. This revision\nadds **Magazines** \u2014 agent-curated collections of articles drawn from one or\nmore Feeds and displayed as a Masonry grid. The user describes in natural\nlanguage what content a Magazine should gather; pressing *Curate* triggers\nfeed sync followed by an agent that selects matching articles, fetches their\ntext and hero images, and appends them to the Magazine.\n\nThe design also scaffolds (schema-only) a future tagging feature in which the\nagent assigns tags to curated Posts to enable navigation, filtering, and\nfeed-discovery flows.\n\n## Extensions\n\nThe following extension dialects are used in this document.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n| `service` | `org.dxos.mdl.service@1.0` |\n\n## Types\n\n```mdl\ntype FeedType\n literals: atproto | rss\n```\n\n```mdl\ntype Feed\n desc: >\n A subscription source. Existing type \u2014 documented here for context.\n SyncFeed pulls Posts from the URL into the space.\n fields:\n name?: string\n url?: string\n type?: FeedType\n description?: string\n link?: string\n iconUrl?: string\n cursor?: string # sync state\n feed?: Ref<Queue> # backing ECHO feed\n```\n\n```mdl\ntype Post\n desc: >\n An individual syndicated article. Existing fields are unchanged.\n This revision adds four optional fields populated by the Magazine\n curation flow. All additions are optional \u2014 existing Posts remain valid.\n fields:\n title?: string\n link?: string # URL of the original article\n description?: string # feed-provided summary (as-is)\n author?: string\n published?: string # ISO 8601 publication date\n guid?: string # feed-provided unique id\n\n # --- Additions ---\n snippet?: string # short description-derived preview (~200 chars) for tiles\n content?: string # full article body plain text, populated on first open\n imageUrl?: string # hero image URL extracted from the article page\n readAt?: string # ISO 8601; presence = read, absence = unread\n tags?: string[] # agent-assigned tags (future tagging feature; always [] today)\n```\n\n```mdl\ntype Magazine\n desc: >\n An agent-curated collection of articles drawn from one or more Feeds.\n Curation is driven by a Routine (see MagazineSkill): the Routine's\n editorial instructions describe what content this Magazine should gather,\n and it references the MagazineSkill (the HOW \u2014 tools and output\n contract). The Routine is created lazily on first curation, its\n instructions seeded with the default editorial methodology and the user's\n `topic`. The agent selects matching Posts from the referenced Feeds,\n enriches them with snippet and hero image, and their refs are appended to\n `posts`.\n fields:\n name?: string\n feeds: Ref<Feed>[] # Feeds to pull from\n topic?: string # create-dialog editorial seed; woven into the Routine's instructions on first curate\n routine?: Ref<Routine> # editorial Routine (lazy; created by the curate op); not rendered as form input\n posts: Ref<Post>[] # curated Post refs (insertion order; UI displays newest-last reversed)\n annotations:\n icon: ph--book-open-text--regular\n hue: indigo\n label: name\n note: >\n The Routine is added to its own `objects` (context) so the agent sees the\n Magazine as bound context, not only the candidate JSON `input`.\n```\n\n## Components\n\n```mdl\ncomponent MagazineArticle\n desc: >\n Article surface bound to a Magazine object. Shows curated articles in a\n Masonry grid and offers a Curate button that triggers feed sync plus\n agent curation.\n props:\n magazine: Magazine\n state:\n running: boolean # Curate flow in progress\n phase?: 'syncing' | 'curating' # shown as button tooltip while running\n slots:\n header?: ReactNode\n actions:\n curate() # run syncFeeds \u2192 invoke CurateMagazine\n openPost(post: Post) # sets post.readAt if unset; dispatches useShowItem (plugin-deck)\n emits:\n onCurateComplete(added: number)\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 [header slot] [\u2728 Curate] \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n \u2502 \u2502tile \u2502 \u2502tile \u2502 \u2502tile \u2502 (Masonry grid) \u2502\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n \u2502 \u2502tile \u2502 \u2502tile \u2502 \u2502\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n PostArticle is rendered by the host layout (complementary drawer,\n sibling plank, or deck companion) \u2014 not composed inline.\n```\n\n```mdl\ncomponent MagazineTile\n desc: >\n Single masonry cell rendering a curated Post. Dimmed when post.readAt is set.\n Click opens the ArticleReader. Tags row appears only when tags.length > 0.\n props:\n post: Post\n feed: Feed # resolved parent feed for name/icon\n emits:\n onOpen(post: Post)\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 [hero image if present] \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Title (2-line clamp) \u2502\n \u2502 Snippet (3-line clamp) \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 Feed \xB7 Author \xB7 Date \u2502\n \u2502 [#tag] [#tag] \u2502 (only when tags.length > 0)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n```mdl\ncomponent PostArticle\n desc: >\n Independent article surface for a single Post. Registered as a React\n surface matching `object(Article, Subscription.Post)` so it can render\n in any article/companion context \u2014 the complementary sidebar in\n 'simple' mode, a sibling plank in 'multi' mode, or the deck companion\n in the default 'deck' mode.\n\n Panel-based layout with a toolbar; the \"Open original\" link lives in\n the toolbar so it's always accessible. post.readAt is set by the\n master (MagazineArticle) when the tile is opened, not by PostArticle.\n props:\n post: Post # via Surface data.subject\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Title [Open original] \u2502 \u2190 toolbar\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Author \xB7 Date \u2502\n \u2502 [hero image] \u2502\n \u2502 Snippet \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Services\n\n```mdl\nservice MagazineSkill\n desc: |\n Agent skill referenced by each Magazine's Routine. Registered as a\n SkillDefinition capability. The skill instructions are limited to\n HOW to operate: how to call the FetchArticleContent tool and the\n completeJob output contract. The editorial methodology (what to select,\n dedup, snippet style, hero-image choice) and the magazine's topic live on\n the Routine's instructions, not here.\n key: org.dxos.skill.magazine\n ops:\n # Agent-facing tools exposed by the skill:\n # - FetchArticleContent\n # (completeJob is provided by the agent runtime; SyncFeed / CurateMagazine /\n # ClearMagazine / LoadPostContent are orchestration/UI, not agent tools.)\n note: >\n The curate op resolves this skill from the registry and references it\n from the Magazine's Routine. The agent reads candidate summaries from the\n `input`, MAY call FetchArticleContent to read full text, then calls\n completeJob with the structured selection.\n```\n\n## Operations\n\n```mdl\nop SyncFeed\n desc: Existing operation. Pulls new Posts from a Feed's source URL.\n input:\n feedId: DXN\n output: number # number of Posts added\n effects: [http, echo:write]\n note: Documented here for context; unchanged by this revision.\n```\n\n```mdl\nop ListCandidatePosts\n desc: >\n Returns lightweight summaries of Posts from the Magazine's referenced\n feeds that have NOT yet been added to the Magazine. Excludes Posts whose\n refs already appear in magazine.posts.\n input:\n magazineId: DXN\n output:\n Array<{\n postId: DXN,\n feedName?: string,\n title?: string,\n description?: string,\n author?: string,\n published?: string\n }>\n effects: [echo:read]\n note: >\n Deliberately excludes the full article body and image \u2014 the agent\n decides from summaries whether to call FetchArticleContent.\n```\n\n```mdl\nop FetchArticleContent\n desc: >\n Fetches post.link over HTTP and returns extracted plain text plus any\n image URLs found (og:image and <img> tags). Pure read \u2014 does not mutate\n the Post. Reusable by the future tagging feature.\n input:\n postId: DXN\n output:\n {\n text: string,\n imageUrls: string[]\n }\n errors:\n FetchFailed: HTTP error or network failure\n NoLink: the Post has no `link` field\n effects: [http, echo:read]\n```\n\n```mdl\nop AddPostToMagazine\n desc: >\n Writes snippet and imageUrl onto the Post and appends the Post's ref to\n magazine.posts. Idempotent \u2014 no-op if the Post ref is already present.\n input:\n magazineId: DXN\n postId: DXN\n snippet: string\n imageUrl?: string\n output: Post\n effects: [echo:write]\n```\n\n```mdl\nop CurateMagazine\n desc: >\n Deterministic one-shot curation invoked by the Curate button. For each\n uncurated candidate Post, derives a snippet and hero image from the Post's\n existing `description` (no HTTP) and appends the enriched Post ref to\n `magazine.posts`. Skips Posts with empty descriptions.\n input:\n magazineId: DXN\n output:\n added: number\n effects: [echo:write]\n note: >\n Exists alongside the skill. The deterministic path gives the button\n an immediate, reliable result without needing an LLM. The agent path\n (via the skill's fine-grained tools) delivers instruction-driven,\n higher-quality curation through chat.\n```\n\n## Features\n\n```mdl\nfeat F-1: Magazine Type\n desc: Introduce the Magazine ECHO type with feeds, instructions, and curated posts.\n\n req F-1.1: User can create a Magazine object via the plugin's create menu.\n req F-1.2: Magazine.name is optional; magazine is labeled by name when set.\n req F-1.3: Magazine.feeds is an array of Ref<Feed>; empty by default.\n req F-1.4: Magazine.routine is an optional Ref<Routine>, created lazily on first curate; hidden from the standard form input. Magazine.topic holds the create-dialog editorial seed until the Routine exists.\n req F-1.5: Magazine.posts is an array of Ref<Post>; empty by default; insertion order preserved.\n```\n\n```mdl\nfeat F-2: Post Extensions\n desc: Add optional snippet, imageUrl, readAt, and tags to existing Post type.\n\n req F-2.1: All new fields are optional; existing Post objects load unchanged.\n req F-2.2: snippet is a plain string; no max length enforced at schema level.\n req F-2.3: imageUrl is a plain string URL.\n req F-2.4: readAt is an ISO 8601 timestamp; presence = read, absence = unread.\n req F-2.5: tags is a string array; defaults to empty; not populated in this revision.\n```\n\n```mdl\nfeat F-3: Magazine Curate Flow\n desc: Sync referenced feeds then invoke the agent to curate articles.\n\n req F-3.1: MagazineArticle toolbar includes a Curate button.\n req F-3.2:\n when: user clicks Curate\n then: button shows spinner, is disabled, tooltip shows phase ('Syncing feeds...' / 'Curating articles...')\n req F-3.3:\n when: Curate runs\n then: SyncFeed invoked in parallel for each Ref in magazine.feeds; per-feed failures are collected but do not abort the flow\n req F-3.4:\n when: all feed syncs resolve (success or failure)\n then: CurateMagazine operation is invoked with the Magazine ref\n req F-3.5:\n when: CurateMagazine completes\n then: button returns to idle; reactive UI renders the new tiles\n req F-3.6:\n when: magazine.feeds is empty\n then: Curate button is disabled with tooltip 'Add at least one feed first'\n req F-3.7: Re-clicking Curate while running is a no-op (button disabled; in-memory running flag).\n```\n\n```mdl\nfeat F-4: Agent Curation Behavior\n desc: Agent uses tools to select, fetch, and add Posts.\n\n req F-4.1: Agent reads magazine.instructions Text and magazine.feeds refs from context.\n req F-4.2: Agent calls ListCandidatePosts(magazineId) to receive unenriched candidates.\n req F-4.3: Agent filters candidates against the instructions and skips low-relevance posts.\n req F-4.4: For each selected Post, agent calls FetchArticleContent(postId) to obtain text and images.\n req F-4.5: Agent produces a concise snippet (target ~200 chars) and selects the best image URL.\n req F-4.6: Agent calls AddPostToMagazine(magazineId, postId, snippet, imageUrl).\n req F-4.7: AddPostToMagazine is idempotent; re-invocation with the same postId is a no-op append.\n```\n\n```mdl\nfeat F-5: Masonry Display\n desc: Render curated Posts in a Masonry grid with rich tiles.\n\n req F-5.1: MagazineArticle renders magazine.posts in reverse insertion order (newest first) in a Masonry grid.\n req F-5.2: Each tile shows imageUrl (if present), title, snippet, feed name, author, published date.\n req F-5.3: Tile tags row renders only when post.tags.length > 0.\n req F-5.4: Tiles where post.readAt is set are visually dimmed.\n req F-5.5: Clicking a tile opens the ArticleReader for that Post.\n req F-5.6: Opening the reader sets post.readAt to the current ISO timestamp if not already set.\n req F-5.7: Empty state \u2014 when magazine.posts is empty \u2014 shows: 'No articles yet. Add feeds and press Curate.'\n req F-5.8: Grid uses the @dxos/react-ui-masonry Masonry component.\n```\n\n```mdl\nfeat F-6: Post Article Detail (master-detail via layout dispatch)\n desc: Post detail is rendered by a standalone PostArticle surface. The\n master (MagazineArticle) does not compose the detail inline;\n instead it asks the deck's `useShowItem` to select the post and\n route the detail to the appropriate place for the current\n layout mode (complementary drawer / sibling plank / companion).\n\n req F-6.1: PostArticle is registered as a React surface matching\n `object(Article, Subscription.Post)` \u2014 usable anywhere an\n article for a Post is requested.\n req F-6.2: PostArticle is a Panel with a Toolbar. The 'Open original'\n link lives in the toolbar. Body shows metadata, hero image,\n and snippet.\n req F-6.3:\n when: user clicks a tile in MagazineArticle\n then:\n - post.readAt is set to the current ISO timestamp if unset\n - useShowItem is invoked with:\n contextId = magazine attendableId\n selectionId = post.id\n companion = linkedSegment('post')\n path = getObjectPathFromObject(post)\n req F-6.4:\n when: useShowItem is invoked in 'simple' layout mode\n then: the complementary sidebar opens on linkedSegment('post')\n req F-6.5:\n when: useShowItem is invoked in 'multi' layout mode\n then: a sibling plank opens for the post's path, pivoted on the magazine\n req F-6.6:\n when: useShowItem is invoked in the default ('deck') layout mode\n then: the current plank's companion changes to linkedSegment('post')\n req F-6.7: An app-graph extension under each Magazine node exposes a\n companion node for linkedSegment('post') whose data resolves\n to the currently-selected Post from magazine.posts.\n```\n\n```mdl\nfeat F-7: Future Tagging (schema scaffolding only)\n desc: Schema fields in place so the tagging feature composes without migration.\n\n req F-7.1: Post.tags exists as Schema.Array(Schema.String).pipe(Schema.optional).\n req F-7.2: MagazineTile renders a tags row conditionally on tags.length > 0.\n req F-7.3: FetchArticleContent is reusable as the input stage for a future TagPosts operation.\n note: >\n Later features (out of scope): TagPosts operation, a MagazineTagger skill\n (or extension of MagazineSkill), tag-filter chips in the toolbar,\n 'Find more like this' action, and 'Suggest feeds' action proposing new Feed\n URLs based on accumulated tag distribution.\n```\n\n## Acceptance\n\n```mdl\ntest T-1: Create Magazine\n given: user is in a space in Composer\n when: user creates a Magazine via the plugin's create menu\n then:\n - Magazine object created in space\n - Magazine.feeds is an empty array\n - Magazine.posts is an empty array\n - Magazine.instructions is a Ref to an empty Text object\n```\n\n```mdl\ntest T-2: Post schema backward compatibility\n given: a Post created before this revision exists in the space\n when: the new plugin version loads\n then:\n - Post loads without schema error\n - Post.snippet, Post.imageUrl, Post.readAt, Post.tags are all undefined / empty\n```\n\n```mdl\ntest T-3: Curate button disabled without feeds\n given: a Magazine with magazine.feeds.length === 0\n when: MagazineArticle renders\n then:\n - Curate button is disabled\n - tooltip text === 'Add at least one feed first'\n```\n\n```mdl\ntest T-4: Curate flow runs sync then deterministic curation\n given: a Magazine with two referenced feeds, each with unsynced remote posts\n when: user clicks Curate\n then:\n - SyncFeed invoked once per referenced feed\n - After all SyncFeed calls resolve, CurateMagazine operation is invoked\n - Magazine.posts grows by the count returned from CurateMagazine\n - Button returns to idle\n```\n\n```mdl\ntest T-4a: Agent curation via skill\n given: a chat referencing a Magazine whose MagazineSkill is active\n when: the agent is prompted to curate the magazine\n then:\n - Agent calls ListCandidatePosts, FetchArticleContent, AddPostToMagazine\n - Magazine.posts grows by the agent-selected count\n note: >\n The skill path is distinct from the Curate button (T-4). The button\n runs the deterministic CurateMagazine op; agents invoke the fine-grained\n tools through chat.\n```\n\n```mdl\ntest T-5: Partial feed sync failure is non-fatal\n given: a Magazine with two feeds; one feed's URL returns 500\n when: user clicks Curate\n then:\n - CurateMagazine still runs against posts from the successful feed\n - Error surfaced via a localized message in the status bar\n```\n\n```mdl\ntest T-6: ListCandidatePosts excludes curated\n given: a Magazine whose posts array already contains refs to two Posts from feed A\n when: ListCandidatePosts(magazineId) is called\n then: returned list contains no entries whose postId matches an existing magazine.posts ref\n```\n\n```mdl\ntest T-7: AddPostToMagazine is idempotent\n given: a Magazine already containing a Post ref\n when: AddPostToMagazine invoked with the same postId\n then:\n - magazine.posts length unchanged\n - Post.snippet and Post.imageUrl left unchanged (no overwrite on duplicate)\n```\n\n```mdl\ntest T-8: FetchArticleContent surfaces images\n given: a Post whose link resolves to an HTML page with og:image and two <img> tags\n when: FetchArticleContent(postId) is called\n then:\n - output.text contains the page's main text\n - output.imageUrls includes the og:image URL\n - output.imageUrls includes URLs from <img> tags\n```\n\n```mdl\ntest T-9: Tile dimming on read\n given: a Magazine with one Post where readAt is unset\n when: user clicks the tile\n then:\n - ArticleReader opens for that Post\n - Post.readAt is set to an ISO timestamp\n - On reader close, the tile renders dimmed\n```\n\n```mdl\ntest T-10: Tile metadata\n given: a curated Post with title, snippet, imageUrl, author, published, and a feed with name\n when: MagazineTile renders\n then:\n - hero image rendered from imageUrl\n - title rendered with 2-line clamp\n - snippet rendered with 3-line clamp\n - metadata row shows feed name, author, and a formatted date\n - tags row is NOT rendered (tags is empty)\n```\n\n---\n\n## Design Notes\n\n### Curation via Routine (this revision)\n\nCuration runs a persisted **Routine** rather than an ephemeral one. The Magazine\nno longer carries an `instructions` Template; it carries an optional\n`routine: Ref<Routine>` plus a `topic` seed string.\n\n- **Eager creation.** `Magazine.make()` creates the `Routine` at magazine\n instantiation time. The Routine's `instructions` Text is seeded with the\n default editorial methodology and an optional topic woven into a `## Topic`\n section; it references the MagazineSkill and is parented to the magazine\n (cascade-delete). The magazine is added to the Routine's `objects` (context).\n- **Run.** The curate op invokes `AgentPrompt` (Run Routine) with\n `prompt: magazine.routine` and the candidate JSON `input`; it decodes the\n `completeJob` structured output and applies selections mechanically (keep-bound,\n per-post snippet/imageUrl). No ephemeral routine is constructed.\n- **Instruction split.** Skill = HOW (FetchArticleContent + completeJob\n contract). Routine = editorial (selection criteria, dedup, snippet style,\n hero-image choice) + topic. This removes the prior overlap where the skill\n held both.\n- **Properties editing.** The `routine` field carries `FormInlineAnnotation`,\n so the Routine's own fields (name, description, instructions) render inline in\n the generic `DefaultProperties` companion \u2014 no custom surface needed.\n- **Template.** `Template` is retained (it is the type of `Skill.instructions`\n and is used across the codebase). The Magazine simply stops using it; the\n Routine's `instructions` is a plain `Ref<Text>`.\n\n### Content Flow\n\nTwo curation paths exist side-by-side:\n\n- **Button path (deterministic).** The Curate toolbar button runs `SyncFeed`\n per referenced feed, then invokes `CurateMagazine`. Snippets and images are\n derived from each Post's existing `description` field (no HTTP, no LLM).\n Chosen for the user-initiated button because it is fast, reliable, and does\n not depend on an LLM runtime being present.\n- **Agent path (skill).** The `MagazineSkill` exposes three fine-grained\n tools (`ListCandidatePosts`, `FetchArticleContent`, `AddPostToMagazine`) and\n an instruction template that tells an agent to consult `Magazine.instructions`\n when selecting Posts. Triggered when an agent chat references the Magazine \u2014\n not when the button is clicked. Delivers higher-quality curation (full-article\n fetch + LLM-driven selection) at the cost of latency and LLM availability.\n\n`SyncFeed` is always run deterministically (either by the button or outside\nthe scope of this feature). Neither curation path fetches feed indices on its\nown \u2014 that is `SyncFeed`'s job.\n\n### Tool Granularity\n\nThree tools rather than one combined `curateMagazine` tool:\n\n- `ListCandidatePosts` returns lightweight summaries so the agent can triage\n cheaply without loading article bodies.\n- `FetchArticleContent` is a read-only HTTP fetch + extract; it is reusable by\n the future `TagPosts` operation which will need the same inputs.\n- `AddPostToMagazine` is the sole mutation. Idempotence lives in this tool\n (by ref-set membership check) rather than in the prompt.\n\nQuality of selection is the agent's job; the tools deliberately don't try to\nrank or filter on the agent's behalf.\n\n### Snippet Storage\n\n`Post.snippet` is a plain inline string, not a `Ref<Text>`. Snippets are\nshort (~200 chars target) and read-only to the user; the `Ref<Text>` indirection\nused for `Agent.instructions` and `Magazine.instructions` is appropriate for\nlong-form, user-editable content and is not needed here.\n\n### Read State\n\nRepresented as an optional ISO timestamp `readAt`, not a boolean or enum.\nAbsence = unread; presence = read (at that time). This leaves room to show\n\"read 2 days ago\" or to expire reads without a schema change.\n\n### Image Extraction\n\n`FetchArticleContent` returns a list of candidate image URLs; the agent picks\nthe \"best\" one for the Post's `imageUrl`. The prompt should prefer `og:image`\nwhen present. If no suitable image exists the Post is still added with\n`imageUrl` undefined \u2014 `MagazineTile` renders without the image region.\n\nHTML parsing uses whatever parser the existing codebase already uses for\nsimilar tasks. If no parser is in use, `linkedom` is the proposed addition \u2014\nsmall, no-DOM-globals footprint, works in Node and browser. Addition of a\nparser dependency is a decision point flagged during implementation planning.\n\n### Deduplication Policy\n\nDeduplication is keyed on Post ref identity, not on URL or guid. If a Post is\nremoved from the Magazine and later re-curated, the agent will re-add it.\nDedup across a Feed's historical sync is out of scope \u2014 `Feed` already\ndeduplicates on `guid` when syncing.\n\n### Relationship to Future Tagging\n\nThe tagging feature is strictly additive to this design:\n\n- `Post.tags` already exists as an optional string array.\n- `FetchArticleContent` is the reusable read-stage for tagging.\n- `MagazineTile` renders a tags row when non-empty \u2014 no UI change needed.\n- A `TagPosts` operation and either an extended prompt in `MagazineSkill`\n or a second `MagazineTagger` skill will complete the feature in a later\n revision.\n\nFurther future work enabled by tags (all out of scope here): tag-filter chips\nin the Magazine toolbar; a \"Find more like this\" action prompting the agent\nto curate Posts with overlapping tags; a \"Suggest feeds\" action proposing new\nFeed URLs based on accumulated tag distribution.\n\n### Out of Scope\n\n- Rendering the full original article body inside the reader (would require\n storing full extracted text; current design stores only the snippet).\n- Scheduled / background Curate runs \u2014 this revision only supports the\n explicit toolbar-triggered flow.\n- Cross-Magazine deduplication \u2014 two Magazines may both curate the same Post.\n- Editing or regenerating a snippet after curation \u2014 re-curation is the path\n to different content.\n";
|
|
17
17
|
|
|
18
18
|
// src/MagazinePlugin.tsx
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../src/MagazinePlugin.tsx", "raw-loader:/
|
|
3
|
+
"sources": ["../../../src/MagazinePlugin.tsx", "raw-loader:/__w/dxos/dxos/packages/plugins/plugin-magazine/PLUGIN.mdl"],
|
|
4
4
|
"sourcesContent": ["//\n// Copyright 2025 DXOS.org\n//\n\nimport { ActivationEvent, Plugin } from '@dxos/app-framework';\nimport { AppActivationEvents, AppPlugin } from '@dxos/app-toolkit';\nimport { Instructions } from '@dxos/compute';\nimport { AttentionEvents } from '@dxos/plugin-attention';\nimport { ClientEvents } from '@dxos/plugin-client';\nimport { StateMap, TagIndex } from '@dxos/schema';\n\nimport {\n AppGraphBuilder,\n CreateObject,\n NavigationResolver,\n OperationHandler,\n ReactSurface,\n RoutineTemplates,\n SkillDefinition,\n} from '#capabilities';\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { Magazine, Subscription } from '#types';\n\n// eslint-disable-next-line import/no-relative-packages\nimport pluginSpec from '../PLUGIN.mdl?raw';\n\nexport const MagazinePlugin = Plugin.define(meta).pipe(\n AppPlugin.addAppGraphModule({\n activatesOn: ActivationEvent.allOf(AppActivationEvents.SetupAppGraph, AttentionEvents.AttentionReady),\n activate: AppGraphBuilder,\n }),\n AppPlugin.addCreateObjectModule({ activate: CreateObject }),\n AppPlugin.addNavigationResolverModule({ activatesOn: ClientEvents.ClientReady, activate: NavigationResolver }),\n AppPlugin.addOperationHandlerModule({ activate: OperationHandler }),\n AppPlugin.addPluginAssetModule({\n asset: { pluginId: meta.profile.key, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },\n }),\n AppPlugin.addSchemaModule({\n schema: [\n Subscription.Subscription,\n Subscription.Post,\n Subscription.PostContent,\n Magazine.Magazine,\n Instructions.Instructions,\n StateMap.StateMap,\n TagIndex.TagIndex,\n ],\n }),\n AppPlugin.addSkillDefinitionModule({ activate: SkillDefinition }),\n AppPlugin.addSurfaceModule({ activate: ReactSurface }),\n AppPlugin.addTranslationsModule({ translations }),\n Plugin.addModule({\n id: 'magazine-automation-templates',\n activatesOn: AppActivationEvents.SetupSchema,\n activate: RoutineTemplates,\n }),\n Plugin.make,\n);\n\nexport default MagazinePlugin;\n", "---\nid: org.dxos.plugin.magazine\nname: MagazinePlugin\nversion: 0.2.0\n---\n\nA Composer plugin for syndicated content. Today it supports RSS and ATproto\nFeeds whose Posts are synced into the space as ECHO objects. This revision\nadds **Magazines** \u2014 agent-curated collections of articles drawn from one or\nmore Feeds and displayed as a Masonry grid. The user describes in natural\nlanguage what content a Magazine should gather; pressing *Curate* triggers\nfeed sync followed by an agent that selects matching articles, fetches their\ntext and hero images, and appends them to the Magazine.\n\nThe design also scaffolds (schema-only) a future tagging feature in which the\nagent assigns tags to curated Posts to enable navigation, filtering, and\nfeed-discovery flows.\n\n## Extensions\n\nThe following extension dialects are used in this document.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n| `service` | `org.dxos.mdl.service@1.0` |\n\n## Types\n\n```mdl\ntype FeedType\n literals: atproto | rss\n```\n\n```mdl\ntype Feed\n desc: >\n A subscription source. Existing type \u2014 documented here for context.\n SyncFeed pulls Posts from the URL into the space.\n fields:\n name?: string\n url?: string\n type?: FeedType\n description?: string\n link?: string\n iconUrl?: string\n cursor?: string # sync state\n feed?: Ref<Queue> # backing ECHO feed\n```\n\n```mdl\ntype Post\n desc: >\n An individual syndicated article. Existing fields are unchanged.\n This revision adds four optional fields populated by the Magazine\n curation flow. All additions are optional \u2014 existing Posts remain valid.\n fields:\n title?: string\n link?: string # URL of the original article\n description?: string # feed-provided summary (as-is)\n author?: string\n published?: string # ISO 8601 publication date\n guid?: string # feed-provided unique id\n\n # --- Additions ---\n snippet?: string # short description-derived preview (~200 chars) for tiles\n content?: string # full article body plain text, populated on first open\n imageUrl?: string # hero image URL extracted from the article page\n readAt?: string # ISO 8601; presence = read, absence = unread\n tags?: string[] # agent-assigned tags (future tagging feature; always [] today)\n```\n\n```mdl\ntype Magazine\n desc: >\n An agent-curated collection of articles drawn from one or more Feeds.\n Curation is driven by a Routine (see MagazineSkill): the Routine's\n editorial instructions describe what content this Magazine should gather,\n and it references the MagazineSkill (the HOW \u2014 tools and output\n contract). The Routine is created lazily on first curation, its\n instructions seeded with the default editorial methodology and the user's\n `topic`. The agent selects matching Posts from the referenced Feeds,\n enriches them with snippet and hero image, and their refs are appended to\n `posts`.\n fields:\n name?: string\n feeds: Ref<Feed>[] # Feeds to pull from\n topic?: string # create-dialog editorial seed; woven into the Routine's instructions on first curate\n routine?: Ref<Routine> # editorial Routine (lazy; created by the curate op); not rendered as form input\n posts: Ref<Post>[] # curated Post refs (insertion order; UI displays newest-last reversed)\n annotations:\n icon: ph--book-open-text--regular\n hue: indigo\n label: name\n note: >\n The Routine is added to its own `objects` (context) so the agent sees the\n Magazine as bound context, not only the candidate JSON `input`.\n```\n\n## Components\n\n```mdl\ncomponent MagazineArticle\n desc: >\n Article surface bound to a Magazine object. Shows curated articles in a\n Masonry grid and offers a Curate button that triggers feed sync plus\n agent curation.\n props:\n magazine: Magazine\n state:\n running: boolean # Curate flow in progress\n phase?: 'syncing' | 'curating' # shown as button tooltip while running\n slots:\n header?: ReactNode\n actions:\n curate() # run syncFeeds \u2192 invoke CurateMagazine\n openPost(post: Post) # sets post.readAt if unset; dispatches useShowItem (plugin-deck)\n emits:\n onCurateComplete(added: number)\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 [header slot] [\u2728 Curate] \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n \u2502 \u2502tile \u2502 \u2502tile \u2502 \u2502tile \u2502 (Masonry grid) \u2502\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n \u2502 \u2502tile \u2502 \u2502tile \u2502 \u2502\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n PostArticle is rendered by the host layout (complementary drawer,\n sibling plank, or deck companion) \u2014 not composed inline.\n```\n\n```mdl\ncomponent MagazineTile\n desc: >\n Single masonry cell rendering a curated Post. Dimmed when post.readAt is set.\n Click opens the ArticleReader. Tags row appears only when tags.length > 0.\n props:\n post: Post\n feed: Feed # resolved parent feed for name/icon\n emits:\n onOpen(post: Post)\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 [hero image if present] \u2502\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Title (2-line clamp) \u2502\n \u2502 Snippet (3-line clamp) \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 Feed \u00B7 Author \u00B7 Date \u2502\n \u2502 [#tag] [#tag] \u2502 (only when tags.length > 0)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n```mdl\ncomponent PostArticle\n desc: >\n Independent article surface for a single Post. Registered as a React\n surface matching `object(Article, Subscription.Post)` so it can render\n in any article/companion context \u2014 the complementary sidebar in\n 'simple' mode, a sibling plank in 'multi' mode, or the deck companion\n in the default 'deck' mode.\n\n Panel-based layout with a toolbar; the \"Open original\" link lives in\n the toolbar so it's always accessible. post.readAt is set by the\n master (MagazineArticle) when the tile is opened, not by PostArticle.\n props:\n post: Post # via Surface data.subject\n layout: |\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Title [Open original] \u2502 \u2190 toolbar\n \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Author \u00B7 Date \u2502\n \u2502 [hero image] \u2502\n \u2502 Snippet \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Services\n\n```mdl\nservice MagazineSkill\n desc: |\n Agent skill referenced by each Magazine's Routine. Registered as a\n SkillDefinition capability. The skill instructions are limited to\n HOW to operate: how to call the FetchArticleContent tool and the\n completeJob output contract. The editorial methodology (what to select,\n dedup, snippet style, hero-image choice) and the magazine's topic live on\n the Routine's instructions, not here.\n key: org.dxos.skill.magazine\n ops:\n # Agent-facing tools exposed by the skill:\n # - FetchArticleContent\n # (completeJob is provided by the agent runtime; SyncFeed / CurateMagazine /\n # ClearMagazine / LoadPostContent are orchestration/UI, not agent tools.)\n note: >\n The curate op resolves this skill from the registry and references it\n from the Magazine's Routine. The agent reads candidate summaries from the\n `input`, MAY call FetchArticleContent to read full text, then calls\n completeJob with the structured selection.\n```\n\n## Operations\n\n```mdl\nop SyncFeed\n desc: Existing operation. Pulls new Posts from a Feed's source URL.\n input:\n feedId: DXN\n output: number # number of Posts added\n effects: [http, echo:write]\n note: Documented here for context; unchanged by this revision.\n```\n\n```mdl\nop ListCandidatePosts\n desc: >\n Returns lightweight summaries of Posts from the Magazine's referenced\n feeds that have NOT yet been added to the Magazine. Excludes Posts whose\n refs already appear in magazine.posts.\n input:\n magazineId: DXN\n output:\n Array<{\n postId: DXN,\n feedName?: string,\n title?: string,\n description?: string,\n author?: string,\n published?: string\n }>\n effects: [echo:read]\n note: >\n Deliberately excludes the full article body and image \u2014 the agent\n decides from summaries whether to call FetchArticleContent.\n```\n\n```mdl\nop FetchArticleContent\n desc: >\n Fetches post.link over HTTP and returns extracted plain text plus any\n image URLs found (og:image and <img> tags). Pure read \u2014 does not mutate\n the Post. Reusable by the future tagging feature.\n input:\n postId: DXN\n output:\n {\n text: string,\n imageUrls: string[]\n }\n errors:\n FetchFailed: HTTP error or network failure\n NoLink: the Post has no `link` field\n effects: [http, echo:read]\n```\n\n```mdl\nop AddPostToMagazine\n desc: >\n Writes snippet and imageUrl onto the Post and appends the Post's ref to\n magazine.posts. Idempotent \u2014 no-op if the Post ref is already present.\n input:\n magazineId: DXN\n postId: DXN\n snippet: string\n imageUrl?: string\n output: Post\n effects: [echo:write]\n```\n\n```mdl\nop CurateMagazine\n desc: >\n Deterministic one-shot curation invoked by the Curate button. For each\n uncurated candidate Post, derives a snippet and hero image from the Post's\n existing `description` (no HTTP) and appends the enriched Post ref to\n `magazine.posts`. Skips Posts with empty descriptions.\n input:\n magazineId: DXN\n output:\n added: number\n effects: [echo:write]\n note: >\n Exists alongside the skill. The deterministic path gives the button\n an immediate, reliable result without needing an LLM. The agent path\n (via the skill's fine-grained tools) delivers instruction-driven,\n higher-quality curation through chat.\n```\n\n## Features\n\n```mdl\nfeat F-1: Magazine Type\n desc: Introduce the Magazine ECHO type with feeds, instructions, and curated posts.\n\n req F-1.1: User can create a Magazine object via the plugin's create menu.\n req F-1.2: Magazine.name is optional; magazine is labeled by name when set.\n req F-1.3: Magazine.feeds is an array of Ref<Feed>; empty by default.\n req F-1.4: Magazine.routine is an optional Ref<Routine>, created lazily on first curate; hidden from the standard form input. Magazine.topic holds the create-dialog editorial seed until the Routine exists.\n req F-1.5: Magazine.posts is an array of Ref<Post>; empty by default; insertion order preserved.\n```\n\n```mdl\nfeat F-2: Post Extensions\n desc: Add optional snippet, imageUrl, readAt, and tags to existing Post type.\n\n req F-2.1: All new fields are optional; existing Post objects load unchanged.\n req F-2.2: snippet is a plain string; no max length enforced at schema level.\n req F-2.3: imageUrl is a plain string URL.\n req F-2.4: readAt is an ISO 8601 timestamp; presence = read, absence = unread.\n req F-2.5: tags is a string array; defaults to empty; not populated in this revision.\n```\n\n```mdl\nfeat F-3: Magazine Curate Flow\n desc: Sync referenced feeds then invoke the agent to curate articles.\n\n req F-3.1: MagazineArticle toolbar includes a Curate button.\n req F-3.2:\n when: user clicks Curate\n then: button shows spinner, is disabled, tooltip shows phase ('Syncing feeds...' / 'Curating articles...')\n req F-3.3:\n when: Curate runs\n then: SyncFeed invoked in parallel for each Ref in magazine.feeds; per-feed failures are collected but do not abort the flow\n req F-3.4:\n when: all feed syncs resolve (success or failure)\n then: CurateMagazine operation is invoked with the Magazine ref\n req F-3.5:\n when: CurateMagazine completes\n then: button returns to idle; reactive UI renders the new tiles\n req F-3.6:\n when: magazine.feeds is empty\n then: Curate button is disabled with tooltip 'Add at least one feed first'\n req F-3.7: Re-clicking Curate while running is a no-op (button disabled; in-memory running flag).\n```\n\n```mdl\nfeat F-4: Agent Curation Behavior\n desc: Agent uses tools to select, fetch, and add Posts.\n\n req F-4.1: Agent reads magazine.instructions Text and magazine.feeds refs from context.\n req F-4.2: Agent calls ListCandidatePosts(magazineId) to receive unenriched candidates.\n req F-4.3: Agent filters candidates against the instructions and skips low-relevance posts.\n req F-4.4: For each selected Post, agent calls FetchArticleContent(postId) to obtain text and images.\n req F-4.5: Agent produces a concise snippet (target ~200 chars) and selects the best image URL.\n req F-4.6: Agent calls AddPostToMagazine(magazineId, postId, snippet, imageUrl).\n req F-4.7: AddPostToMagazine is idempotent; re-invocation with the same postId is a no-op append.\n```\n\n```mdl\nfeat F-5: Masonry Display\n desc: Render curated Posts in a Masonry grid with rich tiles.\n\n req F-5.1: MagazineArticle renders magazine.posts in reverse insertion order (newest first) in a Masonry grid.\n req F-5.2: Each tile shows imageUrl (if present), title, snippet, feed name, author, published date.\n req F-5.3: Tile tags row renders only when post.tags.length > 0.\n req F-5.4: Tiles where post.readAt is set are visually dimmed.\n req F-5.5: Clicking a tile opens the ArticleReader for that Post.\n req F-5.6: Opening the reader sets post.readAt to the current ISO timestamp if not already set.\n req F-5.7: Empty state \u2014 when magazine.posts is empty \u2014 shows: 'No articles yet. Add feeds and press Curate.'\n req F-5.8: Grid uses the @dxos/react-ui-masonry Masonry component.\n```\n\n```mdl\nfeat F-6: Post Article Detail (master-detail via layout dispatch)\n desc: Post detail is rendered by a standalone PostArticle surface. The\n master (MagazineArticle) does not compose the detail inline;\n instead it asks the deck's `useShowItem` to select the post and\n route the detail to the appropriate place for the current\n layout mode (complementary drawer / sibling plank / companion).\n\n req F-6.1: PostArticle is registered as a React surface matching\n `object(Article, Subscription.Post)` \u2014 usable anywhere an\n article for a Post is requested.\n req F-6.2: PostArticle is a Panel with a Toolbar. The 'Open original'\n link lives in the toolbar. Body shows metadata, hero image,\n and snippet.\n req F-6.3:\n when: user clicks a tile in MagazineArticle\n then:\n - post.readAt is set to the current ISO timestamp if unset\n - useShowItem is invoked with:\n contextId = magazine attendableId\n selectionId = post.id\n companion = linkedSegment('post')\n path = getObjectPathFromObject(post)\n req F-6.4:\n when: useShowItem is invoked in 'simple' layout mode\n then: the complementary sidebar opens on linkedSegment('post')\n req F-6.5:\n when: useShowItem is invoked in 'multi' layout mode\n then: a sibling plank opens for the post's path, pivoted on the magazine\n req F-6.6:\n when: useShowItem is invoked in the default ('deck') layout mode\n then: the current plank's companion changes to linkedSegment('post')\n req F-6.7: An app-graph extension under each Magazine node exposes a\n companion node for linkedSegment('post') whose data resolves\n to the currently-selected Post from magazine.posts.\n```\n\n```mdl\nfeat F-7: Future Tagging (schema scaffolding only)\n desc: Schema fields in place so the tagging feature composes without migration.\n\n req F-7.1: Post.tags exists as Schema.Array(Schema.String).pipe(Schema.optional).\n req F-7.2: MagazineTile renders a tags row conditionally on tags.length > 0.\n req F-7.3: FetchArticleContent is reusable as the input stage for a future TagPosts operation.\n note: >\n Later features (out of scope): TagPosts operation, a MagazineTagger skill\n (or extension of MagazineSkill), tag-filter chips in the toolbar,\n 'Find more like this' action, and 'Suggest feeds' action proposing new Feed\n URLs based on accumulated tag distribution.\n```\n\n## Acceptance\n\n```mdl\ntest T-1: Create Magazine\n given: user is in a space in Composer\n when: user creates a Magazine via the plugin's create menu\n then:\n - Magazine object created in space\n - Magazine.feeds is an empty array\n - Magazine.posts is an empty array\n - Magazine.instructions is a Ref to an empty Text object\n```\n\n```mdl\ntest T-2: Post schema backward compatibility\n given: a Post created before this revision exists in the space\n when: the new plugin version loads\n then:\n - Post loads without schema error\n - Post.snippet, Post.imageUrl, Post.readAt, Post.tags are all undefined / empty\n```\n\n```mdl\ntest T-3: Curate button disabled without feeds\n given: a Magazine with magazine.feeds.length === 0\n when: MagazineArticle renders\n then:\n - Curate button is disabled\n - tooltip text === 'Add at least one feed first'\n```\n\n```mdl\ntest T-4: Curate flow runs sync then deterministic curation\n given: a Magazine with two referenced feeds, each with unsynced remote posts\n when: user clicks Curate\n then:\n - SyncFeed invoked once per referenced feed\n - After all SyncFeed calls resolve, CurateMagazine operation is invoked\n - Magazine.posts grows by the count returned from CurateMagazine\n - Button returns to idle\n```\n\n```mdl\ntest T-4a: Agent curation via skill\n given: a chat referencing a Magazine whose MagazineSkill is active\n when: the agent is prompted to curate the magazine\n then:\n - Agent calls ListCandidatePosts, FetchArticleContent, AddPostToMagazine\n - Magazine.posts grows by the agent-selected count\n note: >\n The skill path is distinct from the Curate button (T-4). The button\n runs the deterministic CurateMagazine op; agents invoke the fine-grained\n tools through chat.\n```\n\n```mdl\ntest T-5: Partial feed sync failure is non-fatal\n given: a Magazine with two feeds; one feed's URL returns 500\n when: user clicks Curate\n then:\n - CurateMagazine still runs against posts from the successful feed\n - Error surfaced via a localized message in the status bar\n```\n\n```mdl\ntest T-6: ListCandidatePosts excludes curated\n given: a Magazine whose posts array already contains refs to two Posts from feed A\n when: ListCandidatePosts(magazineId) is called\n then: returned list contains no entries whose postId matches an existing magazine.posts ref\n```\n\n```mdl\ntest T-7: AddPostToMagazine is idempotent\n given: a Magazine already containing a Post ref\n when: AddPostToMagazine invoked with the same postId\n then:\n - magazine.posts length unchanged\n - Post.snippet and Post.imageUrl left unchanged (no overwrite on duplicate)\n```\n\n```mdl\ntest T-8: FetchArticleContent surfaces images\n given: a Post whose link resolves to an HTML page with og:image and two <img> tags\n when: FetchArticleContent(postId) is called\n then:\n - output.text contains the page's main text\n - output.imageUrls includes the og:image URL\n - output.imageUrls includes URLs from <img> tags\n```\n\n```mdl\ntest T-9: Tile dimming on read\n given: a Magazine with one Post where readAt is unset\n when: user clicks the tile\n then:\n - ArticleReader opens for that Post\n - Post.readAt is set to an ISO timestamp\n - On reader close, the tile renders dimmed\n```\n\n```mdl\ntest T-10: Tile metadata\n given: a curated Post with title, snippet, imageUrl, author, published, and a feed with name\n when: MagazineTile renders\n then:\n - hero image rendered from imageUrl\n - title rendered with 2-line clamp\n - snippet rendered with 3-line clamp\n - metadata row shows feed name, author, and a formatted date\n - tags row is NOT rendered (tags is empty)\n```\n\n---\n\n## Design Notes\n\n### Curation via Routine (this revision)\n\nCuration runs a persisted **Routine** rather than an ephemeral one. The Magazine\nno longer carries an `instructions` Template; it carries an optional\n`routine: Ref<Routine>` plus a `topic` seed string.\n\n- **Eager creation.** `Magazine.make()` creates the `Routine` at magazine\n instantiation time. The Routine's `instructions` Text is seeded with the\n default editorial methodology and an optional topic woven into a `## Topic`\n section; it references the MagazineSkill and is parented to the magazine\n (cascade-delete). The magazine is added to the Routine's `objects` (context).\n- **Run.** The curate op invokes `AgentPrompt` (Run Routine) with\n `prompt: magazine.routine` and the candidate JSON `input`; it decodes the\n `completeJob` structured output and applies selections mechanically (keep-bound,\n per-post snippet/imageUrl). No ephemeral routine is constructed.\n- **Instruction split.** Skill = HOW (FetchArticleContent + completeJob\n contract). Routine = editorial (selection criteria, dedup, snippet style,\n hero-image choice) + topic. This removes the prior overlap where the skill\n held both.\n- **Properties editing.** The `routine` field carries `FormInlineAnnotation`,\n so the Routine's own fields (name, description, instructions) render inline in\n the generic `DefaultProperties` companion \u2014 no custom surface needed.\n- **Template.** `Template` is retained (it is the type of `Skill.instructions`\n and is used across the codebase). The Magazine simply stops using it; the\n Routine's `instructions` is a plain `Ref<Text>`.\n\n### Content Flow\n\nTwo curation paths exist side-by-side:\n\n- **Button path (deterministic).** The Curate toolbar button runs `SyncFeed`\n per referenced feed, then invokes `CurateMagazine`. Snippets and images are\n derived from each Post's existing `description` field (no HTTP, no LLM).\n Chosen for the user-initiated button because it is fast, reliable, and does\n not depend on an LLM runtime being present.\n- **Agent path (skill).** The `MagazineSkill` exposes three fine-grained\n tools (`ListCandidatePosts`, `FetchArticleContent`, `AddPostToMagazine`) and\n an instruction template that tells an agent to consult `Magazine.instructions`\n when selecting Posts. Triggered when an agent chat references the Magazine \u2014\n not when the button is clicked. Delivers higher-quality curation (full-article\n fetch + LLM-driven selection) at the cost of latency and LLM availability.\n\n`SyncFeed` is always run deterministically (either by the button or outside\nthe scope of this feature). Neither curation path fetches feed indices on its\nown \u2014 that is `SyncFeed`'s job.\n\n### Tool Granularity\n\nThree tools rather than one combined `curateMagazine` tool:\n\n- `ListCandidatePosts` returns lightweight summaries so the agent can triage\n cheaply without loading article bodies.\n- `FetchArticleContent` is a read-only HTTP fetch + extract; it is reusable by\n the future `TagPosts` operation which will need the same inputs.\n- `AddPostToMagazine` is the sole mutation. Idempotence lives in this tool\n (by ref-set membership check) rather than in the prompt.\n\nQuality of selection is the agent's job; the tools deliberately don't try to\nrank or filter on the agent's behalf.\n\n### Snippet Storage\n\n`Post.snippet` is a plain inline string, not a `Ref<Text>`. Snippets are\nshort (~200 chars target) and read-only to the user; the `Ref<Text>` indirection\nused for `Agent.instructions` and `Magazine.instructions` is appropriate for\nlong-form, user-editable content and is not needed here.\n\n### Read State\n\nRepresented as an optional ISO timestamp `readAt`, not a boolean or enum.\nAbsence = unread; presence = read (at that time). This leaves room to show\n\"read 2 days ago\" or to expire reads without a schema change.\n\n### Image Extraction\n\n`FetchArticleContent` returns a list of candidate image URLs; the agent picks\nthe \"best\" one for the Post's `imageUrl`. The prompt should prefer `og:image`\nwhen present. If no suitable image exists the Post is still added with\n`imageUrl` undefined \u2014 `MagazineTile` renders without the image region.\n\nHTML parsing uses whatever parser the existing codebase already uses for\nsimilar tasks. If no parser is in use, `linkedom` is the proposed addition \u2014\nsmall, no-DOM-globals footprint, works in Node and browser. Addition of a\nparser dependency is a decision point flagged during implementation planning.\n\n### Deduplication Policy\n\nDeduplication is keyed on Post ref identity, not on URL or guid. If a Post is\nremoved from the Magazine and later re-curated, the agent will re-add it.\nDedup across a Feed's historical sync is out of scope \u2014 `Feed` already\ndeduplicates on `guid` when syncing.\n\n### Relationship to Future Tagging\n\nThe tagging feature is strictly additive to this design:\n\n- `Post.tags` already exists as an optional string array.\n- `FetchArticleContent` is the reusable read-stage for tagging.\n- `MagazineTile` renders a tags row when non-empty \u2014 no UI change needed.\n- A `TagPosts` operation and either an extended prompt in `MagazineSkill`\n or a second `MagazineTagger` skill will complete the feature in a later\n revision.\n\nFurther future work enabled by tags (all out of scope here): tag-filter chips\nin the Magazine toolbar; a \"Find more like this\" action prompting the agent\nto curate Posts with overlapping tags; a \"Suggest feeds\" action proposing new\nFeed URLs based on accumulated tag distribution.\n\n### Out of Scope\n\n- Rendering the full original article body inside the reader (would require\n storing full extracted text; current design stores only the snippet).\n- Scheduled / background Curate runs \u2014 this revision only supports the\n explicit toolbar-triggered flow.\n- Cross-Magazine deduplication \u2014 two Magazines may both curate the same Post.\n- Editing or regenerating a snippet after curation \u2014 re-curation is the path\n to different content.\n"],
|
|
5
5
|
"mappings": ";;;AAIA,SAASA,iBAAiBC,cAAc;AACxC,SAASC,qBAAqBC,iBAAiB;AAC/C,SAASC,oBAAoB;AAC7B,SAASC,uBAAuB;AAChC,SAASC,oBAAoB;AAC7B,SAASC,UAAUC,gBAAgB;AAEnC,SACEC,iBACAC,cACAC,oBACAC,kBACAC,cACAC,kBACAC,uBACK;AACP,SAASC,YAAY;AACrB,SAASC,oBAAoB;AAC7B,SAASC,UAAUC,oBAAoB;;;ACtBvC;;;AD2BO,IAAMC,iBAAiBC,OAAOC,OAAOC,IAAAA,EAAMC,KAChDC,UAAUC,kBAAkB;EAC1BC,aAAaC,gBAAgBC,MAAMC,oBAAoBC,eAAeC,gBAAgBC,cAAc;EACpGC,UAAUC;AACZ,CAAA,GACAV,UAAUW,sBAAsB;EAAEF,UAAUG;AAAa,CAAA,GACzDZ,UAAUa,4BAA4B;EAAEX,aAAaY,aAAaC;EAAaN,UAAUO;AAAmB,CAAA,GAC5GhB,UAAUiB,0BAA0B;EAAER,UAAUS;AAAiB,CAAA,GACjElB,UAAUmB,qBAAqB;EAC7BC,OAAO;IAAEC,UAAUvB,KAAKwB,QAAQC;IAAKC,MAAM;IAAcC,SAASC;IAAYC,UAAU;EAAoB;AAC9G,CAAA,GACA3B,UAAU4B,gBAAgB;EACxBC,QAAQ;IACNC,aAAaA;IACbA,aAAaC;IACbD,aAAaE;IACbC,SAASA;IACTC,aAAaA;IACbC,SAASA;IACTC,SAASA;;AAEb,CAAA,GACApC,UAAUqC,yBAAyB;EAAE5B,UAAU6B;AAAgB,CAAA,GAC/DtC,UAAUuC,iBAAiB;EAAE9B,UAAU+B;AAAa,CAAA,GACpDxC,UAAUyC,sBAAsB;EAAEC;AAAa,CAAA,GAC/C9C,OAAO+C,UAAU;EACfC,IAAI;EACJ1C,aAAaG,oBAAoBwC;EACjCpC,UAAUqC;AACZ,CAAA,GACAlD,OAAOmD,IAAI;AAGb,IAAA,yBAAepD;",
|
|
6
6
|
"names": ["ActivationEvent", "Plugin", "AppActivationEvents", "AppPlugin", "Instructions", "AttentionEvents", "ClientEvents", "StateMap", "TagIndex", "AppGraphBuilder", "CreateObject", "NavigationResolver", "OperationHandler", "ReactSurface", "RoutineTemplates", "SkillDefinition", "meta", "translations", "Magazine", "Subscription", "MagazinePlugin", "Plugin", "define", "meta", "pipe", "AppPlugin", "addAppGraphModule", "activatesOn", "ActivationEvent", "allOf", "AppActivationEvents", "SetupAppGraph", "AttentionEvents", "AttentionReady", "activate", "AppGraphBuilder", "addCreateObjectModule", "CreateObject", "addNavigationResolverModule", "ClientEvents", "ClientReady", "NavigationResolver", "addOperationHandlerModule", "OperationHandler", "addPluginAssetModule", "asset", "pluginId", "profile", "key", "path", "content", "pluginSpec", "mimeType", "addSchemaModule", "schema", "Subscription", "Post", "PostContent", "Magazine", "Instructions", "StateMap", "TagIndex", "addSkillDefinitionModule", "SkillDefinition", "addSurfaceModule", "ReactSurface", "addTranslationsModule", "translations", "addModule", "id", "SetupSchema", "RoutineTemplates", "make"]
|
|
@@ -98,7 +98,7 @@ var PostToolbar = ({ post, refreshingAtom, attendableId, onSetStarred, onSetArch
|
|
|
98
98
|
PostToolbar.displayName = "PostToolbar";
|
|
99
99
|
|
|
100
100
|
// src/containers/PostArticle/PostArticle.tsx
|
|
101
|
-
var __dxlog_file = "/
|
|
101
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/containers/PostArticle/PostArticle.tsx";
|
|
102
102
|
var PostArticle = ({ role, subject, attendableId }) => {
|
|
103
103
|
const toolbarAttendableId = attendableId && isLinkedSegment(attendableId) ? getParentId(attendableId) : attendableId;
|
|
104
104
|
const { invokePromise } = useOperationInvoker();
|
|
@@ -227,4 +227,4 @@ var PostArticle = ({ role, subject, attendableId }) => {
|
|
|
227
227
|
export {
|
|
228
228
|
PostArticle as default
|
|
229
229
|
};
|
|
230
|
-
//# sourceMappingURL=PostArticle-
|
|
230
|
+
//# sourceMappingURL=PostArticle-BGIPU7XA.mjs.map
|
|
@@ -3,7 +3,7 @@ import "../chunk-J5LGTIGS.mjs";
|
|
|
3
3
|
// src/capabilities/index.ts
|
|
4
4
|
import { Capability } from "@dxos/app-framework";
|
|
5
5
|
var AppGraphBuilder = Capability.lazy("AppGraphBuilder", () => import("../app-graph-builder-JDPRBVHL.mjs"));
|
|
6
|
-
var RoutineTemplates = Capability.lazy("RoutineTemplates", () => import("../routine-templates-
|
|
6
|
+
var RoutineTemplates = Capability.lazy("RoutineTemplates", () => import("../routine-templates-D5A2L4KL.mjs"));
|
|
7
7
|
var SkillDefinition = Capability.lazy("SkillDefinition", () => import("../skill-definition-3KLMUEC5.mjs"));
|
|
8
8
|
var CreateObject = Capability.lazy("CreateObject", () => import("../create-object-SYZMRR4H.mjs"));
|
|
9
9
|
var OperationHandler = Capability.lazy("OperationHandler", () => import("../operation-handler-JE3743BU.mjs"));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/operations/index.ts
|
|
2
2
|
import { OperationHandlerSet } from "@dxos/compute";
|
|
3
|
-
var MagazineOperationHandlerSet = OperationHandlerSet.lazy(() => import("./clear-magazine-KNY4OWY4.mjs"), () => import("./curate-magazine-
|
|
3
|
+
var MagazineOperationHandlerSet = OperationHandlerSet.lazy(() => import("./clear-magazine-KNY4OWY4.mjs"), () => import("./curate-magazine-7RMKSJC5.mjs"), () => import("./fetch-article-content-CXPKXIMR.mjs"), () => import("./load-post-content-H3S4TKVM.mjs"), () => import("./sync-feed-SDXYF7HU.mjs"));
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
MagazineOperationHandlerSet
|
|
7
7
|
};
|
|
8
|
-
//# sourceMappingURL=chunk-
|
|
8
|
+
//# sourceMappingURL=chunk-LFSQMLPK.mjs.map
|
|
@@ -4,8 +4,8 @@ import "../chunk-J5LGTIGS.mjs";
|
|
|
4
4
|
import { lazy } from "react";
|
|
5
5
|
var FeedArticle = lazy(() => import("../FeedArticle-PEWCFFSP.mjs"));
|
|
6
6
|
var FeedProperties = lazy(() => import("../FeedProperties-EIO7IZOA.mjs"));
|
|
7
|
-
var MagazineArticle = lazy(() => import("../MagazineArticle-
|
|
8
|
-
var PostArticle = lazy(() => import("../PostArticle-
|
|
7
|
+
var MagazineArticle = lazy(() => import("../MagazineArticle-5T7RTIPS.mjs"));
|
|
8
|
+
var PostArticle = lazy(() => import("../PostArticle-BGIPU7XA.mjs"));
|
|
9
9
|
var PostCard = lazy(() => import("../PostCard-VDGN7BBA.mjs"));
|
|
10
10
|
export {
|
|
11
11
|
FeedArticle,
|
|
@@ -66,7 +66,7 @@ var partitionByKeepBound = (posts, keep, isStarred = () => false) => {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
// src/operations/curate-magazine.ts
|
|
69
|
-
var __dxlog_file = "/
|
|
69
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/operations/curate-magazine.ts";
|
|
70
70
|
var curate_magazine_default = FeedOperation_exports.CurateMagazine.pipe(Operation.withHandler(Effect2.fnUntraced(function* ({ magazine: magazineRef }) {
|
|
71
71
|
const magazine = yield* Effect2.promise(() => magazineRef.load());
|
|
72
72
|
const validFeeds = yield* loadValidFeeds(magazine);
|
|
@@ -181,4 +181,4 @@ export {
|
|
|
181
181
|
curate_magazine_default as default,
|
|
182
182
|
resolveSelected
|
|
183
183
|
};
|
|
184
|
-
//# sourceMappingURL=curate-magazine-
|
|
184
|
+
//# sourceMappingURL=curate-magazine-7RMKSJC5.mjs.map
|
package/dist/lib/neutral/{fetch-article-content-CZO6D3QD.mjs → fetch-article-content-CXPKXIMR.mjs}
RENAMED
|
@@ -13,7 +13,7 @@ import * as Effect from "effect/Effect";
|
|
|
13
13
|
import { Operation } from "@dxos/compute";
|
|
14
14
|
import { Database } from "@dxos/echo";
|
|
15
15
|
import { invariant } from "@dxos/invariant";
|
|
16
|
-
var __dxlog_file = "/
|
|
16
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/operations/fetch-article-content.ts";
|
|
17
17
|
var handler = FeedOperation_exports.FetchArticleContent.pipe(Operation.withHandler(Effect.fn(function* ({ post: postRef }) {
|
|
18
18
|
const post = yield* Database.load(postRef);
|
|
19
19
|
invariant(post.link, "Post has no link.", { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 12, S: this, A: ["post.link", "'Post has no link.'"] });
|
|
@@ -28,4 +28,4 @@ var fetch_article_content_default = handler;
|
|
|
28
28
|
export {
|
|
29
29
|
fetch_article_content_default as default
|
|
30
30
|
};
|
|
31
|
-
//# sourceMappingURL=fetch-article-content-
|
|
31
|
+
//# sourceMappingURL=fetch-article-content-CXPKXIMR.mjs.map
|
|
@@ -17,7 +17,7 @@ import * as Effect from "effect/Effect";
|
|
|
17
17
|
import { getSpace } from "@dxos/client/echo";
|
|
18
18
|
import { Operation } from "@dxos/compute";
|
|
19
19
|
import { invariant } from "@dxos/invariant";
|
|
20
|
-
var __dxlog_file = "/
|
|
20
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/operations/load-post-content.ts";
|
|
21
21
|
var load_post_content_default = FeedOperation_exports.LoadPostContent.pipe(Operation.withHandler(Effect.fn(function* ({ post: postRef, force }) {
|
|
22
22
|
const post = postRef.target;
|
|
23
23
|
invariant(post, "Post ref target not loaded.", { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 19, S: this, A: ["post", "'Post ref target not loaded.'"] });
|
|
@@ -62,4 +62,4 @@ var load_post_content_default = FeedOperation_exports.LoadPostContent.pipe(Opera
|
|
|
62
62
|
export {
|
|
63
63
|
load_post_content_default as default
|
|
64
64
|
};
|
|
65
|
-
//# sourceMappingURL=load-post-content-
|
|
65
|
+
//# sourceMappingURL=load-post-content-H3S4TKVM.mjs.map
|