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,504 +0,0 @@
1
- <script setup lang="ts">
2
- // Encore dashboard — read-only landing for /encore.
3
- //
4
- // What this surface IS:
5
- // - A browser over `obligations/<id>/index.md` + every
6
- // `obligations/<id>/<cycleId>.md` cycle file on disk.
7
- // - One row per obligation with cadence / targets / current-cycle
8
- // status. Expanding the row reveals that obligation's cycle
9
- // history (reverse-chron).
10
- //
11
- // What this surface IS NOT:
12
- // - Not editable. There is no "mark done" / "snooze" / "create"
13
- // button — those are LLM-only verbs. The user reaches them by
14
- // asking in chat (the bell flow handles notification clicks).
15
- // - Not a notification surface. The bell stays the channel for
16
- // "the LLM wants your attention"; this page just lists what
17
- // Encore is tracking and what already happened.
18
- //
19
- // Data source: a single `kind: "query"` dispatch with `range: "all"`,
20
- // which returns every obligation + every cycle in one round trip.
21
- // No new server endpoint was added for the page.
22
-
23
- import { computed, onMounted, ref } from "vue";
24
- import { useI18n } from "vue-i18n";
25
- import { pluginEndpoints } from "../api";
26
- import { apiCall } from "../../utils/api";
27
- import { META } from "./manageEncoreMeta";
28
- import type { EncoreEndpoints } from "./manageEncoreDefinition";
29
- import type { EncoreDsl, StepDef } from "../../types/encore-dsl/schema";
30
- import type { Cadence } from "../../types/encore-dsl/cadence";
31
-
32
- // Wire shape mirrors `server/encore/handlers/query.ts`. The
33
- // per-target record matches `TargetRecord` from `server/encore/cycle.ts`
34
- // — duplicated here because that module lives outside `src/` and
35
- // can't be imported from a Vue component.
36
- interface TargetRecord {
37
- values?: Record<string, unknown>;
38
- skipped?: string;
39
- completedSteps?: Record<string, string>;
40
- snoozedSteps?: Record<string, string>;
41
- }
42
-
43
- interface CycleState {
44
- cycleId: string;
45
- cycleStart: string;
46
- cycleDeadline: string;
47
- records: Record<string, TargetRecord>;
48
- }
49
-
50
- interface QueryCycleResult {
51
- cycleId: string;
52
- path: string;
53
- state: CycleState;
54
- body: string;
55
- }
56
-
57
- interface QueryObligationResult {
58
- obligationId: string;
59
- indexPath: string;
60
- dsl: EncoreDsl;
61
- body: string;
62
- cycles: QueryCycleResult[];
63
- }
64
-
65
- interface QueryResponse {
66
- ok: boolean;
67
- message: string;
68
- obligations?: QueryObligationResult[];
69
- }
70
-
71
- interface TicketSummary {
72
- pendingId: string;
73
- obligationId: string;
74
- cycleId: string;
75
- notificationId: string;
76
- stepId: string;
77
- createdAt: string;
78
- }
79
-
80
- interface ListTicketsResponse {
81
- ok: boolean;
82
- message: string;
83
- tickets?: TicketSummary[];
84
- }
85
-
86
- const { t } = useI18n();
87
-
88
- const loading = ref(true);
89
- const errorMessage = ref<string | null>(null);
90
- const obligations = ref<QueryObligationResult[]>([]);
91
- const tickets = ref<TicketSummary[]>([]);
92
- const expanded = ref<Record<string, boolean>>({});
93
-
94
- async function loadObligations(): Promise<void> {
95
- loading.value = true;
96
- errorMessage.value = null;
97
- try {
98
- const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
99
- const { method, url } = endpoints.dispatch;
100
- // Query + listTickets in parallel — they're independent reads
101
- // and we want them both before the row chips/bell render.
102
- const [queryResponse, ticketsResponse] = await Promise.all([
103
- apiCall<QueryResponse>(url, { method, body: { kind: "query", range: "all" } }),
104
- apiCall<ListTicketsResponse>(url, { method, body: { kind: "listTickets" } }),
105
- ]);
106
- if (!queryResponse.ok) {
107
- errorMessage.value = queryResponse.error;
108
- return;
109
- }
110
- obligations.value = queryResponse.data.obligations ?? [];
111
- // A ticket-fetch failure shouldn't block the dashboard — degrade
112
- // gracefully (bells just won't render). Surface the error in the
113
- // console for debug visibility.
114
- if (ticketsResponse.ok) {
115
- tickets.value = ticketsResponse.data.tickets ?? [];
116
- } else {
117
- tickets.value = [];
118
- console.warn("encore-dashboard: listTickets failed —", ticketsResponse.error);
119
- }
120
- } catch (err) {
121
- errorMessage.value = err instanceof Error ? err.message : String(err);
122
- } finally {
123
- loading.value = false;
124
- }
125
- }
126
-
127
- onMounted(() => {
128
- void loadObligations();
129
- });
130
-
131
- // (obligationId, cycleId) → tickets[] for the per-cycle bell badge.
132
- // Computed so it stays in sync when either the query or the ticket
133
- // list reloads. The key is a composite string because Records can't
134
- // be indexed by a tuple.
135
- const ticketsByCycle = computed<Record<string, TicketSummary[]>>(() => {
136
- const map: Record<string, TicketSummary[]> = {};
137
- for (const ticket of tickets.value) {
138
- const key = `${ticket.obligationId}/${ticket.cycleId}`;
139
- if (!map[key]) map[key] = [];
140
- map[key].push(ticket);
141
- }
142
- return map;
143
- });
144
-
145
- function cycleTickets(obligationId: string, cycleId: string): TicketSummary[] {
146
- return ticketsByCycle.value[`${obligationId}/${cycleId}`] ?? [];
147
- }
148
-
149
- // Bell click: navigate to the same URL the host's NotificationBell
150
- // would build for this ticket (/encore?pendingId=…&notificationId=…),
151
- // then EncoreRedirect handles the resolveNotification dispatch and
152
- // onward redirect to /chat/<chatId>. If the obligation has multiple
153
- // live tickets we route to the first — the dashboard isn't the
154
- // place to triage; resolving one usually unblocks the row.
155
- function openTicket(ticket: TicketSummary): void {
156
- const params = new URLSearchParams({
157
- pendingId: ticket.pendingId,
158
- notificationId: ticket.notificationId,
159
- });
160
- window.location.href = `/encore?${params.toString()}`;
161
- }
162
-
163
- function toggle(obligationId: string): void {
164
- expanded.value[obligationId] = !expanded.value[obligationId];
165
- }
166
-
167
- // Per-obligation chat button → server-side `startObligationChat`
168
- // kind seeds a fresh chat with the obligation in context, then we
169
- // full-navigate to /chat/<chatId> (same trick the bell-click flow
170
- // uses so the seeded turn renders on first paint). The button
171
- // stops propagation so clicking it doesn't also toggle the row.
172
- const chatStarting = ref<Record<string, boolean>>({});
173
-
174
- interface StartObligationChatResult {
175
- ok: boolean;
176
- chatId?: string;
177
- navigateTo?: string;
178
- error?: string;
179
- message?: string;
180
- }
181
-
182
- async function startChatForObligation(obligationId: string): Promise<void> {
183
- if (chatStarting.value[obligationId]) return;
184
- chatStarting.value[obligationId] = true;
185
- try {
186
- const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
187
- const { method, url } = endpoints.dispatch;
188
- const response = await apiCall<StartObligationChatResult>(url, {
189
- method,
190
- body: { kind: "startObligationChat", obligationId },
191
- });
192
- if (!response.ok) {
193
- errorMessage.value = response.error;
194
- return;
195
- }
196
- const result = response.data;
197
- if (!result.ok || !result.chatId) {
198
- errorMessage.value = result.error ?? t("encoreDashboard.unexpectedResponse");
199
- return;
200
- }
201
- window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
202
- } catch (err) {
203
- errorMessage.value = err instanceof Error ? err.message : String(err);
204
- } finally {
205
- chatStarting.value[obligationId] = false;
206
- }
207
- }
208
-
209
- // "+ Add" toolbar button → server-side `startSetupChat` kind seeds
210
- // a new chat asking the LLM to walk the user through creating a
211
- // fresh obligation, then full-navigates. Mirrors
212
- // `startChatForObligation` but without an obligationId.
213
- const setupStarting = ref(false);
214
-
215
- async function startSetupChat(): Promise<void> {
216
- if (setupStarting.value) return;
217
- setupStarting.value = true;
218
- try {
219
- const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
220
- const { method, url } = endpoints.dispatch;
221
- const response = await apiCall<StartObligationChatResult>(url, {
222
- method,
223
- body: { kind: "startSetupChat" },
224
- });
225
- if (!response.ok) {
226
- errorMessage.value = response.error;
227
- return;
228
- }
229
- const result = response.data;
230
- if (!result.ok || !result.chatId) {
231
- errorMessage.value = result.error ?? t("encoreDashboard.unexpectedResponse");
232
- return;
233
- }
234
- window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
235
- } catch (err) {
236
- errorMessage.value = err instanceof Error ? err.message : String(err);
237
- } finally {
238
- setupStarting.value = false;
239
- }
240
- }
241
-
242
- // ── derived closure (mirrors server/encore/closure.ts) ──
243
-
244
- function isStepClosed(record: TargetRecord | undefined, step: StepDef): boolean {
245
- if (!record) return false;
246
- if (record.skipped) return true;
247
- return Boolean(record.completedSteps?.[step.id]);
248
- }
249
-
250
- function isTargetClosed(record: TargetRecord | undefined, dsl: EncoreDsl): boolean {
251
- if (!record) return false;
252
- if (record.skipped) return true;
253
- return dsl.steps.every((step) => isStepClosed(record, step));
254
- }
255
-
256
- function isCycleClosed(state: CycleState, dsl: EncoreDsl): boolean {
257
- return dsl.targets.every((target) => isTargetClosed(state.records[target.id], dsl));
258
- }
259
-
260
- // ── display helpers ──
261
-
262
- function cadenceLabel(cadence: Cadence): string {
263
- return t(`encoreDashboard.cadence.${cadence.type}`);
264
- }
265
-
266
- function statusLabel(status: EncoreDsl["status"]): string {
267
- return t(`encoreDashboard.status.${status}`);
268
- }
269
-
270
- function statusClasses(status: EncoreDsl["status"]): string {
271
- if (status === "active") return "bg-green-100 text-green-700";
272
- if (status === "paused") return "bg-yellow-100 text-yellow-700";
273
- return "bg-gray-200 text-gray-600";
274
- }
275
-
276
- function targetCountLabel(dsl: EncoreDsl): string {
277
- return t("encoreDashboard.targetCount", { count: dsl.targets.length });
278
- }
279
-
280
- function formatDate(iso: string): string {
281
- // Cycle window timestamps are ISO; show date-only for readability.
282
- return iso.slice(0, 10);
283
- }
284
-
285
- type StepStatus = "done" | "skipped" | "open";
286
-
287
- function cycleStepStatus(state: CycleState, target: { id: string }, step: StepDef): StepStatus {
288
- const record = state.records[target.id];
289
- if (record?.skipped) return "skipped";
290
- if (record?.completedSteps?.[step.id]) return "done";
291
- return "open";
292
- }
293
-
294
- function stepStatusIcon(status: StepStatus): string {
295
- if (status === "done") return "check_circle";
296
- if (status === "skipped") return "remove_circle_outline";
297
- return "radio_button_unchecked";
298
- }
299
-
300
- function stepStatusClasses(status: StepStatus): string {
301
- // Done is settled history — grey, same theme as the "Closed" chip.
302
- // Skipped sits a touch lighter; open is the live state, so it
303
- // keeps the blue accent.
304
- if (status === "done") return "text-gray-500";
305
- if (status === "skipped") return "text-gray-400";
306
- return "text-blue-500";
307
- }
308
-
309
- // Cycles to display under an obligation header, reverse-chron.
310
- // - When the row is expanded: full history.
311
- // - When the row is collapsed: only cycles that have a live ticket.
312
- // This is the always-visible "pending" surface — the user sees
313
- // what needs attention without clicking expand.
314
- // Server returns cycles ascending; we reverse for newest-first.
315
- function visibleCycles(item: QueryObligationResult): QueryCycleResult[] {
316
- const all = [...item.cycles].reverse();
317
- if (expanded.value[item.obligationId]) return all;
318
- return all.filter((cycle) => cycleTickets(item.obligationId, cycle.cycleId).length > 0);
319
- }
320
-
321
- // Sort: active first, then paused, then retired. Within each
322
- // status, alphabetical by displayName.
323
- const sortedObligations = computed<QueryObligationResult[]>(() => {
324
- const statusRank: Record<EncoreDsl["status"], number> = { active: 0, paused: 1, retired: 2 };
325
- return [...obligations.value].sort((lhs, rhs) => {
326
- const statusDiff = statusRank[lhs.dsl.status] - statusRank[rhs.dsl.status];
327
- if (statusDiff !== 0) return statusDiff;
328
- return lhs.dsl.displayName.localeCompare(rhs.dsl.displayName);
329
- });
330
- });
331
-
332
- interface RecordedValue {
333
- key: string;
334
- value: string;
335
- }
336
-
337
- function recordedValuesForTarget(state: CycleState, targetId: string): RecordedValue[] {
338
- const values = state.records[targetId]?.values;
339
- if (!values) return [];
340
- return Object.entries(values)
341
- .filter(([, value]) => value !== null && value !== undefined && value !== "")
342
- .map(([key, value]) => ({
343
- key,
344
- value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value),
345
- }));
346
- }
347
- </script>
348
-
349
- <template>
350
- <div class="h-full overflow-y-auto" data-testid="encore-dashboard">
351
- <div class="max-w-4xl mx-auto px-4 py-6">
352
- <div class="flex items-center justify-between mb-1">
353
- <h1 class="text-xl font-semibold text-gray-800">{{ t("encoreDashboard.title") }}</h1>
354
- <button
355
- type="button"
356
- class="h-8 px-2.5 flex items-center gap-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-wait"
357
- :disabled="setupStarting"
358
- data-testid="encore-add-button"
359
- @click="startSetupChat()"
360
- >
361
- <span class="material-icons text-base">{{ setupStarting ? "hourglass_empty" : "add" }}</span>
362
- <span>{{ t("encoreDashboard.addButtonLabel") }}</span>
363
- </button>
364
- </div>
365
- <p class="text-sm text-gray-500 mb-6">{{ t("encoreDashboard.subtitle") }}</p>
366
-
367
- <div v-if="loading" class="text-sm text-gray-500">{{ t("encoreDashboard.loading") }}</div>
368
-
369
- <div v-else-if="errorMessage" class="text-sm text-red-600">{{ t("encoreDashboard.errorPrefix") }}{{ errorMessage }}</div>
370
-
371
- <div v-else-if="sortedObligations.length === 0" class="text-sm text-gray-500 border border-dashed border-gray-300 rounded p-6 text-center">
372
- {{ t("encoreDashboard.empty") }}
373
- </div>
374
-
375
- <ul v-else class="space-y-2">
376
- <li
377
- v-for="item in sortedObligations"
378
- :key="item.obligationId"
379
- class="border border-gray-200 rounded bg-white"
380
- :data-testid="`encore-obligation-${item.obligationId}`"
381
- >
382
- <div class="flex items-stretch hover:bg-gray-50 transition-colors">
383
- <button
384
- type="button"
385
- class="flex-1 min-w-0 px-4 py-3 flex items-center gap-3 text-left"
386
- :aria-expanded="!!expanded[item.obligationId]"
387
- @click="toggle(item.obligationId)"
388
- >
389
- <span class="material-icons text-gray-400 text-base">{{ expanded[item.obligationId] ? "expand_more" : "chevron_right" }}</span>
390
- <span class="flex-1 min-w-0">
391
- <span class="block text-sm font-medium text-gray-800 truncate">{{ item.dsl.displayName }}</span>
392
- <span class="block text-xs text-gray-500 mt-0.5">
393
- {{ cadenceLabel(item.dsl.cadence) }} · {{ targetCountLabel(item.dsl) }} · {{ item.cycles.length }} {{ t("encoreDashboard.cyclesSuffix") }}
394
- </span>
395
- </span>
396
- <!-- "Active" is the default and would just be noise on
397
- every row — show the chip only for the off-normal
398
- states (paused / retired). -->
399
- <span
400
- v-if="item.dsl.status !== 'active'"
401
- class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
402
- :class="statusClasses(item.dsl.status)"
403
- >
404
- {{ statusLabel(item.dsl.status) }}
405
- </span>
406
- </button>
407
- <button
408
- type="button"
409
- class="px-3 flex items-center justify-center text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors disabled:opacity-50 disabled:cursor-wait"
410
- :title="t('encoreDashboard.chatButtonTitle')"
411
- :aria-label="t('encoreDashboard.chatButtonTitle')"
412
- :disabled="!!chatStarting[item.obligationId]"
413
- :data-testid="`encore-obligation-chat-${item.obligationId}`"
414
- @click="startChatForObligation(item.obligationId)"
415
- >
416
- <span class="material-icons text-base">{{ chatStarting[item.obligationId] ? "hourglass_empty" : "chat_bubble_outline" }}</span>
417
- </button>
418
- </div>
419
-
420
- <div v-if="visibleCycles(item).length > 0" class="border-t border-gray-100 px-4 py-3 bg-gray-50">
421
- <ul class="space-y-3">
422
- <li
423
- v-for="cycle in visibleCycles(item)"
424
- :key="cycle.cycleId"
425
- class="bg-white border border-gray-200 rounded p-3"
426
- :data-testid="`encore-cycle-${item.obligationId}-${cycle.cycleId}`"
427
- >
428
- <div class="flex items-center gap-2 mb-2">
429
- <span class="text-sm font-mono text-gray-700">{{ cycle.cycleId }}</span>
430
- <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative arrow between two date values, language-neutral -->
431
- <span class="text-xs text-gray-400">{{ formatDate(cycle.state.cycleStart) }} → {{ formatDate(cycle.state.cycleDeadline) }}</span>
432
- <!-- Right-aligned group so the layout still pins to
433
- the right when the "closed" chip is hidden
434
- (open cycles render no chip, only the per-step
435
- icons inside the row signal what's left). -->
436
- <span class="ml-auto inline-flex items-center gap-2">
437
- <!-- Light grey signals "settled history, nothing to do." -->
438
- <span
439
- v-if="isCycleClosed(cycle.state, item.dsl)"
440
- class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500"
441
- >
442
- {{ t("encoreDashboard.cycleClosed") }}
443
- </span>
444
- <button
445
- v-if="cycleTickets(item.obligationId, cycle.cycleId).length > 0"
446
- type="button"
447
- class="relative inline-flex items-center justify-center w-6 h-6 rounded text-amber-500 hover:text-amber-600 hover:bg-amber-50 transition-colors"
448
- :title="t('encoreDashboard.bellButtonTitle')"
449
- :aria-label="t('encoreDashboard.bellButtonTitle')"
450
- :data-testid="`encore-cycle-bell-${item.obligationId}-${cycle.cycleId}`"
451
- @click="openTicket(cycleTickets(item.obligationId, cycle.cycleId)[0])"
452
- >
453
- <span class="material-icons text-base">notifications_active</span>
454
- <span
455
- v-if="cycleTickets(item.obligationId, cycle.cycleId).length > 1"
456
- class="absolute -top-1 -right-1 min-w-[14px] h-3.5 px-1 rounded-full bg-red-500 text-white text-[9px] leading-[14px] text-center font-semibold"
457
- >
458
- {{ cycleTickets(item.obligationId, cycle.cycleId).length }}
459
- </span>
460
- </button>
461
- </span>
462
- </div>
463
- <div class="space-y-1">
464
- <div v-for="target in item.dsl.targets" :key="target.id" class="text-xs">
465
- <div class="flex items-center gap-2">
466
- <span class="font-medium text-gray-600 min-w-[6rem] truncate">{{ target.displayName ?? target.id }}</span>
467
- <span
468
- v-for="step in item.dsl.steps"
469
- :key="step.id"
470
- class="inline-flex items-center gap-1 text-gray-500"
471
- :title="step.displayName ?? step.id"
472
- >
473
- <span class="material-icons text-xs" :class="stepStatusClasses(cycleStepStatus(cycle.state, target, step))">{{
474
- stepStatusIcon(cycleStepStatus(cycle.state, target, step))
475
- }}</span>
476
- <span class="truncate max-w-[10rem]">{{ step.displayName ?? step.id }}</span>
477
- </span>
478
- </div>
479
- <div v-if="recordedValuesForTarget(cycle.state, target.id).length > 0" class="ml-[6.5rem] mt-1 flex flex-wrap gap-1">
480
- <span
481
- v-for="entry in recordedValuesForTarget(cycle.state, target.id)"
482
- :key="entry.key"
483
- class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-gray-100 text-[11px] text-gray-600"
484
- >
485
- <span class="text-gray-400">{{ entry.key }}:</span>
486
- <span class="font-mono">{{ entry.value }}</span>
487
- </span>
488
- </div>
489
- </div>
490
- </div>
491
- </li>
492
- </ul>
493
- </div>
494
- <!-- Expanded by the user, but the obligation has no cycles
495
- on disk yet. Falls outside the visibleCycles section
496
- because that section is hidden when the list is empty. -->
497
- <div v-else-if="expanded[item.obligationId]" class="border-t border-gray-100 px-4 py-3 bg-gray-50 text-xs text-gray-500">
498
- {{ t("encoreDashboard.noCycles") }}
499
- </div>
500
- </li>
501
- </ul>
502
- </div>
503
- </div>
504
- </template>
@@ -1,116 +0,0 @@
1
- <script setup lang="ts">
2
- // Chat-on-mount redirect — extracted from View.vue (#feat-encore-page).
3
- //
4
- // Why this branch exists at all:
5
- // - The tick NEVER calls chat.start(). If it did, the chat would
6
- // appear in the user's sidebar before they engaged with the
7
- // notification (the "abandoned chat" problem).
8
- // - Chat creation must be deferred until the user clicks the bell.
9
- // The bell's `navigateTarget` is just a URL, so to intercept the
10
- // click we own the destination route (/encore?pendingId=…) and
11
- // run plugin code on mount.
12
- // - On mount this component dispatches resolveNotification (which
13
- // starts the chat server-side) and redirects to /chat/<chatId>
14
- // via a FULL navigation. The full nav guarantees the
15
- // `from encore` chip renders on first paint — same trick
16
- // debug-plugin uses.
17
- //
18
- // Notification clearing is NOT done here — that's the LLM's job
19
- // once it's talking to the user in the resulting chat (the LLM
20
- // calls markStepDone / markTargetSkipped with the pendingId; the
21
- // MCP handler reads the ticket and calls notifier.clear). The only
22
- // clear-here case is an orphan ticket (already swept) — the server
23
- // clears the bell entry by notificationId and returns
24
- // { orphan: true }, and we render the "already resolved" line.
25
-
26
- import { computed, onMounted, ref } from "vue";
27
- import { pluginEndpoints } from "../api";
28
- import { apiCall } from "../../utils/api";
29
- import { META } from "./manageEncoreMeta";
30
- import type { EncoreEndpoints } from "./manageEncoreDefinition";
31
-
32
- const props = defineProps<{ pendingId: string }>();
33
-
34
- const status = ref<"starting" | "redirecting" | "error" | "orphan">("starting");
35
- const errorMessage = ref<string | null>(null);
36
-
37
- // The host's NotificationBell.vue splices `notificationId=<entryId>`
38
- // onto every navigateTarget at click time (see appendNotificationId
39
- // in src/components/NotificationBell.vue). Reading it lets the
40
- // server clear orphan bell entries whose ticket was already swept.
41
- const notificationId = computed<string | null>(() => {
42
- if (typeof window === "undefined") return null;
43
- const params = new URLSearchParams(window.location.search);
44
- return params.get("notificationId");
45
- });
46
-
47
- interface ResolveResult {
48
- ok: boolean;
49
- chatId?: string;
50
- navigateTo?: string;
51
- orphan?: boolean;
52
- error?: string;
53
- message?: string;
54
- }
55
-
56
- async function resolveAndRedirect(): Promise<void> {
57
- try {
58
- const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
59
- const { method, url } = endpoints.dispatch;
60
- const response = await apiCall<ResolveResult>(url, {
61
- method,
62
- body: {
63
- kind: "resolveNotification",
64
- pendingId: props.pendingId,
65
- notificationId: notificationId.value ?? undefined,
66
- },
67
- });
68
- if (!response.ok) {
69
- status.value = "error";
70
- errorMessage.value = response.error;
71
- return;
72
- }
73
- const result = response.data;
74
- if (result.orphan) {
75
- status.value = "orphan";
76
- errorMessage.value = result.message ?? "This notification was already resolved.";
77
- return;
78
- }
79
- if (!result.ok || !result.chatId) {
80
- status.value = "error";
81
- errorMessage.value = result.error ?? "resolveNotification returned no chatId";
82
- return;
83
- }
84
- status.value = "redirecting";
85
- // Full-page navigation (not Vue-router push) so the seeded
86
- // user turn renders with the `from encore` chip on first
87
- // paint — same mechanism debug-plugin uses.
88
- window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
89
- } catch (err) {
90
- status.value = "error";
91
- errorMessage.value = err instanceof Error ? err.message : String(err);
92
- }
93
- }
94
-
95
- onMounted(() => {
96
- void resolveAndRedirect();
97
- });
98
- </script>
99
-
100
- <template>
101
- <!-- eslint-disable @intlify/vue-i18n/no-raw-text -- chat-on-mount page is a transient redirect, not a user-facing surface; strings stay out of the 8-locale bundle (matches debug-plugin's View). -->
102
- <div class="h-full flex items-center justify-center text-sm text-gray-500">
103
- <div v-if="status === 'starting'">Starting chat…</div>
104
- <div v-else-if="status === 'redirecting'">Redirecting to chat…</div>
105
- <div v-else-if="status === 'orphan'" class="text-center max-w-md px-4">
106
- <div class="text-gray-700 mb-2">This notification has already been resolved.</div>
107
- <a href="/chat" class="block text-blue-600 hover:underline text-sm">← back to chat</a>
108
- </div>
109
- <div v-else class="text-center max-w-md px-4">
110
- <div class="text-red-600 mb-2">Couldn't open the resolution chat.</div>
111
- <div class="text-xs text-gray-500">{{ errorMessage }}</div>
112
- <a href="/chat" class="block mt-3 text-blue-600 hover:underline text-sm">← back to chat</a>
113
- </div>
114
- </div>
115
- <!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
116
- </template>
@@ -1,36 +0,0 @@
1
- <script setup lang="ts">
2
- // Encore page entry — branches between two unrelated surfaces that
3
- // happen to share the /encore route:
4
- //
5
- // 1. `?pendingId=<uuid>` present → `EncoreRedirect.vue`
6
- // Notification-click landing. Dispatches `resolveNotification`
7
- // on mount, then full-navigates to the resulting /chat/<chatId>.
8
- // The user never actually sees this surface — transient (~300ms).
9
- //
10
- // 2. No `pendingId` → `EncoreDashboard.vue`
11
- // Read-only browser over `obligations/` (active obligations +
12
- // cycle history). Reached from the top-bar launcher; no
13
- // mutating affordances — those are LLM-only verbs.
14
- //
15
- // The branch lives here (rather than in the router) because the
16
- // route name and Vue surface stay one-to-one — App.vue mounts a
17
- // single component per `currentPage`.
18
-
19
- import { computed } from "vue";
20
- import { useRoute } from "vue-router";
21
- import EncoreRedirect from "./EncoreRedirect.vue";
22
- import EncoreDashboard from "./EncoreDashboard.vue";
23
-
24
- const route = useRoute();
25
-
26
- const pendingId = computed<string | null>(() => {
27
- const value = route.query.pendingId;
28
- if (typeof value === "string" && value.length > 0) return value;
29
- return null;
30
- });
31
- </script>
32
-
33
- <template>
34
- <EncoreRedirect v-if="pendingId" :pending-id="pendingId" />
35
- <EncoreDashboard v-else />
36
- </template>