@webship.co/workspace 1.0.0-beta1 → 1.0.0-beta2
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/LICENSE +339 -0
- package/README.md +28 -364
- package/core/bin/workspace.js +135 -0
- package/core/config/settings.labels.yml +514 -0
- package/core/config/settings.template.yml +292 -0
- package/core/config/style.yml +53 -0
- package/core/config/theme.default.yml +161 -0
- package/core/config/theme.dispatch.yml +166 -0
- package/core/config/theme.drupal.yml +175 -0
- package/core/config/theme.ember.yml +160 -0
- package/core/config/theme.github.yml +172 -0
- package/core/config/theme.gitlab.yml +168 -0
- package/core/config/theme.press.yml +161 -0
- package/core/config/theme.slate.yml +160 -0
- package/core/config/theme.upsun.yml +174 -0
- package/core/docs/architecture.svg +65 -0
- package/core/docs/getting-started.md +51 -0
- package/core/docs/plugins.md +27 -0
- package/core/docs/releases.md +23 -0
- package/core/docs/testing.md +15 -0
- package/core/docs/workspaces.md +25 -0
- package/core/scripts/args/arg-ai-items.sh +65 -0
- package/core/scripts/args/arg-autostart.sh +31 -0
- package/core/scripts/args/arg-avatar-agent.sh +28 -0
- package/core/scripts/args/arg-avatar-catalog.sh +12 -0
- package/core/scripts/args/arg-avatar-character.sh +68 -0
- package/core/scripts/args/arg-avatar-voice.sh +24 -0
- package/core/scripts/args/arg-avatar.sh +22 -0
- package/core/scripts/args/arg-checks.sh +36 -0
- package/core/scripts/args/arg-clone.sh +42 -0
- package/core/scripts/args/arg-commands.sh +51 -0
- package/core/scripts/args/arg-cucumber.sh +34 -0
- package/core/scripts/args/arg-distribution.sh +30 -0
- package/core/scripts/args/arg-drupal.sh +39 -0
- package/core/scripts/args/arg-drupal_cms.sh +18 -0
- package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
- package/core/scripts/args/arg-graphify.sh +36 -0
- package/core/scripts/args/arg-graphs.sh +12 -0
- package/core/scripts/args/arg-hostnames.sh +23 -0
- package/core/scripts/args/arg-milvus-backup.sh +8 -0
- package/core/scripts/args/arg-milvus-mcp.sh +12 -0
- package/core/scripts/args/arg-milvus.sh +32 -0
- package/core/scripts/args/arg-obsidian.sh +17 -0
- package/core/scripts/args/arg-odoo.sh +18 -0
- package/core/scripts/args/arg-penpot-backup.sh +8 -0
- package/core/scripts/args/arg-penpot-mcp.sh +15 -0
- package/core/scripts/args/arg-penpot-token.sh +17 -0
- package/core/scripts/args/arg-penpot.sh +44 -0
- package/core/scripts/args/arg-rag.sh +21 -0
- package/core/scripts/args/arg-ragify.sh +35 -0
- package/core/scripts/args/arg-reclaim.sh +18 -0
- package/core/scripts/args/arg-release.sh +21 -0
- package/core/scripts/args/arg-screen.sh +28 -0
- package/core/scripts/args/arg-site-template.sh +13 -0
- package/core/scripts/args/arg-spdd.sh +16 -0
- package/core/scripts/args/arg-specs.sh +22 -0
- package/core/scripts/args/arg-storage.sh +36 -0
- package/core/scripts/args/arg-testing.sh +36 -0
- package/core/scripts/args/arg-webship.sh +34 -0
- package/core/scripts/args/arg-webships-project.sh +22 -0
- package/core/scripts/args/arg-webships-site-template.sh +17 -0
- package/core/scripts/args/arg-webships.sh +34 -0
- package/core/scripts/args/arg-website-site-template.sh +17 -0
- package/core/scripts/args/arg-website.sh +25 -0
- package/core/scripts/args/arg-wordpress.sh +18 -0
- package/core/scripts/autostart-run.sh +162 -0
- package/core/scripts/autostart-stop.sh +30 -0
- package/core/scripts/autostart-watch.sh +43 -0
- package/core/scripts/autostart.sh +26 -0
- package/core/scripts/bootstrap-functions.sh +117 -0
- package/core/scripts/bootstrap-libraries.sh +10 -0
- package/core/scripts/bootstrap.sh +71 -0
- package/core/scripts/ddev-shim/ddev +122 -0
- package/core/scripts/docker-gid.sh +119 -0
- package/core/scripts/functions/arg-site-template.sh +28 -0
- package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
- package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
- package/core/scripts/functions/fun-ai-items.sh +621 -0
- package/core/scripts/functions/fun-ai-providers.sh +72 -0
- package/core/scripts/functions/fun-autostart.sh +429 -0
- package/core/scripts/functions/fun-avatar.sh +441 -0
- package/core/scripts/functions/fun-backup-item.sh +50 -0
- package/core/scripts/functions/fun-build-distribution.sh +226 -0
- package/core/scripts/functions/fun-build-site-template.sh +104 -0
- package/core/scripts/functions/fun-build-webships.sh +161 -0
- package/core/scripts/functions/fun-build-website.sh +199 -0
- package/core/scripts/functions/fun-checks.sh +243 -0
- package/core/scripts/functions/fun-clone-project.sh +289 -0
- package/core/scripts/functions/fun-commands.sh +521 -0
- package/core/scripts/functions/fun-database.sh +63 -0
- package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
- package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
- package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
- package/core/scripts/functions/fun-default-settings.sh +28 -0
- package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
- package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
- package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
- package/core/scripts/functions/fun-distribution-webship.sh +28 -0
- package/core/scripts/functions/fun-distribution-webships.sh +33 -0
- package/core/scripts/functions/fun-distribution-website.sh +19 -0
- package/core/scripts/functions/fun-drush.sh +48 -0
- package/core/scripts/functions/fun-elevenlabs.sh +179 -0
- package/core/scripts/functions/fun-files.sh +19 -0
- package/core/scripts/functions/fun-git-worktrees.sh +142 -0
- package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
- package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
- package/core/scripts/functions/fun-milvus.sh +584 -0
- package/core/scripts/functions/fun-odoo.sh +121 -0
- package/core/scripts/functions/fun-penpot.sh +724 -0
- package/core/scripts/functions/fun-project-name.sh +20 -0
- package/core/scripts/functions/fun-rag.sh +253 -0
- package/core/scripts/functions/fun-ragify.sh +165 -0
- package/core/scripts/functions/fun-reclaim.sh +297 -0
- package/core/scripts/functions/fun-release.sh +159 -0
- package/core/scripts/functions/fun-remove-project.sh +319 -0
- package/core/scripts/functions/fun-screen.sh +96 -0
- package/core/scripts/functions/fun-spdd.sh +676 -0
- package/core/scripts/functions/fun-specs-share.sh +237 -0
- package/core/scripts/functions/fun-storage.sh +347 -0
- package/core/scripts/functions/fun-testing.sh +329 -0
- package/core/scripts/functions/fun-users.sh +87 -0
- package/core/scripts/functions/fun-version.sh +72 -0
- package/core/scripts/functions/fun-video-edit.sh +48 -0
- package/core/scripts/functions/fun-voice.sh +45 -0
- package/core/scripts/functions/fun-wordpress.sh +59 -0
- package/core/scripts/host-mounts.sh +161 -0
- package/core/scripts/install.sh +602 -0
- package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
- package/core/scripts/libs/argparse.sh +93 -0
- package/core/scripts/libs/bash-yaml.sh +52 -0
- package/core/scripts/libs/checks_plan.js +312 -0
- package/core/scripts/libs/elevenlabs.py +627 -0
- package/core/scripts/libs/progress-bar.sh +19 -0
- package/core/scripts/libs/rag_index.py +446 -0
- package/core/scripts/libs/video_edit.py +636 -0
- package/core/scripts/libs/voice.py +430 -0
- package/core/scripts/plugins/jira/host-install.sh +119 -0
- package/core/scripts/plugins/jira/host-mounts.txt +2 -0
- package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
- package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
- package/core/scripts/plugins/upsun/host-install.sh +99 -0
- package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
- package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
- package/core/scripts/plugins.sh +152 -0
- package/core/scripts/release.sh +69 -0
- package/core/scripts/storage.sh +39 -0
- package/core/scripts/sync-tooling.sh +246 -0
- package/core/scripts/tests/cmd-smoke-test.sh +109 -0
- package/core/scripts/trust-https.sh +257 -0
- package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
- package/core/workspace-app/.ddev/config.yaml +400 -0
- package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
- package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
- package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
- package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
- package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
- package/core/workspace-app/README.md +380 -0
- package/{actions → core/workspace-app/actions}/ddev.js +0 -1
- package/{actions → core/workspace-app/actions}/rag.js +0 -1
- package/{actions → core/workspace-app/actions}/testing.js +0 -1
- package/{assistant.js → core/workspace-app/assistant.js} +1 -10
- package/{git-info.js → core/workspace-app/git-info.js} +17 -1
- package/core/workspace-app/package.json +29 -0
- package/core/workspace-app/plugin-catalog.js +50 -0
- package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
- package/{server.js → core/workspace-app/server.js} +18 -0
- package/core/workspace-app/tests/features/actions-rail.feature +44 -0
- package/core/workspace-app/tests/features/ai-items.feature +23 -0
- package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
- package/core/workspace-app/tests/features/checks.feature +39 -0
- package/core/workspace-app/tests/features/clone-project.feature +30 -0
- package/core/workspace-app/tests/features/command-authoring.feature +58 -0
- package/core/workspace-app/tests/features/dashboard.feature +57 -0
- package/core/workspace-app/tests/features/menus.feature +29 -0
- package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
- package/core/workspace-app/tests/features/plugins.feature +20 -0
- package/core/workspace-app/tests/features/project-actions.feature +40 -0
- package/core/workspace-app/tests/features/screens.feature +29 -0
- package/core/workspace-app/tests/features/settings.feature +46 -0
- package/core/workspace-app/tests/features/test-reports.feature +32 -0
- package/core/workspace-app/tests/features/theme.feature +40 -0
- package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
- package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
- package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
- package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
- package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
- package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
- package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
- package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
- package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
- package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
- package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
- package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
- package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
- package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
- package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
- package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
- package/core/workspace-app/tests/unit/assistant.test.js +61 -0
- package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
- package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
- package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
- package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
- package/core/workspace-app/tests/unit/browsers.test.js +76 -0
- package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
- package/core/workspace-app/tests/unit/build-order.test.js +153 -0
- package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
- package/core/workspace-app/tests/unit/builders.test.js +158 -0
- package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
- package/core/workspace-app/tests/unit/changelog.test.js +177 -0
- package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
- package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
- package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
- package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
- package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
- package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
- package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
- package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
- package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
- package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
- package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
- package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
- package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
- package/core/workspace-app/tests/unit/create-card.test.js +85 -0
- package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
- package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
- package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
- package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
- package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
- package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
- package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
- package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
- package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
- package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
- package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
- package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
- package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
- package/core/workspace-app/tests/unit/density.test.js +52 -0
- package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
- package/core/workspace-app/tests/unit/event-log.test.js +102 -0
- package/core/workspace-app/tests/unit/favicon.test.js +68 -0
- package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
- package/core/workspace-app/tests/unit/git-info.test.js +26 -0
- package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
- package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
- package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
- package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
- package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
- package/core/workspace-app/tests/unit/images-create.test.js +59 -0
- package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
- package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
- package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
- package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
- package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
- package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
- package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
- package/core/workspace-app/tests/unit/narration.test.js +186 -0
- package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
- package/core/workspace-app/tests/unit/navigation.test.js +254 -0
- package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
- package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
- package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
- package/core/workspace-app/tests/unit/notes.test.js +128 -0
- package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
- package/core/workspace-app/tests/unit/page-width.test.js +59 -0
- package/core/workspace-app/tests/unit/php-version.test.js +90 -0
- package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
- package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
- package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
- package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
- package/core/workspace-app/tests/unit/plugins.test.js +58 -0
- package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
- package/core/workspace-app/tests/unit/project-make.test.js +143 -0
- package/core/workspace-app/tests/unit/project-name.test.js +202 -0
- package/core/workspace-app/tests/unit/reader.test.js +62 -0
- package/core/workspace-app/tests/unit/release.test.js +92 -0
- package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
- package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
- package/core/workspace-app/tests/unit/round-11.test.js +31 -0
- package/core/workspace-app/tests/unit/round-12.test.js +37 -0
- package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
- package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
- package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
- package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
- package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
- package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
- package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
- package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
- package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
- package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
- package/core/workspace-app/tests/unit/screens.test.js +74 -0
- package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
- package/core/workspace-app/tests/unit/session-name.test.js +64 -0
- package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
- package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
- package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
- package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
- package/core/workspace-app/tests/unit/speech.test.js +290 -0
- package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
- package/core/workspace-app/tests/unit/status-language.test.js +47 -0
- package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
- package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
- package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
- package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
- package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
- package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
- package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
- package/core/workspace-app/tests/unit/themes.test.js +453 -0
- package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
- package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
- package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
- package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
- package/core/workspace-app/tests/unit/version-check.test.js +98 -0
- package/core/workspace-app/tests/unit/version-link.test.js +50 -0
- package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
- package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
- package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
- package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
- package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
- package/core/workspace-app/tests/unit/waiting.test.js +48 -0
- package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
- package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
- package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
- package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
- package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
- package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
- package/package.json +38 -36
- /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
- /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
- /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
- /package/{actions → core/workspace-app/actions}/audio.js +0 -0
- /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
- /package/{actions → core/workspace-app/actions}/checks.js +0 -0
- /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
- /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
- /package/{actions → core/workspace-app/actions}/composer.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
- /package/{actions → core/workspace-app/actions}/folders.js +0 -0
- /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
- /package/{actions → core/workspace-app/actions}/images.js +0 -0
- /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
- /package/{actions → core/workspace-app/actions}/node.js +0 -0
- /package/{actions → core/workspace-app/actions}/notes.js +0 -0
- /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
- /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
- /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
- /package/{actions → core/workspace-app/actions}/screens.js +0 -0
- /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
- /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
- /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
- /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
- /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
- /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
- /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
- /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
- /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
- /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
- /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
- /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
- /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
- /package/{checks.js → core/workspace-app/checks.js} +0 -0
- /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
- /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
- /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
- /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
- /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
- /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
- /package/{commands.js → core/workspace-app/commands.js} +0 -0
- /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
- /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
- /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
- /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
- /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
- /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
- /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
- /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
- /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
- /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
- /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
- /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/README.md +0 -0
- /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
- /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
- /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
- /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
- /package/{folders.js → core/workspace-app/folders.js} +0 -0
- /package/{format.js → core/workspace-app/format.js} +0 -0
- /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
- /package/{html.js → core/workspace-app/html.js} +0 -0
- /package/{icons.js → core/workspace-app/icons.js} +0 -0
- /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
- /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
- /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
- /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
- /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
- /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
- /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
- /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
- /package/{images.js → core/workspace-app/images.js} +0 -0
- /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
- /package/{linger.js → core/workspace-app/linger.js} +0 -0
- /package/{lists.js → core/workspace-app/lists.js} +0 -0
- /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
- /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
- /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
- /package/{notes.js → core/workspace-app/notes.js} +0 -0
- /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
- /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
- /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
- /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
- /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
- /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
- /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
- /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
- /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
- /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
- /package/{public → core/workspace-app/public}/css/base.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/favicon.ico +0 -0
- /package/{public → core/workspace-app/public}/favicon.png +0 -0
- /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
- /package/{public → core/workspace-app/public}/logo.png +0 -0
- /package/{public → core/workspace-app/public}/media-player.js +0 -0
- /package/{public → core/workspace-app/public}/mic.js +0 -0
- /package/{public → core/workspace-app/public}/narration.js +0 -0
- /package/{public → core/workspace-app/public}/palette.js +0 -0
- /package/{public → core/workspace-app/public}/sortable.js +0 -0
- /package/{public → core/workspace-app/public}/suggest.js +0 -0
- /package/{public → core/workspace-app/public}/terminal.js +0 -0
- /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
- /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
- /package/{public → core/workspace-app/public}/ui.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
- /package/{rag.js → core/workspace-app/rag.js} +0 -0
- /package/{reader.js → core/workspace-app/reader.js} +0 -0
- /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
- /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
- /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
- /package/{screenrc → core/workspace-app/screenrc} +0 -0
- /package/{screens.js → core/workspace-app/screens.js} +0 -0
- /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
- /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
- /package/{speech.js → core/workspace-app/speech.js} +0 -0
- /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
- /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
- /package/{themes.js → core/workspace-app/themes.js} +0 -0
- /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
- /package/{urls.js → core/workspace-app/urls.js} +0 -0
- /package/{validate.js → core/workspace-app/validate.js} +0 -0
- /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
- /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
- /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
- /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
- /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
- /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
- /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assistant's avatar. What must never drift: every move the assistant is told it may write exists in
|
|
3
|
+
* the avatar; the 3D part is fetched only when the avatar is on (an import map, a module loaded on
|
|
4
|
+
* demand); `assistant.avatar: none` takes the control and the stage away; and the avatar's lines are
|
|
5
|
+
* asked for only while it is shown.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { test } = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const app = (f) => fs.readFileSync(path.join(__dirname, '../..', f), 'utf8');
|
|
14
|
+
const { AVATAR_GESTURES } = require('../../assistant');
|
|
15
|
+
|
|
16
|
+
test('every move the assistant may write is one the avatar can do', () => {
|
|
17
|
+
const avatar = app('public/assistant-avatar.js');
|
|
18
|
+
const moves = [...avatar.matchAll(/^ {2}([a-z]+): \{ duration:/gm)].map((m) => m[1]);
|
|
19
|
+
for (const g of AVATAR_GESTURES) assert.ok(moves.includes(g), `[${g}] is offered but the avatar has no such move`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('the libraries and the model are vendored, and fetched only when the avatar is shown', () => {
|
|
23
|
+
for (const f of ['public/vendor/three/three.module.min.js', 'public/vendor/three/three.core.min.js', 'public/vendor/three/LICENSE',
|
|
24
|
+
'public/vendor/three/addons/loaders/GLTFLoader.js', 'public/vendor/three-vrm/three-vrm.module.min.js', 'public/vendor/three-vrm/LICENSE',
|
|
25
|
+
'public/avatar/druplicon.vrm']) {
|
|
26
|
+
assert.ok(fs.existsSync(path.join(__dirname, '../..', f)), f);
|
|
27
|
+
}
|
|
28
|
+
const ui = app('public/assistant-avatar-ui.js');
|
|
29
|
+
assert.ok(ui.includes("import('/assistant-avatar.js')"), 'the 3D module is imported on demand');
|
|
30
|
+
const server = app('server.js');
|
|
31
|
+
assert.ok(server.includes('<script type="importmap">'), 'the import map that resolves three and three-vrm');
|
|
32
|
+
assert.ok(/\$\{avatarOn \? `<script src="\$\{assetUrl\('\/assistant-avatar-ui\.js'\)\}" defer>/.test(server), 'the wiring loads only when the avatar is on');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the avatar lines are asked for only while it is shown, and never left in the reply', () => {
|
|
36
|
+
const assistant = app('assistant.js');
|
|
37
|
+
assert.ok(assistant.includes('...(avatar ? ['), 'the avatar instructions are conditional');
|
|
38
|
+
for (const line of ['BOARD', 'DRAW', 'SHOW', 'VIDEO', 'BROWSE', 'SEARCH', 'MOVE', 'SIZE']) {
|
|
39
|
+
assert.ok(assistant.includes(`/^\\s*${line}:\\s*`), `${line}: is taken out of the reply text`);
|
|
40
|
+
}
|
|
41
|
+
assert.ok(app('public/ui.js').includes("avatar: Boolean(window.wsAssistantAvatarOn && window.wsAssistantAvatarOn()), avatarName: window.wsAvatarName ? window.wsAvatarName() : '', avatarId: window.wsAvatarId ? window.wsAvatarId() : '', page: window.wsAvatarContext ? window.wsAvatarContext() : null"));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('every control is on the assistant side, and nothing is drawn on the avatar itself', () => {
|
|
45
|
+
const server = app('server.js');
|
|
46
|
+
const stage = server.slice(server.indexOf('<div class="assistant-avatar-stage"'), server.indexOf('<div class="assistant-avatar-chips"'));
|
|
47
|
+
assert.ok(stage.includes('<canvas'), 'the stage is the character');
|
|
48
|
+
assert.ok(!/<button/.test(stage), 'and carries no button: they are all in the panel bar');
|
|
49
|
+
for (const c of ['assistant-dock', 'dock-voice', 'dock-talk', 'dock-mic', 'data-kit="board"', 'data-pop="windows"', 'data-pop="settings"', 'assistant-avatar-chips']) {
|
|
50
|
+
assert.ok(server.includes(c), c);
|
|
51
|
+
}
|
|
52
|
+
assert.ok(!server.includes('data-pop="moves"'), 'the moves are the reply\'s to choose, not a list in the menu');
|
|
53
|
+
assert.ok(!server.includes('data-pop="open"'), 'no box for typing links in the panel: Druplicon opens pages when asked');
|
|
54
|
+
const menu = server.slice(server.indexOf('<div class="uk-inline assistant-avatar-menu">'), server.indexOf('<div class="uk-inline assistant-menu">'));
|
|
55
|
+
for (const c of ['assistant-avatar-toggle', 'data-kit="board"', 'data-pop="settings"', 'assistant-avatar-pops']) assert.ok(menu.includes(c), `${c} is in the robot button's menu`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('an empty saved volume never silences the voice', () => {
|
|
59
|
+
const ui = app('public/ui.js');
|
|
60
|
+
assert.ok(ui.includes("if (raw !== '' && raw !== null && Number.isFinite(v) && v >= min && v <= max) narrationParams[key] = v;"),
|
|
61
|
+
'volume, speed and pitch: an empty cookie means not chosen, never 0');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('every big move the assistant may write is one the page can do, and spin, walk and leap exist', () => {
|
|
65
|
+
const { AVATAR_BIG_MOVES } = require('../../assistant');
|
|
66
|
+
const ui = app('public/assistant-avatar-ui.js');
|
|
67
|
+
const big = ui.slice(ui.indexOf('const BIG = {'), ui.indexOf('};', ui.indexOf('const BIG = {')));
|
|
68
|
+
for (const m of Object.keys(AVATAR_BIG_MOVES)) assert.match(big, new RegExp(`(async )?${m}(\\(\\)|:)`), `[${m}] is offered but the page has no such big move`);
|
|
69
|
+
const avatar = app('public/assistant-avatar.js');
|
|
70
|
+
for (const m of ['walk', 'spin', 'leap']) assert.match(avatar, new RegExp(`^ ${m}: \\{ duration:`, 'm'), m);
|
|
71
|
+
assert.ok(/if \(reduced\(\)\) return still/.test(big), 'reduced motion keeps it on the spot');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the avatar scripts parse: a clash in one leaves the page with no avatar at all', () => {
|
|
75
|
+
for (const f of ['public/assistant-avatar.js', 'public/assistant-avatar-moves.js', 'public/assistant-avatar-ui.js']) {
|
|
76
|
+
// Modules: their imports and exports taken off, the rest must compile as a function body.
|
|
77
|
+
const body = app(f).replace(/^import .*$/gm, '').replace(/^export (const|function|async function|let|class) /gm, '$1 ');
|
|
78
|
+
assert.doesNotThrow(() => new Function(body), f);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('every avatar may speak in an ElevenLabs voice, and none does by default', () => {
|
|
83
|
+
const ui = app('public/assistant-avatar-ui.js');
|
|
84
|
+
assert.ok(ui.includes("voice().set('voiceName', String(getPref(`ws-voice-name-${a.id}`, '') || ''))"), 'a voice per avatar, free until one is picked');
|
|
85
|
+
assert.ok(app('public/ui.js').includes("if (/^elevenlabs:/i.test(String(narrationParams.voiceName || ''))) delete narrationParams.voiceName;"),
|
|
86
|
+
'settings.yml cannot make a paid voice the default');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('the interface glyphs are vendored and used, never a temporary Figma link', () => {
|
|
90
|
+
const dir = path.join(__dirname, '../../public/vendor/webship-icons');
|
|
91
|
+
for (const f of ['ui/sound-on.svg', 'ui/sound-off.svg', 'ui/play.svg', 'ui/pause.svg', 'ui/stop.svg', 'ui/headset.svg', 'ui/share.svg', 'ui/menu.svg',
|
|
92
|
+
'ui/bookmark-outlined.svg']) {
|
|
93
|
+
const svg = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
94
|
+
assert.match(svg, /^<svg[^>]*width="[\d.]+" height="[\d.]+"/, `${f} is an SVG with its own size`);
|
|
95
|
+
}
|
|
96
|
+
const ui = app('public/assistant-avatar-ui.js');
|
|
97
|
+
for (const name of ['sound-on', 'sound-off', 'play', 'pause', 'stop', 'headset', 'share', 'bookmark-outlined']) assert.ok(ui.includes(`'${name}'`), `${name} is used`);
|
|
98
|
+
for (const f of ['public/assistant-avatar-ui.js', 'server.js', 'public/css/components/assistant-avatar.css']) assert.ok(!/figma\.com\/api\/mcp\/asset/.test(app(f)), `${f} links no temporary Figma asset`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('while it works the avatar shows a status as the Claude Code CLI does, and acts each one out', () => {
|
|
102
|
+
const ui = app('public/assistant-avatar-ui.js');
|
|
103
|
+
const list = ui.slice(ui.indexOf('const STATUSES = ['), ui.indexOf('];', ui.indexOf('const STATUSES = [')));
|
|
104
|
+
const moves = [...list.matchAll(/\['([A-Z][a-z]+)', '([a-z]+)'\]/g)];
|
|
105
|
+
assert.ok(moves.length >= 6 && moves.some(([, v]) => v === 'Tempering'), 'Thinking, Tempering and the rest');
|
|
106
|
+
const avatar = app('public/assistant-avatar.js');
|
|
107
|
+
for (const [, , m] of moves) assert.match(avatar, new RegExp(`^ ${m}: \\{ duration:`, 'm'), `${m} is a shared move, so every avatar can do it`);
|
|
108
|
+
assert.ok(ui.includes("avatar.gesture(failed ? 'shrug' : 'thumbsup')"), 'Done or Stopped, with a move');
|
|
109
|
+
assert.ok(app('public/ui.js').includes("el.onError = () => setWorking(false, true);"), 'a failed request says so');
|
|
110
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who the avatar can be and which voices it speaks in (assistant-cast.js). What must never drift: only
|
|
3
|
+
* the avatars settings.yml names are offered, a model is only ever served from inside the avatars
|
|
4
|
+
* workspace, the ElevenLabs key stays a placeholder until a real one is set, and a sentence already
|
|
5
|
+
* spoken is never paid for twice.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { test } = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { avatarCast, findAvatar, elevenKey, elevenSpeak, elevenVoices } = require('../../assistant-cast');
|
|
14
|
+
const { assistantSettings, workspaceLanguage } = require('../../speech');
|
|
15
|
+
|
|
16
|
+
function tree() {
|
|
17
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cast-'));
|
|
18
|
+
const p = path.join(root, 'avatars', 'scratch');
|
|
19
|
+
fs.mkdirSync(path.join(p, 'characters'), { recursive: true });
|
|
20
|
+
fs.mkdirSync(path.join(p, 'public', 'models'), { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(p, 'public', 'models', 'osa-coffee.vrm'), 'glTF');
|
|
22
|
+
fs.writeFileSync(path.join(p, 'public', 'models', 'labiba-meme.vrm'), 'glTF');
|
|
23
|
+
fs.writeFileSync(path.join(p, 'characters', 'osa-coffee.json'), JSON.stringify({ name: 'Coffee', model: '/models/osa-coffee.vrm', credit: 'Coffee by Polygonal-Mind. CC0.' }));
|
|
24
|
+
fs.writeFileSync(path.join(p, 'characters', 'escape.json'), JSON.stringify({ name: 'Out', model: '/models/../../../../etc/passwd' }));
|
|
25
|
+
fs.writeFileSync(path.join(root, 'secret.vrm'), 'no');
|
|
26
|
+
fs.symlinkSync(path.join(root, 'secret.vrm'), path.join(p, 'public', 'models', 'linked.vrm'));
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('only the avatars settings.yml names are offered, each found in the avatars workspace', () => {
|
|
31
|
+
const root = tree();
|
|
32
|
+
const cast = avatarCast({ assistant: { avatars: ['druplicon', 'coffee', 'labiba-meme', 'missing', '../x', 'escape', 'linked'] } }, root);
|
|
33
|
+
assert.deepStrictEqual(cast.map((a) => a.id), ['druplicon', 'coffee', 'labiba-meme']);
|
|
34
|
+
assert.strictEqual(cast[1].name, 'Coffee');
|
|
35
|
+
assert.match(cast[1].credit, /CC0/);
|
|
36
|
+
assert.match(cast[1].model, /^\/avatar\/model\/coffee\.vrm\?v=[0-9a-z]+$/, 'the link carries the file time, so a rebuilt model is fetched');
|
|
37
|
+
assert.ok(cast[2].credit, 'a bare model still says where its licence is');
|
|
38
|
+
assert.strictEqual(findAvatar('linked', root), null, 'a link out of the avatars workspace is never served');
|
|
39
|
+
assert.deepStrictEqual(avatarCast({}, root).map((a) => a.id), ['druplicon'], 'the built-in one by default');
|
|
40
|
+
assert.ok(fs.existsSync(path.join(__dirname, '../../public/avatar/druplicon.vrm')), 'Druplicon ships with the dashboard');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('the avatar it starts as is one of the list, or none', () => {
|
|
44
|
+
assert.strictEqual(assistantSettings({ assistant: { avatar: 'coffee', avatars: ['druplicon', 'coffee'] } }).avatar, 'coffee');
|
|
45
|
+
assert.strictEqual(assistantSettings({ assistant: { avatar: 'coffee' } }).avatar, 'druplicon', 'Druplicon when the one named is not listed');
|
|
46
|
+
assert.strictEqual(assistantSettings({ assistant: { avatar: 'none', avatars: ['coffee'] } }).avatar, 'none');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the Workspace language sets the voice and the microphone unless they are pinned', () => {
|
|
50
|
+
assert.strictEqual(workspaceLanguage({ language: 'ar-JO' }), 'ar-JO');
|
|
51
|
+
assert.strictEqual(workspaceLanguage({ language: 'klingon' }), 'en-US');
|
|
52
|
+
assert.strictEqual(assistantSettings({ language: 'ar-JO' }).speechLanguage, 'ar-JO');
|
|
53
|
+
assert.strictEqual(assistantSettings({ language: 'ar-JO', assistant: { speech_language: 'en-GB' } }).speechLanguage, 'en-GB');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('ElevenLabs stays off until a real key is set, and the key never reaches the page', async () => {
|
|
57
|
+
assert.strictEqual(elevenKey({ elevenlabs: { api_key: 'sk-change-this' } }), '');
|
|
58
|
+
assert.strictEqual(elevenKey({ elevenlabs: { api_key: 'CHANGE_ME' } }), '');
|
|
59
|
+
assert.deepStrictEqual(await elevenVoices({}), { connected: false, voices: [] });
|
|
60
|
+
const ui = fs.readFileSync(path.join(__dirname, '../../public/assistant-avatar-ui.js'), 'utf8');
|
|
61
|
+
assert.ok(!/xi-api-key|api_key/.test(ui), 'the page only posts words to /actions/assistant/speak');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a sentence already spoken is played from the cache, never paid for twice', async () => {
|
|
65
|
+
const settings = { elevenlabs: { api_key: 'a'.repeat(32) } };
|
|
66
|
+
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'voice-'));
|
|
67
|
+
let calls = 0;
|
|
68
|
+
const fetchImpl = async (url, opts) => {
|
|
69
|
+
calls += 1;
|
|
70
|
+
assert.match(url, /\/v1\/text-to-speech\/EXAVITQu4vr4xnSDxMaL\?/);
|
|
71
|
+
assert.strictEqual(opts.headers['xi-api-key'], 'a'.repeat(32));
|
|
72
|
+
return { ok: true, status: 200, arrayBuffer: async () => Buffer.from('mp3') };
|
|
73
|
+
};
|
|
74
|
+
const ask = { voice: 'EXAVITQu4vr4xnSDxMaL', text: 'Hello team.' };
|
|
75
|
+
assert.strictEqual(String(await elevenSpeak(settings, ask, { fetchImpl, cacheDir })), 'mp3');
|
|
76
|
+
assert.strictEqual(String(await elevenSpeak(settings, ask, { fetchImpl, cacheDir })), 'mp3');
|
|
77
|
+
assert.strictEqual(calls, 1);
|
|
78
|
+
await assert.rejects(elevenSpeak(settings, { voice: '../v1/user', text: 'x' }, { fetchImpl, cacheDir }), /voice/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("an avatar's own moves exist, and the set is for work: no headscarf, nothing meant for children", async () => {
|
|
82
|
+
const { AVATAR_MOVE_SETS } = require('../../assistant');
|
|
83
|
+
const src = fs.readFileSync(path.join(__dirname, '../../public/assistant-avatar-moves.js'), 'utf8');
|
|
84
|
+
const defined = [...src.matchAll(/^ {4}([a-z]+): \{$/gm)].map((m) => m[1]);
|
|
85
|
+
for (const m of Object.keys(AVATAR_MOVE_SETS.labiba)) assert.ok(defined.includes(m), `[${m}] is offered but Labiba has no such move`);
|
|
86
|
+
for (const m of ['hijab', 'kiss', 'hug', 'dab', 'peekaboo', 'love']) assert.ok(!defined.includes(m), `${m} is not a move for work`);
|
|
87
|
+
assert.strictEqual(require('../../assistant-cast').moveSetOf('labiba-meme'), 'labiba');
|
|
88
|
+
assert.strictEqual(require('../../assistant-cast').moveSetOf('druplicon'), '', 'Druplicon has the shared moves and no set of its own');
|
|
89
|
+
for (const [set, moves] of Object.entries(AVATAR_MOVE_SETS)) {
|
|
90
|
+
for (const m of Object.keys(moves)) assert.ok(defined.includes(m), `[${m}] is offered but the ${set} set has no such move`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('hair colours and agents come from settings, checked', () => {
|
|
95
|
+
const { hairOf, agentOf } = require('../../assistant-cast');
|
|
96
|
+
assert.strictEqual(hairOf({ assistant: { avatar_hair: { 'labiba-meme': '"#0074E0"' } } }, 'labiba-meme'), '#0074e0');
|
|
97
|
+
assert.strictEqual(hairOf({ assistant: { avatar_hair: { x: 'url(evil)' } } }, 'x'), '');
|
|
98
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'home-'));
|
|
99
|
+
fs.mkdirSync(path.join(home, '.claude', 'agents'), { recursive: true });
|
|
100
|
+
fs.writeFileSync(path.join(home, '.claude', 'agents', 'avatar-druplicon.md'), '---\nname: avatar-druplicon\n---\n');
|
|
101
|
+
const s = { assistant: { avatar_agents: { druplicon: 'avatar-druplicon', coffee: 'not-installed', bot: '../../etc' } } };
|
|
102
|
+
assert.strictEqual(agentOf(s, 'druplicon', home), 'avatar-druplicon');
|
|
103
|
+
assert.strictEqual(agentOf(s, 'coffee', home), '', 'an agent that is not installed is not used');
|
|
104
|
+
assert.strictEqual(agentOf(s, 'bot', home), '');
|
|
105
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ui = fs.readFileSync(path.join(__dirname, '../..', 'public/ui.js'), 'utf8');
|
|
7
|
+
|
|
8
|
+
test('the send button animates when a message is sent, by click or Enter, unless motion is reduced', () => {
|
|
9
|
+
assert.ok(ui.includes('.input-button.inside-end.ws-sent svg { animation: ws-send-fly'));
|
|
10
|
+
assert.ok(ui.includes('@keyframes ws-send-fly'));
|
|
11
|
+
const frames = ui.slice(ui.indexOf('@keyframes ws-send-fly'), ui.indexOf('@keyframes ws-send-fly') + 400);
|
|
12
|
+
assert.ok(frames.includes('translate:') && !frames.includes('transform:'), 'transform on the arrow is forced to none, so the individual properties are used');
|
|
13
|
+
assert.ok(ui.includes('@media (prefers-reduced-motion: reduce) { .input-button.inside-end.ws-sent svg { animation: none; } }'));
|
|
14
|
+
assert.ok(ui.includes('narrator.cancel(); playSendAnimation(); setWorking(true); }'), 'it is tied to the message being sent, not to a click');
|
|
15
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggested prompts in the assistant panel.
|
|
3
|
+
*
|
|
4
|
+
* What matters is what they are NOT: they never name a project, a branch, a command or a key, because
|
|
5
|
+
* a name in a suggestion goes stale the day the thing is renamed, and a list that points at things
|
|
6
|
+
* that are gone teaches the reader to skip the list. They are made from what a workspace states about
|
|
7
|
+
* itself, so a new workspace gets sensible ones with no code change.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { suggestionsFor, defaultSuggestions, cleanSuggestions, MAX_SUGGESTIONS, MAX_LENGTH } = require('../../assistant-suggestions');
|
|
16
|
+
const { loadWorkspaces } = require('../../workspaces');
|
|
17
|
+
|
|
18
|
+
const app = (f) => fs.readFileSync(path.join(__dirname, '../..', f), 'utf8');
|
|
19
|
+
// The pronoun is the one capital a plain sentence may carry besides its first letter.
|
|
20
|
+
const plain = (s) => s.replace(/\bI\b/g, 'i');
|
|
21
|
+
const meta = (over) => ({ noun: 'project', nounPlural: 'projects', kind: 'projects', typeActions: new Set(['ddev', 'build', 'backup', 'tests', 'clone']), ...over });
|
|
22
|
+
|
|
23
|
+
test('every real workspace gets a short, general list', () => {
|
|
24
|
+
const all = loadWorkspaces();
|
|
25
|
+
assert.ok(Object.keys(all).length > 5, 'there are workspaces to check');
|
|
26
|
+
for (const [key, ws] of Object.entries(all)) {
|
|
27
|
+
const list = suggestionsFor({ context: `workspace:${key}`, meta: ws });
|
|
28
|
+
assert.ok(list.length >= 3 && list.length <= MAX_SUGGESTIONS, `${key} has ${list.length}`);
|
|
29
|
+
// Plain words only: a capital to start, lowercase after, and a question mark. That is what keeps a
|
|
30
|
+
// project name, a script name, a key or a number out of every one of them.
|
|
31
|
+
for (const s of list) assert.match(plain(s), /^[A-Z][a-z ,']*\?$/, `${key}: "${s}" names something or is not a question`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the pages that belong to no workspace get general ones too', () => {
|
|
36
|
+
for (const context of ['home', 'assistant', 'settings', 'commands']) {
|
|
37
|
+
const list = suggestionsFor({ context, meta: null });
|
|
38
|
+
assert.ok(list.length >= 3, context);
|
|
39
|
+
for (const s of list) assert.match(plain(s), /^[A-Z][a-z ,']*\?$/, `${context}: "${s}"`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a workspace is asked about its own kind of thing', () => {
|
|
44
|
+
const dev = defaultSuggestions('workspace:dev', meta());
|
|
45
|
+
assert.ok(dev.some((s) => /projects/.test(s)));
|
|
46
|
+
const agents = defaultSuggestions('workspace:ai-agents', meta({ noun: 'agent', nounPlural: 'agents', kind: 'files', typeActions: new Set() }));
|
|
47
|
+
assert.ok(agents.every((s) => /agent/.test(s)), 'every one is about agents');
|
|
48
|
+
assert.ok(agents.some((s) => /add an agent/.test(s)), 'an agent, not a agent');
|
|
49
|
+
const skill = defaultSuggestions('workspace:ai-skills', meta({ noun: 'skill', nounPlural: 'skills', kind: 'files', typeActions: new Set() }));
|
|
50
|
+
assert.ok(skill.some((s) => /add a skill/.test(s)));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('it never offers what the workspace cannot do', () => {
|
|
54
|
+
const clones = defaultSuggestions('workspace:modules', meta({ noun: 'module', nounPlural: 'modules', typeActions: new Set(['build', 'graphify']) }));
|
|
55
|
+
assert.ok(!clones.some((s) => /running\?$/.test(s) && /which are running/.test(s)), 'a folder of git clones has nothing "running"');
|
|
56
|
+
assert.ok(!clones.some((s) => /back up/.test(s)), 'and cannot back anything up');
|
|
57
|
+
assert.ok(clones.some((s) => /not committed/.test(s)));
|
|
58
|
+
const site = defaultSuggestions('workspace:dev', meta());
|
|
59
|
+
assert.ok(site.some((s) => /which are running/.test(s)));
|
|
60
|
+
assert.ok(site.some((s) => /back up/.test(s)));
|
|
61
|
+
const bare = defaultSuggestions('workspace:x', meta({ typeActions: new Set(['ddev']) }));
|
|
62
|
+
assert.ok(!bare.some((s) => /build a new/.test(s)) && !bare.some((s) => /back up/.test(s)));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the backups page asks about backups', () => {
|
|
66
|
+
const list = defaultSuggestions('backups:dev', meta());
|
|
67
|
+
assert.ok(list.every((s) => /back ?up/.test(s)));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a workspace can say its own, and an empty list switches them off', () => {
|
|
71
|
+
const m = meta();
|
|
72
|
+
assert.deepStrictEqual(suggestionsFor({ context: 'workspace:dev', meta: m, own: ['What is in here?', 'How do I start?'] }), ['What is in here?', 'How do I start?']);
|
|
73
|
+
assert.deepStrictEqual(suggestionsFor({ context: 'workspace:dev', meta: m, own: [] }), [], 'none, not the defaults');
|
|
74
|
+
assert.strictEqual(suggestionsFor({ context: 'workspace:dev', meta: m, own: undefined }).length >= 3, true, 'absent means the defaults');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('settings.yml gives one list to every page that has none of its own', () => {
|
|
78
|
+
const m = meta();
|
|
79
|
+
assert.deepStrictEqual(suggestionsFor({ context: 'workspace:dev', meta: m, global: ['Where is it?'] }), ['Where is it?']);
|
|
80
|
+
assert.deepStrictEqual(suggestionsFor({ context: 'workspace:dev', meta: m, own: ['Mine?'], global: ['Where is it?'] }), ['Mine?'], 'the workspace wins');
|
|
81
|
+
assert.deepStrictEqual(suggestionsFor({ context: 'home', meta: null, global: [] }), [], 'an empty global list switches it off');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('what a person writes is cleaned the way the defaults are made', () => {
|
|
85
|
+
assert.strictEqual(cleanSuggestions('not a list'), null);
|
|
86
|
+
assert.strictEqual(cleanSuggestions(undefined), null);
|
|
87
|
+
assert.deepStrictEqual(cleanSuggestions([' spaced out ', '', ' ', 42, null, { a: 1 }, 'Spaced out']), ['spaced out'], 'trimmed, one line, deduplicated, strings only');
|
|
88
|
+
assert.strictEqual(cleanSuggestions(Array.from({ length: 12 }, (_, i) => `Question ${i}?`)).length, MAX_SUGGESTIONS);
|
|
89
|
+
assert.strictEqual(cleanSuggestions(['x'.repeat(500)])[0].length, MAX_LENGTH);
|
|
90
|
+
assert.deepStrictEqual(cleanSuggestions(['one\ntwo']), ['one two'], 'one line each');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the page hands the list to the panel, and a workspace file can carry its own', () => {
|
|
94
|
+
const server = app('server.js');
|
|
95
|
+
assert.ok(server.includes("require('./assistant-suggestions')"));
|
|
96
|
+
assert.ok(server.includes('suggestions });') || server.includes('suggestions };'), 'it is part of the settings the panel is given');
|
|
97
|
+
assert.ok(server.includes('own: meta ? meta.assistantSuggestions : undefined'));
|
|
98
|
+
assert.ok(app('workspaces.js').includes('assistantSuggestions: (wsSettings.assistant || {}).suggestions'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('the panel shows them only on an empty conversation, and a click fills the composer without sending', () => {
|
|
102
|
+
const ui = app('public/ui.js');
|
|
103
|
+
assert.ok(ui.includes('else showSuggestions();'), 'a thread with turns in it never sees them');
|
|
104
|
+
assert.ok(ui.includes('deep-chat-temporary-message'), 'gone once anything is sent, never saved to the thread');
|
|
105
|
+
assert.ok(ui.includes("'ws-suggestion':"));
|
|
106
|
+
// The handler that runs on a click must not be able to send: the assistant has a shell.
|
|
107
|
+
const handler = ui.slice(ui.indexOf("'ws-suggestion':"), ui.indexOf("'ws-suggestion':") + 500);
|
|
108
|
+
assert.ok(handler.includes('fillComposer('));
|
|
109
|
+
assert.ok(!handler.includes('submitUserMessage'));
|
|
110
|
+
assert.ok(ui.includes('escapeHtml(s)'), 'text goes into the markup escaped');
|
|
111
|
+
assert.ok(ui.includes('suggestTries += 1) < 8'), 'the retry is bounded');
|
|
112
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const app = (f) => fs.readFileSync(path.join(__dirname, '../..', f), 'utf8');
|
|
7
|
+
const server = app('server.js');
|
|
8
|
+
const ui = app('public/ui.js');
|
|
9
|
+
const menu = server.slice(server.indexOf('function assistantMenuItemsHtml'), server.indexOf('const AI_MARK'));
|
|
10
|
+
|
|
11
|
+
test('the assistant options offer a check button for each part of the prompt', () => {
|
|
12
|
+
for (const key of ['voice', 'mic', 'mode', 'suggest']) assert.ok(menu.includes(`['${key}', `), key);
|
|
13
|
+
assert.ok(menu.includes('class="uk-checkbox assistant-switch" type="checkbox" data-switch="${key}"'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('every switch in the menu has a handler on the page', () => {
|
|
17
|
+
const keys = [...menu.matchAll(/\['(voice|mic|mode|suggest)', '/g)].map((m) => m[1]);
|
|
18
|
+
for (const key of keys) assert.match(ui, new RegExp(`\\n ${key}: \\{`), `no handler for ${key}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('the choice is kept per browser and the setting is only where it starts', () => {
|
|
22
|
+
for (const name of ['ws-mic', 'ws-mode', 'ws-suggest']) assert.ok(ui.includes(`'${name}'`), name);
|
|
23
|
+
assert.ok(ui.includes("prefSwitch('ws-mic', cfg.voiceInput !== false"));
|
|
24
|
+
assert.ok(ui.includes("prefSwitch('ws-mode', true"));
|
|
25
|
+
assert.ok(ui.includes("prefSwitch('ws-suggest', true"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('the microphone is hidden by an attribute on the panel, and only when it exists at all', () => {
|
|
29
|
+
assert.ok(ui.includes(':host([data-assistant-mic="off"]) #microphone-button { display: none !important; }'));
|
|
30
|
+
assert.ok(ui.includes("el.setAttribute('data-assistant-mic', 'off')"));
|
|
31
|
+
assert.ok(ui.includes('show: cfg.speechToText !== false'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('prompt mode is hidden on the selector itself', () => {
|
|
35
|
+
assert.ok(ui.includes("picker.style.display = on ? '' : 'none'"));
|
|
36
|
+
assert.ok(ui.includes('show: !!picker'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('suggestions honour the switch when loaded, can be removed, and are not shown over a conversation', () => {
|
|
40
|
+
assert.ok(ui.includes("'ws-suggest', null) : null) !== '0'"));
|
|
41
|
+
assert.ok(ui.includes('!suggestOn()) return;'));
|
|
42
|
+
assert.ok(ui.includes('if (conversationEmpty) showSuggestions()'));
|
|
43
|
+
assert.ok(ui.includes("closest('[class*=\"outer-message-container\"]')"));
|
|
44
|
+
assert.ok(ui.includes('init: null'), 'the switch must not show them before the history has loaded');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a switch with nothing to switch is hidden, not offered', () => {
|
|
48
|
+
assert.ok(ui.includes("if (!sw || !sw.show) { box.closest('li').hidden = true; return; }"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('the copies of the menu for each way of showing the assistant stay in step', () => {
|
|
52
|
+
assert.ok(ui.includes('.assistant-switch[data-switch="${box.dataset.switch}"]'));
|
|
53
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the assistant is shown: sidebar, wide, floating, full page — plus the switch that closes it.
|
|
3
|
+
*
|
|
4
|
+
* Modelled on Drupal AI's own chatbot (a toggle in the admin toolbar, a menu in the panel header
|
|
5
|
+
* carrying Clear history and Close, an expand control, and the state on the document element), so
|
|
6
|
+
* what is checked here is the part that would break quietly: the state must be applied BEFORE first
|
|
7
|
+
* paint, the view must be a default rather than an override, and the full-page view must not leave
|
|
8
|
+
* two copies of one conversation on the screen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
const { test } = require('node:test');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const app = path.join(__dirname, '..', '..');
|
|
17
|
+
const server = fs.readFileSync(path.join(app, 'server.js'), 'utf8');
|
|
18
|
+
const ui = fs.readFileSync(path.join(app, 'public', 'ui.js'), 'utf8');
|
|
19
|
+
const css = fs.readFileSync(path.join(app, 'public', 'css', 'components', 'assistant-views.css'), 'utf8');
|
|
20
|
+
const { styleSettings } = require(path.join(app, 'workspaces'));
|
|
21
|
+
|
|
22
|
+
test('the view and the open state are settings, with a fallback rather than a blank panel', () => {
|
|
23
|
+
const s = styleSettings();
|
|
24
|
+
assert.ok(['sidebar', 'wide', 'float', 'full'].includes(s.assistantView), s.assistantView);
|
|
25
|
+
assert.ok(['yes', 'no'].includes(s.assistantOpen), s.assistantOpen);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('the state is applied before first paint, and a stored choice wins over the setting', () => {
|
|
29
|
+
const head = server.slice(server.indexOf("var d=document.documentElement"), server.indexOf('</script>'));
|
|
30
|
+
// Read through wsPref: the stored choice is a cookie on the hub domain, so it is the same choice on
|
|
31
|
+
// every workspace subdomain rather than one per origin.
|
|
32
|
+
assert.match(head, /wsPref\('ws-assistant-view',null\)\|\|'\$\{style\.assistantView\}'/,
|
|
33
|
+
'the setting is the default, the stored choice wins — as with style.mode');
|
|
34
|
+
assert.match(head, /indexOf\(v\)<0\)v='sidebar'/, 'an unknown view must fall back, not render nothing');
|
|
35
|
+
assert.match(head, /d\.dataset\.assistantView=v/);
|
|
36
|
+
assert.match(head, /classList\.add\('assistant-closed'\)/);
|
|
37
|
+
// The panel is 25rem of the layout: deciding this in the deferred ui.js would draw the page one
|
|
38
|
+
// way and reflow it a frame later, which is the bug the rail's own pre-paint line exists for. So
|
|
39
|
+
// ui.js may only CHANGE the class, from the one function a click reaches.
|
|
40
|
+
const setsClosed = ui.match(/classList\.toggle\('assistant-closed'/g) || [];
|
|
41
|
+
assert.strictEqual(setsClosed.length, 1, 'only setAssistantOpen may write that class');
|
|
42
|
+
const fn = ui.slice(ui.indexOf('function setAssistantOpen'), ui.indexOf('function setAssistantOpen') + 400);
|
|
43
|
+
assert.match(fn, /classList\.toggle\('assistant-closed', !open\)/, 'and that is where it lives');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the panel header carries the menu, wider and close — and the menu carries the rest', () => {
|
|
47
|
+
// Up to the close button: the avatar's own menu (when it is on) comes first and is long.
|
|
48
|
+
const from = server.indexOf('class="assistant-controls"');
|
|
49
|
+
const head = server.slice(from, server.indexOf('assistant-hide', from) + 200);
|
|
50
|
+
assert.match(head, /assistant-ctl[^>]*Assistant options/, 'the kebab');
|
|
51
|
+
assert.match(head, /assistant-wider/, 'wider');
|
|
52
|
+
assert.match(head, /assistant-hide/, 'close');
|
|
53
|
+
// The items themselves are one function, since the toolbar hangs the same menu.
|
|
54
|
+
const menu = server.slice(server.indexOf('function assistantMenuItemsHtml'), server.indexOf('const AI_MARK'));
|
|
55
|
+
assert.match(menu, /hx-post="\/actions\/chat-reset"[\s\S]*?hx-trigger="confirmed-remove"/,
|
|
56
|
+
'New chat must ask first, like every other action that throws something away');
|
|
57
|
+
assert.match(menu, /class="assistant-view-pick" data-view="\$\{v\}"/, 'the views are rendered from a list');
|
|
58
|
+
assert.match(menu, /assistant-close-pick/, 'and closing it is offered where the views are');
|
|
59
|
+
for (const [v, label] of [['sidebar', 'Sidebar'], ['wide', 'Wide'], ['float', 'Floating'], ['full', 'Full page']]) {
|
|
60
|
+
assert.match(menu, new RegExp(`\\['${v}', '${label}'`), `no way to choose ${v}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('the control is in the rail, not in the toolbar', () => {
|
|
65
|
+
// The toolbar is the corner the brand and the page identity own. The rail is where the things that
|
|
66
|
+
// apply to whatever page you are on live, which is exactly what this is.
|
|
67
|
+
const left = server.slice(server.indexOf('<div class="uk-navbar-left">'), server.indexOf('<div class="uk-navbar-right">'));
|
|
68
|
+
assert.ok(!/assistant-toggle|assistant-switch/.test(left), 'the toolbar must not carry the switch');
|
|
69
|
+
const rail = server.slice(server.indexOf("label: 'Go'"), server.indexOf("] });", server.indexOf("label: 'Go'")));
|
|
70
|
+
assert.match(rail, /<button type="button" class="action-btn actions-assistant/,
|
|
71
|
+
'the rail row is the control, and a real button — UIKit puts aria-haspopup on whatever precedes the '
|
|
72
|
+
+ 'dropdown, and that attribute is not allowed on a span');
|
|
73
|
+
assert.match(rail, /\$\{assistantMenuItemsHtml\(\)\}/, 'and it carries the whole menu');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('show and hide are two items the stylesheet picks between', () => {
|
|
77
|
+
const menu = server.slice(server.indexOf('function assistantMenuItemsHtml'), server.indexOf('const AI_MARK'));
|
|
78
|
+
assert.match(menu, /assistant-when-closed[\s\S]*?assistant-open-pick/, 'Show, for when it is hidden');
|
|
79
|
+
assert.match(menu, /assistant-when-open[\s\S]*?assistant-close-pick/, 'Hide, for when it is not');
|
|
80
|
+
// Neither needs a script to rename itself: which applies is a state the stylesheet already knows.
|
|
81
|
+
assert.match(css, /html\.assistant-closed \.assistant-when-open,\s*\n\s*html:not\(\.assistant-closed\) \.assistant-when-closed \{ display: none; \}/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('the full-page view is a place, not a layout', () => {
|
|
85
|
+
assert.match(ui, /if \(view === 'full'\)[\s\S]*?window\.location\.href = '\/assistant'/,
|
|
86
|
+
'choosing it must go there');
|
|
87
|
+
assert.match(ui, /window\.location\.pathname === '\/assistant'[\s\S]*?referrer/,
|
|
88
|
+
'leaving it must leave the page too — /assistant has no sidebar to become');
|
|
89
|
+
assert.match(server, /pathname === '\/assistant'/, 'the page itself');
|
|
90
|
+
// Two panels on one screen would be two views of one conversation, both live.
|
|
91
|
+
assert.match(server, /context === 'assistant' \? '' : `<aside class="assistant-side">/,
|
|
92
|
+
'the page must not also carry the docked panel');
|
|
93
|
+
assert.match(css, /html\[data-assistant-view="full"\] \.assistant-side \{ display: none; \}/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the full-page view is escapable from an ordinary page', () => {
|
|
97
|
+
// In that view no page but /assistant draws a panel, so the panel's own menu is not there to use.
|
|
98
|
+
// The toolbar carries the same menu, or picking Full page and then navigating anywhere would leave
|
|
99
|
+
// no way back to a sidebar — the tile alone only leads back to /assistant.
|
|
100
|
+
assert.match(server, /function assistantMenuItemsHtml\(\)/, 'one menu, hung from two places');
|
|
101
|
+
// To the end of the control, not a fixed window: the comments around it grow, and a slice that is
|
|
102
|
+
// one character short reads as the menu having been taken away.
|
|
103
|
+
const railStart = server.indexOf('class="uk-inline actions-assistant-wrap"');
|
|
104
|
+
const rail = server.slice(railStart, server.indexOf('</div>`', railStart));
|
|
105
|
+
assert.match(rail, /\$\{assistantMenuItemsHtml\(\)\}/, 'the rail carries the menu, so any page can change the view');
|
|
106
|
+
const header = server.slice(server.indexOf('class="uk-inline assistant-menu"'), server.indexOf('class="uk-inline assistant-menu"') + 600);
|
|
107
|
+
assert.match(header, /\$\{assistantMenuItemsHtml\(\)\}/, 'and it must be the same menu, not a second copy');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('showing it does not pretend to open a page that is already open', () => {
|
|
111
|
+
// On /assistant the assistant IS the page: writing a closed/open state there set something nobody
|
|
112
|
+
// could see, which then decided the panel on the next ordinary page for no reason the user gave.
|
|
113
|
+
const handler = ui.slice(ui.indexOf("e.target.closest('.assistant-open-pick')"), ui.indexOf("e.target.closest('.assistant-open-pick')") + 500);
|
|
114
|
+
assert.match(handler, /if \(window\.location\.pathname === '\/assistant'\) return;/);
|
|
115
|
+
assert.ok(handler.indexOf("pathname === '/assistant'") < handler.indexOf('setAssistantOpen'),
|
|
116
|
+
'it must return before writing the open state');
|
|
117
|
+
assert.match(handler, /assistantView\(\) === 'full'[\s\S]*?window\.location\.href = '\/assistant'/,
|
|
118
|
+
'and in the full-page view showing it means going there');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the floating panel can be moved, and only that one', () => {
|
|
122
|
+
// Where a floating panel should sit depends on what is under it, and that changes page by page.
|
|
123
|
+
assert.match(ui, /function draggableAssistant/);
|
|
124
|
+
assert.match(ui, /dataset\.assistantView !== 'float'\) return;/, 'only the floating view is draggable');
|
|
125
|
+
// A drag that began on the close button would close the panel on release.
|
|
126
|
+
assert.match(ui, /e\.target\.closest\('button, a, input, select, \.assistant-dropdown'\)\) return;/,
|
|
127
|
+
'the header is the handle, minus its controls');
|
|
128
|
+
assert.match(ui, /const maxX = Math\.max\(0, window\.innerWidth - r\.width\)/, 'it is clamped into the viewport');
|
|
129
|
+
assert.match(ui, /window\.addEventListener\('resize', applyStored\)/,
|
|
130
|
+
'a window that got smaller must not leave it off-screen');
|
|
131
|
+
// Leftover inline positions would fight the stylesheet in every other view.
|
|
132
|
+
assert.match(ui, /el\.style\.left = el\.style\.top = el\.style\.bottom = el\.style\.right = '';/);
|
|
133
|
+
assert.match(css, /html\[data-assistant-view="float"\] \.assistant-side \.assistant-head \{\s*\n\s*cursor: grab;/,
|
|
134
|
+
'and it has to say it can be dragged');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('the launcher is the way back in, in every view', () => {
|
|
138
|
+
// The control lives in the rail now, and a collapsed rail is an icon strip a newcomer has no reason
|
|
139
|
+
// to search — so a closed assistant must leave something on the page that opens it.
|
|
140
|
+
assert.match(css, /\.assistant-launcher \{ display: none; \}/, 'not while the panel is open');
|
|
141
|
+
assert.match(css, /html\.assistant-closed \.assistant-launcher \{/, 'and in every view when it is closed');
|
|
142
|
+
assert.ok(!/html\[data-assistant-view="float"\]\.assistant-closed \.assistant-launcher \{/.test(css),
|
|
143
|
+
'not the floating view alone any more');
|
|
144
|
+
assert.match(ui, /closest\('\.assistant-launcher'\)[\s\S]*?assistantView\(\) === 'full'[\s\S]*?'\/assistant'/,
|
|
145
|
+
'and in the full-page view it leads to the page, since there is no panel to show');
|
|
146
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const app = (f) => fs.readFileSync(path.join(__dirname, '../..', f), 'utf8');
|
|
7
|
+
const ui = app('public/ui.js');
|
|
8
|
+
const css = app('public/css/components/working-indicators.css');
|
|
9
|
+
|
|
10
|
+
test('the Send button shows three animated dots while the assistant works, in place of the arrow', () => {
|
|
11
|
+
assert.ok(ui.includes(':host([data-assistant-working]) .input-button.inside-end::after'));
|
|
12
|
+
assert.ok(ui.includes('animation: ws-dots 1.2s ease-in-out infinite;'));
|
|
13
|
+
assert.ok(ui.includes(':host([data-assistant-working]) .input-button.inside-end svg { opacity: 0; }'));
|
|
14
|
+
assert.equal((ui.match(/radial-gradient\(circle, var\(--text-inverse\) 2\.4px/g) || []).length, 3, 'three dots, in the theme token and not a colour');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('deep-chat\'s own dots on the button are hidden so only one animation shows', () => {
|
|
18
|
+
assert.ok(ui.includes(':host([data-assistant-working]) .loading-submit-button { display: none !important; }'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('the dots bounce one after another', () => {
|
|
22
|
+
const frames = ui.slice(ui.indexOf('@keyframes ws-dots'), ui.indexOf('@keyframes ws-dots') + 500);
|
|
23
|
+
const raised = [...frames.matchAll(/(\d+)% \{ background-position: ([^;]+);/g)].filter((m) => m[1] !== '0').map((m) => m[2].split(',').map((p) => p.trim()).findIndex((p) => p.endsWith('30%')));
|
|
24
|
+
assert.deepEqual(raised.filter((i) => i >= 0), [0, 1, 2]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('the panel header pulses the avatar and sweeps a light while it works', () => {
|
|
28
|
+
assert.ok(css.includes('.assistant-panel[data-working] .assistant-head::after'));
|
|
29
|
+
assert.ok(css.includes('.assistant-panel[data-working] .assistant-avatar { animation: assistant-pulse'));
|
|
30
|
+
assert.ok(css.includes('@keyframes assistant-sweep') && css.includes('@keyframes assistant-pulse'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('motion is switched off for people who prefer reduced motion', () => {
|
|
34
|
+
assert.ok(ui.includes('@media (prefers-reduced-motion: reduce) { :host([data-assistant-working]) .input-button.inside-end::after { animation: none; } }'));
|
|
35
|
+
assert.match(css, /prefers-reduced-motion: reduce\) \{\s*\.assistant-panel\[data-working\] \.assistant-head::after,\s*\.assistant-panel\[data-working\] \.assistant-avatar \{ animation: none; \}/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('working starts when a message is sent and ends on the answer, a reply of any kind, an error, or after a time limit', () => {
|
|
39
|
+
assert.ok(ui.includes("if (message.role === 'user') { conversationEmpty = false; narrator.cancel(); playSendAnimation(); setWorking(true); }"));
|
|
40
|
+
assert.ok(ui.includes('else setWorking(false);'), 'an assistant message ends it');
|
|
41
|
+
assert.ok(ui.includes('el.onError = () => setWorking(false, true);'), 'an error ends it, and says it failed');
|
|
42
|
+
assert.ok(ui.includes(' setWorking(false);\n if (response && response.html)'), 'the response interceptor ends it');
|
|
43
|
+
assert.ok(ui.includes('setTimeout(() => setWorking(false), 16 * 60 * 1000)'), 'a request that never answers cannot leave it running for ever');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the state is on the panel and the chat, so both the page and the shadow styles can read it', () => {
|
|
47
|
+
assert.ok(ui.includes("el.toggleAttribute('data-assistant-working', on)"));
|
|
48
|
+
assert.ok(ui.includes("document.querySelectorAll('.assistant-panel').forEach((p) => p.toggleAttribute('data-working', on))"));
|
|
49
|
+
});
|