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,504 @@
1
+ <template>
2
+ <form class="flex flex-col gap-3" data-testid="accounting-entry-form" @submit.prevent="onSubmit">
3
+ <!-- Edit mode mounts inside the row's expanded detail panel,
4
+ which already gives the user enough context (the row above
5
+ shows date / kind / memo / lines). Hide the redundant
6
+ "Edit journal entry" title there; the editBanner below
7
+ still surfaces the void-and-replace consequence. -->
8
+ <h3 v-if="!isEditing" class="text-base font-semibold">{{ t("pluginAccounting.entryForm.title") }}</h3>
9
+ <div class="flex flex-wrap gap-3">
10
+ <label class="text-xs text-gray-500 flex flex-col gap-1">
11
+ {{ t("pluginAccounting.entryForm.dateLabel") }}
12
+ <input v-model="date" type="date" required class="h-8 px-2 rounded border border-gray-300 text-sm bg-white" data-testid="accounting-entry-date" />
13
+ </label>
14
+ <label class="text-xs text-gray-500 flex flex-col gap-1 grow min-w-0">
15
+ {{ t("pluginAccounting.entryForm.memoLabel") }}
16
+ <input v-model="memo" type="text" class="h-8 px-2 rounded border border-gray-300 text-sm bg-white" data-testid="accounting-entry-memo" />
17
+ </label>
18
+ </div>
19
+ <table class="w-full text-sm">
20
+ <thead>
21
+ <tr class="text-xs text-gray-500 border-b border-gray-200">
22
+ <th class="text-left py-1 px-2">{{ t("pluginAccounting.entryForm.accountLabel") }}</th>
23
+ <th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.debitLabel") }}</th>
24
+ <th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.creditLabel") }}</th>
25
+ <th v-if="anyTaxLine" class="text-left py-1 px-2 w-40">{{ t("pluginAccounting.entryForm.taxRegistrationIdLabel") }}</th>
26
+ <th class="py-1 px-2"></th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <tr v-for="(line, idx) in lines" :key="idx" class="border-b border-gray-100">
31
+ <td class="py-1 px-2">
32
+ <select
33
+ v-model="line.accountCode"
34
+ class="h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white"
35
+ :data-testid="`accounting-entry-line-account-${idx}`"
36
+ >
37
+ <option value="">{{ DASH }}</option>
38
+ <option v-for="account in selectableAccounts" :key="account.code" :value="account.code">{{ formatAccountLabel(account) }}</option>
39
+ </select>
40
+ </td>
41
+ <td class="py-1 px-2">
42
+ <input
43
+ v-model.number="line.debit"
44
+ type="number"
45
+ :step="step"
46
+ min="0"
47
+ class="h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white"
48
+ :data-testid="`accounting-entry-line-debit-${idx}`"
49
+ @input="onDebitInput(line)"
50
+ />
51
+ </td>
52
+ <td class="py-1 px-2">
53
+ <input
54
+ v-model.number="line.credit"
55
+ type="number"
56
+ :step="step"
57
+ min="0"
58
+ class="h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white"
59
+ :data-testid="`accounting-entry-line-credit-${idx}`"
60
+ @input="onCreditInput(line)"
61
+ />
62
+ </td>
63
+ <!-- Tax-registration ID column appears only when at least
64
+ one line picks an input-tax account (14xx — see
65
+ isTaxAccountCode). Within a column-visible row, the
66
+ input itself only renders for the lines that actually
67
+ pick a 14xx account; other lines render an empty cell
68
+ so the row keeps its column alignment. -->
69
+ <td v-if="anyTaxLine" class="py-1 px-2">
70
+ <template v-if="isTaxLine(line)">
71
+ <input
72
+ v-model="line.taxRegistrationId"
73
+ type="text"
74
+ :maxlength="MAX_TAX_REGISTRATION_ID_LENGTH"
75
+ :placeholder="t('pluginAccounting.entryForm.taxRegistrationIdPlaceholder')"
76
+ :class="[
77
+ 'h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none',
78
+ isTaxRegistrationIdInvalid(line)
79
+ ? 'border-red-500 ring-1 ring-red-500'
80
+ : isTaxRegistrationIdMissing(line)
81
+ ? 'border-amber-500 ring-1 ring-amber-500'
82
+ : 'border-gray-300 focus:ring-1 focus:ring-blue-500',
83
+ ]"
84
+ :data-testid="`accounting-entry-line-tax-registration-id-${idx}`"
85
+ :aria-describedby="isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : undefined"
86
+ />
87
+ <!-- Non-color cue for the amber border. Polite live
88
+ region so screen readers are nudged when the
89
+ user finishes typing an amount and the warning
90
+ first appears, without interrupting other speech. -->
91
+ <p
92
+ v-if="isTaxRegistrationIdMissing(line)"
93
+ :id="`accounting-entry-line-tax-registration-id-warning-${idx}`"
94
+ class="text-xs text-amber-600 mt-1"
95
+ role="status"
96
+ aria-live="polite"
97
+ :data-testid="`accounting-entry-line-tax-registration-id-warning-${idx}`"
98
+ >
99
+ {{ t("pluginAccounting.entryForm.taxRegistrationIdMissingWarning") }}
100
+ </p>
101
+ </template>
102
+ </td>
103
+ <td class="py-1 px-2 text-right">
104
+ <button v-if="lines.length > 2" type="button" class="text-xs text-red-500 hover:underline" @click="lines.splice(idx, 1)">
105
+ {{ t("pluginAccounting.entryForm.removeLine") }}
106
+ </button>
107
+ </td>
108
+ </tr>
109
+ </tbody>
110
+ </table>
111
+ <div class="flex items-center justify-between">
112
+ <div class="flex items-center gap-2">
113
+ <button
114
+ type="button"
115
+ 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"
116
+ data-testid="accounting-entry-add-line"
117
+ @click="addLine"
118
+ >
119
+ <span class="material-icons text-base">add</span>
120
+ <span>{{ t("pluginAccounting.entryForm.addLine") }}</span>
121
+ </button>
122
+ <button
123
+ type="button"
124
+ 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"
125
+ data-testid="accounting-entry-manage-accounts"
126
+ @click="showAccountsModal = true"
127
+ >
128
+ <span class="material-icons text-base">tune</span>
129
+ <span>{{ t("pluginAccounting.accounts.manageButton") }}</span>
130
+ </button>
131
+ </div>
132
+ <span :class="balanced ? 'text-green-600' : 'text-red-500'" class="text-xs" data-testid="accounting-entry-balance">
133
+ {{ balanced ? t("pluginAccounting.entryForm.balanced") : t("pluginAccounting.entryForm.imbalance", { amount: imbalanceText }) }}
134
+ </span>
135
+ </div>
136
+ <p v-if="error" class="text-xs text-red-500" data-testid="accounting-entry-error">{{ error }}</p>
137
+ <p v-if="successMessage" class="text-xs text-green-600" data-testid="accounting-entry-success">{{ successMessage }}</p>
138
+ <div class="flex items-center justify-between gap-2">
139
+ <!-- editBanner sits on the action row in edit mode (instead
140
+ of as a separate paragraph above the form) so the panel
141
+ is shorter and the user reads the void-and-replace
142
+ consequence right next to the button that triggers it. -->
143
+ <p v-if="isEditing" class="text-xs text-gray-500 flex-1 min-w-0" data-testid="accounting-entry-edit-banner">
144
+ {{ t("pluginAccounting.entryForm.editBanner") }}
145
+ </p>
146
+ <span v-else></span>
147
+ <div class="flex items-center gap-2">
148
+ <button
149
+ type="button"
150
+ class="h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50"
151
+ :disabled="submitting"
152
+ data-testid="accounting-entry-cancel-edit"
153
+ @click="emit('cancel')"
154
+ >
155
+ {{ t("pluginAccounting.common.cancel") }}
156
+ </button>
157
+ <button
158
+ type="submit"
159
+ class="h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50"
160
+ :disabled="!balanced || submitting || editLocked"
161
+ data-testid="accounting-entry-submit"
162
+ >
163
+ {{ submitButtonLabel }}
164
+ </button>
165
+ </div>
166
+ </div>
167
+ </form>
168
+ <!-- Sibling of the parent <form> on purpose: the modal renders
169
+ its own <form @submit.prevent> for the inline editor, and
170
+ nesting <form>s is invalid HTML that breaks Enter-key submit
171
+ routing in some browsers. Vue 3 multi-root templates let us
172
+ keep the markup flat with no wrapper div. -->
173
+ <AccountsModal v-if="showAccountsModal" :book-id="bookId" :accounts="accounts" @close="showAccountsModal = false" />
174
+ </template>
175
+
176
+ <script setup lang="ts">
177
+ import { computed, ref, watch } from "vue";
178
+ import { useI18n } from "vue-i18n";
179
+ import { addEntries, voidEntry, type Account, type JournalEntry, type JournalLine } from "../api";
180
+ import { formatAmount, inputStepFor } from "../currencies";
181
+ import { localDateString } from "../dates";
182
+ import { countryHasFeature, type SupportedCountryCode } from "../countries";
183
+ import { isTaxAccountCode } from "./accountNumbering";
184
+ import AccountsModal from "./AccountsModal.vue";
185
+
186
+ const { t } = useI18n();
187
+
188
+ const props = defineProps<{ bookId: string; accounts: Account[]; currency: string; country?: SupportedCountryCode; entryToEdit?: JournalEntry | null }>();
189
+ const emit = defineEmits<{ submitted: []; cancel: [] }>();
190
+
191
+ const showAccountsModal = ref(false);
192
+
193
+ const DASH = "—";
194
+
195
+ function formatAccountLabel(account: Account): string {
196
+ // Name first so type-to-search in the <select> matches the
197
+ // human-meaningful word; the code goes in trailing parens.
198
+ return `${account.name} (${account.code})`;
199
+ }
200
+
201
+ // Hide deactivated accounts from the entry dropdown — accounting
202
+ // integrity requires keeping them in the chart of accounts (any
203
+ // historical journal line still references the code), but new
204
+ // entries should not be able to land on a soft-deleted account.
205
+ const selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));
206
+ const selectableAccountCodes = computed<Set<string>>(() => new Set(selectableAccounts.value.map((account) => account.code)));
207
+
208
+ interface FormLine {
209
+ accountCode: string;
210
+ debit: number | null;
211
+ credit: number | null;
212
+ taxRegistrationId: string;
213
+ }
214
+
215
+ // Mirrors server/accounting/journal.ts MAX_TAX_REGISTRATION_ID_LENGTH.
216
+ // Duplicated rather than imported to keep the front-end bundle from
217
+ // pulling in server modules (the existing client / server type alias
218
+ // pattern in api.ts does the same — both sides own their copy of the
219
+ // shape).
220
+ const MAX_TAX_REGISTRATION_ID_LENGTH = 32;
221
+
222
+ function blankLine(): FormLine {
223
+ return { accountCode: "", debit: null, credit: null, taxRegistrationId: "" };
224
+ }
225
+
226
+ function isTaxRegistrationIdInvalid(line: FormLine): boolean {
227
+ return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;
228
+ }
229
+
230
+ function isTaxLine(line: FormLine): boolean {
231
+ return line.accountCode !== "" && isTaxAccountCode(line.accountCode);
232
+ }
233
+
234
+ // Soft warning: a postable tax line in a jurisdiction the role
235
+ // prompt requires a counterparty registration number for (JP, EU,
236
+ // GB, IN, AU, NZ, CA — see COUNTRY_FEATURES.warnMissingTaxRegistrationId)
237
+ // gets an amber border + helper text when the field is blank. The
238
+ // form lets the user post anyway (some suppliers genuinely won't
239
+ // have one), but the silent-strip in `toApiLines` no longer goes
240
+ // unnoticed. `function` declarations hoist, so calling `isPostable`
241
+ // here is fine even though it appears later in the file.
242
+ function isTaxRegistrationIdMissing(line: FormLine): boolean {
243
+ if (!isTaxLine(line)) return false;
244
+ if (!isPostable(line)) return false;
245
+ if (!countryHasFeature("warnMissingTaxRegistrationId", props.country)) return false;
246
+ return line.taxRegistrationId.trim() === "";
247
+ }
248
+
249
+ const date = ref(localDateString());
250
+ const memo = ref("");
251
+ const lines = ref<FormLine[]>([blankLine(), blankLine()]);
252
+ const submitting = ref(false);
253
+ const error = ref<string | null>(null);
254
+ const successMessage = ref<string | null>(null);
255
+
256
+ const isEditing = computed<boolean>(() => Boolean(props.entryToEdit));
257
+ const submitButtonLabel = computed<string>(() => {
258
+ if (submitting.value) {
259
+ return isEditing.value ? t("pluginAccounting.entryForm.updating") : t("pluginAccounting.entryForm.submitting");
260
+ }
261
+ return isEditing.value ? t("pluginAccounting.entryForm.update") : t("pluginAccounting.entryForm.submit");
262
+ });
263
+
264
+ // One-shot lock: once the user has clicked Update on an edit, the
265
+ // submit button is dead until they Cancel (or land on a different
266
+ // entry). Edit = void + addEntries as two sequential calls; if the
267
+ // void succeeds and the add fails, a second Submit would try to
268
+ // void an already-voided original. We don't add retry plumbing
269
+ // for that — policy is "report the error, do not retry". The user
270
+ // cancels out and re-enters manually.
271
+ const editAttempted = ref(false);
272
+ const editLocked = computed(() => isEditing.value && editAttempted.value);
273
+
274
+ function addLine(): void {
275
+ lines.value.push(blankLine());
276
+ }
277
+
278
+ // Toggling ensures a single line never has both sides set — the
279
+ // server validates this too, but doing it on input prevents a
280
+ // confusing UX where the running total goes negative as the user
281
+ // types.
282
+ function onDebitInput(line: FormLine): void {
283
+ if (line.debit !== null && line.debit !== 0) line.credit = null;
284
+ }
285
+ function onCreditInput(line: FormLine): void {
286
+ if (line.credit !== null && line.credit !== 0) line.debit = null;
287
+ }
288
+
289
+ // Imbalance is computed off lines that are *postable* (have an
290
+ // accountCode + a positive amount). Without that filter,
291
+ // `balanced` could be `true` even when `toApiLines()` would drop a
292
+ // row, and the user would hit a confusing "needs ≥ 2 lines" error
293
+ // from the server on submit.
294
+ const imbalance = computed<number>(() => {
295
+ let sum = 0;
296
+ for (const line of lines.value) {
297
+ if (!isPostable(line)) continue;
298
+ if (isPositiveAmount(line.debit)) sum += line.debit;
299
+ if (isPositiveAmount(line.credit)) sum -= line.credit;
300
+ }
301
+ return sum;
302
+ });
303
+ const hasAtLeastTwoPostableLines = computed(() => {
304
+ let count = 0;
305
+ for (const line of lines.value) {
306
+ if (!isPostable(line)) continue;
307
+ count += 1;
308
+ if (count >= 2) return true;
309
+ }
310
+ return false;
311
+ });
312
+ // Show the tax-registration ID column only when at least one line
313
+ // targets a 14xx (input-tax) account; otherwise the column is
314
+ // wasted space for the typical entry that has no input-tax line.
315
+ const anyTaxLine = computed(() => lines.value.some(isTaxLine));
316
+ const hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));
317
+ const balanced = computed(() => Math.abs(imbalance.value) <= 0.005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);
318
+ const imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));
319
+ const step = computed(() => inputStepFor(props.currency));
320
+
321
+ function isPositiveAmount(value: unknown): value is number {
322
+ // Robust against the empty string `v-model.number` produces when
323
+ // the user clears a previously-typed field — `"" ?? 0 === 0` is
324
+ // false so a naive truthy check would let the empty input through
325
+ // as a phantom zero amount.
326
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
327
+ }
328
+
329
+ function isPostable(line: FormLine): boolean {
330
+ if (!line.accountCode) return false;
331
+ // Defence-in-depth against a code that was selectable when the
332
+ // user picked it but got deactivated mid-edit. Hiding the
333
+ // option from the dropdown alone isn't enough — the form's
334
+ // `accountCode` value is sticky, so a stale selection would
335
+ // still be POSTed if the user just hits submit. Gating
336
+ // postability here also flows through to `balanced` and
337
+ // `hasAtLeastTwoPostableLines`, so the submit button disables
338
+ // and the user gets immediate feedback.
339
+ if (!selectableAccountCodes.value.has(line.accountCode)) return false;
340
+ return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);
341
+ }
342
+
343
+ function toApiLines(): JournalLine[] {
344
+ const out: JournalLine[] = [];
345
+ for (const line of lines.value) {
346
+ if (!isPostable(line)) continue;
347
+ const apiLine: JournalLine = { accountCode: line.accountCode };
348
+ if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;
349
+ if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;
350
+ // Only persist taxRegistrationId on tax-related lines —
351
+ // otherwise a value typed against an earlier account choice
352
+ // would leak through after the user switched the line to a
353
+ // non-tax account (the input field disappears but the form
354
+ // state lingers).
355
+ if (isTaxLine(line)) {
356
+ const trimmedTaxId = line.taxRegistrationId.trim();
357
+ if (trimmedTaxId !== "") apiLine.taxRegistrationId = trimmedTaxId;
358
+ }
359
+ out.push(apiLine);
360
+ }
361
+ return out;
362
+ }
363
+
364
+ async function onSubmit(): Promise<void> {
365
+ if (submitting.value || !balanced.value || editLocked.value) return;
366
+ submitting.value = true;
367
+ error.value = null;
368
+ successMessage.value = null;
369
+ try {
370
+ // Edit flow: void the original, then post the replacement.
371
+ // Two sequential calls — not atomic, no retry. Marking
372
+ // `editAttempted` *before* the void disables the submit button
373
+ // for the rest of this edit session (the `editLocked` guard
374
+ // and the button's :disabled both honour it), so a partial
375
+ // failure can't trigger a second void on the already-voided
376
+ // original. On any error: show the message, user must Cancel
377
+ // and re-enter manually.
378
+ const editingId = props.entryToEdit?.id;
379
+ if (editingId) {
380
+ editAttempted.value = true;
381
+ const voidResult = await voidEntry({
382
+ bookId: props.bookId,
383
+ entryId: editingId,
384
+ reason: t("pluginAccounting.entryForm.editVoidReason"),
385
+ });
386
+ if (!voidResult.ok) {
387
+ error.value = voidResult.error;
388
+ return;
389
+ }
390
+ }
391
+ const result = await addEntries({
392
+ bookId: props.bookId,
393
+ entries: [
394
+ {
395
+ date: date.value,
396
+ memo: memo.value.trim() || undefined,
397
+ lines: toApiLines(),
398
+ ...(editingId ? { replacesEntryId: editingId } : {}),
399
+ },
400
+ ],
401
+ });
402
+ if (!result.ok) {
403
+ error.value = result.error;
404
+ return;
405
+ }
406
+ successMessage.value = editingId ? t("pluginAccounting.entryForm.editSuccess") : t("pluginAccounting.entryForm.success");
407
+ lines.value = [blankLine(), blankLine()];
408
+ memo.value = "";
409
+ emit("submitted");
410
+ } catch (err) {
411
+ // apiPost normally folds network / HTTP failures into
412
+ // `result.ok = false`, so this branch should be rare. It's a
413
+ // belt-and-braces guard against a runtime failure leaving the
414
+ // submit button stuck — the user gets a visible error
415
+ // instead of an unhandled rejection.
416
+ error.value = err instanceof Error ? err.message : String(err);
417
+ } finally {
418
+ submitting.value = false;
419
+ }
420
+ }
421
+
422
+ // Reset the entire draft when bookId switches under us (rare but
423
+ // possible via BookSwitcher while the form is open). Carrying the
424
+ // previous book's lines and account codes into the new book is
425
+ // the worst kind of silent failure — the new book might not even
426
+ // have the same chart of accounts.
427
+ watch(
428
+ () => props.bookId,
429
+ () => {
430
+ lines.value = [blankLine(), blankLine()];
431
+ memo.value = "";
432
+ date.value = localDateString();
433
+ error.value = null;
434
+ successMessage.value = null;
435
+ },
436
+ );
437
+
438
+ // Edit mode: when the parent hands us an entry to edit, pre-fill
439
+ // every field so the user can tweak and resubmit. When it clears
440
+ // the prop (after submit / cancel / book switch), wipe back to a
441
+ // blank draft so the next "New entry" tab visit is fresh. Mapping
442
+ // `entry.lines` (the wire shape with optional `debit` / `credit`)
443
+ // onto `FormLine` (which uses nullable numbers + a string
444
+ // taxRegistrationId) is straightforward — pad missing optionals
445
+ // to null / "".
446
+ watch(
447
+ () => props.entryToEdit,
448
+ (entry) => {
449
+ error.value = null;
450
+ successMessage.value = null;
451
+ // Fresh edit (or exit from edit mode) → unlock the submit
452
+ // button so the new edit session has a clean shot.
453
+ editAttempted.value = false;
454
+ if (!entry) {
455
+ lines.value = [blankLine(), blankLine()];
456
+ memo.value = "";
457
+ date.value = localDateString();
458
+ return;
459
+ }
460
+ date.value = entry.date;
461
+ memo.value = entry.memo ?? "";
462
+ lines.value = entry.lines.map((line) => ({
463
+ accountCode: line.accountCode,
464
+ debit: typeof line.debit === "number" ? line.debit : null,
465
+ credit: typeof line.credit === "number" ? line.credit : null,
466
+ taxRegistrationId: line.taxRegistrationId ?? "",
467
+ }));
468
+ if (lines.value.length < 2) {
469
+ while (lines.value.length < 2) lines.value.push(blankLine());
470
+ }
471
+ },
472
+ { immediate: true },
473
+ );
474
+
475
+ // If an account the user already picked gets deactivated mid-edit
476
+ // (e.g. via the Manage Accounts modal in this form, or from
477
+ // another tab via pubsub), clear the line's accountCode so the
478
+ // <select> visibly resets to "—". Without this, the option is
479
+ // gone but the form's bound value still holds the stale code,
480
+ // which (a) leaves the user staring at a blank-looking select and
481
+ // (b) used to slip through to submit before the isPostable guard
482
+ // landed. Belt + suspenders.
483
+ watch(selectableAccountCodes, (codes) => {
484
+ for (const line of lines.value) {
485
+ if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = "";
486
+ }
487
+ });
488
+ </script>
489
+
490
+ <style scoped>
491
+ /* Hide the WebKit / Firefox spin buttons on amount inputs. The
492
+ step attribute still controls validation; this is purely UI.
493
+ Accounting amount fields don't benefit from a spinner — users
494
+ type the number and the up/down arrows just clutter the row. */
495
+ input[type="number"]::-webkit-outer-spin-button,
496
+ input[type="number"]::-webkit-inner-spin-button {
497
+ -webkit-appearance: none;
498
+ margin: 0;
499
+ }
500
+ input[type="number"] {
501
+ -moz-appearance: textfield;
502
+ appearance: textfield;
503
+ }
504
+ </style>