mulmoclaude 0.6.4 → 0.7.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.
Files changed (302) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/{html2canvas-CDGcmOD3-XVrO-eyz.js → html2canvas-CDGcmOD3-CKJ6vKPo.js} +1 -1
  3. package/client/assets/{index-zZIqEbNX.js → index-BG_JJcKI.js} +193 -197
  4. package/client/assets/index-DCoo3kpR.css +2 -0
  5. package/client/assets/{index.es-DqtpmBm8-DHT6q10o.js → index.es-DqtpmBm8-DFXjJgCa.js} +1 -1
  6. package/client/assets/lib-Dpph7PBN.js +114 -0
  7. package/client/assets/marp-cCGismx0.js +3452 -0
  8. package/client/assets/schemas-DuYzyHQc.js +64 -0
  9. package/client/index.html +5 -4
  10. package/package.json +7 -5
  11. package/server/agent/backend/claude-code.ts +44 -10
  12. package/server/agent/backend/fake-echo.ts +5 -1
  13. package/server/agent/config.ts +49 -12
  14. package/server/agent/mcp-server.ts +13 -2
  15. package/server/agent/mcp-tools/handlePermission.ts +115 -0
  16. package/server/agent/mcp-tools/index.ts +2 -1
  17. package/server/agent/mcp-tools/x.ts +18 -2
  18. package/server/agent/prompt.ts +3 -45
  19. package/server/api/csrfGuard.ts +122 -32
  20. package/server/api/routes/collections.ts +320 -0
  21. package/server/api/routes/config.ts +45 -0
  22. package/server/api/routes/feeds.ts +70 -0
  23. package/server/api/routes/files.ts +167 -0
  24. package/server/api/routes/marp-themes.ts +15 -0
  25. package/server/api/routes/mulmo-script.ts +139 -0
  26. package/server/api/routes/pdf.ts +108 -0
  27. package/server/api/routes/plugins.ts +10 -0
  28. package/server/api/routes/shortcuts.ts +49 -0
  29. package/server/api/routes/wiki.ts +35 -0
  30. package/server/build/dispatcher.mjs +40 -22
  31. package/server/events/notifications.ts +14 -9
  32. package/server/index.ts +49 -14
  33. package/server/plugins/preset-list.ts +18 -10
  34. package/server/plugins/preset-loader.ts +11 -4
  35. package/server/prompts/index.ts +1 -3
  36. package/server/prompts/system/system.md +7 -2
  37. package/server/system/env.ts +14 -0
  38. package/server/utils/clientDir.ts +7 -0
  39. package/server/utils/files/index.ts +0 -2
  40. package/server/utils/files/shortcuts-io.ts +63 -0
  41. package/server/utils/slug.ts +3 -4
  42. package/server/workspace/billing-migration.ts +69 -0
  43. package/server/workspace/collections/delete.ts +186 -0
  44. package/server/workspace/collections/discovery.ts +730 -0
  45. package/server/workspace/collections/index.ts +23 -0
  46. package/server/workspace/collections/io.ts +287 -0
  47. package/server/workspace/collections/notifications.ts +404 -0
  48. package/server/workspace/collections/paths.ts +125 -0
  49. package/server/workspace/collections/spawn.ts +213 -0
  50. package/server/workspace/collections/templatePath.ts +36 -0
  51. package/server/workspace/collections/types.ts +334 -0
  52. package/server/workspace/collections/watcher.ts +398 -0
  53. package/server/workspace/feeds/engine.ts +135 -0
  54. package/server/workspace/feeds/fetch/httpClient.ts +127 -0
  55. package/server/workspace/feeds/fetch/rssParser.ts +117 -0
  56. package/server/workspace/feeds/index.ts +8 -0
  57. package/server/workspace/feeds/ingestTypes.ts +66 -0
  58. package/server/workspace/feeds/pathResolver.ts +74 -0
  59. package/server/workspace/feeds/paths.ts +30 -0
  60. package/server/workspace/feeds/projectItem.ts +92 -0
  61. package/server/workspace/feeds/registry.ts +43 -0
  62. package/server/workspace/feeds/retrievers/httpJson.ts +19 -0
  63. package/server/workspace/feeds/retrievers/index.ts +27 -0
  64. package/server/workspace/feeds/retrievers/registerAll.ts +5 -0
  65. package/server/workspace/feeds/retrievers/rss.ts +24 -0
  66. package/server/workspace/feeds/state.ts +55 -0
  67. package/server/workspace/helps/billing-clients-worklog.md +215 -0
  68. package/server/workspace/helps/billing-invoice.md +457 -0
  69. package/server/workspace/helps/collection-skills.md +664 -0
  70. package/server/workspace/helps/feeds.md +110 -0
  71. package/server/workspace/helps/index.md +9 -3
  72. package/server/workspace/helps/portfolio-tracker.md +211 -0
  73. package/server/workspace/helps/presentation-deck.md +828 -0
  74. package/server/workspace/helps/todo-collection.md +140 -0
  75. package/server/workspace/helps/vocabulary.md +106 -0
  76. package/server/workspace/hooks/handlers/skillBridge.ts +101 -40
  77. package/server/workspace/marp-themes.ts +46 -0
  78. package/server/workspace/paths.ts +46 -11
  79. package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +13 -0
  80. package/server/workspace/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
  81. package/server/workspace/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
  82. package/server/workspace/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
  83. package/server/workspace/skills-preset/mc-wiki-promote/SKILL.md +175 -0
  84. package/server/workspace/skills-preset.ts +376 -2
  85. package/server/workspace/wiki-pages/io.ts +34 -2
  86. package/server/workspace/workspace.ts +20 -1
  87. package/src/App.vue +70 -41
  88. package/src/components/BackendOfflineBanner.vue +56 -0
  89. package/src/components/ChatInput.vue +72 -6
  90. package/src/components/CollectionCalendarView.vue +243 -0
  91. package/src/components/CollectionDashboardView.vue +181 -0
  92. package/src/components/CollectionDayView.vue +308 -0
  93. package/src/components/CollectionEmbedView.vue +69 -0
  94. package/src/components/CollectionKanbanView.vue +196 -0
  95. package/src/components/CollectionRecordModal.vue +93 -0
  96. package/src/components/CollectionRecordPanel.vue +567 -0
  97. package/src/components/CollectionView.vue +1748 -0
  98. package/src/components/CollectionsIndexView.vue +152 -0
  99. package/src/components/ConfirmModal.vue +344 -0
  100. package/src/components/FeedsView.vue +225 -0
  101. package/src/components/FileContentRenderer.vue +122 -30
  102. package/src/components/FileTree.vue +218 -2
  103. package/src/components/FileTreePane.vue +2 -0
  104. package/src/components/FilesView.vue +95 -17
  105. package/src/components/PinToggle.vue +52 -0
  106. package/src/components/PluginLauncher.vue +97 -37
  107. package/src/components/RightSidebar.vue +74 -3
  108. package/src/components/RolesView.vue +1 -1
  109. package/src/components/SettingsModal.vue +146 -72
  110. package/src/components/SlashCommandMenu.vue +56 -0
  111. package/src/components/StackView.vue +128 -48
  112. package/src/components/collectionEmbed.ts +29 -0
  113. package/src/components/collectionTypes.ts +177 -0
  114. package/src/composables/collections/useCollectionRendering.ts +350 -0
  115. package/src/composables/useConfirm.ts +70 -0
  116. package/src/composables/useFileTree.ts +35 -3
  117. package/src/composables/useImeAwareEnter.ts +14 -0
  118. package/src/composables/usePdfDownload.ts +6 -1
  119. package/src/composables/useShortcuts.ts +163 -0
  120. package/src/composables/useSlashCommandMenu.ts +138 -0
  121. package/src/config/apiRoutes.ts +46 -13
  122. package/src/config/createFilePolicy.ts +82 -0
  123. package/src/config/roles.ts +46 -47
  124. package/src/config/systemFileDescriptors.ts +0 -30
  125. package/src/config/toolNames.ts +1 -5
  126. package/src/config/workspacePaths.ts +4 -9
  127. package/src/lang/de.ts +154 -221
  128. package/src/lang/en.ts +153 -218
  129. package/src/lang/es.ts +154 -219
  130. package/src/lang/fr.ts +155 -221
  131. package/src/lang/index.ts +55 -0
  132. package/src/lang/ja.ts +153 -219
  133. package/src/lang/ko.ts +153 -218
  134. package/src/lang/pt-BR.ts +154 -219
  135. package/src/lang/zh.ts +152 -218
  136. package/src/lib/vue-i18n.ts +15 -45
  137. package/src/lib/wiki-page/graph.ts +108 -0
  138. package/src/main.ts +2 -5
  139. package/src/plugins/_generated/metas.ts +2 -8
  140. package/src/plugins/_generated/registrations.ts +2 -4
  141. package/src/plugins/_generated/server-bindings.ts +5 -12
  142. package/src/plugins/manageSkills/View.vue +36 -68
  143. package/src/plugins/manageSkills/presetDetection.ts +25 -0
  144. package/src/plugins/markdown/MarpView.vue +301 -0
  145. package/src/plugins/markdown/Preview.vue +26 -3
  146. package/src/plugins/markdown/View.vue +230 -1
  147. package/src/plugins/markdown/definition.ts +21 -1
  148. package/src/plugins/presentCollection/Preview.vue +30 -0
  149. package/src/plugins/presentCollection/View.vue +78 -0
  150. package/src/plugins/presentCollection/definition.ts +30 -0
  151. package/src/plugins/presentCollection/index.ts +25 -0
  152. package/src/plugins/presentCollection/meta.ts +15 -0
  153. package/src/plugins/presentCollection/plugin.ts +39 -0
  154. package/src/plugins/presentCollection/types.ts +13 -0
  155. package/src/plugins/presentForm/View.vue +56 -6
  156. package/src/plugins/presentForm/types.ts +3 -3
  157. package/src/plugins/presentMulmoScript/View.vue +252 -5
  158. package/src/plugins/presentMulmoScript/helpers.ts +18 -0
  159. package/src/plugins/presentMulmoScript/meta.ts +11 -0
  160. package/src/plugins/scheduler/AutomationsView.vue +13 -11
  161. package/src/plugins/scheduler/automationsDefinition.ts +7 -10
  162. package/src/plugins/scheduler/automationsMeta.ts +27 -0
  163. package/src/plugins/scheduler/index.ts +19 -38
  164. package/src/plugins/wiki/View.vue +120 -27
  165. package/src/plugins/wiki/components/WikiGraphView.vue +75 -0
  166. package/src/plugins/wiki/components/WikiPageBody.vue +18 -0
  167. package/src/plugins/wiki/index.ts +6 -0
  168. package/src/plugins/wiki/route.ts +8 -1
  169. package/src/router/index.ts +31 -34
  170. package/src/router/pageRoutes.ts +2 -7
  171. package/src/tools/types.ts +4 -3
  172. package/src/types/notification.ts +5 -9
  173. package/src/types/session.ts +11 -0
  174. package/src/types/shortcuts.ts +37 -0
  175. package/src/utils/agent/eventDispatch.ts +4 -0
  176. package/src/utils/api.ts +47 -1
  177. package/src/utils/canvas/stackGrouping.ts +96 -0
  178. package/src/utils/chat/permalink.ts +20 -0
  179. package/src/utils/collections/actionVisible.ts +55 -0
  180. package/src/utils/collections/calendarGrid.ts +328 -0
  181. package/src/utils/collections/collectionViewMode.ts +42 -0
  182. package/src/utils/collections/derivedFormula.ts +364 -0
  183. package/src/utils/collections/draft.ts +160 -0
  184. package/src/utils/collections/enumColors.ts +130 -0
  185. package/src/utils/collections/itemLabel.ts +42 -0
  186. package/src/utils/confirmDelete.ts +13 -0
  187. package/src/utils/markdown/marpAspect.ts +28 -0
  188. package/src/utils/markdown/marpCustomSize.ts +120 -0
  189. package/src/utils/markdown/marpDetect.ts +15 -0
  190. package/src/utils/markdown/marpTheme.ts +109 -0
  191. package/src/utils/markdown/wikiEmbedHandlers.ts +1 -1
  192. package/src/utils/path/workspaceLinkRouter.ts +126 -9
  193. package/src/utils/session/sessionEntries.ts +1 -0
  194. package/src/utils/session/sessionFactory.ts +1 -0
  195. package/src/utils/session/sessionHelpers.ts +6 -1
  196. package/client/assets/index-CyBr8Mkr.css +0 -2
  197. package/server/api/routes/encore.ts +0 -55
  198. package/server/api/routes/news.ts +0 -133
  199. package/server/api/routes/sources.ts +0 -550
  200. package/server/encore/INVARIANTS.md +0 -272
  201. package/server/encore/boot.ts +0 -39
  202. package/server/encore/closure.ts +0 -36
  203. package/server/encore/cycle.ts +0 -276
  204. package/server/encore/dispatch.ts +0 -103
  205. package/server/encore/handlers/amend.ts +0 -99
  206. package/server/encore/handlers/appendNote.ts +0 -74
  207. package/server/encore/handlers/defineEncore.ts +0 -42
  208. package/server/encore/handlers/listTickets.ts +0 -107
  209. package/server/encore/handlers/markStepDone.ts +0 -41
  210. package/server/encore/handlers/markTargetSkipped.ts +0 -33
  211. package/server/encore/handlers/query.ts +0 -138
  212. package/server/encore/handlers/recordValues.ts +0 -44
  213. package/server/encore/handlers/resolveNotification.ts +0 -121
  214. package/server/encore/handlers/setup.ts +0 -81
  215. package/server/encore/handlers/shared.ts +0 -137
  216. package/server/encore/handlers/snooze.ts +0 -87
  217. package/server/encore/handlers/startObligationChat.ts +0 -64
  218. package/server/encore/handlers/startSetupChat.ts +0 -50
  219. package/server/encore/lock.ts +0 -61
  220. package/server/encore/notifier.ts +0 -123
  221. package/server/encore/obligation.ts +0 -25
  222. package/server/encore/paths.ts +0 -78
  223. package/server/encore/reconcile.ts +0 -661
  224. package/server/encore/tick.ts +0 -191
  225. package/server/encore/yaml-fm.ts +0 -63
  226. package/server/prompts/system/news-concierge.md +0 -24
  227. package/server/prompts/system/sources-context.md +0 -16
  228. package/server/utils/files/encore-io.ts +0 -111
  229. package/server/workspace/helps/encore-dsl.md +0 -482
  230. package/server/workspace/helps/sources.md +0 -42
  231. package/server/workspace/news/reader.ts +0 -247
  232. package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +0 -106
  233. package/server/workspace/sources/arxivDiscovery.ts +0 -182
  234. package/server/workspace/sources/classifier.ts +0 -268
  235. package/server/workspace/sources/fetchers/arxiv.ts +0 -170
  236. package/server/workspace/sources/fetchers/github.ts +0 -106
  237. package/server/workspace/sources/fetchers/githubIssues.ts +0 -210
  238. package/server/workspace/sources/fetchers/githubReleases.ts +0 -186
  239. package/server/workspace/sources/fetchers/index.ts +0 -71
  240. package/server/workspace/sources/fetchers/registerAll.ts +0 -15
  241. package/server/workspace/sources/fetchers/rss.ts +0 -141
  242. package/server/workspace/sources/fetchers/rssParser.ts +0 -295
  243. package/server/workspace/sources/httpFetcher.ts +0 -230
  244. package/server/workspace/sources/interests.ts +0 -120
  245. package/server/workspace/sources/paths.ts +0 -110
  246. package/server/workspace/sources/pipeline/dedup.ts +0 -60
  247. package/server/workspace/sources/pipeline/fetch.ts +0 -182
  248. package/server/workspace/sources/pipeline/index.ts +0 -301
  249. package/server/workspace/sources/pipeline/notify.ts +0 -80
  250. package/server/workspace/sources/pipeline/plan.ts +0 -68
  251. package/server/workspace/sources/pipeline/summarize.ts +0 -189
  252. package/server/workspace/sources/pipeline/write.ts +0 -185
  253. package/server/workspace/sources/rateLimiter.ts +0 -148
  254. package/server/workspace/sources/registry.ts +0 -304
  255. package/server/workspace/sources/robots.ts +0 -271
  256. package/server/workspace/sources/sourceState.ts +0 -142
  257. package/server/workspace/sources/taxonomy.ts +0 -74
  258. package/server/workspace/sources/types.ts +0 -153
  259. package/server/workspace/sources/urls.ts +0 -112
  260. package/src/components/NewsView.vue +0 -267
  261. package/src/components/SourcesManager.vue +0 -915
  262. package/src/components/SourcesView.vue +0 -45
  263. package/src/components/TodoExplorer.vue +0 -423
  264. package/src/components/todo/TodoAddDialog.vue +0 -135
  265. package/src/components/todo/TodoEditDialog.vue +0 -51
  266. package/src/components/todo/TodoEditPanel.vue +0 -117
  267. package/src/components/todo/TodoKanbanView.vue +0 -290
  268. package/src/components/todo/TodoListView.vue +0 -88
  269. package/src/components/todo/TodoTableView.vue +0 -210
  270. package/src/composables/useNewsItems.ts +0 -38
  271. package/src/composables/useNewsReadState.ts +0 -75
  272. package/src/plugins/encore/EncoreDashboard.vue +0 -504
  273. package/src/plugins/encore/EncoreRedirect.vue +0 -116
  274. package/src/plugins/encore/View.vue +0 -36
  275. package/src/plugins/encore/defineEncoreDefinition.ts +0 -74
  276. package/src/plugins/encore/defineEncoreMeta.ts +0 -13
  277. package/src/plugins/encore/index.ts +0 -93
  278. package/src/plugins/encore/manageEncoreDefinition.ts +0 -100
  279. package/src/plugins/encore/manageEncoreMeta.ts +0 -36
  280. package/src/plugins/manageSource/Preview.vue +0 -33
  281. package/src/plugins/manageSource/View.vue +0 -13
  282. package/src/plugins/manageSource/definition.ts +0 -66
  283. package/src/plugins/manageSource/index.ts +0 -75
  284. package/src/plugins/manageSource/meta.ts +0 -21
  285. package/src/plugins/scheduler/CalendarView.vue +0 -23
  286. package/src/plugins/scheduler/Preview.vue +0 -73
  287. package/src/plugins/scheduler/View.vue +0 -608
  288. package/src/plugins/scheduler/calendarDefinition.ts +0 -47
  289. package/src/plugins/scheduler/calendarMeta.ts +0 -28
  290. package/src/plugins/scheduler/multiDayHelpers.ts +0 -95
  291. package/src/plugins/scheduler/viewModes.ts +0 -26
  292. package/src/types/encore-dsl/at-expression.ts +0 -120
  293. package/src/types/encore-dsl/at-resolver.ts +0 -32
  294. package/src/types/encore-dsl/cadence.ts +0 -289
  295. package/src/types/encore-dsl/schema.ts +0 -288
  296. package/src/utils/filesPreview/schedulerPreview.ts +0 -44
  297. package/src/utils/filesPreview/todoPreview.ts +0 -51
  298. package/src/utils/sources/filter.ts +0 -69
  299. /package/client/assets/{JsonEditor-D6WBWLoa.js → JsonEditor-C_RDoefj.js} +0 -0
  300. /package/client/assets/{chunk-D8eiyYIV-LcKZGJv5.js → chunk-D8eiyYIV-BY16KEZc.js} +0 -0
  301. /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-BufT4RJl.js} +0 -0
  302. /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-z2wCIsir.js} +0 -0
