mulmoclaude 0.5.2 → 0.6.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 (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <!-- Full-tab chart-of-accounts list. Distinct from AccountsModal
3
+ (called from the entry / opening forms): this one fills the
4
+ canvas, surfaces a single "Manage accounts" button at the top,
5
+ and emits `selectAccount` so the parent View can route the
6
+ click into the Ledger tab pre-filtered to that account. -->
7
+ <div class="flex flex-col gap-3" data-testid="accounting-accounts-list">
8
+ <div class="flex flex-wrap items-center justify-end gap-2">
9
+ <button
10
+ type="button"
11
+ class="h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50"
12
+ data-testid="accounting-accounts-manage"
13
+ @click="showManageModal = true"
14
+ >
15
+ <span class="material-icons text-base">tune</span>
16
+ <span>{{ t("pluginAccounting.accounts.manageButton") }}</span>
17
+ </button>
18
+ </div>
19
+ <section v-for="group in groups" :key="group.type" class="flex flex-col gap-1">
20
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wide">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>
21
+ <p v-if="group.accounts.length === 0" class="text-xs text-gray-400 italic px-1">{{ t("pluginAccounting.accounts.listEmpty") }}</p>
22
+ <ul v-else class="flex flex-col">
23
+ <li
24
+ v-for="account in group.accounts"
25
+ :key="account.code"
26
+ tabindex="0"
27
+ role="button"
28
+ :aria-label="t('pluginAccounting.accounts.openLedgerAria', { code: account.code, name: account.name })"
29
+ class="flex items-center gap-3 px-2 py-1.5 border-b border-gray-100 hover:bg-blue-50 cursor-pointer text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded"
30
+ :data-testid="`accounting-account-row-${account.code}`"
31
+ @click="onSelect(account)"
32
+ @keydown.enter.prevent.self="onKeyActivate($event, account)"
33
+ @keydown.space.prevent.self="onKeyActivate($event, account)"
34
+ >
35
+ <span class="font-mono text-xs w-16 shrink-0">{{ account.code }}</span>
36
+ <span class="text-sm flex-1 min-w-0 truncate">{{ account.name }}</span>
37
+ </li>
38
+ </ul>
39
+ </section>
40
+ <AccountsModal v-if="showManageModal" :book-id="bookId" :accounts="accounts" @close="showManageModal = false" @changed="onAccountsChanged" />
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { computed, ref } from "vue";
46
+ import { useI18n } from "vue-i18n";
47
+ import type { Account, AccountType } from "../api";
48
+ import AccountsModal from "./AccountsModal.vue";
49
+
50
+ const { t } = useI18n();
51
+
52
+ const props = defineProps<{ bookId: string; accounts: Account[] }>();
53
+ const emit = defineEmits<{ selectAccount: [code: string]; changed: [] }>();
54
+
55
+ const ACCOUNT_TYPES: readonly AccountType[] = ["asset", "liability", "equity", "income", "expense"];
56
+
57
+ const showManageModal = ref(false);
58
+
59
+ interface AccountGroup {
60
+ type: AccountType;
61
+ accounts: Account[];
62
+ }
63
+
64
+ function byCode(left: Account, right: Account): number {
65
+ return left.code.localeCompare(right.code);
66
+ }
67
+
68
+ // Soft-deleted accounts (active === false) are hidden — managing
69
+ // them lives in the Manage Accounts modal, where Reactivate is one
70
+ // click away.
71
+ const groups = computed<AccountGroup[]>(() =>
72
+ ACCOUNT_TYPES.map((type) => ({
73
+ type,
74
+ accounts: props.accounts
75
+ .filter((account) => account.type === type && account.active !== false)
76
+ .slice()
77
+ .sort(byCode),
78
+ })),
79
+ );
80
+
81
+ function onSelect(account: Account): void {
82
+ emit("selectAccount", account.code);
83
+ }
84
+
85
+ // Keyboard activation: Enter / Space on a focused row. The
86
+ // `.prevent.self` modifiers in the template stop the default scroll
87
+ // (Space) and ensure we don't fire when the event bubbles up from
88
+ // a focused descendant (currently none, but defensive for future
89
+ // row content).
90
+ function onKeyActivate(event: KeyboardEvent, account: Account): void {
91
+ if (event.repeat) return;
92
+ emit("selectAccount", account.code);
93
+ }
94
+
95
+ function onAccountsChanged(): void {
96
+ // Forward to the parent — `bookVersion` already drives the
97
+ // accounts refetch in View.vue, so the list updates without us
98
+ // doing anything extra. Bubble the event in case a future
99
+ // consumer needs it.
100
+ emit("changed");
101
+ }
102
+ </script>
@@ -0,0 +1,300 @@
1
+ <template>
2
+ <!-- Manage-accounts modal. Opened from JournalEntryForm and
3
+ OpeningBalancesForm. Lists the current chart of accounts
4
+ grouped by type, with inline add / edit. Stays open across
5
+ saves so the user can fix several accounts in a row. -->
6
+ <div
7
+ class="fixed inset-0 z-50 bg-black/20 flex items-center justify-center"
8
+ role="dialog"
9
+ aria-modal="true"
10
+ aria-labelledby="accounting-accounts-modal-title"
11
+ data-testid="accounting-accounts-modal"
12
+ @click.self="onBackdropClick"
13
+ @keydown.esc="emit('close')"
14
+ >
15
+ <div class="bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col">
16
+ <header class="flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0">
17
+ <h3 id="accounting-accounts-modal-title" class="text-base font-semibold">{{ t("pluginAccounting.accounts.modalTitle") }}</h3>
18
+ <button
19
+ ref="closeButton"
20
+ type="button"
21
+ class="h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100"
22
+ data-testid="accounting-accounts-close"
23
+ :aria-label="t('pluginAccounting.common.cancel')"
24
+ @click="emit('close')"
25
+ >
26
+ <span class="material-icons text-base">close</span>
27
+ </button>
28
+ </header>
29
+ <div class="flex-1 overflow-auto px-4 py-3 flex flex-col gap-3">
30
+ <p v-if="successMessage" class="text-xs text-green-600" data-testid="accounting-accounts-success">{{ successMessage }}</p>
31
+ <p v-if="toggleError" class="text-xs text-red-500" data-testid="accounting-accounts-toggle-error">{{ toggleError }}</p>
32
+ <section v-for="group in groups" :key="group.type" class="flex flex-col gap-1">
33
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wide">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>
34
+ <div v-if="group.accounts.length === 0" class="text-xs text-gray-400 italic px-1">{{ t("pluginAccounting.common.empty") }}</div>
35
+ <template v-for="account in group.accounts" :key="account.code">
36
+ <AccountRow v-if="editingCode !== account.code" :account="account" @edit="onEdit(account)" @toggle-active="onToggleActive(account)" />
37
+ <AccountEditor
38
+ v-else
39
+ :draft="draft"
40
+ :is-new="false"
41
+ :busy="saving"
42
+ :error="error"
43
+ :existing-accounts="accounts"
44
+ @save="onSave"
45
+ @cancel="onCancelEditor"
46
+ />
47
+ </template>
48
+ <div v-if="addingNew && draft.type === group.type" :ref="(node) => bindNewEditor(node, group.type)">
49
+ <AccountEditor :draft="draft" is-new :busy="saving" :error="error" :existing-accounts="accounts" @save="onSave" @cancel="onCancelEditor" />
50
+ </div>
51
+ <button
52
+ v-else
53
+ type="button"
54
+ class="self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100"
55
+ :data-testid="`accounting-accounts-add-${group.type}`"
56
+ @click="onAdd(group.type)"
57
+ >
58
+ <span class="material-icons text-sm">add</span>
59
+ <span>{{ t("pluginAccounting.accounts.addToCategory", { type: t(`pluginAccounting.accounts.typeOption.${group.type}`) }) }}</span>
60
+ </button>
61
+ </section>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
69
+ import { useI18n } from "vue-i18n";
70
+ import { upsertAccount, type Account, type AccountType } from "../api";
71
+ import AccountRow from "./AccountRow.vue";
72
+ import AccountEditor from "./AccountEditor.vue";
73
+ import type { AccountDraft } from "./accountDraft";
74
+ import { validateAccountDraft, type AccountValidationError } from "./accountValidation";
75
+ import { suggestNextCode } from "./accountNumbering";
76
+
77
+ const { t } = useI18n();
78
+
79
+ const props = defineProps<{ bookId: string; accounts: Account[] }>();
80
+ const emit = defineEmits<{ close: []; changed: [] }>();
81
+
82
+ // Order matches conventional financial-statement layout (B/S then
83
+ // P/L). Section titles are pulled from i18n via the literal type
84
+ // keys, so this array drives both ordering and visibility.
85
+ const ACCOUNT_TYPES: readonly AccountType[] = ["asset", "liability", "equity", "income", "expense"];
86
+ const SUCCESS_FADE_MS = 2500;
87
+
88
+ const VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {
89
+ emptyCode: "pluginAccounting.accounts.errorEmptyCode",
90
+ reservedCode: "pluginAccounting.accounts.errorReservedCode",
91
+ invalidCodeFormat: "pluginAccounting.accounts.errorInvalidCodeFormat",
92
+ codeTypeMismatch: "pluginAccounting.accounts.errorCodeTypeMismatch",
93
+ emptyName: "pluginAccounting.accounts.errorEmptyName",
94
+ duplicateCode: "pluginAccounting.accounts.errorDuplicateCode",
95
+ duplicateName: "pluginAccounting.accounts.errorDuplicateName",
96
+ };
97
+
98
+ interface AccountGroup {
99
+ type: AccountType;
100
+ accounts: Account[];
101
+ }
102
+
103
+ const groups = computed<AccountGroup[]>(() =>
104
+ ACCOUNT_TYPES.map((type) => ({
105
+ type,
106
+ accounts: props.accounts
107
+ .filter((account) => account.type === type)
108
+ .slice()
109
+ .sort(byCode),
110
+ })),
111
+ );
112
+
113
+ function byCode(left: Account, right: Account): number {
114
+ return left.code.localeCompare(right.code);
115
+ }
116
+
117
+ const editingCode = ref<string | null>(null);
118
+ const addingNew = ref(false);
119
+ const draft = ref<AccountDraft>(emptyDraft("asset"));
120
+ const saving = ref(false);
121
+ const error = ref<string | null>(null);
122
+ // Toggle (Deactivate / Reactivate) keeps its own state. Sharing
123
+ // `saving` / `error` with the editor would (a) hide a toggle
124
+ // failure when no editor is mounted to render `:error`, and (b)
125
+ // blank out an in-progress editor's validation message and
126
+ // freeze its Save button when the user fires a toggle on a
127
+ // different row.
128
+ const toggleSaving = ref(false);
129
+ const toggleError = ref<string | null>(null);
130
+ const successMessage = ref<string | null>(null);
131
+ const closeButton = ref<HTMLButtonElement | null>(null);
132
+ const newEditorWrapper = ref<HTMLDivElement | null>(null);
133
+ let successTimer: ReturnType<typeof setTimeout> | null = null;
134
+
135
+ function emptyDraft(type: AccountType): AccountDraft {
136
+ return { code: "", name: "", type, note: "" };
137
+ }
138
+
139
+ function draftForNew(type: AccountType): AccountDraft {
140
+ return { code: suggestNextCode(type, props.accounts), name: "", type, note: "" };
141
+ }
142
+
143
+ // Vue's `:ref` on a v-for-style element gives us back either the
144
+ // node or null (on unmount). We only want to capture the editor
145
+ // belonging to the section that owns the current draft, so the
146
+ // section type is checked here rather than relying on the order
147
+ // in which Vue invokes the function refs.
148
+ function bindNewEditor(node: Element | object | null, sectionType: AccountType): void {
149
+ if (sectionType !== draft.value.type) return;
150
+ newEditorWrapper.value = (node as HTMLDivElement | null) ?? null;
151
+ }
152
+
153
+ function onEdit(account: Account): void {
154
+ // Collapse any other editor first so only one is open at a time.
155
+ addingNew.value = false;
156
+ error.value = null;
157
+ draft.value = { code: account.code, name: account.name, type: account.type, note: account.note ?? "" };
158
+ editingCode.value = account.code;
159
+ }
160
+
161
+ function onAdd(type: AccountType): void {
162
+ editingCode.value = null;
163
+ error.value = null;
164
+ draft.value = draftForNew(type);
165
+ addingNew.value = true;
166
+ // Scroll the new in-place editor into view in case the section
167
+ // sits below the visible viewport — opening the editor without
168
+ // scrolling would leave the user staring at unchanged content
169
+ // above the fold.
170
+ void nextTick(() => {
171
+ newEditorWrapper.value?.scrollIntoView({ behavior: "smooth", block: "nearest" });
172
+ });
173
+ }
174
+
175
+ function onCancelEditor(): void {
176
+ editingCode.value = null;
177
+ addingNew.value = false;
178
+ error.value = null;
179
+ draft.value = emptyDraft("asset");
180
+ }
181
+
182
+ function validateDraft(next: AccountDraft, isNew: boolean): string | null {
183
+ const code = validateAccountDraft(next, props.accounts, isNew);
184
+ return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);
185
+ }
186
+
187
+ async function onSave(next: AccountDraft): Promise<void> {
188
+ if (saving.value) return;
189
+ const isNew = addingNew.value;
190
+ const validation = validateDraft(next, isNew);
191
+ if (validation !== null) {
192
+ error.value = validation;
193
+ return;
194
+ }
195
+ saving.value = true;
196
+ error.value = null;
197
+ try {
198
+ const account: Account = {
199
+ code: next.code.trim(),
200
+ name: next.name.trim(),
201
+ type: next.type,
202
+ };
203
+ const note = next.note.trim();
204
+ if (note.length > 0) account.note = note;
205
+ // Preserve the existing active flag on edit — the editor
206
+ // doesn't surface the field, so reading from props.accounts
207
+ // is the only place the truth lives.
208
+ if (!isNew) {
209
+ const existing = props.accounts.find((entry) => entry.code === account.code);
210
+ if (existing?.active === false) account.active = false;
211
+ }
212
+ const result = await upsertAccount(account, props.bookId);
213
+ if (!result.ok) {
214
+ error.value = result.error;
215
+ return;
216
+ }
217
+ onCancelEditor();
218
+ showSuccess(t("pluginAccounting.accounts.success"));
219
+ emit("changed");
220
+ } catch (err) {
221
+ // apiPost normally folds network / HTTP failures into
222
+ // result.ok=false, so this is a belt-and-braces guard against
223
+ // a runtime failure that would otherwise leave the Save button
224
+ // stuck on "Saving…".
225
+ error.value = err instanceof Error ? err.message : String(err);
226
+ } finally {
227
+ saving.value = false;
228
+ }
229
+ }
230
+
231
+ async function onToggleActive(account: Account): Promise<void> {
232
+ // No confirm dialog: deactivation hides the account from the
233
+ // entry/ledger dropdowns but is fully reversible via Reactivate
234
+ // on the same row, and historical entries are unaffected. A
235
+ // confirm prompt was over-protective for an action that's a
236
+ // single click to undo.
237
+ //
238
+ // Toggle uses its own `toggleSaving` / `toggleError` refs rather
239
+ // than the AccountEditor's shared `saving` / `error` so that a
240
+ // toggle failure still surfaces (via the toggle banner) when no
241
+ // editor is mounted to render `:error`.
242
+ if (toggleSaving.value) return;
243
+ // Dismiss any open editor — the row about to (de)activate may be
244
+ // the same one being edited, and even when it isn't, the user has
245
+ // shifted attention to the toggle. Unsaved edits are dropped per
246
+ // product call: reopening Edit is one click.
247
+ onCancelEditor();
248
+ const willDeactivate = account.active !== false;
249
+ toggleSaving.value = true;
250
+ toggleError.value = null;
251
+ try {
252
+ const next: Account = {
253
+ code: account.code,
254
+ name: account.name,
255
+ type: account.type,
256
+ };
257
+ if (account.note !== undefined && account.note.length > 0) next.note = account.note;
258
+ // Send the active flag explicitly so the server can tell
259
+ // "user wants to (de)activate" apart from "user is editing
260
+ // and didn't mention active" — the latter inherits the
261
+ // existing flag and would otherwise turn Reactivate into a
262
+ // no-op.
263
+ next.active = !willDeactivate;
264
+ const result = await upsertAccount(next, props.bookId);
265
+ if (!result.ok) {
266
+ toggleError.value = result.error;
267
+ return;
268
+ }
269
+ emit("changed");
270
+ } catch (err) {
271
+ toggleError.value = err instanceof Error ? err.message : String(err);
272
+ } finally {
273
+ toggleSaving.value = false;
274
+ }
275
+ }
276
+
277
+ function showSuccess(message: string): void {
278
+ successMessage.value = message;
279
+ if (successTimer !== null) clearTimeout(successTimer);
280
+ successTimer = setTimeout(() => {
281
+ successMessage.value = null;
282
+ successTimer = null;
283
+ }, SUCCESS_FADE_MS);
284
+ }
285
+
286
+ function onBackdropClick(): void {
287
+ emit("close");
288
+ }
289
+
290
+ onMounted(() => {
291
+ // Initial focus on the close button so Esc / Tab work
292
+ // immediately and the user isn't dropped into an editor field
293
+ // they didn't ask for.
294
+ void nextTick(() => closeButton.value?.focus());
295
+ });
296
+
297
+ onUnmounted(() => {
298
+ if (successTimer !== null) clearTimeout(successTimer);
299
+ });
300
+ </script>
@@ -0,0 +1,186 @@
1
+ <template>
2
+ <div class="flex flex-col gap-3" data-testid="accounting-balance-sheet">
3
+ <div class="flex flex-wrap items-end gap-3">
4
+ <label class="text-xs text-gray-500 flex flex-col gap-1">
5
+ {{ t("pluginAccounting.balanceSheet.shortcutLabel") }}
6
+ <select
7
+ :value="selectedShortcut"
8
+ class="h-8 px-2 rounded border border-gray-300 text-sm bg-white"
9
+ data-testid="accounting-bs-shortcut"
10
+ @change="onShortcutChange(($event.target as HTMLSelectElement).value)"
11
+ >
12
+ <option value="" hidden></option>
13
+ <option value="thisMonth">{{ t("pluginAccounting.balanceSheet.thisMonth") }}</option>
14
+ <option value="lastMonth">{{ t("pluginAccounting.balanceSheet.lastMonth") }}</option>
15
+ <option value="lastQuarter">{{ t("pluginAccounting.balanceSheet.lastQuarter") }}</option>
16
+ <option value="lastYear">{{ t("pluginAccounting.balanceSheet.lastYear") }}</option>
17
+ </select>
18
+ </label>
19
+ <label class="text-xs text-gray-500 flex flex-col gap-1">
20
+ {{ t("pluginAccounting.balanceSheet.asOfLabel") }}
21
+ <input v-model="period" type="month" class="h-8 px-2 rounded border border-gray-300 text-sm" data-testid="accounting-bs-period" />
22
+ </label>
23
+ <button class="h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50" @click="refresh">
24
+ <span class="material-icons text-base align-middle">refresh</span>
25
+ </button>
26
+ </div>
27
+ <p v-if="loading" class="text-xs text-gray-400">{{ t("pluginAccounting.common.loading") }}</p>
28
+ <p v-else-if="error" class="text-xs text-red-500">{{ t("pluginAccounting.common.error", { error }) }}</p>
29
+ <template v-else-if="balanceSheet">
30
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
31
+ <section v-for="section in balanceSheet.sections" :key="section.type" class="border border-gray-200 rounded p-3">
32
+ <h4 class="text-sm font-semibold mb-2">{{ sectionLabel(section.type) }}</h4>
33
+ <table class="w-full text-sm">
34
+ <tbody>
35
+ <tr
36
+ v-for="row in section.rows"
37
+ :key="row.accountCode"
38
+ class="border-b border-gray-100"
39
+ :class="
40
+ isEarningsRow(row)
41
+ ? 'italic text-gray-600'
42
+ : 'hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400'
43
+ "
44
+ :tabindex="isEarningsRow(row) ? -1 : 0"
45
+ :role="isEarningsRow(row) ? undefined : 'button'"
46
+ :aria-label="isEarningsRow(row) ? undefined : t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })"
47
+ :data-testid="isEarningsRow(row) ? undefined : `accounting-bs-row-${row.accountCode}`"
48
+ @click="onRowClick(row)"
49
+ @keydown.enter.prevent.self="onKeyActivate($event, row)"
50
+ @keydown.space.prevent.self="onKeyActivate($event, row)"
51
+ >
52
+ <td class="py-1 px-1">
53
+ <span v-if="!isEarningsRow(row)" class="font-mono text-[10px] text-gray-400 mr-2">{{ row.accountCode }}</span
54
+ >{{ rowName(row) }}
55
+ </td>
56
+ <td class="py-1 px-1 text-right font-mono">{{ formatAmount(row.balance) }}</td>
57
+ </tr>
58
+ </tbody>
59
+ <tfoot>
60
+ <tr class="font-semibold border-t border-gray-300">
61
+ <td class="py-1 px-1">{{ t("pluginAccounting.balanceSheet.total") }}</td>
62
+ <td class="py-1 px-1 text-right">{{ formatAmount(section.total) }}</td>
63
+ </tr>
64
+ </tfoot>
65
+ </table>
66
+ </section>
67
+ </div>
68
+ <p :class="Math.abs(balanceSheet.imbalance) <= 0.01 ? 'text-green-600' : 'text-red-500'" class="text-xs" data-testid="accounting-bs-imbalance">
69
+ {{ t("pluginAccounting.balanceSheet.imbalance", { amount: formatAmount(balanceSheet.imbalance) }) }}
70
+ </p>
71
+ </template>
72
+ </div>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { computed, ref, watch } from "vue";
77
+ import { useI18n } from "vue-i18n";
78
+ import { getBalanceSheet, type BalanceSheet } from "../api";
79
+ import { formatAmount as formatAmountWithCurrency } from "../currencies";
80
+ import { decemberOfPreviousYearString, lastMonthOfPreviousQuarterString, localMonthString, previousMonthString } from "../dates";
81
+ import { useLatestRequest } from "./useLatestRequest";
82
+
83
+ const { t } = useI18n();
84
+
85
+ const props = defineProps<{ bookId: string; currency: string; version: number }>();
86
+ const emit = defineEmits<{ selectAccount: [code: string] }>();
87
+
88
+ const period = ref(localMonthString());
89
+ const balanceSheet = ref<BalanceSheet | null>(null);
90
+ const loading = ref(false);
91
+ const error = ref<string | null>(null);
92
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
93
+
94
+ function formatAmount(value: number): string {
95
+ return formatAmountWithCurrency(value, props.currency);
96
+ }
97
+
98
+ function sectionLabel(type: string): string {
99
+ if (type === "asset") return t("pluginAccounting.balanceSheet.sections.asset");
100
+ if (type === "liability") return t("pluginAccounting.balanceSheet.sections.liability");
101
+ if (type === "equity") return t("pluginAccounting.balanceSheet.sections.equity");
102
+ return type;
103
+ }
104
+
105
+ // The server adds a synthetic "Current period earnings" row to the
106
+ // Equity section so the B/S balances during the period (before
107
+ // closing entries fold income/expense into Retained Earnings).
108
+ // `_currentEarnings` is the sentinel accountCode emitted by the
109
+ // server — see CURRENT_EARNINGS_ACCOUNT_CODE in
110
+ // server/accounting/report.ts.
111
+ const CURRENT_EARNINGS_ACCOUNT_CODE = "_currentEarnings";
112
+
113
+ interface BSRow {
114
+ accountCode: string;
115
+ accountName: string;
116
+ balance: number;
117
+ }
118
+
119
+ function isEarningsRow(row: BSRow): boolean {
120
+ return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;
121
+ }
122
+
123
+ function rowName(row: BSRow): string {
124
+ return isEarningsRow(row) ? t("pluginAccounting.balanceSheet.currentEarnings") : row.accountName;
125
+ }
126
+
127
+ // Earnings row is synthetic (no underlying account on file), so it
128
+ // can't be drilled into. Real-account rows route to the Ledger tab
129
+ // pre-filtered to that account — same pattern as AccountsList.
130
+ function onRowClick(row: BSRow): void {
131
+ if (isEarningsRow(row)) return;
132
+ emit("selectAccount", row.accountCode);
133
+ }
134
+
135
+ function onKeyActivate(event: KeyboardEvent, row: BSRow): void {
136
+ if (event.repeat) return;
137
+ if (isEarningsRow(row)) return;
138
+ emit("selectAccount", row.accountCode);
139
+ }
140
+
141
+ // Mirrors the DateRangePicker pattern: hidden "" sentinel for the
142
+ // "no preset matches" custom state, otherwise the dropdown shows
143
+ // whichever shortcut produces the current period. Re-evaluates `now`
144
+ // on every read so the labels stay correct across midnight without
145
+ // any cache-invalidation plumbing.
146
+ type Shortcut = "thisMonth" | "lastMonth" | "lastQuarter" | "lastYear";
147
+ type SelectedShortcut = Shortcut | "";
148
+
149
+ const selectedShortcut = computed<SelectedShortcut>(() => {
150
+ const { value } = period;
151
+ const now = new Date();
152
+ if (value === localMonthString(now)) return "thisMonth";
153
+ if (value === previousMonthString(now)) return "lastMonth";
154
+ if (value === lastMonthOfPreviousQuarterString(now)) return "lastQuarter";
155
+ if (value === decemberOfPreviousYearString(now)) return "lastYear";
156
+ return "";
157
+ });
158
+
159
+ function onShortcutChange(raw: string): void {
160
+ const now = new Date();
161
+ if (raw === "thisMonth") period.value = localMonthString(now);
162
+ else if (raw === "lastMonth") period.value = previousMonthString(now);
163
+ else if (raw === "lastQuarter") period.value = lastMonthOfPreviousQuarterString(now);
164
+ else if (raw === "lastYear") period.value = decemberOfPreviousYearString(now);
165
+ }
166
+
167
+ async function refresh(): Promise<void> {
168
+ const token = beginRequest();
169
+ loading.value = true;
170
+ error.value = null;
171
+ try {
172
+ const result = await getBalanceSheet({ kind: "month", period: period.value }, props.bookId);
173
+ if (!isCurrent(token)) return;
174
+ if (!result.ok) {
175
+ error.value = result.error;
176
+ balanceSheet.value = null;
177
+ return;
178
+ }
179
+ balanceSheet.value = result.data.balanceSheet;
180
+ } finally {
181
+ if (isCurrent(token)) loading.value = false;
182
+ }
183
+ }
184
+
185
+ watch(() => [props.bookId, props.version, period.value], refresh, { immediate: true });
186
+ </script>