@webship.co/workspace 0.0.0-stage → 1.0.0-beta1

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 (307) hide show
  1. package/PLUGINS.md +249 -0
  2. package/README.md +379 -2
  3. package/WORKSPACES.md +69 -0
  4. package/actions/ai-items.js +293 -0
  5. package/actions/audio.js +48 -0
  6. package/actions/build-order.js +79 -0
  7. package/actions/checks.js +95 -0
  8. package/actions/claude-session.js +104 -0
  9. package/actions/commands-center.js +258 -0
  10. package/actions/composer.js +267 -0
  11. package/actions/ddev-addons.js +90 -0
  12. package/actions/ddev-mail.js +80 -0
  13. package/actions/ddev.js +237 -0
  14. package/actions/folders.js +165 -0
  15. package/actions/images-svg.js +33 -0
  16. package/actions/images.js +145 -0
  17. package/actions/navigation.js +190 -0
  18. package/actions/node.js +222 -0
  19. package/actions/notes.js +55 -0
  20. package/actions/plugins.js +36 -0
  21. package/actions/project-make.js +28 -0
  22. package/actions/rag.js +202 -0
  23. package/actions/row-actions.js +262 -0
  24. package/actions/screens.js +270 -0
  25. package/actions/testing.js +121 -0
  26. package/actions/video-edit.js +54 -0
  27. package/ai-inventory.js +182 -0
  28. package/ai-items.js +202 -0
  29. package/assistant-cast.js +176 -0
  30. package/assistant-suggestions.js +91 -0
  31. package/assistant.js +484 -0
  32. package/audio-voice.js +528 -0
  33. package/auth-google.js +260 -0
  34. package/build-ai.js +52 -0
  35. package/build-order-page.js +191 -0
  36. package/build-order.js +313 -0
  37. package/builder-args.js +117 -0
  38. package/changelog.js +302 -0
  39. package/chat-threads.js +134 -0
  40. package/checks.js +105 -0
  41. package/claude-args.js +282 -0
  42. package/claude-sessions.js +129 -0
  43. package/command-author.js +117 -0
  44. package/command-block.js +20 -0
  45. package/commands/defaults.yml +159 -0
  46. package/commands-page.js +460 -0
  47. package/commands.js +568 -0
  48. package/cucumber.js +43 -0
  49. package/ddev-addons-page.js +124 -0
  50. package/ddev-addons.js +154 -0
  51. package/ddev-custom-commands.js +96 -0
  52. package/ddev-domains.js +84 -0
  53. package/ddev-import.js +93 -0
  54. package/ddev-mail-page.js +167 -0
  55. package/ddev-mail.js +110 -0
  56. package/ddev-options.js +145 -0
  57. package/ddev-snapshots.js +126 -0
  58. package/ddev.js +145 -0
  59. package/docs/DEVELOPER-GUIDE.md +224 -0
  60. package/docs/README.md +252 -0
  61. package/docs/REFERENCE.md +684 -0
  62. package/docs/USER-GUIDE.md +697 -0
  63. package/docs/screenshots/assistant/avatar-board.png +0 -0
  64. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  65. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  66. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  67. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  68. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  69. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  70. package/docs/screenshots/assistant/avatar-status.png +0 -0
  71. package/docs/screenshots/create-card/create-card.png +0 -0
  72. package/docs/screenshots/create-card/start-ai.png +0 -0
  73. package/docs/screenshots/guide-clone-address.png +0 -0
  74. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  75. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  76. package/docs/screenshots/guide-ddev-menu.png +0 -0
  77. package/docs/screenshots/guide-domains.png +0 -0
  78. package/docs/screenshots/guide-home.png +0 -0
  79. package/docs/screenshots/guide-ide-menu.png +0 -0
  80. package/docs/screenshots/guide-remove-refusal.png +0 -0
  81. package/docs/screenshots/guide-rows-worktree.png +0 -0
  82. package/docs/screenshots/guide-screens.png +0 -0
  83. package/docs/screenshots/guide-settings.png +0 -0
  84. package/docs/screenshots/guide-workspace-page.png +0 -0
  85. package/docs/screenshots/home/card-settings.png +0 -0
  86. package/docs/screenshots/home/drag-reorder.png +0 -0
  87. package/docs/screenshots/home/edit-card.png +0 -0
  88. package/docs/screenshots/images/claude-code-image.png +0 -0
  89. package/docs/screenshots/images/edit-with-ai.png +0 -0
  90. package/docs/screenshots/images/gallery.png +0 -0
  91. package/docs/screenshots/images/generate-description.png +0 -0
  92. package/docs/screenshots/images/generate-plan.png +0 -0
  93. package/docs/screenshots/images/providers.png +0 -0
  94. package/docs/screenshots/images/svg-form.png +0 -0
  95. package/docs/screenshots/images/svg-result.png +0 -0
  96. package/docs/screenshots/images/upload.png +0 -0
  97. package/docs/screenshots/new-folder/2-form.png +0 -0
  98. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  99. package/docs/screenshots/new-folder/files-folder.png +0 -0
  100. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  101. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  102. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  103. package/docs/screenshots/readme/dashboard-light.png +0 -0
  104. package/docs/screenshots/readme/dev-dark.png +0 -0
  105. package/docs/screenshots/readme/dev-light.png +0 -0
  106. package/docs/screenshots/readme/docs-dark.png +0 -0
  107. package/docs/screenshots/readme/docs-light.png +0 -0
  108. package/docs/screenshots/readme/icons/ai.svg +4 -0
  109. package/docs/screenshots/readme/icons/code.svg +4 -0
  110. package/docs/screenshots/readme/icons/docs.svg +4 -0
  111. package/docs/screenshots/readme/icons/services.svg +4 -0
  112. package/docs/screenshots/readme/icons/sites.svg +4 -0
  113. package/docs/screenshots/readme/test-report-dark.png +0 -0
  114. package/docs/screenshots/readme/test-report-light.png +0 -0
  115. package/docs/screenshots/readme/test-scenarios.png +0 -0
  116. package/docs/screenshots/readme/test-steps.png +0 -0
  117. package/docs/screenshots/readme/videos-dark.png +0 -0
  118. package/docs/screenshots/readme/videos-light.png +0 -0
  119. package/docs/screenshots/screens-menu.png +0 -0
  120. package/docs/screenshots/screens-new.png +0 -0
  121. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  122. package/docs/screenshots/settings/delete-1-row.png +0 -0
  123. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  124. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  125. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  126. package/event-log.js +59 -0
  127. package/files-browser.js +247 -0
  128. package/folders.js +121 -0
  129. package/format.js +73 -0
  130. package/git-info.js +81 -0
  131. package/github-auth.js +62 -0
  132. package/html.js +10 -0
  133. package/icons.js +83 -0
  134. package/images-ai.js +447 -0
  135. package/images-create.js +226 -0
  136. package/images-page.js +417 -0
  137. package/images-records.js +39 -0
  138. package/images-render.js +36 -0
  139. package/images-svg-core.js +214 -0
  140. package/images-svg-sanitize.js +188 -0
  141. package/images-svg.js +241 -0
  142. package/images.js +270 -0
  143. package/jobs.js +430 -0
  144. package/linger.js +21 -0
  145. package/lists.js +292 -0
  146. package/media-info.js +136 -0
  147. package/navigation-page.js +264 -0
  148. package/navigation.js +394 -0
  149. package/notes.js +0 -0
  150. package/package.json +47 -4
  151. package/palette-index.js +46 -0
  152. package/playwright.config.js +23 -0
  153. package/plugin-install.js +239 -0
  154. package/plugin-loader.js +466 -0
  155. package/plugin-manifest.js +83 -0
  156. package/plugin-settings.js +44 -0
  157. package/plugins/workspace-notes/index.js +59 -0
  158. package/plugins-page.js +103 -0
  159. package/project-make.js +241 -0
  160. package/project-name.js +210 -0
  161. package/public/assistant-avatar-moves.js +646 -0
  162. package/public/assistant-avatar-ui.js +1223 -0
  163. package/public/assistant-avatar.js +530 -0
  164. package/public/avatar/druplicon.vrm +0 -0
  165. package/public/css/base.css +260 -0
  166. package/public/css/components/ai-assistant.css +263 -0
  167. package/public/css/components/assistant-avatar.css +279 -0
  168. package/public/css/components/assistant-views.css +198 -0
  169. package/public/css/components/build-order.css +74 -0
  170. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  171. package/public/css/components/command-palette.css +61 -0
  172. package/public/css/components/commands-center.css +139 -0
  173. package/public/css/components/confirm-dialog.css +40 -0
  174. package/public/css/components/create-card.css +8 -0
  175. package/public/css/components/ddev-menu.css +23 -0
  176. package/public/css/components/deep-chat.css +17 -0
  177. package/public/css/components/density.css +11 -0
  178. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  179. package/public/css/components/folders.css +5 -0
  180. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  181. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  182. package/public/css/components/grid-and-list.css +203 -0
  183. package/public/css/components/help-page.css +36 -0
  184. package/public/css/components/hero.css +16 -0
  185. package/public/css/components/images.css +70 -0
  186. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  187. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  188. package/public/css/components/modal-fit.css +113 -0
  189. package/public/css/components/page-width.css +23 -0
  190. package/public/css/components/plugins-page.css +9 -0
  191. package/public/css/components/project-cards.css +256 -0
  192. package/public/css/components/project-rows.css +390 -0
  193. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  194. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  195. package/public/css/components/running-commands-as-toasts.css +327 -0
  196. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  197. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  198. package/public/css/components/sound-player.css +84 -0
  199. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  200. package/public/css/components/tab-modal.css +142 -0
  201. package/public/css/components/terminals.css +244 -0
  202. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  203. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  204. package/public/css/components/video-edit.css +6 -0
  205. package/public/css/components/web-ide-open-with.css +23 -0
  206. package/public/css/components/working-indicators.css +158 -0
  207. package/public/css/components/workspace-cards.css +79 -0
  208. package/public/favicon.ico +0 -0
  209. package/public/favicon.png +0 -0
  210. package/public/htmx.min.js +1 -0
  211. package/public/logo.png +0 -0
  212. package/public/media-player.js +215 -0
  213. package/public/mic.js +255 -0
  214. package/public/narration.js +110 -0
  215. package/public/palette.js +278 -0
  216. package/public/sortable.js +251 -0
  217. package/public/suggest.js +337 -0
  218. package/public/terminal.js +903 -0
  219. package/public/themes/default/theme.css +267 -0
  220. package/public/themes/ember/theme.css +180 -0
  221. package/public/themes/press/theme.css +290 -0
  222. package/public/themes/slate/theme.css +182 -0
  223. package/public/touch-icon.png +0 -0
  224. package/public/ui.js +2721 -0
  225. package/public/vendor/ace/LICENSE.txt +24 -0
  226. package/public/vendor/ace/ace.js +23 -0
  227. package/public/vendor/ace/ext-language_tools.js +8 -0
  228. package/public/vendor/ace/ext-searchbox.js +8 -0
  229. package/public/vendor/ace/mode-css.js +8 -0
  230. package/public/vendor/ace/mode-html.js +8 -0
  231. package/public/vendor/ace/mode-javascript.js +8 -0
  232. package/public/vendor/ace/mode-markdown.js +8 -0
  233. package/public/vendor/ace/mode-sh.js +8 -0
  234. package/public/vendor/ace/mode-yaml.js +8 -0
  235. package/public/vendor/ace/theme-textmate.js +8 -0
  236. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  237. package/public/vendor/ace/worker-yaml.js +1 -0
  238. package/public/vendor/deep-chat.LICENSE +21 -0
  239. package/public/vendor/deep-chat.bundle.js +1 -0
  240. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  241. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  242. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  243. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  244. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  245. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  246. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  248. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  249. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  251. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  252. package/public/vendor/tabler-LICENSE.txt +21 -0
  253. package/public/vendor/tabler-sprite.svg +1 -0
  254. package/public/vendor/three/LICENSE +21 -0
  255. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  256. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  257. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  258. package/public/vendor/three/three.core.min.js +6 -0
  259. package/public/vendor/three/three.module.min.js +6 -0
  260. package/public/vendor/three-vrm/LICENSE +22 -0
  261. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  262. package/public/vendor/uikit-icons.min.js +1 -0
  263. package/public/vendor/uikit.min.css +1 -0
  264. package/public/vendor/uikit.min.js +1 -0
  265. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  266. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  267. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  268. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  269. package/public/vendor/webship-icons/ui/play.svg +1 -0
  270. package/public/vendor/webship-icons/ui/share.svg +1 -0
  271. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  272. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  273. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  274. package/public/vendor/xterm/addon-fit.js +2 -0
  275. package/public/vendor/xterm/addon-unicode11.js +2 -0
  276. package/public/vendor/xterm/addon-web-links.js +2 -0
  277. package/public/vendor/xterm/xterm.css +218 -0
  278. package/public/vendor/xterm/xterm.js +2 -0
  279. package/rag.js +169 -0
  280. package/reader.js +132 -0
  281. package/repo-blobs.js +66 -0
  282. package/report-pdf.js +337 -0
  283. package/row-layout.js +1439 -0
  284. package/screenrc +37 -0
  285. package/screens.js +1063 -0
  286. package/scripts/code-server-start.sh +190 -0
  287. package/scripts/images-claude-finish.js +107 -0
  288. package/scripts/screen-host.sh +114 -0
  289. package/scripts/screen-runner.js +362 -0
  290. package/scripts/screen-then.js +47 -0
  291. package/scripts/update.sh +350 -0
  292. package/server.js +7923 -0
  293. package/settings-file.js +423 -0
  294. package/speech.js +297 -0
  295. package/tab-modal.js +75 -0
  296. package/test-reports.js +260 -0
  297. package/themes.js +381 -0
  298. package/transcripts.js +168 -0
  299. package/urls.js +13 -0
  300. package/validate.js +18 -0
  301. package/version-check.js +94 -0
  302. package/video-edit.js +588 -0
  303. package/waiting.js +85 -0
  304. package/workspace-card-tools.js +41 -0
  305. package/workspace-delete.js +139 -0
  306. package/workspace-types.js +85 -0
  307. package/workspaces.js +553 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Make a video or an audio recording about a project: the form of the row's More menu posts here.