@@ -1,267 +0,0 @@
1
- <template>
2
- <div class="h-full flex flex-col bg-white" data-testid="news-view">
3
- <!-- Header: title + filter chips + actions. -->
4
- <div class="px-3 py-2 border-b border-gray-200 flex flex-wrap items-center gap-2 shrink-0">
5
- <h1 class="text-base font-semibold text-gray-900 mr-3">{{ t("pluginNews.title") }}</h1>
6
- <span class="text-xs text-gray-500" data-testid="news-counts">{{
7
- t("pluginNews.itemCount", {
8
- unread: unreadCount,
9
- total: items.length,
10
- })
11
- }}</span>
12
- <div class="ml-auto flex items-center gap-2">
13
- <div class="flex border border-gray-300 rounded overflow-hidden" role="tablist">
14
- <button
15
- v-for="filter in readFilterChoices"
16
- :key="filter.value"
17
- :class="[
18
- 'h-8 px-2.5 flex items-center gap-1 text-sm transition-colors',
19
- readFilter === filter.value ? 'bg-blue-50 text-blue-600 font-medium' : 'bg-white text-gray-600 hover:bg-gray-50',
20
- ]"
21
- :data-testid="`news-filter-${filter.value}`"
22
- :aria-pressed="readFilter === filter.value"
23
- @click="readFilter = filter.value"
24
- >
25
- {{ filter.label }}
26
- </button>
27
- </div>
28
- <button
29
- class="h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
30
- :disabled="unreadCount === 0"
31
- data-testid="news-mark-all-read"
32
- @click="markAllReadNow"
33
- >
34
- {{ t("pluginNews.markAllRead") }}
35
- </button>
36
- </div>
37
- </div>
38
-
39
- <!-- Source filter chip row (only sources with items). -->
40
- <div v-if="sourceChoices.length > 1" class="px-3 py-2 border-b border-gray-100 flex flex-wrap items-center gap-1 shrink-0">
41
- <FilterChip
42
- v-for="choice in sourceChoices"
43
- :key="choice.slug"
44
- :active="sourceFilter === choice.slug"
45
- :label="choice.label"
46
- :count="choice.count"
47
- :data-testid="`news-source-${choice.slug}`"
48
- @click="sourceFilter = choice.slug"
49
- />
50
- </div>
51
-
52
- <!-- Body: list (left) + detail (right). -->
53
- <div class="flex-1 min-h-0 flex">
54
- <!-- List pane -->
55
- <div class="w-80 shrink-0 border-r border-gray-200 overflow-y-auto" data-testid="news-list">
56
- <div v-if="loading" class="p-4 text-sm text-gray-400">{{ t("common.loading") }}</div>
57
- <div v-else-if="error" class="p-4 text-sm text-red-600 bg-red-50" role="alert">
58
- {{ t("pluginNews.loadError", { error }) }}
59
- </div>
60
- <div v-else-if="visibleItems.length === 0" class="p-4 text-sm text-gray-400">{{ t("pluginNews.empty") }}</div>
61
- <ul v-else class="divide-y divide-gray-100">
62
- <li
63
- v-for="item in visibleItems"
64
- :key="item.id"
65
- :class="['px-3 py-2 cursor-pointer', selectedId === item.id ? 'bg-blue-50' : 'hover:bg-gray-50']"
66
- :data-testid="`news-item-${item.id}`"
67
- @click="selectItem(item.id)"
68
- >
69
- <div class="flex items-start gap-2">
70
- <span
71
- v-if="!isRead(item.id)"
72
- class="mt-1 w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0"
73
- :title="t('pluginNews.unread')"
74
- :aria-label="t('pluginNews.unread')"
75
- />
76
- <div class="min-w-0 flex-1">
77
- <div :class="['text-sm leading-snug', isRead(item.id) ? 'text-gray-500' : 'text-gray-900 font-medium']">
78
- {{ item.title }}
79
- </div>
80
- <div class="mt-0.5 flex items-center gap-2 text-[11px] text-gray-500">
81
- <span class="truncate">{{ item.sourceSlug }}</span>
82
- <span>{{ formatSmartTime(item.publishedAt) }}</span>
83
- </div>
84
- </div>
85
- </div>
86
- </li>
87
- </ul>
88
- </div>
89
-
90
- <!-- Detail pane -->
91
- <div class="flex-1 min-w-0 flex flex-col" data-testid="news-detail">
92
- <div v-if="!selected" class="flex-1 flex items-center justify-center text-sm text-gray-400">
93
- {{ t("pluginNews.selectPrompt") }}
94
- </div>
95
- <template v-else>
96
- <div class="flex-1 min-h-0 overflow-y-auto">
97
- <div class="px-6 py-4 max-w-3xl">
98
- <h2 class="text-xl font-semibold text-gray-900 leading-snug">{{ selected.title }}</h2>
99
- <div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500">
100
- <span>{{ selected.sourceSlug }}</span>
101
- <span>{{ formatSmartTime(selected.publishedAt) }}</span>
102
- <span v-for="cat in selected.categories" :key="cat" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">
103
- {{ cat }}
104
- </span>
105
- </div>
106
- <a
107
- :href="selected.url"
108
- target="_blank"
109
- rel="noopener noreferrer"
110
- class="mt-3 inline-flex items-center gap-1 text-sm text-blue-600 hover:underline"
111
- data-testid="news-open-original"
112
- >
113
- <span class="material-icons text-sm">open_in_new</span>
114
- {{ t("pluginNews.openOriginal") }}
115
- </a>
116
- <div class="mt-4">
117
- <div v-if="bodyLoading" class="text-sm text-gray-400">{{ t("common.loading") }}</div>
118
- <div v-else-if="bodyError" class="text-sm text-red-600">{{ t("pluginNews.bodyError", { error: bodyError }) }}</div>
119
- <div v-else-if="!body" class="text-sm text-gray-400 italic">{{ t("pluginNews.noBody") }}</div>
120
- <!-- eslint-disable-next-line vue/no-v-html -- marked.parse output of app-owned news body; trusted in-process render -->
121
- <div v-else class="markdown-content prose prose-slate max-w-none" @click="handleExternalLinkClick" v-html="renderedBody"></div>
122
- </div>
123
- </div>
124
- </div>
125
- <PageChatComposer
126
- :key="selected.id"
127
- :placeholder="t('pluginNews.chatPlaceholder')"
128
- :prepend-text="`Read this article. ${selected.url}`"
129
- allow-empty
130
- test-id-prefix="news-page-chat"
131
- />
132
- </template>
133
- </div>
134
- </div>
135
- </div>
136
- </template>
137
-
138
- <script setup lang="ts">
139
- import { computed, onMounted, ref, watch } from "vue";
140
- import { useI18n } from "vue-i18n";
141
- import { marked } from "marked";
142
- import { useRoute } from "vue-router";
143
- import { API_ROUTES } from "../config/apiRoutes";
144
- import { apiGet } from "../utils/api";
145
- import { formatSmartTime } from "../utils/format/date";
146
- import { useNewsItems } from "../composables/useNewsItems";
147
- import { handleExternalLinkClick } from "../utils/dom/externalLink";
148
- import { useNewsReadState } from "../composables/useNewsReadState";
149
- import { parseFrontmatter } from "../utils/markdown/frontmatter";
150
- import FilterChip from "./FilterChip.vue";
151
- import PageChatComposer from "./PageChatComposer.vue";
152
-
153
- const { t } = useI18n();
154
- const route = useRoute();
155
-
156
- const { items, loading, error, load: loadItems } = useNewsItems();
157
- const { isRead, markRead, markAllRead, load: loadReadState } = useNewsReadState();
158
-
159
- type ReadFilter = "all" | "unread";
160
- const readFilter = ref<ReadFilter>("unread");
161
- const sourceFilter = ref<string>("all");
162
- const selectedId = ref<string | null>(null);
163
- const body = ref<string | null>(null);
164
- const bodyLoading = ref(false);
165
- const bodyError = ref<string | null>(null);
166
-
167
- const readFilterChoices = computed<{ value: ReadFilter; label: string }[]>(() => [
168
- { value: "unread", label: t("pluginNews.filterUnread") },
169
- { value: "all", label: t("pluginNews.filterAll") },
170
- ]);
171
-
172
- const visibleItems = computed(() =>
173
- items.value.filter((item) => {
174
- if (readFilter.value === "unread" && isRead(item.id)) return false;
175
- if (sourceFilter.value !== "all" && item.sourceSlug !== sourceFilter.value) return false;
176
- return true;
177
- }),
178
- );
179
-
180
- // Source chips: derived from the current items list, sorted by
181
- // per-source count desc so the busiest source surfaces first.
182
- const sourceChoices = computed<{ slug: string; label: string; count: number }[]>(() => {
183
- const counts = new Map<string, number>();
184
- for (const item of items.value) {
185
- counts.set(item.sourceSlug, (counts.get(item.sourceSlug) ?? 0) + 1);
186
- }
187
- const sorted = Array.from(counts.entries())
188
- .sort(([, leftCount], [, rightCount]) => rightCount - leftCount)
189
- .map(([slug, count]) => ({ slug, label: slug, count }));
190
- return [{ slug: "all", label: t("pluginNews.allSources"), count: items.value.length }, ...sorted];
191
- });
192
-
193
- const unreadCount = computed(() => items.value.filter((item) => !isRead(item.id)).length);
194
-
195
- const selected = computed(() => items.value.find((item) => item.id === selectedId.value) ?? null);
196
-
197
- // Strip frontmatter before marked() renders the body. RSS-derived
198
- // content typically has no `---\n...\n---` envelope, but a feed
199
- // that mirrors a markdown blog could carry one — and we don't
200
- // want it to surface as a stray `<hr>` plus key:value plain text.
201
- // `parseFrontmatter` is a no-op for header-less inputs, so this
202
- // is safe for the common case (#895 PR D — closes the issue's
203
- // "Vue 側 ... news ... frontmatter が body に出ない" requirement).
204
- const renderedBody = computed(() => {
205
- if (!body.value) return "";
206
- const { body: bodyOnly } = parseFrontmatter(body.value);
207
- return marked(bodyOnly, { breaks: true, gfm: true });
208
- });
209
-
210
- function selectItem(itemId: string): void {
211
- selectedId.value = itemId;
212
- // Auto mark-as-read on selection. Defer slightly so a rapid arrow-
213
- // key scroll doesn't burn through the unread queue accidentally —
214
- // we only mark when the user dwells on a card.
215
- setTimeout(() => {
216
- if (selectedId.value === itemId) markRead(itemId);
217
- }, 250);
218
- }
219
-
220
- function markAllReadNow(): void {
221
- markAllRead(items.value.map((item) => item.id));
222
- }
223
-
224
- // Body fetch fires whenever the selection changes. Cancellation via
225
- // a token: a stale response just no-ops if the user moved on.
226
- let bodyToken = 0;
227
- watch(
228
- () => selectedId.value,
229
- async (itemId) => {
230
- body.value = null;
231
- bodyError.value = null;
232
- if (!itemId) return;
233
- bodyLoading.value = true;
234
- const token = ++bodyToken;
235
- const url = API_ROUTES.news.itemBody.replace(":id", encodeURIComponent(itemId));
236
- const result = await apiGet<{ body: string | null }>(url);
237
- // eslint-disable-next-line security/detect-possible-timing-attacks -- in-memory race-token guard, not an auth compare
238
- if (token !== bodyToken) return;
239
- bodyLoading.value = false;
240
- if (!result.ok) {
241
- bodyError.value = result.error;
242
- return;
243
- }
244
- body.value = result.data.body;
245
- },
246
- );
247
-
248
- // Apply `?source=<slug>` deep link from the Sources page once items
249
- // land — the sourceFilter only takes effect if the slug is one of
250
- // the registered sources in the current items list.
251
- function applyRouteSourceFilter(): void {
252
- const querySource = route.query.source;
253
- if (typeof querySource === "string" && querySource.length > 0) {
254
- sourceFilter.value = querySource;
255
- }
256
- }
257
-
258
- onMounted(async () => {
259
- applyRouteSourceFilter();
260
- await Promise.all([loadItems(), loadReadState()]);
261
- });
262
-
263
- watch(
264
- () => route.query.source,
265
- () => applyRouteSourceFilter(),
266
- );
267
- </script>