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

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 (311) 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/graphs.js +107 -0
  16. package/actions/images-svg.js +33 -0
  17. package/actions/images.js +145 -0
  18. package/actions/navigation.js +190 -0
  19. package/actions/node.js +222 -0
  20. package/actions/notes.js +55 -0
  21. package/actions/obsidian.js +59 -0
  22. package/actions/plugins.js +36 -0
  23. package/actions/project-make.js +28 -0
  24. package/actions/rag.js +202 -0
  25. package/actions/row-actions.js +262 -0
  26. package/actions/screens.js +270 -0
  27. package/actions/testing.js +121 -0
  28. package/actions/video-edit.js +54 -0
  29. package/ai-inventory.js +182 -0
  30. package/ai-items.js +202 -0
  31. package/assistant-cast.js +176 -0
  32. package/assistant-suggestions.js +91 -0
  33. package/assistant.js +484 -0
  34. package/audio-voice.js +528 -0
  35. package/auth-google.js +260 -0
  36. package/build-ai.js +52 -0
  37. package/build-order-page.js +191 -0
  38. package/build-order.js +313 -0
  39. package/builder-args.js +117 -0
  40. package/changelog.js +302 -0
  41. package/chat-threads.js +134 -0
  42. package/checks.js +105 -0
  43. package/claude-args.js +282 -0
  44. package/claude-sessions.js +129 -0
  45. package/command-author.js +117 -0
  46. package/command-block.js +20 -0
  47. package/commands/defaults.yml +159 -0
  48. package/commands-page.js +460 -0
  49. package/commands.js +568 -0
  50. package/cucumber.js +43 -0
  51. package/ddev-addons-page.js +124 -0
  52. package/ddev-addons.js +154 -0
  53. package/ddev-custom-commands.js +96 -0
  54. package/ddev-domains.js +84 -0
  55. package/ddev-import.js +93 -0
  56. package/ddev-mail-page.js +167 -0
  57. package/ddev-mail.js +110 -0
  58. package/ddev-options.js +145 -0
  59. package/ddev-snapshots.js +126 -0
  60. package/ddev.js +145 -0
  61. package/docs/DEVELOPER-GUIDE.md +224 -0
  62. package/docs/README.md +252 -0
  63. package/docs/REFERENCE.md +684 -0
  64. package/docs/USER-GUIDE.md +697 -0
  65. package/docs/screenshots/assistant/avatar-board.png +0 -0
  66. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  67. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  68. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  69. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  70. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  71. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  72. package/docs/screenshots/assistant/avatar-status.png +0 -0
  73. package/docs/screenshots/create-card/create-card.png +0 -0
  74. package/docs/screenshots/create-card/start-ai.png +0 -0
  75. package/docs/screenshots/guide-clone-address.png +0 -0
  76. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  77. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  78. package/docs/screenshots/guide-ddev-menu.png +0 -0
  79. package/docs/screenshots/guide-domains.png +0 -0
  80. package/docs/screenshots/guide-home.png +0 -0
  81. package/docs/screenshots/guide-ide-menu.png +0 -0
  82. package/docs/screenshots/guide-remove-refusal.png +0 -0
  83. package/docs/screenshots/guide-rows-worktree.png +0 -0
  84. package/docs/screenshots/guide-screens.png +0 -0
  85. package/docs/screenshots/guide-settings.png +0 -0
  86. package/docs/screenshots/guide-workspace-page.png +0 -0
  87. package/docs/screenshots/home/card-settings.png +0 -0
  88. package/docs/screenshots/home/drag-reorder.png +0 -0
  89. package/docs/screenshots/home/edit-card.png +0 -0
  90. package/docs/screenshots/images/claude-code-image.png +0 -0
  91. package/docs/screenshots/images/edit-with-ai.png +0 -0
  92. package/docs/screenshots/images/gallery.png +0 -0
  93. package/docs/screenshots/images/generate-description.png +0 -0
  94. package/docs/screenshots/images/generate-plan.png +0 -0
  95. package/docs/screenshots/images/providers.png +0 -0
  96. package/docs/screenshots/images/svg-form.png +0 -0
  97. package/docs/screenshots/images/svg-result.png +0 -0
  98. package/docs/screenshots/images/upload.png +0 -0
  99. package/docs/screenshots/new-folder/2-form.png +0 -0
  100. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  101. package/docs/screenshots/new-folder/files-folder.png +0 -0
  102. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  103. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  104. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  105. package/docs/screenshots/readme/dashboard-light.png +0 -0
  106. package/docs/screenshots/readme/dev-dark.png +0 -0
  107. package/docs/screenshots/readme/dev-light.png +0 -0
  108. package/docs/screenshots/readme/docs-dark.png +0 -0
  109. package/docs/screenshots/readme/docs-light.png +0 -0
  110. package/docs/screenshots/readme/icons/ai.svg +4 -0
  111. package/docs/screenshots/readme/icons/code.svg +4 -0
  112. package/docs/screenshots/readme/icons/docs.svg +4 -0
  113. package/docs/screenshots/readme/icons/services.svg +4 -0
  114. package/docs/screenshots/readme/icons/sites.svg +4 -0
  115. package/docs/screenshots/readme/test-report-dark.png +0 -0
  116. package/docs/screenshots/readme/test-report-light.png +0 -0
  117. package/docs/screenshots/readme/test-scenarios.png +0 -0
  118. package/docs/screenshots/readme/test-steps.png +0 -0
  119. package/docs/screenshots/readme/videos-dark.png +0 -0
  120. package/docs/screenshots/readme/videos-light.png +0 -0
  121. package/docs/screenshots/screens-menu.png +0 -0
  122. package/docs/screenshots/screens-new.png +0 -0
  123. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  124. package/docs/screenshots/settings/delete-1-row.png +0 -0
  125. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  126. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  127. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  128. package/event-log.js +59 -0
  129. package/files-browser.js +247 -0
  130. package/folders.js +121 -0
  131. package/format.js +73 -0
  132. package/git-info.js +81 -0
  133. package/github-auth.js +62 -0
  134. package/graphs.js +104 -0
  135. package/html.js +10 -0
  136. package/icons.js +83 -0
  137. package/images-ai.js +447 -0
  138. package/images-create.js +226 -0
  139. package/images-page.js +417 -0
  140. package/images-records.js +39 -0
  141. package/images-render.js +36 -0
  142. package/images-svg-core.js +214 -0
  143. package/images-svg-sanitize.js +188 -0
  144. package/images-svg.js +241 -0
  145. package/images.js +270 -0
  146. package/jobs.js +430 -0
  147. package/linger.js +21 -0
  148. package/lists.js +292 -0
  149. package/media-info.js +136 -0
  150. package/navigation-page.js +264 -0
  151. package/navigation.js +394 -0
  152. package/notes.js +0 -0
  153. package/obsidian.js +405 -0
  154. package/package.json +47 -4
  155. package/palette-index.js +46 -0
  156. package/playwright.config.js +23 -0
  157. package/plugin-install.js +239 -0
  158. package/plugin-loader.js +448 -0
  159. package/plugin-manifest.js +83 -0
  160. package/plugin-settings.js +44 -0
  161. package/plugins/workspace-notes/index.js +59 -0
  162. package/plugins-page.js +103 -0
  163. package/project-make.js +241 -0
  164. package/project-name.js +210 -0
  165. package/public/assistant-avatar-moves.js +646 -0
  166. package/public/assistant-avatar-ui.js +1223 -0
  167. package/public/assistant-avatar.js +530 -0
  168. package/public/avatar/druplicon.vrm +0 -0
  169. package/public/css/base.css +260 -0
  170. package/public/css/components/ai-assistant.css +263 -0
  171. package/public/css/components/assistant-avatar.css +279 -0
  172. package/public/css/components/assistant-views.css +198 -0
  173. package/public/css/components/build-order.css +74 -0
  174. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  175. package/public/css/components/command-palette.css +61 -0
  176. package/public/css/components/commands-center.css +139 -0
  177. package/public/css/components/confirm-dialog.css +40 -0
  178. package/public/css/components/create-card.css +8 -0
  179. package/public/css/components/ddev-menu.css +23 -0
  180. package/public/css/components/deep-chat.css +17 -0
  181. package/public/css/components/density.css +11 -0
  182. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  183. package/public/css/components/folders.css +5 -0
  184. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  185. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  186. package/public/css/components/grid-and-list.css +203 -0
  187. package/public/css/components/help-page.css +36 -0
  188. package/public/css/components/hero.css +16 -0
  189. package/public/css/components/images.css +70 -0
  190. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  191. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  192. package/public/css/components/modal-fit.css +113 -0
  193. package/public/css/components/page-width.css +23 -0
  194. package/public/css/components/plugins-page.css +9 -0
  195. package/public/css/components/project-cards.css +256 -0
  196. package/public/css/components/project-rows.css +390 -0
  197. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  198. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  199. package/public/css/components/running-commands-as-toasts.css +327 -0
  200. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  201. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  202. package/public/css/components/sound-player.css +84 -0
  203. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  204. package/public/css/components/tab-modal.css +142 -0
  205. package/public/css/components/terminals.css +244 -0
  206. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  207. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  208. package/public/css/components/video-edit.css +6 -0
  209. package/public/css/components/web-ide-open-with.css +23 -0
  210. package/public/css/components/working-indicators.css +158 -0
  211. package/public/css/components/workspace-cards.css +79 -0
  212. package/public/favicon.ico +0 -0
  213. package/public/favicon.png +0 -0
  214. package/public/htmx.min.js +1 -0
  215. package/public/logo.png +0 -0
  216. package/public/media-player.js +215 -0
  217. package/public/mic.js +255 -0
  218. package/public/narration.js +110 -0
  219. package/public/palette.js +278 -0
  220. package/public/sortable.js +251 -0
  221. package/public/suggest.js +337 -0
  222. package/public/terminal.js +903 -0
  223. package/public/themes/default/theme.css +267 -0
  224. package/public/themes/ember/theme.css +180 -0
  225. package/public/themes/press/theme.css +290 -0
  226. package/public/themes/slate/theme.css +182 -0
  227. package/public/touch-icon.png +0 -0
  228. package/public/ui.js +2721 -0
  229. package/public/vendor/ace/LICENSE.txt +24 -0
  230. package/public/vendor/ace/ace.js +23 -0
  231. package/public/vendor/ace/ext-language_tools.js +8 -0
  232. package/public/vendor/ace/ext-searchbox.js +8 -0
  233. package/public/vendor/ace/mode-css.js +8 -0
  234. package/public/vendor/ace/mode-html.js +8 -0
  235. package/public/vendor/ace/mode-javascript.js +8 -0
  236. package/public/vendor/ace/mode-markdown.js +8 -0
  237. package/public/vendor/ace/mode-sh.js +8 -0
  238. package/public/vendor/ace/mode-yaml.js +8 -0
  239. package/public/vendor/ace/theme-textmate.js +8 -0
  240. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  241. package/public/vendor/ace/worker-yaml.js +1 -0
  242. package/public/vendor/deep-chat.LICENSE +21 -0
  243. package/public/vendor/deep-chat.bundle.js +1 -0
  244. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  245. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  246. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  248. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  249. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  251. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  252. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  253. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  254. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  255. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  256. package/public/vendor/tabler-LICENSE.txt +21 -0
  257. package/public/vendor/tabler-sprite.svg +1 -0
  258. package/public/vendor/three/LICENSE +21 -0
  259. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  260. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  261. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  262. package/public/vendor/three/three.core.min.js +6 -0
  263. package/public/vendor/three/three.module.min.js +6 -0
  264. package/public/vendor/three-vrm/LICENSE +22 -0
  265. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  266. package/public/vendor/uikit-icons.min.js +1 -0
  267. package/public/vendor/uikit.min.css +1 -0
  268. package/public/vendor/uikit.min.js +1 -0
  269. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  270. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  271. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  272. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  273. package/public/vendor/webship-icons/ui/play.svg +1 -0
  274. package/public/vendor/webship-icons/ui/share.svg +1 -0
  275. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  276. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  277. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  278. package/public/vendor/xterm/addon-fit.js +2 -0
  279. package/public/vendor/xterm/addon-unicode11.js +2 -0
  280. package/public/vendor/xterm/addon-web-links.js +2 -0
  281. package/public/vendor/xterm/xterm.css +218 -0
  282. package/public/vendor/xterm/xterm.js +2 -0
  283. package/rag.js +169 -0
  284. package/reader.js +132 -0
  285. package/repo-blobs.js +66 -0
  286. package/report-pdf.js +337 -0
  287. package/row-layout.js +1440 -0
  288. package/screenrc +37 -0
  289. package/screens.js +1063 -0
  290. package/scripts/code-server-start.sh +190 -0
  291. package/scripts/images-claude-finish.js +107 -0
  292. package/scripts/screen-host.sh +114 -0
  293. package/scripts/screen-runner.js +362 -0
  294. package/scripts/screen-then.js +47 -0
  295. package/scripts/update.sh +350 -0
  296. package/server.js +8140 -0
  297. package/settings-file.js +423 -0
  298. package/speech.js +297 -0
  299. package/tab-modal.js +75 -0
  300. package/test-reports.js +260 -0
  301. package/themes.js +381 -0
  302. package/transcripts.js +168 -0
  303. package/urls.js +13 -0
  304. package/validate.js +18 -0
  305. package/version-check.js +94 -0
  306. package/video-edit.js +588 -0
  307. package/waiting.js +85 -0
  308. package/workspace-card-tools.js +41 -0
  309. package/workspace-delete.js +139 -0
  310. package/workspace-types.js +85 -0
  311. package/workspaces.js +557 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * "@" and "/" suggestions in any prompt field, the way the Claude Code CLI offers them.