3
+ * It starts a Claude Code session in the project through the same handler New screen uses, briefed by
4
+ * `project-make.js`, so it shows on Screens like every other session.
5
+ */
6
+
7
+ const { esc } = require('../html');
8
+ const make = require('../project-make');
9
+
10
+ async function project_make({ form, send, res }) {
11
+ const brief = make.brief(form);
12
+ if (brief.error) return send(`<div class="msg error">${esc(brief.error)}</div>`, 400);
13
+ let out = null;
14
+ await require('./screens')['/actions/screen-new']({
15
+ form: { target: brief.target, kind: 'claude', session: brief.session, command: brief.prompt },
16
+ send: (html, status = 200) => { out = { html, status }; },
17
+ res,
18
+ });
19
+ if (!out || out.status >= 400) return send((out && out.html) || '<div class="msg error">The session did not start.</div>', (out && out.status) || 500);
20
+ const name = `${brief.project ? `${brief.ws}--${brief.project}` : brief.ws}--${brief.session}`.toLowerCase();
21
+ const dest = make.KIND[brief.kind].dest;
22
+ return send(`<div class="msg assistant">
23
+ <p>The AI (Claude Code) is working${brief.project ? ` in <strong>${esc(brief.ws)}/${esc(brief.project)}</strong>` : ''}, in the session <code>${esc(name)}</code>.</p>
24
+ <p class="uk-text-meta">It writes the words first and waits for your yes. Watch and answer it on <a href="/screens">Screens</a>, or
25
+ <code>screen -r ${esc(name)}</code> in your terminal. The result lands in <a href="https://${esc(dest)}.${esc(require('../workspaces').hubDomain())}">${esc(dest)}</a>.</p></div>`);
26
+ }
27
+
28
+ module.exports = { '/actions/project-make': project_make };
package/actions/rag.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Indexing a project into Milvus, searching it, and choosing which server holds it.
3
+ *
4
+ * One module per area of the dashboard, each exporting the handlers it owns keyed by the path that
5
+ * reaches them. server.js merges the maps and dispatches; nothing here knows about routing, and
6
+ * adding an action is adding an entry rather than another branch in a 1,250-line function.
7
+ *
8
+ * Every handler takes the same context: the parsed form, the response, a `send` that writes an HTML
9
+ * fragment, and the workspace the request named.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const { esc } = require('../html');
16
+ const { humanSize, humanTime } = require('../format');
17
+ const { ROOT, loadWorkspaces, workspaceDir } = require('../workspaces');
18
+ const {
19
+ run, jobFragment, runningJobKey, startJob, ddevProjectName, listProjects,
20
+ startInScreen, screenStartedHtml, screenAction,
21
+ } = require('../jobs');
22
+ const { graphStoreDir } = require('../graphs');
23
+ const {
24
+ milvusProjectName, ragInstances, ragBindings, ragInstanceFor, ragWriteBinding,
25
+ ragCollectionName, milvusPost, ragCollections, ragInvalidate,
26
+ } = require('../rag');
27
+ const {
28
+ findTestReports, testReportFor, invalidateTestReports, testMediaHtml, testingStackOf,
29
+ } = require('../test-reports');
30
+ const { NAME_RE, SCRIPT_RE } = require('../validate');
31
+ const { commandBlock } = require('../command-block');
32
+ const { wsUrl } = require('../urls');
33
+ const { findGraphifyScript, findRagifyScript } = require('../workspaces');
34
+ const { DDEV_ACTIONS } = require('../ddev');
35
+
36
+ async function rag_mcp_command({ form, res, send, workspace, pathname }) {
37
+ const projectName = String(form.projectName || '');
38
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
39
+ const instance = ragInstanceFor(workspace, projectName);
40
+ const collection = ragCollectionName(workspace, projectName);
41
+ return send(commandBlock(
42
+ `Serve the ${instance} RAG server to the Claude Code CLI`,
43
+ [`cd ${ROOT}/rag && bash cmd-milvus-mcp.sh ${instance} -a register`,
44
+ `claude mcp add --scope user rag-${instance} -- uvx --from mcp-server-milvus==0.1.1.dev9 --with "mcp<2" mcp-server-milvus --milvus-uri http://127.0.0.1:19530`],
45
+ `Run either one <strong>on the host</strong>, in a terminal. One registration covers the whole `
46
+ + `server, so every collection on it — including <code>${esc(collection)}</code> — is searchable `
47
+ + `from the same 13 tools, and a new index needs no new registration. The workspace command is `
48
+ + `the safer of the two: it resolves the published port for you and proves the server starts `
49
+ + `before registering it.`));
50
+ }
51
+
52
+ // Bind a project to a RAG server, or unbind it back to the shared default.
53
+ async function rag_bind({ form, res, send, workspace, pathname }) {
54
+ const projectName = String(form.projectName || '');
55
+ const instance = String(form.instance || '');
56
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
57
+ if (instance && !ragInstances().includes(instance)) {
58
+ return send('<div class="msg error">No such RAG server in the rag workspace.</div>', 400);
59
+ }
60
+ ragWriteBinding(workspace, projectName, instance);
61
+ ragInvalidate();
62
+ res.setHeader('HX-Trigger', 'refresh-projects');
63
+ const where = instance || `${milvusProjectName()} (the shared default)`;
64
+ return send(`<div class="msg assistant">🔗 <strong>${esc(projectName)}</strong> now indexes into <strong>${esc(where)}</strong>.
65
+ Re-index it to fill that server — the old collection stays where it was, so drop it first if it is stale.</div>`);
66
+ }
67
+
68
+ // Build a new RAG server, and bind this project to it. Two steps deliberately kept in one
69
+ // action: a server built and then not bound is a 2 GB stack nothing uses.
70
+ async function rag_server_new({ form, res, send, workspace, pathname }) {
71
+ const projectName = String(form.projectName || '');
72
+ const name = String(form.name || '').trim();
73
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
74
+ if (!NAME_RE.test(name)) return send('<div class="msg error">A server name is letters, digits, dashes and underscores.</div>', 400);
75
+ if (ragInstances().includes(name)) return send(`<div class="msg error">A RAG server called ${esc(name)} already exists — pick it from the list instead.</div>`, 409);
76
+ const ragDir = workspaceDir('rag');
77
+ const script = 'cmd-milvus-project.sh';
78
+ if (!fs.existsSync(path.join(ragDir, script))) return send('<div class="msg error">No Milvus builder in the rag workspace.</div>', 400);
79
+ const jobKey = `milvus-build:${name}`;
80
+ if (runningJobKey(jobKey)) return send('<div class="msg error">That server is already being built.</div>', 409);
81
+ // Bound first: the build takes minutes, and the binding is what makes the row point at the new
82
+ // server the moment it answers.
83
+ ragWriteBinding(workspace, projectName, name);
84
+ ragInvalidate();
85
+ return screenAction(send, { workspace: 'rag', project: name, projectDir: ragDir, verb: 'milvus-build',
86
+ cmdLine: `bash ${JSON.stringify(script)} ${JSON.stringify(name)}`, label: `build the RAG server ${name}` });
87
+ }
88
+
89
+ // Build or rebuild a project's RAG index. The script does the work — this only guards the
90
+ // arguments and keeps two runs off one collection, since a re-index drops and recreates it and
91
+ // two of those racing would leave half of each.
92
+ async function ragify({ form, res, send, workspace, pathname }) {
93
+ const dir = workspaceDir(workspace);
94
+ const script = findRagifyScript(dir);
95
+ const projectName = String(form.projectName || '');
96
+ if (!script) return send('<div class="msg error">No ragify script in this workspace.</div>', 400);
97
+ if (!NAME_RE.test(projectName) || !listProjects(dir).includes(projectName)) {
98
+ return send('<div class="msg error">Unknown project.</div>', 400);
99
+ }
100
+ const instance = ragInstanceFor(workspace, projectName);
101
+ const { up } = await ragCollections(instance);
102
+ if (!up) {
103
+ return send(`<div class="msg error">The ${esc(instance)} RAG server is not running — start it from the <a href="${wsUrl('rag')}">RAG workspace</a> first.</div>`, 409);
104
+ }
105
+ const jobKey = `ragify:${workspace}/${projectName}`;
106
+ if (runningJobKey(jobKey)) {
107
+ return send('<div class="msg error">This project is already being indexed.</div>', 409);
108
+ }
109
+ // BM25 inside the database: no embedding model, no API key, nothing leaves the machine. Long
110
+ // timeout because a large tree is thousands of chunks.
111
+ const started = await startInScreen({ workspace, project: projectName, projectDir: dir, ownerDir: path.join(dir, projectName), verb: 'ragify',
112
+ cmdLine: `bash ${JSON.stringify(script)} ${JSON.stringify(projectName)}`, label: 'ragify' });
113
+ return send(screenStartedHtml(started, 'ragify', projectName), started.ok ? 200 : (started.already ? 409 : 500));
114
+ }
115
+
116
+ async function rag_info({ form, res, send, workspace, pathname }) {
117
+ const projectName = String(form.projectName || '');
118
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
119
+ const collection = ragCollectionName(workspace, projectName);
120
+ const instance = ragInstanceFor(workspace, projectName);
121
+ const described = await milvusPost('/v2/vectordb/collections/describe', { collectionName: collection }, 4000, instance);
122
+ const stats = await milvusPost('/v2/vectordb/collections/get_stats', { collectionName: collection }, 4000, instance);
123
+ if (!described || described.code !== 0) {
124
+ return send('<div class="msg error">Milvus did not answer for this collection — is the instance running?</div>', 409);
125
+ }
126
+ const fields = (described.data.fields || []).map((f) => f.name);
127
+ const functions = (described.data.functions || []).map((f) => `${f.name} (${f.type || 'function'})`);
128
+ const rows = stats && stats.code === 0 ? stats.data.rowCount : '?';
129
+ return send(`<div class="msg assistant">
130
+ <strong>${esc(collection)}</strong><br>
131
+ Chunks: <strong>${esc(String(rows))}</strong><br>
132
+ Fields: <code>${esc(fields.join(', '))}</code><br>
133
+ Functions: <code>${esc(functions.join(', ') || '—')}</code><br>
134
+ <span class="uk-text-meta">Retrieval is BM25, built inside Milvus — no embedding model and no API key.</span>
135
+ </div>`);
136
+ }
137
+
138
+ // Search one index. Straight over Milvus's REST API rather than through the Python worker: the
139
+ // dashboard container has no pymilvus, and full-text search on a BM25 function field takes the
140
+ // query as plain text in `data`.
141
+ async function rag_search({ form, res, send, workspace, pathname }) {
142
+ const projectName = String(form.projectName || '');
143
+ const query = String(form.query || '').trim();
144
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
145
+ if (!query) return send('<p class="uk-text-meta">Type a question first.</p>');
146
+ const collection = ragCollectionName(workspace, projectName);
147
+ const body = await milvusPost('/v2/vectordb/entities/search', {
148
+ collectionName: collection,
149
+ data: [query],
150
+ annsField: 'sparse',
151
+ limit: 8,
152
+ outputFields: ['path', 'start_line', 'end_line', 'text'],
153
+ }, 20000, ragInstanceFor(workspace, projectName));
154
+ if (!body || body.code !== 0) {
155
+ return send(`<div class="msg error">Milvus refused the search${body && body.message ? `: ${esc(body.message)}` : ''}.</div>`);
156
+ }
157
+ const hits = body.data || [];
158
+ if (!hits.length) return send(`<p class="uk-text-meta">No passage in <code>${esc(collection)}</code> matches ${esc(query)}.</p>`);
159
+ const cards = hits.map((h) => {
160
+ // The path is indexed as the first line of the chunk (so a filename matches even when the
161
+ // words are not in the file) — show the body, not that duplicate.
162
+ const text = String(h.text || '');
163
+ const nl = text.indexOf('\n');
164
+ const bodyText = nl === -1 ? text : text.slice(nl + 1);
165
+ return `<div class="uk-card uk-card-default uk-card-small uk-card-body uk-margin-small">
166
+ <div class="uk-text-bold">${esc(h.path)}<span class="uk-text-meta">:${esc(String(h.start_line))}-${esc(String(h.end_line))}</span>
167
+ <span class="uk-label">score ${esc(String(Math.round((h.distance || 0) * 100) / 100))}</span></div>
168
+ <pre class="uk-margin-small-top" style="max-height:16rem;overflow:auto"><code>${esc(bodyText.split('\n').slice(0, 24).join('\n'))}</code></pre>
169
+ </div>`;
170
+ }).join('');
171
+ return send(`<p class="uk-text-meta">${hits.length} passages for ${esc(query)} in <code>${esc(collection)}</code>, best first.</p>${cards}`);
172
+ }
173
+
174
+ // Drop one index. Regenerable by definition — a re-index rebuilds it from the tree — and the
175
+ // project itself is never touched: nothing of the index lives in it.
176
+ async function rag_remove({ form, res, send, workspace, pathname }) {
177
+ if (form.confirm !== 'yes') return send('<div class="msg error">Deleting requires confirmation.</div>', 400);
178
+ const projectName = String(form.projectName || '');
179
+ if (!NAME_RE.test(projectName)) return send('<div class="msg error">Unknown project.</div>', 400);
180
+ if (runningJobKey(`ragify:${workspace}/${projectName}`)) {
181
+ return send('<div class="msg error">An index run is in progress for this project — wait for it to finish.</div>', 409);
182
+ }
183
+ const collection = ragCollectionName(workspace, projectName);
184
+ const body = await milvusPost('/v2/vectordb/collections/drop', { collectionName: collection }, 4000,
185
+ ragInstanceFor(workspace, projectName));
186
+ if (!body || body.code !== 0) {
187
+ return send('<div class="msg error">Milvus did not drop the collection — is the instance running?</div>', 409);
188
+ }
189
+ ragInvalidate();
190
+ res.setHeader('HX-Trigger', 'refresh-projects');
191
+ return send(`<div class="msg assistant">🗑️ Deleted the index <strong>${esc(collection)}</strong>. Rebuild it any time from the RAG menu.</div>`);
192
+ }
193
+
194
+ module.exports = {
195
+ '/actions/rag-mcp-command': rag_mcp_command,
196
+ '/actions/rag-bind': rag_bind,
197
+ '/actions/rag-server-new': rag_server_new,
198
+ '/actions/ragify': ragify,
199
+ '/actions/rag-info': rag_info,
200
+ '/actions/rag-search': rag_search,
201
+ '/actions/rag-remove': rag_remove,
202
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Commands → Buttons (rows management), and running the actions it lets someone add.
3
+ *
4
+ * Every change answers with the editor re-rendered, on the tab it came from. Each form carries the file's
5
+ * mtime from when it was drawn, and a write refuses with 409 when the file changed since — another tab,
6
+ * or a hand edit — rather than silently writing over it. An action change made from the Rows tab carries
7
+ * the rest of that form, so a drag not yet saved is not lost by adding, editing or deleting an action.
8
+ *
9
+ * An action is run by id only. Its steps come from the file, never from the request; the only things a
10
+ * request carries into a run are option values, each checked against its option and single-quoted.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const { esc } = require('../html');
17
+ const { workspaceDir } = require('../workspaces');
18
+ const { screenStartedHtml, screenSessionName } = require('../jobs');
19
+ const { NAME_RE } = require('../validate');
20
+ const { lingerTail } = require('../linger');
21
+ const { hasOwnGit } = require('../git-info');
22
+ const { startUnlessRunning, terminalLink, isDdevProject, hasComposerJson, SAFE_PATH_RE } = require('./composer');
23
+ const { hasPackageJson } = require('./node');
24
+ const rowLayout = require('../row-layout');
25
+ const catalog = require('../commands');
26
+
27
+ const paneOf = (form) => (['0', '1', '2'].includes(String(form.pane)) ? Number(form.pane) : 0);
28
+ // The card someone was working in stays open when the editor comes back.
29
+ const editor = (send, form, message, code = 200) => send(rowLayout.rowActionsEditorHtml(message, paneOf(form),
30
+ { openButton: String(form.open_button || '') }), code);
31
+ const ok = (text) => `<div class="msg assistant">✅ ${text}</div>`;
32
+ const bad = (text) => `<div class="msg error">${text}</div>`;
33
+
34
+ /** Write, or answer with why not. True when it was written. */
35
+ function write(send, form, fn) {
36
+ try {
37
+ fn();
38
+ return true;
39
+ } catch (err) {
40
+ if (err && err.code === 'CHANGED') {
41
+ editor(send, form, bad('Row actions changed since this page was loaded (another tab, or a hand edit), so nothing was saved. This is the current state.'), 409);
42
+ } else {
43
+ editor(send, form, bad(`Could not save: ${esc(String(err && err.message || err))}`), 500);
44
+ }
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /** The layout with whatever the Rows form carried applied to it (unchanged when the request had no form). */
50
+ function arranged(form) {
51
+ const layout = rowLayout.loadRowLayoutFresh();
52
+ return rowLayout.arrangementFromForm(layout, form);
53
+ }
54
+
55
+ // One step a line. A line `use: <command id>` runs that command of the catalog; any other line is a command line.
56
+ const stepsFrom = (text) => String(text || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
57
+ .map((l) => { const m = /^use:\s*(\S+)$/.exec(l); return m ? { use: m[1] } : l; });
58
+
59
+ async function arrange_save({ form, send }) {
60
+ const next = rowLayout.normalizeLayout(arranged(form));
61
+ if (!write(send, form, () => rowLayout.saveRowLayout(next, form.mtime))) return undefined;
62
+ return editor(send, form, ok('Rows saved. Reload a workspace page to see them.'));
63
+ }
64
+
65
+ async function rows_op({ form, send }) {
66
+ const doc = arranged(form);
67
+ const i = Number(form.index);
68
+ const op = String(form.op || '');
69
+ if (op === 'add') {
70
+ if (doc.rows.length >= rowLayout.MAX_ROWS) return editor(send, form, bad(`Up to ${rowLayout.MAX_ROWS} rows.`), 400);
71
+ doc.rows.push([]);
72
+ } else if (!Number.isInteger(i) || i < 0 || i >= doc.rows.length) {
73
+ return editor(send, form, bad('No such row.'), 400);
74
+ } else if (op === 'remove') {
75
+ if (doc.rows.length === 1) return editor(send, form, bad('A project row keeps at least one row.'), 400);
76
+ const [gone] = doc.rows.splice(i, 1);
77
+ doc.rows[Math.max(0, i - 1)].push(...gone);
78
+ } else if (op === 'up' || op === 'down') {
79
+ const j = op === 'up' ? i - 1 : i + 1;
80
+ if (j < 0 || j >= doc.rows.length) return editor(send, form, bad('That row cannot move further.'), 400);
81
+ [doc.rows[i], doc.rows[j]] = [doc.rows[j], doc.rows[i]];
82
+ } else {
83
+ return editor(send, form, bad('Unknown row operation.'), 400);
84
+ }
85
+ if (!write(send, form, () => rowLayout.saveRowLayout(rowLayout.normalizeLayout(doc), form.mtime))) return undefined;
86
+ const said = { add: 'Row added', remove: 'Row removed — its buttons moved to the row above', up: 'Row moved up', down: 'Row moved down' }[op];
87
+ return editor(send, form, ok(`${said}. Reload a workspace page to see it.`));
88
+ }
89
+
90
+ async function custom_add({ form, send }) {
91
+ const doc = arranged(form);
92
+ const key = form.for_button === '__new__' ? 'new' : String(form.for_button || '');
93
+ const field = (n) => (form[`add_${n}_${key}`] === undefined ? '' : String(form[`add_${n}_${key}`]));
94
+ // A command picked with nothing typed is the whole action: it takes that command's label, group and node filter.
95
+ const typed = stepsFrom(field('steps'));
96
+ const use = field('use').trim();
97
+ const alone = use && !typed.length ? catalog.findCommand(use) : null;
98
+ const label = field('label').trim() || (alone ? alone.label : '');
99
+ const taken = new Set(doc.custom.map((c) => c.id));
100
+ const base = rowLayout.slug(label);
101
+ let id = base;
102
+ for (let n = 2; taken.has(id); n += 1) id = `${base}-${n}`;
103
+ const entry = rowLayout.cleanCustom({
104
+ id, label,
105
+ button: key === 'new' ? field('button') : key,
106
+ group: field('group').trim() || (alone ? alone.group : 'Custom'),
107
+ when: alone && (!field('when') || field('when') === 'any') ? alone.when : (field('when') || 'any'),
108
+ steps: use ? [...typed, { use }] : typed,
109
+ keepgoing: field('keepgoing') === '1',
110
+ confirm: field('confirm') === '1',
111
+ open: field('open'),
112
+ });
113
+ const why = rowLayout.customRefusal(entry) || rowLayout.useRefusal(entry);
114
+ if (why) return editor(send, form, bad(`Not added: ${esc(why)}.`), 400);
115
+ doc.custom.push(rowLayout.customToDoc(entry));
116
+ if (!write(send, form, () => rowLayout.saveRowLayout(rowLayout.normalizeLayout(doc), form.mtime))) return undefined;
117
+ return editor(send, form, ok(`Added ${esc(entry.label)} to ${esc(rowLayout.buttonLabel(entry.button))}. Reload a workspace page to see it.`));
118
+ }
119
+
120
+ async function custom_save({ form, send }) {
121
+ const doc = arranged(form);
122
+ const id = String(form.id || '');
123
+ const at = doc.custom.findIndex((c) => c.id === id);
124
+ if (at < 0) return editor(send, form, bad('No such action — it may have been deleted.'), 404);
125
+ const old = rowLayout.cleanCustom(doc.custom[at]);
126
+ const field = (n) => (form[`edit_${n}_${id}`] === undefined ? '' : String(form[`edit_${n}_${id}`]));
127
+ // A step whose text did not change keeps its on_error; options are kept whole (Definition edits those).
128
+ const steps = stepsFrom(field('steps')).map((step, i) => {
129
+ const was = old.steps[i];
130
+ const text = typeof step === 'string' ? step : `use: ${step.use}`;
131
+ if (!was || !was.onError || rowLayout.stepText(was) !== text) return step;
132
+ return typeof step === 'string' ? { command: step, on_error: was.onError } : { use: step.use, on_error: was.onError };
133
+ });
134
+ const entry = rowLayout.cleanCustom({
135
+ ...rowLayout.customToDoc(old),
136
+ command: undefined,
137
+ label: field('label').trim(),
138
+ group: field('group').trim() || 'Custom',
139
+ when: field('when').trim().split(/\s+/).filter(Boolean),
140
+ steps,
141
+ keepgoing: field('keepgoing') === '1',
142
+ confirm: field('confirm') === '1',
143
+ open: field('open'),
144
+ });
145
+ const why = rowLayout.customRefusal(entry) || rowLayout.useRefusal(entry);
146
+ if (why) return editor(send, form, bad(`Not saved: ${esc(why)}.`), 400);
147
+ doc.custom[at] = rowLayout.customToDoc(entry);
148
+ if (!write(send, form, () => rowLayout.saveRowLayout(rowLayout.normalizeLayout(doc), form.mtime))) return undefined;
149
+ return editor(send, form, ok(`Saved ${esc(entry.label)}. Reload a workspace page to see it.`));
150
+ }
151
+
152
+ async function custom_remove({ form, send }) {
153
+ const doc = arranged(form);
154
+ const id = String(form.id || '');
155
+ if (!doc.custom.some((c) => c.id === id)) return editor(send, form, bad('No such action — only actions you added can be deleted.'), 404);
156
+ doc.custom = doc.custom.filter((c) => c.id !== id);
157
+ if (!write(send, form, () => rowLayout.saveRowLayout(rowLayout.normalizeLayout(doc), form.mtime))) return undefined;
158
+ return editor(send, form, ok('Action deleted.'));
159
+ }
160
+
161
+ async function layout_reset({ form, send }) {
162
+ if (!write(send, form, () => rowLayout.resetRowLayout(form.mtime))) return undefined;
163
+ return editor(send, form, ok('Back to the defaults.'));
164
+ }
165
+
166
+ function reportHtml(report) {
167
+ return `${report.errors.map((e) => bad(esc(e))).join('')}${report.notes.length
168
+ ? `<div class="msg assistant">${report.errors.length ? '' : '✅ '}${report.notes.map(esc).join(' ')}</div>` : ''}`;
169
+ }
170
+
171
+ async function definition_check({ form, send }) {
172
+ const report = rowLayout.checkDefinition(form.definition, String(form.mode || 'replace'), rowLayout.loadRowLayoutFresh());
173
+ if (!report.errors.length) report.notes.unshift('It loads cleanly.');
174
+ return send(reportHtml(report), report.errors.length ? 400 : 200);
175
+ }
176
+
177
+ async function definition_save({ form, send }) {
178
+ const report = rowLayout.checkDefinition(form.definition, String(form.mode || 'replace'), rowLayout.loadRowLayoutFresh());
179
+ if (report.errors.length) return editor(send, { ...form, pane: '2' }, `${bad('Nothing was saved:')}${reportHtml(report)}`, 400);
180
+ if (!write(send, { ...form, pane: '2' }, () => rowLayout.saveRowLayout(report.next, form.mtime))) return undefined;
181
+ return editor(send, { ...form, pane: '2' }, ok(`Loaded. ${report.notes.map(esc).join(' ')}`));
182
+ }
183
+
184
+ /** The action and the folder it would run in, or an answer saying why not. */
185
+ function actionTarget({ form, send, workspace }) {
186
+ const projectName = String(form.projectName || '');
187
+ if (!NAME_RE.test(projectName)) { send(bad('Invalid project name.'), 400); return { done: true }; }
188
+ const entry = rowLayout.loadRowLayout().custom.find((c) => c.id === String(form.id || ''));
189
+ if (!entry) { send(bad('No such action — it may have been deleted in Commands → Buttons.'), 404); return { done: true }; }
190
+ const why = rowLayout.customRefusal(entry);
191
+ if (why) { send(bad(`Not running ${esc(entry.label)}: ${esc(why)}.`), 400); return { done: true }; }
192
+ const projectDir = path.join(workspaceDir(workspace), projectName);
193
+ let isDir = false;
194
+ try { isDir = fs.statSync(projectDir).isDirectory(); } catch (_) { /* not there */ }
195
+ if (!isDir) { send(bad(`No folder called ${esc(projectName)} here.`), 400); return { done: true }; }
196
+ if (!SAFE_PATH_RE.test(projectDir)) { send(bad('Not running it: the folder path has characters a shell line cannot carry safely.'), 400); return { done: true }; }
197
+ const ctx = { key: workspace, isDdev: isDdevProject(projectDir), hasComposer: hasComposerJson(projectDir), hasNode: hasPackageJson(projectDir), hasGit: hasOwnGit(projectDir) };
198
+ if (!rowLayout.whenMatches(entry.when, ctx)) { send(bad(`${esc(entry.label)} is only for ${esc(rowLayout.describeWhen(entry.when))}.`), 400); return { done: true }; }
199
+ const used = rowLayout.useRefusal(entry);
200
+ if (used) { send(bad(`Not running ${esc(entry.label)}: ${esc(used)}.`), 400); return { done: true }; }
201
+ // A command a step uses keeps its own node filter: the action's filter does not widen it.
202
+ for (const { cmd } of rowLayout.usedCommands(entry)) {
203
+ if (!rowLayout.whenMatches(cmd.when, ctx)) {
204
+ send(bad(`${esc(entry.label)} uses ${esc(cmd.label)}, which is only for ${esc(rowLayout.describeWhen(cmd.when))}.`), 400);
205
+ return { done: true };
206
+ }
207
+ }
208
+ return { entry, projectName, projectDir };
209
+ }
210
+
211
+ async function custom_options(args) {
212
+ const t = actionTarget(args);
213
+ if (t.done) return undefined;
214
+ return args.send(rowLayout.actionOptionsFormHtml(t.entry, { workspace: args.workspace, project: t.projectName }));
215
+ }
216
+
217
+ async function custom_run(args) {
218
+ const { form, send, workspace } = args;
219
+ const t = actionTarget(args);
220
+ if (t.done) return undefined;
221
+ const { entry, projectName, projectDir } = t;
222
+ const options = rowLayout.effectiveOptions(entry);
223
+ if (options.length && form.submitted !== '1') {
224
+ return send(bad(`${esc(entry.label)} asks for ${options.length === 1 ? 'an option' : 'options'} — run it from its menu, which opens the form.`), 400);
225
+ }
226
+ const { values, error } = rowLayout.resolveOptions({ ...entry, options }, form);
227
+ if (error) return send(bad(`Not running ${esc(entry.label)}: ${esc(error)}.`), 400);
228
+ const expanded = catalog.expandSteps(entry, { workspace, project: projectName, projectDir });
229
+ if (expanded.error) return send(bad(`Not running ${esc(entry.label)}: ${esc(expanded.error)}.`), 400);
230
+
231
+ const verb = `custom-${entry.id}`;
232
+ // The last run of this action here is about to have its log truncated: write down how it ended first.
233
+ rowLayout.settleRun(screenSessionName(projectName, verb, workspace));
234
+ const started = await startUnlessRunning({ workspace, project: projectName, projectDir, verb,
235
+ cmdLine: `${rowLayout.actionCommandLine(expanded.entry, values)}; ${lingerTail(entry.label)}`, label: entry.label });
236
+ if (started.stillRunning) {
237
+ return send(bad(`<code>${esc(entry.label)}</code> is still running on <strong>${esc(projectName)}</strong> — ${terminalLink(started.session, 'open it')}. Left alone.`), 409);
238
+ }
239
+ if (!started.ok) return send(screenStartedHtml(started, entry.label, projectName), started.already ? 409 : 500);
240
+ rowLayout.recordRun({ at: new Date().toISOString(), id: entry.id, label: entry.label, workspace, project: projectName, session: started.session });
241
+
242
+ if (entry.open === 'screen') {
243
+ // Its terminal, at once, in the dashboard's one modal — the same terminal the SCREENS menu opens.
244
+ // Nothing visible lands in the page: this only fetches the terminal into the modal and shows it.
245
+ return send(`<div hidden hx-get="/fragments/screen-term/${encodeURIComponent(started.session)}" hx-trigger="load" hx-target="#editor-modal-body" hx-swap="innerHTML"
246
+ hx-on::after:request="UIkit.modal('#editor-modal').show()"></div>`);
247
+ }
248
+ return send(`<div class="msg assistant flash-now"><code>${esc(entry.label)}</code> on <strong>${esc(projectName)}</strong> — ${terminalLink(started.session, 'open the output')}</div>`);
249
+ }
250
+
251
+ module.exports = {
252
+ '/actions/row-arrange-save': arrange_save,
253
+ '/actions/row-rows': rows_op,
254
+ '/actions/row-custom-add': custom_add,
255
+ '/actions/row-custom-save': custom_save,
256
+ '/actions/row-custom-remove': custom_remove,
257
+ '/actions/row-layout-reset': layout_reset,
258
+ '/actions/row-definition-check': definition_check,
259
+ '/actions/row-definition-save': definition_save,
260
+ '/actions/row-custom-options': custom_options,
261
+ '/actions/row-custom-run': custom_run,
262
+ };