@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.
- package/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/graphs.js +107 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/obsidian.js +59 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/graphs.js +104 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/obsidian.js +405 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +448 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1440 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +8140 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- 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
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
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
|
+
}());
|