3
+ *
4
+ * The dashboard already had this in ONE place — the Ace editor on the New screen form,
5
+ * "@" only — which is the field people use least. The AI Assistant composer, which is on
6
+ * every page, had nothing: you had to know the exact name of an agent or a skill and
7
+ * type it from memory, out of 87 agents and 42 skills.
8
+ *
9
+ * This attaches to a plain <input> or <textarea>, because that is what the composer is:
10
+ * deep-chat owns its own input inside a shadow root, so there is no editor API to hook
11
+ * and no way to reuse Ace's completer. The popup is therefore built from scratch, and
12
+ * kept deliberately small — it is a list, a filter and four keys.
13
+ *
14
+ * @ agents and skills — a capability you name
15
+ * / commands and skills — something you invoke
16
+ *
17
+ * Prompts are offered by neither: a prompt is a whole `claude …` launch command for a NEW
18
+ * session, so it cannot be dropped into a message that is already running. The New screen
19
+ * form has its own template picker for those.
20
+ *
21
+ * Attach by marking a field: data-suggest="/fragments/ai-suggest/<workspace>/<project>"
22
+ * (the path may be the bare endpoint), or call attachSuggest(el, url) directly — which is
23
+ * what ui.js does for deep-chat's input once it appears in the shadow DOM.
24
+ */
25
+ (function () {
26
+ 'use strict';
27
+
28
+ /* --------------------------------------------------------------------------
29
+ * Reading and writing the field.
30
+ *
31
+ * Two shapes, one interface. A textarea has .value and .selectionStart; deep-chat's
32
+ * composer is a contenteditable DIV (`#text-input`) with neither, so the caret has to
33
+ * come from the Selection API and be restored by building a Range. Everything below
34
+ * this line works in characters and never touches either directly — which is what let
35
+ * the same popup serve the New screen form and the assistant.
36
+ * ------------------------------------------------------------------------ */
37
+
38
+ const isCE = (el) => !('value' in el) && el.isContentEditable;
39
+
40
+ /** Caret offset in characters from the start of a contenteditable's text. */
41
+ function ceCaret(el) {
42
+ const sel = (el.ownerDocument.defaultView || window).getSelection
43
+ ? (el.getRootNode().getSelection ? el.getRootNode().getSelection() : window.getSelection())
44
+ : null;
45
+ if (!sel || sel.rangeCount === 0) return el.textContent.length;
46
+ const range = sel.getRangeAt(0);
47
+ if (!el.contains(range.startContainer)) return el.textContent.length;
48
+ const probe = range.cloneRange();
49
+ probe.selectNodeContents(el);
50
+ probe.setEnd(range.startContainer, range.startOffset);
51
+ return probe.toString().length;
52
+ }
53
+
54
+ /** Put the caret at a character offset, walking the text nodes to find it. */
55
+ function ceSetCaret(el, offset) {
56
+ const doc = el.ownerDocument;
57
+ const walker = doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
58
+ let seen = 0;
59
+ let node = walker.nextNode();
60
+ while (node) {
61
+ const len = node.nodeValue.length;
62
+ if (seen + len >= offset) {
63
+ const range = doc.createRange();
64
+ range.setStart(node, Math.max(0, offset - seen));
65
+ range.collapse(true);
66
+ const sel = el.getRootNode().getSelection ? el.getRootNode().getSelection() : window.getSelection();
67
+ if (sel) { sel.removeAllRanges(); sel.addRange(range); }
68
+ return;
69
+ }
70
+ seen += len;
71
+ node = walker.nextNode();
72
+ }
73
+ // Past the end (an empty field): collapse into the element itself.
74
+ const range = doc.createRange();
75
+ range.selectNodeContents(el);
76
+ range.collapse(false);
77
+ const sel = el.getRootNode().getSelection ? el.getRootNode().getSelection() : window.getSelection();
78
+ if (sel) { sel.removeAllRanges(); sel.addRange(range); }
79
+ }
80
+
81
+ function readField(el) {
82
+ if (isCE(el)) return { value: el.textContent || '', caret: ceCaret(el) };
83
+ const value = el.value || '';
84
+ return { value, caret: el.selectionStart == null ? value.length : el.selectionStart };
85
+ }
86
+
87
+ function writeField(el, value, caret) {
88
+ if (isCE(el)) {
89
+ el.textContent = value;
90
+ ceSetCaret(el, caret);
91
+ return;
92
+ }
93
+ el.value = value;
94
+ try { el.setSelectionRange(caret, caret); } catch (_) { /* not all inputs allow it */ }
95
+ }
96
+
97
+ const OPEN_CLASS = 'ws-suggest-open';
98
+ const TRIGGERS = ['@', '/'];
99
+
100
+ // One fetch per (url, trigger, query), briefly. A keystroke is a request otherwise, and
101
+ // the server memoises for five seconds anyway — this keeps the typing from queueing.
102
+ const cache = new Map();
103
+ const CACHE_TTL = 4000;
104
+
105
+ async function fetchItems(url, trigger, typed) {
106
+ const key = `${url}|${trigger}|${typed}`;
107
+ const hit = cache.get(key);
108
+ if (hit && Date.now() - hit.at < CACHE_TTL) return hit.items;
109
+ const qs = `trigger=${encodeURIComponent(trigger)}&q=${encodeURIComponent(typed)}`;
110
+ try {
111
+ const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}${qs}`, {
112
+ headers: { Accept: 'application/json' },
113
+ });
114
+ if (!res.ok) return [];
115
+ const items = await res.json();
116
+ cache.set(key, { at: Date.now(), items });
117
+ return items;
118
+ } catch (_) {
119
+ return []; // offline, or the endpoint is gone: the field still types
120
+ }
121
+ }
122
+
123
+ /**
124
+ * What the caret is currently asking for, or null.
125
+ *
126
+ * A trigger only counts at the START of a word — after nothing, or after whitespace.
127
+ * Without that rule an email address opens the agent list on every "@", and a file path
128
+ * opens the command list on every "/". Once a space is typed the token is finished and
129
+ * the popup closes, which is also how the CLI behaves.
130
+ */
131
+ function tokenAt(value, caret) {
132
+ const upto = value.slice(0, caret);
133
+ for (const trigger of TRIGGERS) {
134
+ const at = upto.lastIndexOf(trigger);
135
+ if (at === -1) continue;
136
+ const before = at === 0 ? '' : upto[at - 1];
137
+ if (before && !/\s/.test(before)) continue;
138
+ const typed = upto.slice(at + 1);
139
+ // A finished token, or something that is plainly not a name.
140
+ if (/[\s"']/.test(typed)) continue;
141
+ // `/` is a command only at the very start of the message, exactly like the CLI:
142
+ // otherwise every date and path in a sentence would open the list.
143
+ if (trigger === '/' && at !== 0 && !/^\s*$/.test(upto.slice(0, at))) continue;
144
+ return { trigger, at, typed };
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // The vendored Tabler sprite, not emoji. The first version used 🤖 📎 ⌘ and they render
150
+ // as TOFU BOXES wherever no emoji font is installed — which includes the headless
151
+ // chromium these screenshots come from, and any server-side render. The dashboard
152
+ // already ships public/vendor/tabler-sprite.svg and paints it through .tabler-icon, so
153
+ // these inherit the same stroke and brand colour as every other icon on the page.
154
+ const KIND_ICON = { agent: 'robot', skill: 'paperclip', command: 'command' };
155
+ const iconSvg = (kind) => `<svg class="tabler-icon" width="15" height="15" aria-hidden="true">`
156
+ + `<use href="/vendor/tabler-sprite.svg#${KIND_ICON[kind] || 'point'}"></use></svg>`;
157
+
158
+ function popupFor(el) {
159
+ if (el._wsSuggestBox) return el._wsSuggestBox;
160
+ const box = document.createElement('div');
161
+ box.className = 'ws-suggest';
162
+ box.setAttribute('role', 'listbox');
163
+ box.hidden = true;
164
+ // On <body> rather than beside the field: the composer's input lives inside
165
+ // deep-chat's shadow root, and a popup appended there inherits that component's own
166
+ // overflow and stacking. Positioned absolutely against the field's rect instead.
167
+ document.body.appendChild(box);
168
+ el._wsSuggestBox = box;
169
+ return box;
170
+ }
171
+
172
+ // Wide enough for the longest name there is. They run to 47 characters
173
+ // (`upsun-webship-sandbox-1-playground-3-demo-reset`,
174
+ // `webship-site-launch-pre-deployment-checklist-dev`) and an ambiguous one carries a
175
+ // ` (agent)` qualifier on top — around 56 monospace characters, which no 400px
176
+ // assistant rail can show. The popup is therefore allowed to be WIDER THAN THE FIELD
177
+ // it belongs to: the name is the thing being chosen, so truncating it defeats the
178
+ // list. Bounded by the viewport, and never narrower than the field.
179
+ const MIN_WIDTH = 560;
180
+ const MAX_WIDTH = 780;
181
+
182
+ function place(box, el) {
183
+ const r = el.getBoundingClientRect();
184
+ const room = window.innerWidth - 24;
185
+ const width = Math.min(Math.max(MIN_WIDTH, r.width), MAX_WIDTH, room);
186
+ box.style.width = `${width}px`;
187
+ // Aligned to the field, but pulled back inside the viewport when that would overflow
188
+ // — the rail sits on the right, so a wider-than-field popup would run off the edge.
189
+ box.style.left = `${Math.max(12, Math.min(r.left, window.innerWidth - width - 12))}px`;
190
+ // Above the field when there is not enough room below — a composer sits at the bottom
191
+ // of its panel, so below is usually the wrong side.
192
+ const below = window.innerHeight - r.bottom;
193
+ const wanted = Math.min(320, box.scrollHeight || 320);
194
+ if (below < wanted + 16 && r.top > below) {
195
+ box.style.top = `${Math.max(8, r.top - wanted - 6)}px`;
196
+ box.style.maxHeight = `${Math.min(wanted, r.top - 16)}px`;
197
+ } else {
198
+ box.style.top = `${r.bottom + 6}px`;
199
+ box.style.maxHeight = `${Math.max(120, below - 16)}px`;
200
+ }
201
+ }
202
+
203
+ function attachSuggest(el, url) {
204
+ if (!el || el._wsSuggest) return;
205
+ el._wsSuggest = true;
206
+ const box = popupFor(el);
207
+ let items = [];
208
+ let active = 0;
209
+ let token = null;
210
+
211
+ const close = () => {
212
+ box.hidden = true;
213
+ box.classList.remove(OPEN_CLASS);
214
+ token = null;
215
+ items = [];
216
+ el.removeAttribute('aria-activedescendant');
217
+ };
218
+
219
+ const render = () => {
220
+ if (!items.length) return close();
221
+ box.innerHTML = items.map((it, i) => `
222
+ <div class="ws-suggest-row${i === active ? ' is-active' : ''}" role="option"
223
+ id="ws-suggest-${i}" aria-selected="${i === active}" data-i="${i}">
224
+ <span class="ws-suggest-icon">${iconSvg(it.kind)}</span>
225
+ <span class="ws-suggest-name">${token.trigger}${escapeHtml(it.name)}</span>
226
+ <span class="ws-suggest-kind">${escapeHtml(it.kind)}</span>
227
+ <span class="ws-suggest-where">${escapeHtml(it.where)}</span>
228
+ <div class="ws-suggest-desc">${escapeHtml(it.description || '')}</div>
229
+ </div>`).join('');
230
+ box.hidden = false;
231
+ box.classList.add(OPEN_CLASS);
232
+ place(box, el);
233
+ const activeRow = box.querySelector('.is-active');
234
+ if (activeRow) activeRow.scrollIntoView({ block: 'nearest' });
235
+ el.setAttribute('aria-activedescendant', `ws-suggest-${active}`);
236
+ };
237
+
238
+ const insert = (it) => {
239
+ if (!token) return;
240
+ const { value, caret } = readField(el);
241
+ // A name can carry no space, so nothing needs quoting: the token is replaced with
242
+ // the trigger and the name, then one space so the next word starts cleanly.
243
+ const before = value.slice(0, token.at);
244
+ const after = value.slice(caret);
245
+ // The kind is kept ONLY when the name needs it. Several items exist as both an
246
+ // agent and a skill — webship-demo-video-manager and webship-penpot-designer among
247
+ // them — and they are not interchangeable: the agent drives a whole job, the skill
248
+ // carries the steps. `@webship-demo-video-manager` alone does not say which, so an
249
+ // ambiguous name is inserted as `@name (kind)` and an unambiguous one stays clean.
250
+ const ambiguous = items.filter((o) => o.name === it.name).length > 1;
251
+ const insertion = ambiguous
252
+ ? `${token.trigger}${it.name} (${it.kind}) `
253
+ : `${token.trigger}${it.name} `;
254
+ writeField(el, `${before}${insertion}${after}`, before.length + insertion.length);
255
+ close();
256
+ // deep-chat and any htmx-bound field listen for input, not for assignment.
257
+ el.dispatchEvent(new Event('input', { bubbles: true }));
258
+ el.focus();
259
+ };
260
+
261
+ const refresh = async () => {
262
+ const { value, caret } = readField(el);
263
+ const t = tokenAt(value, caret);
264
+ if (!t) return close();
265
+ token = t;
266
+ const found = await fetchItems(url, t.trigger, t.typed);
267
+ // The field may have moved on while the request was in flight.
268
+ const now = readField(el);
269
+ const still = tokenAt(now.value, now.caret);
270
+ if (!still || still.trigger !== t.trigger || still.at !== t.at) return;
271
+ token = still;
272
+ items = found;
273
+ active = 0;
274
+ render();
275
+ };
276
+
277
+ let timer = null;
278
+ const schedule = () => { clearTimeout(timer); timer = setTimeout(refresh, 90); };
279
+ el.addEventListener('input', schedule);
280
+ // Arrow keys and clicks move the caret without changing the text, which can move it
281
+ // into or out of a token: `@webship-work|space` should still be offering completions.
282
+ el.addEventListener('keyup', (e) => {
283
+ if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) schedule();
284
+ });
285
+ el.addEventListener('click', schedule);
286
+ el.addEventListener('blur', () => setTimeout(close, 150));
287
+ el.addEventListener('keydown', (e) => {
288
+ if (box.hidden) return;
289
+ // Keys the popup owns while it is open, and only then — so typing is never captured
290
+ // by a list that is not showing.
291
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
292
+ e.preventDefault();
293
+ active = (active + (e.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length;
294
+ render();
295
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
296
+ // Enter would SEND the message, which is the one thing it must not do while a
297
+ // suggestion is highlighted. stopPropagation as well as preventDefault, because
298
+ // deep-chat's own handler is on an ancestor.
299
+ e.preventDefault();
300
+ e.stopPropagation();
301
+ insert(items[active]);
302
+ } else if (e.key === 'Escape') {
303
+ e.preventDefault();
304
+ e.stopPropagation();
305
+ close();
306
+ }
307
+ // Capture: deep-chat's Enter handler is bound on its own container, so a bubbling
308
+ // listener here would run after the message had already been sent.
309
+ }, true);
310
+
311
+ box.addEventListener('mousedown', (e) => {
312
+ const row = e.target.closest('.ws-suggest-row');
313
+ if (!row) return;
314
+ e.preventDefault(); // do not blur the field before inserting
315
+ insert(items[Number(row.dataset.i)]);
316
+ });
317
+ window.addEventListener('resize', () => { if (!box.hidden) place(box, el); });
318
+ window.addEventListener('scroll', () => { if (!box.hidden) place(box, el); }, true);
319
+ }
320
+
321
+ function escapeHtml(s) {
322
+ return String(s).replace(/[&<>"']/g, (c) => ({
323
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
324
+ }[c]));
325
+ }
326
+
327
+ /** Every field that asked for suggestions, including ones arriving in a fragment. */
328
+ function attachAll(root) {
329
+ (root || document).querySelectorAll('[data-suggest]').forEach((el) => {
330
+ attachSuggest(el, el.dataset.suggest);
331
+ });
332
+ }
333
+
334
+ window.wsSuggest = { attachSuggest, attachAll, tokenAt };
335
+ document.addEventListener('DOMContentLoaded', () => attachAll());
336
+ document.addEventListener('htmx:after:swap', (e) => attachAll(e.detail && e.detail.target));
337
+ }());