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,271 @@
1
+ <template>
2
+ <form class="flex flex-col gap-3" data-testid="accounting-opening-form" @submit.prevent="onSubmit">
3
+ <div class="flex items-center justify-between gap-2">
4
+ <h3 class="text-base font-semibold">{{ t("pluginAccounting.openingForm.title") }}</h3>
5
+ <button
6
+ type="button"
7
+ 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"
8
+ data-testid="accounting-opening-manage-accounts"
9
+ @click="showAccountsModal = true"
10
+ >
11
+ <span class="material-icons text-base">tune</span>
12
+ <span>{{ t("pluginAccounting.accounts.manageButton") }}</span>
13
+ </button>
14
+ </div>
15
+ <p class="text-xs text-gray-500">{{ t("pluginAccounting.openingForm.explainer") }}</p>
16
+ <p class="text-xs text-blue-600" data-testid="accounting-opening-empty-hint">{{ t("pluginAccounting.openingForm.emptyHint") }}</p>
17
+ <div v-if="existing" class="text-xs text-gray-500" data-testid="accounting-opening-existing">
18
+ {{ t("pluginAccounting.openingForm.setBy", { date: existing.date }) }}
19
+ <span v-if="existing" class="text-amber-600 ml-2">{{ t("pluginAccounting.openingForm.replaceWarning") }}</span>
20
+ </div>
21
+ <p v-else class="text-xs text-gray-400" data-testid="accounting-opening-none">{{ t("pluginAccounting.openingForm.none") }}</p>
22
+ <label class="text-xs text-gray-500 flex flex-col gap-1 w-fit">
23
+ {{ t("pluginAccounting.openingForm.asOfLabel") }}
24
+ <input v-model="asOfDate" type="date" required class="h-8 px-2 rounded border border-gray-300 text-sm" data-testid="accounting-opening-asof" />
25
+ </label>
26
+ <table class="w-full text-sm">
27
+ <thead>
28
+ <tr class="text-xs text-gray-500 border-b border-gray-200">
29
+ <th class="text-left py-1 px-2">{{ t("pluginAccounting.entryForm.accountLabel") }}</th>
30
+ <th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.debitLabel") }}</th>
31
+ <th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.creditLabel") }}</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <tr v-for="account in bsAccounts" :key="account.code" class="border-b border-gray-100">
36
+ <td class="py-1 px-2">
37
+ <span class="font-mono text-[10px] text-gray-400 mr-2">{{ account.code }}</span>
38
+ <span>{{ account.name }}</span>
39
+ <span class="ml-2 text-xs text-gray-400">{{ account.type }}</span>
40
+ </td>
41
+ <td class="py-1 px-2">
42
+ <input
43
+ v-model.number="rows[account.code].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"
48
+ :data-testid="`accounting-opening-debit-${account.code}`"
49
+ @input="onDebitInput(account.code)"
50
+ />
51
+ </td>
52
+ <td class="py-1 px-2">
53
+ <input
54
+ v-model.number="rows[account.code].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"
59
+ :data-testid="`accounting-opening-credit-${account.code}`"
60
+ @input="onCreditInput(account.code)"
61
+ />
62
+ </td>
63
+ </tr>
64
+ </tbody>
65
+ </table>
66
+ <div class="flex items-center justify-between">
67
+ <span class="text-xs text-gray-400">{{ t("pluginAccounting.openingForm.explainer2") }}</span>
68
+ <span :class="balanced ? 'text-green-600' : 'text-red-500'" class="text-xs" data-testid="accounting-opening-balance">
69
+ {{ balanced ? t("pluginAccounting.entryForm.balanced") : t("pluginAccounting.entryForm.imbalance", { amount: imbalanceText }) }}
70
+ </span>
71
+ </div>
72
+ <p v-if="error" class="text-xs text-red-500" data-testid="accounting-opening-error">{{ error }}</p>
73
+ <p v-if="successMessage" class="text-xs text-green-600" data-testid="accounting-opening-success">{{ successMessage }}</p>
74
+ <div class="flex justify-end">
75
+ <button
76
+ type="submit"
77
+ class="h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50"
78
+ :disabled="!balanced || submitting"
79
+ data-testid="accounting-opening-submit"
80
+ >
81
+ {{ submitting ? t("pluginAccounting.entryForm.submitting") : t("pluginAccounting.openingForm.submit") }}
82
+ </button>
83
+ </div>
84
+ </form>
85
+ <!-- Sibling of the parent <form> on purpose: the modal renders
86
+ its own <form @submit.prevent> for the inline editor, and
87
+ nesting <form>s is invalid HTML that breaks Enter-key submit
88
+ routing in some browsers. Vue 3 multi-root templates let us
89
+ keep the markup flat with no wrapper div. -->
90
+ <AccountsModal v-if="showAccountsModal" :book-id="bookId" :accounts="accounts" @close="showAccountsModal = false" />
91
+ </template>
92
+
93
+ <script setup lang="ts">
94
+ import { computed, ref, watch } from "vue";
95
+ import { useI18n } from "vue-i18n";
96
+ import { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from "../api";
97
+ import { formatAmount, inputStepFor } from "../currencies";
98
+ import { localDateString } from "../dates";
99
+ import { useLatestRequest } from "./useLatestRequest";
100
+ import AccountsModal from "./AccountsModal.vue";
101
+
102
+ const { t } = useI18n();
103
+
104
+ const props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();
105
+ const emit = defineEmits<{ submitted: [] }>();
106
+
107
+ const showAccountsModal = ref(false);
108
+
109
+ interface OpeningRow {
110
+ debit: number | null;
111
+ credit: number | null;
112
+ }
113
+
114
+ const asOfDate = ref(localDateString());
115
+ const rows = ref<Record<string, OpeningRow>>({});
116
+ const existing = ref<JournalEntry | null>(null);
117
+ const submitting = ref(false);
118
+ const error = ref<string | null>(null);
119
+ const successMessage = ref<string | null>(null);
120
+ const { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();
121
+
122
+ const bsAccounts = computed(() =>
123
+ props.accounts.filter((account) => (account.type === "asset" || account.type === "liability" || account.type === "equity") && account.active !== false),
124
+ );
125
+
126
+ function ensureRows(): void {
127
+ for (const account of bsAccounts.value) {
128
+ if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };
129
+ }
130
+ }
131
+
132
+ function onDebitInput(code: string): void {
133
+ const row = rows.value[code];
134
+ if (row.debit !== null && row.debit !== 0) row.credit = null;
135
+ }
136
+ function onCreditInput(code: string): void {
137
+ const row = rows.value[code];
138
+ if (row.credit !== null && row.credit !== 0) row.debit = null;
139
+ }
140
+
141
+ const imbalance = computed<number>(() => {
142
+ // Iterate the live bsAccounts (already active-filtered) rather
143
+ // than rows.value keys so a row for a now-inactive account
144
+ // doesn't tilt `balanced` against what `toApiLines` will
145
+ // actually post.
146
+ let sum = 0;
147
+ for (const account of bsAccounts.value) {
148
+ const row = rows.value[account.code];
149
+ if (!row) continue;
150
+ if (typeof row.debit === "number") sum += row.debit;
151
+ if (typeof row.credit === "number") sum -= row.credit;
152
+ }
153
+ return sum;
154
+ });
155
+ // An all-empty form is valid: it submits as a zero-line opening
156
+ // marker so the user can unlock the rest of the UI without
157
+ // committing to specific balances on day one.
158
+ const balanced = computed(() => Math.abs(imbalance.value) <= 0.005);
159
+ const imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));
160
+ const step = computed(() => inputStepFor(props.currency));
161
+
162
+ function isPositiveAmount(value: unknown): value is number {
163
+ // Robust against the empty string `v-model.number` produces when
164
+ // the user clears a previously-typed field — without this, the
165
+ // skip condition `value === 0` was false for `""` and the form
166
+ // emitted ghost lines like `{accountCode: "3000"}` with no
167
+ // amount on either side.
168
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
169
+ }
170
+
171
+ function toApiLines(): JournalLine[] {
172
+ const out: JournalLine[] = [];
173
+ // Iterate the visible bsAccounts list (which already filters
174
+ // out inactive accounts) rather than `rows.value` keys. A row
175
+ // for an account that was active when the user typed amounts
176
+ // and then got deactivated mid-edit would otherwise still post —
177
+ // the row stays in the map even after the v-for stops rendering
178
+ // it, so iterating keys would silently land entries on a
179
+ // soft-deleted account.
180
+ for (const account of bsAccounts.value) {
181
+ const row = rows.value[account.code];
182
+ if (!row) continue;
183
+ const debitOk = isPositiveAmount(row.debit);
184
+ const creditOk = isPositiveAmount(row.credit);
185
+ if (!debitOk && !creditOk) continue;
186
+ const line: JournalLine = { accountCode: account.code };
187
+ if (debitOk) line.debit = row.debit as number;
188
+ if (creditOk) line.credit = row.credit as number;
189
+ out.push(line);
190
+ }
191
+ return out;
192
+ }
193
+
194
+ function freshRows(): Record<string, OpeningRow> {
195
+ const out: Record<string, OpeningRow> = {};
196
+ for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };
197
+ return out;
198
+ }
199
+
200
+ async function loadExisting(): Promise<void> {
201
+ // Always start from a fresh row map so a book without an
202
+ // opening doesn't inherit the previous book's draft values.
203
+ const token = beginLoad();
204
+ const next = freshRows();
205
+ const result = await getOpeningBalances(props.bookId);
206
+ // Drop the result if the user has switched books since this
207
+ // call started — otherwise stale rows would land on the new
208
+ // book's form.
209
+ if (!isCurrentLoad(token)) return;
210
+ if (!result.ok) {
211
+ existing.value = null;
212
+ rows.value = next;
213
+ return;
214
+ }
215
+ existing.value = result.data.opening;
216
+ if (result.data.opening) {
217
+ asOfDate.value = result.data.opening.date;
218
+ for (const line of result.data.opening.lines) {
219
+ next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };
220
+ }
221
+ } else {
222
+ asOfDate.value = localDateString();
223
+ }
224
+ rows.value = next;
225
+ }
226
+
227
+ async function onSubmit(): Promise<void> {
228
+ if (submitting.value || !balanced.value) return;
229
+ submitting.value = true;
230
+ error.value = null;
231
+ successMessage.value = null;
232
+ try {
233
+ const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });
234
+ if (!result.ok) {
235
+ error.value = result.error;
236
+ return;
237
+ }
238
+ successMessage.value = t("pluginAccounting.openingForm.success");
239
+ emit("submitted");
240
+ } catch (err) {
241
+ error.value = err instanceof Error ? err.message : String(err);
242
+ } finally {
243
+ submitting.value = false;
244
+ }
245
+ }
246
+
247
+ watch(
248
+ () => [props.bookId, props.version, props.accounts.length],
249
+ () => {
250
+ ensureRows();
251
+ void loadExisting();
252
+ },
253
+ { immediate: true },
254
+ );
255
+ </script>
256
+
257
+ <style scoped>
258
+ /* Hide the WebKit / Firefox spin buttons on amount inputs. The
259
+ step attribute still controls validation; this is purely UI.
260
+ Accounting amount fields don't benefit from a spinner — users
261
+ type the number and the up/down arrows just clutter the row. */
262
+ input[type="number"]::-webkit-outer-spin-button,
263
+ input[type="number"]::-webkit-inner-spin-button {
264
+ -webkit-appearance: none;
265
+ margin: 0;
266
+ }
267
+ input[type="number"] {
268
+ -moz-appearance: textfield;
269
+ appearance: textfield;
270
+ }
271
+ </style>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <div class="flex flex-col gap-3" data-testid="accounting-profit-loss">
3
+ <div class="flex flex-wrap items-end gap-3">
4
+ <DateRangePicker v-model="range" :fiscal-year-end="resolvedFiscalYearEnd" :opening-date="openingDate" />
5
+ <button class="h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50" @click="refresh">
6
+ <span class="material-icons text-base align-middle">refresh</span>
7
+ </button>
8
+ </div>
9
+ <p v-if="loading" class="text-xs text-gray-400">{{ t("pluginAccounting.common.loading") }}</p>
10
+ <p v-else-if="error" class="text-xs text-red-500">{{ t("pluginAccounting.common.error", { error }) }}</p>
11
+ <template v-else-if="profitLoss">
12
+ <section class="border border-gray-200 rounded p-3">
13
+ <h4 class="text-sm font-semibold mb-2">{{ t("pluginAccounting.profitLoss.income") }}</h4>
14
+ <table class="w-full text-sm">
15
+ <tbody>
16
+ <tr
17
+ v-for="row in profitLoss.income.rows"
18
+ :key="row.accountCode"
19
+ class="border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
20
+ tabindex="0"
21
+ role="button"
22
+ :aria-label="t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })"
23
+ :data-testid="`accounting-pl-row-${row.accountCode}`"
24
+ @click="onRowClick(row.accountCode)"
25
+ @keydown.enter.prevent.self="onKeyActivate($event, row.accountCode)"
26
+ @keydown.space.prevent.self="onKeyActivate($event, row.accountCode)"
27
+ >
28
+ <td class="py-1 px-1">
29
+ <span class="font-mono text-[10px] text-gray-400 mr-2">{{ row.accountCode }}</span
30
+ >{{ row.accountName }}
31
+ </td>
32
+ <td class="py-1 px-1 text-right font-mono">{{ formatAmount(row.amount) }}</td>
33
+ </tr>
34
+ </tbody>
35
+ <tfoot>
36
+ <tr class="font-semibold border-t border-gray-300">
37
+ <td class="py-1 px-1">{{ t("pluginAccounting.balanceSheet.total") }}</td>
38
+ <td class="py-1 px-1 text-right">{{ formatAmount(profitLoss.income.total) }}</td>
39
+ </tr>
40
+ </tfoot>
41
+ </table>
42
+ </section>
43
+ <section class="border border-gray-200 rounded p-3">
44
+ <h4 class="text-sm font-semibold mb-2">{{ t("pluginAccounting.profitLoss.expense") }}</h4>
45
+ <table class="w-full text-sm">
46
+ <tbody>
47
+ <tr
48
+ v-for="row in profitLoss.expense.rows"
49
+ :key="row.accountCode"
50
+ class="border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
51
+ tabindex="0"
52
+ role="button"
53
+ :aria-label="t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })"
54
+ :data-testid="`accounting-pl-row-${row.accountCode}`"
55
+ @click="onRowClick(row.accountCode)"
56
+ @keydown.enter.prevent.self="onKeyActivate($event, row.accountCode)"
57
+ @keydown.space.prevent.self="onKeyActivate($event, row.accountCode)"
58
+ >
59
+ <td class="py-1 px-1">
60
+ <span class="font-mono text-[10px] text-gray-400 mr-2">{{ row.accountCode }}</span
61
+ >{{ row.accountName }}
62
+ </td>
63
+ <td class="py-1 px-1 text-right font-mono">{{ formatAmount(row.amount) }}</td>
64
+ </tr>
65
+ </tbody>
66
+ <tfoot>
67
+ <tr class="font-semibold border-t border-gray-300">
68
+ <td class="py-1 px-1">{{ t("pluginAccounting.balanceSheet.total") }}</td>
69
+ <td class="py-1 px-1 text-right">{{ formatAmount(profitLoss.expense.total) }}</td>
70
+ </tr>
71
+ </tfoot>
72
+ </table>
73
+ </section>
74
+ <div class="flex justify-end items-center gap-2 text-sm font-semibold" data-testid="accounting-pl-net">
75
+ <span>{{ t("pluginAccounting.profitLoss.netIncome") }}</span>
76
+ <span :class="profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'">{{ formatAmount(profitLoss.netIncome) }}</span>
77
+ </div>
78
+ </template>
79
+ </div>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ import { computed, ref, watch } from "vue";
84
+ import { useI18n } from "vue-i18n";
85
+ import { getProfitLoss, type ProfitLoss } from "../api";
86
+ import { formatAmount as formatAmountWithCurrency } from "../currencies";
87
+ import { currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from "../fiscalYear";
88
+ import { useLatestRequest } from "./useLatestRequest";
89
+ import DateRangePicker from "./DateRangePicker.vue";
90
+
91
+ const { t } = useI18n();
92
+
93
+ const props = defineProps<{
94
+ bookId: string;
95
+ currency: string;
96
+ version: number;
97
+ fiscalYearEnd?: FiscalYearEnd;
98
+ /** Opening-balance date for the active book — drives the "Lifetime"
99
+ * shortcut in the date picker (from = openingDate, to = today).
100
+ * When absent, the picker hides Lifetime; "All" still works. */
101
+ openingDate?: string;
102
+ }>();
103
+
104
+ const emit = defineEmits<{ selectAccount: [code: string] }>();
105
+
106
+ const resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));
107
+
108
+ function onRowClick(code: string): void {
109
+ emit("selectAccount", code);
110
+ }
111
+
112
+ function onKeyActivate(event: KeyboardEvent, code: string): void {
113
+ if (event.repeat) return;
114
+ emit("selectAccount", code);
115
+ }
116
+
117
+ // Default = current fiscal year. Reset by the bookId/fiscalYearEnd
118
+ // watcher below so switching books or changing the FY-end in
119
+ // settings drops a stale custom range from the prior book.
120
+ const range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));
121
+ const profitLoss = ref<ProfitLoss | null>(null);
122
+ const loading = ref(false);
123
+ const error = ref<string | null>(null);
124
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
125
+
126
+ function formatAmount(value: number): string {
127
+ return formatAmountWithCurrency(value, props.currency);
128
+ }
129
+
130
+ async function refresh(): Promise<void> {
131
+ const token = beginRequest();
132
+ loading.value = true;
133
+ error.value = null;
134
+ try {
135
+ // P&L always sends a range. Empty-side gets a sentinel so "All"
136
+ // (both empty) means "every entry" rather than an empty window.
137
+ const fromBound = range.value.from || "0000-01-01";
138
+ const toBound = range.value.to || "9999-12-31";
139
+ const result = await getProfitLoss({ kind: "range", from: fromBound, to: toBound }, props.bookId);
140
+ if (!isCurrent(token)) return;
141
+ if (!result.ok) {
142
+ error.value = result.error;
143
+ profitLoss.value = null;
144
+ return;
145
+ }
146
+ profitLoss.value = result.data.profitLoss;
147
+ } finally {
148
+ if (isCurrent(token)) loading.value = false;
149
+ }
150
+ }
151
+
152
+ watch(
153
+ () => [props.bookId, resolvedFiscalYearEnd.value],
154
+ () => {
155
+ range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
156
+ },
157
+ );
158
+
159
+ watch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });
160
+ </script>
@@ -0,0 +1,13 @@
1
+ // Shared draft shape for the AccountsModal editor row. Lives in
2
+ // its own module so AccountsModal.vue and AccountEditor.vue can
3
+ // both type the prop / emit without one importing the other —
4
+ // they form a parent / child pair, not a re-export chain.
5
+
6
+ import type { AccountType } from "../api";
7
+
8
+ export interface AccountDraft {
9
+ code: string;
10
+ name: string;
11
+ type: AccountType;
12
+ note: string;
13
+ }
@@ -0,0 +1,103 @@
1
+ // Account-code numbering convention. The chart of accounts uses
2
+ // 4-digit codes whose leading digit identifies the type:
3
+ //
4
+ // 1xxx → asset
5
+ // 2xxx → liability
6
+ // 3xxx → equity
7
+ // 4xxx → income
8
+ // 5xxx → expense
9
+ //
10
+ // Within those bands, the second digit `4` is reserved for tax-
11
+ // related accounts on both sides of the balance sheet:
12
+ //
13
+ // 14xx → tax-related current assets
14
+ // (1400 Input Tax Receivable / 仮払消費税, plus future
15
+ // withholding-tax-receivable / etc. siblings)
16
+ // 24xx → tax-related current liabilities
17
+ // (2400 Sales Tax Payable / 仮受消費税, plus future
18
+ // withholding-tax-payable / etc. siblings)
19
+ //
20
+ // Special-case UI (Ledger T-number column, JournalEntryForm
21
+ // per-line tax-registration ID input) is **input-tax-only** — it
22
+ // keys off `isTaxAccountCode`, which matches 14xx (purchase side)
23
+ // only. Output-tax / sales-side lines (24xx) intentionally don't
24
+ // surface a counterparty registration field: the seller's
25
+ // obligation is to put their *own* registration number on the
26
+ // invoice they issue, not to capture the customer's. So a custom
27
+ // suspense account added in the 14xx band participates without
28
+ // any opt-in step; 24xx accounts book the liability without the
29
+ // extra column.
30
+ //
31
+ // Lives in its own module so AccountsModal, AccountEditor, and the
32
+ // validation helper can share the same constants without circular
33
+ // imports between Vue components.
34
+
35
+ import type { Account, AccountType } from "../api";
36
+
37
+ export const ACCOUNT_TYPE_PREFIX: Record<AccountType, number> = {
38
+ asset: 1,
39
+ liability: 2,
40
+ equity: 3,
41
+ income: 4,
42
+ expense: 5,
43
+ };
44
+
45
+ const TAX_ACCOUNT_PREFIXES: readonly string[] = ["14"];
46
+
47
+ /** Returns `true` for codes whose first two digits identify a
48
+ * tax-related current asset (`14xx`) — i.e. the input-tax /
49
+ * purchase side of consumption / sales / VAT bookkeeping. Drives
50
+ * Ledger column visibility and the JournalEntryForm per-line
51
+ * tax-registration ID input. Output-tax (24xx) is intentionally
52
+ * excluded: the counterparty's registration ID is only
53
+ * load-bearing for input-tax-credit eligibility on purchases. */
54
+ export function isTaxAccountCode(code: string): boolean {
55
+ return TAX_ACCOUNT_PREFIXES.some((prefix) => code.startsWith(prefix));
56
+ }
57
+
58
+ const ACCOUNT_CODE_RE = /^\d{4}$/;
59
+ const SUGGESTED_GAP = 10;
60
+
61
+ export function isValidAccountCode(code: string): boolean {
62
+ return ACCOUNT_CODE_RE.test(code);
63
+ }
64
+
65
+ export function typeForCode(code: string): AccountType | null {
66
+ if (!isValidAccountCode(code)) return null;
67
+ const leading = Number.parseInt(code[0], 10);
68
+ for (const [type, prefix] of Object.entries(ACCOUNT_TYPE_PREFIX) as [AccountType, number][]) {
69
+ if (prefix === leading) return type;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export function codeMatchesType(code: string, type: AccountType): boolean {
75
+ return typeForCode(code) === type;
76
+ }
77
+
78
+ /** Suggest the next free 4-digit code for `type`. Picks max-in-range
79
+ * + SUGGESTED_GAP so users keep room to insert sibling accounts
80
+ * later (the standard accounting convention). Falls back to the
81
+ * prefix base when the range is empty, and to max+1 when +gap would
82
+ * spill out of the 4-digit prefix window. */
83
+ export function suggestNextCode(type: AccountType, accounts: readonly Account[]): string {
84
+ const prefix = ACCOUNT_TYPE_PREFIX[type];
85
+ const inRange: number[] = [];
86
+ for (const account of accounts) {
87
+ if (!isValidAccountCode(account.code)) continue;
88
+ const value = Number.parseInt(account.code, 10);
89
+ if (Math.floor(value / 1000) !== prefix) continue;
90
+ inRange.push(value);
91
+ }
92
+ if (inRange.length === 0) return `${prefix}000`;
93
+ const max = Math.max(...inRange);
94
+ const candidate = max + SUGGESTED_GAP;
95
+ if (Math.floor(candidate / 1000) === prefix && candidate <= 9999) return String(candidate);
96
+ // Range is dense at the top — fall back to a unit step. If even
97
+ // that overflows the prefix window the chart is essentially full
98
+ // for that type; surface the overflow rather than silently
99
+ // suggesting a code in the next type's range.
100
+ const fallback = max + 1;
101
+ if (Math.floor(fallback / 1000) === prefix && fallback <= 9999) return String(fallback);
102
+ return `${prefix}999`;
103
+ }
@@ -0,0 +1,75 @@
1
+ // Pure validation for the AccountsModal editor draft. Lives in its
2
+ // own module so unit tests can exercise the boundary cases (reserved
3
+ // `_` prefix, duplicate code, empty fields) without spinning up Vue
4
+ // or i18n. The component maps the returned error code to a
5
+ // localized message.
6
+ //
7
+ // The `_`-prefix rule mirrors the server's check in
8
+ // server/accounting/service.ts:upsertAccount — codes starting with
9
+ // `_` are reserved for synthetic report rows. Catching it client-
10
+ // side avoids a round-trip and surfaces the localized message
11
+ // instead of the raw server error.
12
+
13
+ import type { Account } from "../api";
14
+ import type { AccountDraft } from "./accountDraft";
15
+ import { codeMatchesType, isValidAccountCode } from "./accountNumbering";
16
+
17
+ export const RESERVED_PREFIX = "_";
18
+
19
+ export type CodeValidationError = "emptyCode" | "reservedCode" | "invalidCodeFormat" | "codeTypeMismatch" | "duplicateCode";
20
+ export type NameValidationError = "emptyName" | "duplicateName";
21
+ export type AccountValidationError = CodeValidationError | NameValidationError;
22
+
23
+ /**
24
+ * Validate just the code field. Split out from the full draft
25
+ * validator so AccountEditor can paint a per-field red border in
26
+ * realtime without re-running the name check on every keystroke.
27
+ */
28
+ export function validateCodeField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): CodeValidationError | null {
29
+ const trimmedCode = draft.code.trim();
30
+ if (trimmedCode.length === 0) return "emptyCode";
31
+ if (trimmedCode.startsWith(RESERVED_PREFIX)) return "reservedCode";
32
+ // 4-digit numbering is enforced for new accounts only: pre-existing
33
+ // books may already hold legacy codes the user added before the
34
+ // rule landed, and changing the code would orphan their journal
35
+ // lines (codes are immutable once created — see codeReadOnlyHint).
36
+ if (isNew && !isValidAccountCode(trimmedCode)) return "invalidCodeFormat";
37
+ if (isNew && !codeMatchesType(trimmedCode, draft.type)) return "codeTypeMismatch";
38
+ if (isNew && existing.some((account) => account.code === trimmedCode)) return "duplicateCode";
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Validate just the name field. Empty + duplicate (case-insensitive,
44
+ * trimmed) against other accounts. On edit, the account being edited
45
+ * is excluded from the duplicate check via `draft.code` — otherwise
46
+ * every save would flag the user's own row as a collision.
47
+ */
48
+ export function validateNameField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): NameValidationError | null {
49
+ const trimmedName = draft.name.trim();
50
+ if (trimmedName.length === 0) return "emptyName";
51
+ const folded = trimmedName.toLowerCase();
52
+ const collides = existing.some((account) => {
53
+ if (!isNew && account.code === draft.code.trim()) return false;
54
+ return account.name.trim().toLowerCase() === folded;
55
+ });
56
+ if (collides) return "duplicateName";
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Validate a draft about to be sent to `upsertAccount`. Returns
62
+ * `null` on success or an error code on failure. Caller maps the
63
+ * code to a localized message.
64
+ *
65
+ * `existing` is the current chart of accounts — used to detect a
66
+ * duplicate code on a brand-new entry (otherwise the server would
67
+ * silently overwrite the existing account, which is rarely what
68
+ * the user typing into the "Add account" form intended).
69
+ *
70
+ * Code errors take precedence over name errors so the user fixes
71
+ * one stable issue at a time as they type.
72
+ */
73
+ export function validateAccountDraft(draft: AccountDraft, existing: readonly Account[], isNew: boolean): AccountValidationError | null {
74
+ return validateCodeField(draft, existing, isNew) ?? validateNameField(draft, existing, isNew);
75
+ }
@@ -0,0 +1,44 @@
1
+ // Stale-response guard for watcher-driven async fetches.
2
+ //
3
+ // Pattern: a watcher fires on bookId / filter / version changes
4
+ // and kicks off `apiPost(...)`. Without coordination, a slower
5
+ // earlier request can resolve after a newer one and overwrite the
6
+ // fresh state with stale data. This composable hands out a
7
+ // monotonic token before each await; the caller checks that the
8
+ // token is still current after the await before mutating state.
9
+ //
10
+ // Usage:
11
+ //
12
+ // const { begin, isCurrent } = useLatestRequest();
13
+ // async function refresh() {
14
+ // const token = begin();
15
+ // const result = await api.fetch(...);
16
+ // if (!isCurrent(token)) return; // a newer refresh started
17
+ // applyState(result);
18
+ // }
19
+ //
20
+ // Cheap and dependency-free. Each component holds its own
21
+ // `useLatestRequest()` instance — there's no shared state across
22
+ // components.
23
+
24
+ export interface LatestRequestApi {
25
+ /** Returns the token of the new request. Increments the
26
+ * internal counter; older outstanding requests will fail
27
+ * `isCurrent`. */
28
+ begin: () => number;
29
+ /** True if `token` is still the most recently issued one. */
30
+ isCurrent: (token: number) => boolean;
31
+ }
32
+
33
+ export function useLatestRequest(): LatestRequestApi {
34
+ let counter = 0;
35
+ return {
36
+ begin(): number {
37
+ counter += 1;
38
+ return counter;
39
+ },
40
+ isCurrent(token: number): boolean {
41
+ return token === counter;
42
+ },
43
+ };
44
+ }