@antelopejs/dms-ai 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/presence.js.map +1 -1
- package/dist/channels/bridge.js +4 -2
- package/dist/channels/bridge.js.map +1 -1
- package/dist/channels/sse-stream.d.ts +1 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +9 -21
- package/dist/config.js.map +1 -1
- package/dist/constants/channels.d.ts +5 -1
- package/dist/constants/channels.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/build-id.js +1 -1
- package/dist/lifecycle/build-id.js.map +1 -1
- package/dist/lifecycle/module-roots.js +12 -14
- package/dist/lifecycle/module-roots.js.map +1 -1
- package/dist/lifecycle/sidecar-owner.js.map +1 -1
- package/dist/lifecycle/skill-sources.js +35 -22
- package/dist/lifecycle/skill-sources.js.map +1 -1
- package/dist/lifecycle/spawn-sidecar.js +11 -6
- package/dist/lifecycle/spawn-sidecar.js.map +1 -1
- package/dist/logging/log-buffer.js +6 -2
- package/dist/logging/log-buffer.js.map +1 -1
- package/dist/pages/activity/columns.d.ts +66 -2
- package/dist/pages/activity/columns.js.map +1 -1
- package/dist/pages/activity/page.js.map +1 -1
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/overview/recent-changes.js.map +1 -1
- package/dist/pages/settings/availability.js.map +1 -1
- package/dist/pages/skills/page.js.map +1 -1
- package/dist/pages-registry.js +9 -10
- package/dist/pages-registry.js.map +1 -1
- package/dist/routes/activity.d.ts +5 -4
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/changes.d.ts +6 -4
- package/dist/routes/changes.js.map +1 -1
- package/dist/routes/codex-account.d.ts +5 -1
- package/dist/routes/codex-account.js +2 -2
- package/dist/routes/codex-account.js.map +1 -1
- package/dist/routes/database.d.ts +21 -4
- package/dist/routes/database.js.map +1 -1
- package/dist/routes/metric-shapes.js +8 -5
- package/dist/routes/metric-shapes.js.map +1 -1
- package/dist/routes/metrics.d.ts +4 -2
- package/dist/routes/metrics.js +2 -2
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/pages-registry.js.map +1 -1
- package/dist/routes/settings.d.ts +3 -2
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/sidecar-info.js.map +1 -1
- package/dist/routes/sidecar-results.d.ts +2 -2
- package/dist/routes/sidecar-results.js +10 -10
- package/dist/routes/sidecar-results.js.map +1 -1
- package/dist/routes/skills.d.ts +2 -1
- package/dist/routes/skills.js +6 -4
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/status-shapes.js +12 -12
- package/dist/routes/status-shapes.js.map +1 -1
- package/dist/routes/undo-confirm.js.map +1 -1
- package/dist/sidecar/http-client.js.map +1 -1
- package/dist/vocabulary/tools.js.map +1 -1
- package/frontend-vue/app/chat/components/ChatView.vue +33 -133
- package/frontend-vue/app/chat/components/ProviderNotice.vue +77 -0
- package/frontend-vue/app/chat/components/UndoConflictModal.vue +98 -0
- package/frontend-vue/app/chat/composables/useChangeSetActions.ts +1 -1
- package/frontend-vue/app/plugins/ai.client.ts +10 -2
- package/frontend-vue/app/runtime/assistant-actions.ts +41 -0
- package/frontend-vue/app/runtime/assistant-session.ts +8 -2
- package/frontend-vue/app/runtime/panel-intents.ts +3 -1
- package/frontend-vue/build/backend-boundary.ts +89 -0
- package/frontend-vue/dms.frontend.build.ts +6 -0
- package/frontend-vue/package.json +2 -1
- package/frontend-vue/pnpm-lock.yaml +3 -0
- package/frontend-vue/tests/assistant-actions.test.ts +72 -0
- package/frontend-vue/tests/backend-boundary.test.ts +130 -0
- package/frontend-vue/tests/panel-approvals.test.ts +190 -0
- package/frontend-vue/tests/panel-questions.test.ts +169 -0
- package/frontend-vue/tests/panel-states.test.ts +1 -556
- package/frontend-vue/tests/panel-tools.test.ts +165 -0
- package/frontend-vue/tests/sidecar-guest.test.ts +39 -71
- package/frontend-vue/tests/support/panel-fixtures.ts +110 -0
- package/frontend-vue/tests/support/panel-harness.ts +2 -2
- package/frontend-vue/vitest.config.ts +2 -1
- package/package.json +20 -9
- package/sidecar/dist/agent/attachment-files.js.map +1 -1
- package/sidecar/dist/agent/call-ledger.d.ts +3 -2
- package/sidecar/dist/agent/call-ledger.js +1 -1
- package/sidecar/dist/agent/call-ledger.js.map +1 -1
- package/sidecar/dist/agent/command-analysis.js +13 -14
- package/sidecar/dist/agent/command-analysis.js.map +1 -1
- package/sidecar/dist/agent/conversation-modes.js.map +1 -1
- package/sidecar/dist/agent/edit-targets.d.ts +5 -1
- package/sidecar/dist/agent/edit-targets.js +8 -6
- package/sidecar/dist/agent/edit-targets.js.map +1 -1
- package/sidecar/dist/agent/edit-tracker.d.ts +2 -1
- package/sidecar/dist/agent/edit-tracker.js +3 -4
- package/sidecar/dist/agent/edit-tracker.js.map +1 -1
- package/sidecar/dist/agent/effective-mode.js.map +1 -1
- package/sidecar/dist/agent/line-diff.js +10 -8
- package/sidecar/dist/agent/line-diff.js.map +1 -1
- package/sidecar/dist/agent/permission-bus.js +2 -1
- package/sidecar/dist/agent/permission-bus.js.map +1 -1
- package/sidecar/dist/agent/permission-preview.d.ts +1 -1
- package/sidecar/dist/agent/permission-preview.js +54 -37
- package/sidecar/dist/agent/permission-preview.js.map +1 -1
- package/sidecar/dist/agent/permission-rules.js +4 -6
- package/sidecar/dist/agent/permission-rules.js.map +1 -1
- package/sidecar/dist/agent/provider.d.ts +2 -1
- package/sidecar/dist/agent/question-bus.js.map +1 -1
- package/sidecar/dist/agent/read-access.d.ts +2 -1
- package/sidecar/dist/agent/read-access.js +4 -7
- package/sidecar/dist/agent/read-access.js.map +1 -1
- package/sidecar/dist/agent/runner.js.map +1 -1
- package/sidecar/dist/agent/session.js +2 -6
- package/sidecar/dist/agent/session.js.map +1 -1
- package/sidecar/dist/agent/switching-runner.js.map +1 -1
- package/sidecar/dist/agent/system-prompt.js.map +1 -1
- package/sidecar/dist/agent/tool-summary.d.ts +2 -1
- package/sidecar/dist/agent/tool-summary.js +26 -13
- package/sidecar/dist/agent/tool-summary.js.map +1 -1
- package/sidecar/dist/agent/typecheck.d.ts +1 -4
- package/sidecar/dist/agent/typecheck.js +3 -2
- package/sidecar/dist/agent/typecheck.js.map +1 -1
- package/sidecar/dist/audit/activity-query.js +4 -8
- package/sidecar/dist/audit/activity-query.js.map +1 -1
- package/sidecar/dist/audit/activity-target.d.ts +4 -2
- package/sidecar/dist/audit/activity-target.js +10 -9
- package/sidecar/dist/audit/activity-target.js.map +1 -1
- package/sidecar/dist/audit/activity.js +4 -3
- package/sidecar/dist/audit/activity.js.map +1 -1
- package/sidecar/dist/audit/call-audit.js.map +1 -1
- package/sidecar/dist/builder/builder-client.d.ts +2 -1
- package/sidecar/dist/builder/builder-client.js +6 -8
- package/sidecar/dist/builder/builder-client.js.map +1 -1
- package/sidecar/dist/builder/capability.js.map +1 -1
- package/sidecar/dist/checkpoints/checkpoints.js.map +1 -1
- package/sidecar/dist/checkpoints/shadow-git.js +6 -2
- package/sidecar/dist/checkpoints/shadow-git.js.map +1 -1
- package/sidecar/dist/constants/agent.js.map +1 -1
- package/sidecar/dist/constants/attachments.js.map +1 -1
- package/sidecar/dist/constants/builder.js.map +1 -1
- package/sidecar/dist/constants/chatgpt.js.map +1 -1
- package/sidecar/dist/constants/checkpoints.d.ts +1 -1
- package/sidecar/dist/constants/checkpoints.js +6 -6
- package/sidecar/dist/constants/checkpoints.js.map +1 -1
- package/sidecar/dist/constants/claude.d.ts +3 -4
- package/sidecar/dist/constants/claude.js +16 -16
- package/sidecar/dist/constants/claude.js.map +1 -1
- package/sidecar/dist/constants/cli.js.map +1 -1
- package/sidecar/dist/constants/codex.d.ts +4 -5
- package/sidecar/dist/constants/codex.js +25 -25
- package/sidecar/dist/constants/codex.js.map +1 -1
- package/sidecar/dist/constants/database.js.map +1 -1
- package/sidecar/dist/constants/mcp.js.map +1 -1
- package/sidecar/dist/constants/paths.js.map +1 -1
- package/sidecar/dist/constants/project.d.ts +1 -1
- package/sidecar/dist/constants/project.js.map +1 -1
- package/sidecar/dist/constants/providers.js.map +1 -1
- package/sidecar/dist/constants/questions.js.map +1 -1
- package/sidecar/dist/constants/read-access.d.ts +1 -1
- package/sidecar/dist/constants/read-access.js +7 -7
- package/sidecar/dist/constants/read-access.js.map +1 -1
- package/sidecar/dist/constants/timing.js.map +1 -1
- package/sidecar/dist/database/database-client.d.ts +3 -2
- package/sidecar/dist/database/database-client.js +13 -9
- package/sidecar/dist/database/database-client.js.map +1 -1
- package/sidecar/dist/host/project-info.js +13 -7
- package/sidecar/dist/host/project-info.js.map +1 -1
- package/sidecar/dist/index.d.ts +1 -1
- package/sidecar/dist/index.js +60 -34
- package/sidecar/dist/index.js.map +1 -1
- package/sidecar/dist/logs/logs-client.js +12 -2
- package/sidecar/dist/logs/logs-client.js.map +1 -1
- package/sidecar/dist/mcp/define-tool.d.ts +9 -8
- package/sidecar/dist/mcp/define-tool.js.map +1 -1
- package/sidecar/dist/mcp/http-binding.js.map +1 -1
- package/sidecar/dist/mcp/tools/ask-user.js.map +1 -1
- package/sidecar/dist/mcp/tools/builder-catalog.d.ts +6 -1
- package/sidecar/dist/mcp/tools/builder-catalog.js +32 -24
- package/sidecar/dist/mcp/tools/builder-catalog.js.map +1 -1
- package/sidecar/dist/mcp/tools/builder-gate.js.map +1 -1
- package/sidecar/dist/mcp/tools/builder-project.d.ts +2 -1
- package/sidecar/dist/mcp/tools/builder-project.js +60 -43
- package/sidecar/dist/mcp/tools/builder-project.js.map +1 -1
- package/sidecar/dist/mcp/tools/builder.js +8 -9
- package/sidecar/dist/mcp/tools/builder.js.map +1 -1
- package/sidecar/dist/mcp/tools/database.js +5 -9
- package/sidecar/dist/mcp/tools/database.js.map +1 -1
- package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
- package/sidecar/dist/mcp/tools/get-current-page.js.map +1 -1
- package/sidecar/dist/mcp/tools/list-pages.js.map +1 -1
- package/sidecar/dist/mcp/tools/navigate-to-page.js +6 -6
- package/sidecar/dist/mcp/tools/navigate-to-page.js.map +1 -1
- package/sidecar/dist/mcp/tools/preview-blocks.js +15 -11
- package/sidecar/dist/mcp/tools/preview-blocks.js.map +1 -1
- package/sidecar/dist/mcp/tools/query-logs.js.map +1 -1
- package/sidecar/dist/mcp/tools/typecheck.js.map +1 -1
- package/sidecar/dist/metrics/aggregate.js.map +1 -1
- package/sidecar/dist/metrics/types.js.map +1 -1
- package/sidecar/dist/pages/registry-client.js +14 -5
- package/sidecar/dist/pages/registry-client.js.map +1 -1
- package/sidecar/dist/pages/route-match.js.map +1 -1
- package/sidecar/dist/protocol/events.d.ts +7 -7
- package/sidecar/dist/protocol/events.js.map +1 -1
- package/sidecar/dist/protocol/json.d.ts +5 -0
- package/sidecar/dist/protocol/json.js +3 -0
- package/sidecar/dist/protocol/json.js.map +1 -0
- package/sidecar/dist/protocol/messages.d.ts +4 -4
- package/sidecar/dist/protocol/messages.js +3 -3
- package/sidecar/dist/protocol/messages.js.map +1 -1
- package/sidecar/dist/providers/claude/activity.d.ts +4 -2
- package/sidecar/dist/providers/claude/activity.js +29 -38
- package/sidecar/dist/providers/claude/activity.js.map +1 -1
- package/sidecar/dist/providers/claude/adapter.js +62 -91
- package/sidecar/dist/providers/claude/adapter.js.map +1 -1
- package/sidecar/dist/providers/claude/attachments.js +4 -4
- package/sidecar/dist/providers/claude/attachments.js.map +1 -1
- package/sidecar/dist/providers/claude/config.js.map +1 -1
- package/sidecar/dist/providers/claude/module.js.map +1 -1
- package/sidecar/dist/providers/claude/provider.d.ts +4 -2
- package/sidecar/dist/providers/claude/provider.js.map +1 -1
- package/sidecar/dist/providers/claude/resolve-binary.js +6 -4
- package/sidecar/dist/providers/claude/resolve-binary.js.map +1 -1
- package/sidecar/dist/providers/claude/safe-mode.js +14 -3
- package/sidecar/dist/providers/claude/safe-mode.js.map +1 -1
- package/sidecar/dist/providers/codex/adapter.js +124 -46
- package/sidecar/dist/providers/codex/adapter.js.map +1 -1
- package/sidecar/dist/providers/codex/attachments.js +1 -1
- package/sidecar/dist/providers/codex/attachments.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/credentials.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/http.js +8 -5
- package/sidecar/dist/providers/codex/chatgpt/http.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/lock.js +3 -3
- package/sidecar/dist/providers/codex/chatgpt/lock.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/login.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/oauth.js +51 -8
- package/sidecar/dist/providers/codex/chatgpt/oauth.js.map +1 -1
- package/sidecar/dist/providers/codex/chatgpt/store.js +23 -23
- package/sidecar/dist/providers/codex/chatgpt/store.js.map +1 -1
- package/sidecar/dist/providers/codex/client.d.ts +4 -3
- package/sidecar/dist/providers/codex/client.js +10 -3
- package/sidecar/dist/providers/codex/client.js.map +1 -1
- package/sidecar/dist/providers/codex/config.js.map +1 -1
- package/sidecar/dist/providers/codex/module.js.map +1 -1
- package/sidecar/dist/providers/codex/permissions.d.ts +2 -2
- package/sidecar/dist/providers/codex/permissions.js +20 -6
- package/sidecar/dist/providers/codex/permissions.js.map +1 -1
- package/sidecar/dist/providers/codex/process.d.ts +3 -1
- package/sidecar/dist/providers/codex/process.js +35 -36
- package/sidecar/dist/providers/codex/process.js.map +1 -1
- package/sidecar/dist/providers/codex/provider.d.ts +2 -0
- package/sidecar/dist/providers/codex/provider.js +23 -7
- package/sidecar/dist/providers/codex/provider.js.map +1 -1
- package/sidecar/dist/providers/codex/resolve-binary.js +2 -2
- package/sidecar/dist/providers/codex/resolve-binary.js.map +1 -1
- package/sidecar/dist/providers/codex/stop.d.ts +6 -1
- package/sidecar/dist/providers/codex/stop.js +29 -22
- package/sidecar/dist/providers/codex/stop.js.map +1 -1
- package/sidecar/dist/providers/registry.js +4 -5
- package/sidecar/dist/providers/registry.js.map +1 -1
- package/sidecar/dist/server/chat-socket-registry.d.ts +6 -6
- package/sidecar/dist/server/chat-socket-registry.js.map +1 -1
- package/sidecar/dist/server/conversation-runs.d.ts +1 -1
- package/sidecar/dist/server/conversation-runs.js +4 -5
- package/sidecar/dist/server/conversation-runs.js.map +1 -1
- package/sidecar/dist/server/host-socket-registry.d.ts +4 -3
- package/sidecar/dist/server/host-socket-registry.js.map +1 -1
- package/sidecar/dist/server/http-io.d.ts +1 -1
- package/sidecar/dist/server/http-io.js +2 -3
- package/sidecar/dist/server/http-io.js.map +1 -1
- package/sidecar/dist/server/http-params.js.map +1 -1
- package/sidecar/dist/server/http-routes.d.ts +9 -2
- package/sidecar/dist/server/http-routes.js +15 -8
- package/sidecar/dist/server/http-routes.js.map +1 -1
- package/sidecar/dist/server/http.js +19 -10
- package/sidecar/dist/server/http.js.map +1 -1
- package/sidecar/dist/server/pending-queue.js +4 -1
- package/sidecar/dist/server/pending-queue.js.map +1 -1
- package/sidecar/dist/server/raw-data.js +2 -1
- package/sidecar/dist/server/raw-data.js.map +1 -1
- package/sidecar/dist/server/routing.js +1 -1
- package/sidecar/dist/server/routing.js.map +1 -1
- package/sidecar/dist/server/safety-net.js +20 -9
- package/sidecar/dist/server/safety-net.js.map +1 -1
- package/sidecar/dist/server/tool-calls.js +7 -5
- package/sidecar/dist/server/tool-calls.js.map +1 -1
- package/sidecar/dist/server/turns.js +2 -1
- package/sidecar/dist/server/turns.js.map +1 -1
- package/sidecar/dist/server/ws.js +2 -6
- package/sidecar/dist/server/ws.js.map +1 -1
- package/sidecar/dist/skills/build-catalog.js.map +1 -1
- package/sidecar/dist/skills/frontmatter.js.map +1 -1
- package/sidecar/dist/skills/plugin-wrapper.js.map +1 -1
- package/sidecar/dist/skills/resolve-sources.js.map +1 -1
- package/sidecar/dist/skills/skill-usage.js +18 -12
- package/sidecar/dist/skills/skill-usage.js.map +1 -1
- package/sidecar/dist/state/conversations.js.map +1 -1
- package/sidecar/dist/state/lock.js +6 -2
- package/sidecar/dist/state/lock.js.map +1 -1
- package/sidecar/dist/state/settings-store.d.ts +31 -1
- package/sidecar/dist/state/settings-store.js +23 -24
- package/sidecar/dist/state/settings-store.js.map +1 -1
- package/sidecar/dist/state/settings-types.js.map +1 -1
- package/sidecar/dist/state/sidecar-version.js +3 -1
- package/sidecar/dist/state/sidecar-version.js.map +1 -1
- package/sidecar/dist/state/store.js +1 -1
- package/sidecar/dist/state/store.js.map +1 -1
- package/sidecar/dist/state/types.js.map +1 -1
- package/sidecar/package.json +1 -7
- package/skills/dms-ai/cases.json +47 -0
- package/skills/dms-ai-code-mode/cases.json +38 -0
- package/skills/dms-builder-safe/cases.json +67 -0
- package/skills/dms-external-database/REFERENCE.md +34 -9
- package/skills/dms-external-database/SKILL.md +24 -11
- package/skills/dms-external-database/cases.json +47 -0
- package/frontend-vue/tests/i18n-panel.test.ts +0 -118
- package/frontend-vue/tests/i18n-views.test.ts +0 -63
- package/frontend-vue/tests/panel-header-height.test.ts +0 -40
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
3
|
+
import type { Plugin } from "vite";
|
|
4
|
+
|
|
5
|
+
const BACKEND_PACKAGE = "@antelopejs/dms-ai";
|
|
6
|
+
const BACKEND_SOURCE = "src";
|
|
7
|
+
const QUERY_OR_HASH = /[?#].*$/;
|
|
8
|
+
|
|
9
|
+
const backendRoots = new Map<string, string | null>();
|
|
10
|
+
|
|
11
|
+
function packageName(manifest: string): string | undefined {
|
|
12
|
+
try {
|
|
13
|
+
const parsed: { name?: string } = JSON.parse(
|
|
14
|
+
readFileSync(manifest, "utf8"),
|
|
15
|
+
);
|
|
16
|
+
return parsed.name;
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function backendRoot(directory: string): string | null {
|
|
23
|
+
const known = backendRoots.get(directory);
|
|
24
|
+
if (known !== undefined) return known;
|
|
25
|
+
const manifest = join(directory, "package.json");
|
|
26
|
+
const parent = dirname(directory);
|
|
27
|
+
let root: string | null = null;
|
|
28
|
+
if (existsSync(manifest)) {
|
|
29
|
+
root = packageName(manifest) === BACKEND_PACKAGE ? directory : null;
|
|
30
|
+
} else if (parent !== directory) {
|
|
31
|
+
root = backendRoot(parent);
|
|
32
|
+
}
|
|
33
|
+
backendRoots.set(directory, root);
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The real path of a resolved module id when it lies under the `src`
|
|
39
|
+
* directory of the backend package, or `undefined`. The package that owns the
|
|
40
|
+
* file decides, not a path, so the answer is the same in the repository and in
|
|
41
|
+
* the workspace the DMS builds the frontend in. Virtual ids and ids that are
|
|
42
|
+
* not files are ignored.
|
|
43
|
+
*/
|
|
44
|
+
function backendSourceFile(id: string): string | undefined {
|
|
45
|
+
const path = id.replace(QUERY_OR_HASH, "");
|
|
46
|
+
if (!isAbsolute(path)) return undefined;
|
|
47
|
+
let real: string;
|
|
48
|
+
try {
|
|
49
|
+
real = realpathSync(path);
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const root = backendRoot(dirname(real));
|
|
54
|
+
if (root === null) return undefined;
|
|
55
|
+
return relative(root, real).split(sep)[0] === BACKEND_SOURCE
|
|
56
|
+
? real
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fails the build, the dev server and the tests when a module of the frontend
|
|
62
|
+
* resolves to a file under the backend's `src`: the browser reaches the
|
|
63
|
+
* backend only through its HTTP routes. It checks what the whole resolution
|
|
64
|
+
* chain returns, so an alias is seen through and a symlink the resolver
|
|
65
|
+
* returns is followed to its real path, and it runs before the other plugins
|
|
66
|
+
* (`enforce: "pre"`).
|
|
67
|
+
*/
|
|
68
|
+
export function backendBoundary(): Plugin {
|
|
69
|
+
return {
|
|
70
|
+
name: "dms-ai:backend-boundary",
|
|
71
|
+
enforce: "pre",
|
|
72
|
+
async resolveId(source, importer, options) {
|
|
73
|
+
const resolved = await this.resolve(source, importer, {
|
|
74
|
+
...options,
|
|
75
|
+
skipSelf: true,
|
|
76
|
+
});
|
|
77
|
+
if (resolved === null || resolved.external) return resolved;
|
|
78
|
+
const file = backendSourceFile(resolved.id);
|
|
79
|
+
if (file === undefined) return resolved;
|
|
80
|
+
const from =
|
|
81
|
+
importer === undefined
|
|
82
|
+
? "an entry point"
|
|
83
|
+
: importer.replace(QUERY_OR_HASH, "");
|
|
84
|
+
return this.error(
|
|
85
|
+
`Backend boundary: ${from} imports ${file}, which is under src of ${BACKEND_PACKAGE}. The frontend reaches the backend only through its HTTP routes.`,
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"packageManager": "pnpm@10.6.5",
|
|
8
8
|
"engines": {
|
|
9
|
-
"@antelopejs/dms-frontend": ">=0.9.
|
|
9
|
+
"@antelopejs/dms-frontend": ">=0.9.1 <0.10.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "pnpm --dir \"${DMS_FRONTEND_WORKSPACE:?Set DMS_FRONTEND_WORKSPACE to the generated Inertia workspace}\" build",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
|
38
38
|
"typescript": "^5.8.2",
|
|
39
39
|
"typescript-eslint": "^8.0.0",
|
|
40
|
+
"vite": "^7.3.2",
|
|
40
41
|
"vitest": "^4.1.5",
|
|
41
42
|
"vue-tsc": "^3.1.1"
|
|
42
43
|
}
|
|
@@ -60,6 +60,9 @@ importers:
|
|
|
60
60
|
typescript-eslint:
|
|
61
61
|
specifier: ^8.0.0
|
|
62
62
|
version: 8.70.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
|
|
63
|
+
vite:
|
|
64
|
+
specifier: ^7.3.2
|
|
65
|
+
version: 7.3.2(jiti@2.7.0)(terser@5.46.2)(yaml@2.8.4)
|
|
63
66
|
vitest:
|
|
64
67
|
specifier: ^4.1.5
|
|
65
68
|
version: 4.1.5(jsdom@29.1.1)(vite@7.3.2(jiti@2.7.0)(terser@5.46.2)(yaml@2.8.4))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { flushPromises } from "@vue/test-utils";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { shallowRef } from "vue";
|
|
5
|
+
import { publicAssistantActions } from "../app/runtime/assistant-actions";
|
|
6
|
+
import {
|
|
7
|
+
createHarness,
|
|
8
|
+
openPanel,
|
|
9
|
+
sentOfType,
|
|
10
|
+
unmountPanel,
|
|
11
|
+
} from "./support/panel-harness";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
unmountPanel();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const PROMPT =
|
|
18
|
+
"Use dms-external-database on connection roaster, module builder-db-roaster-tables.";
|
|
19
|
+
|
|
20
|
+
describe("the assistant actions another module's layer injects", () => {
|
|
21
|
+
it("start a new conversation with the composer prefilled and nothing sent", async () => {
|
|
22
|
+
const harness = createHarness();
|
|
23
|
+
const panel = await openPanel(harness);
|
|
24
|
+
const actions = publicAssistantActions(shallowRef(harness.session));
|
|
25
|
+
expect(actions.isAvailable).toBe(true);
|
|
26
|
+
actions.startConversation(PROMPT);
|
|
27
|
+
await flushPromises();
|
|
28
|
+
expect((panel.find("textarea").element as HTMLTextAreaElement).value).toBe(
|
|
29
|
+
PROMPT,
|
|
30
|
+
);
|
|
31
|
+
expect(sentOfType(harness, "user_message")).toEqual([]);
|
|
32
|
+
expect(sentOfType(harness, "set_conversation_mode")).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("set the new conversation to plan mode before prefilling it, when asked", async () => {
|
|
36
|
+
const harness = createHarness();
|
|
37
|
+
const panel = await openPanel(harness);
|
|
38
|
+
const actions = publicAssistantActions(shallowRef(harness.session));
|
|
39
|
+
actions.startConversation(PROMPT, { mode: "plan" });
|
|
40
|
+
await flushPromises();
|
|
41
|
+
const started = sentOfType(harness, "hello").at(-1)?.conversationId;
|
|
42
|
+
expect(sentOfType(harness, "set_conversation_mode")).toEqual([
|
|
43
|
+
expect.objectContaining({ conversationId: started, mode: "plan" }),
|
|
44
|
+
]);
|
|
45
|
+
expect((panel.find("textarea").element as HTMLTextAreaElement).value).toBe(
|
|
46
|
+
PROMPT,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("set plan mode once the stream is up, when the panel opens before it", async () => {
|
|
51
|
+
const harness = createHarness();
|
|
52
|
+
await openPanel(harness);
|
|
53
|
+
harness.setChannelStatus("disconnected");
|
|
54
|
+
publicAssistantActions(shallowRef(harness.session)).startConversation(
|
|
55
|
+
PROMPT,
|
|
56
|
+
{ mode: "plan" },
|
|
57
|
+
);
|
|
58
|
+
await flushPromises();
|
|
59
|
+
expect(sentOfType(harness, "set_conversation_mode")).toEqual([]);
|
|
60
|
+
harness.setChannelStatus("connected");
|
|
61
|
+
await flushPromises();
|
|
62
|
+
expect(sentOfType(harness, "set_conversation_mode")).toEqual([
|
|
63
|
+
expect.objectContaining({ mode: "plan" }),
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("are unavailable without a session, and starting then does nothing", () => {
|
|
68
|
+
const actions = publicAssistantActions(shallowRef(null));
|
|
69
|
+
expect(actions.isAvailable).toBe(false);
|
|
70
|
+
expect(() => actions.startConversation(PROMPT)).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
realpathSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
symlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { build, type Plugin } from "vite";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
import { backendBoundary } from "../build/backend-boundary.ts";
|
|
14
|
+
|
|
15
|
+
let root: string;
|
|
16
|
+
|
|
17
|
+
function write(file: string, text: string): void {
|
|
18
|
+
const path = join(root, file);
|
|
19
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
20
|
+
writeFileSync(path, text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function manifest(directory: string, name: string): void {
|
|
24
|
+
write(`${directory}/package.json`, JSON.stringify({ name }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A frontend entry with `source` as its only import, built for real. */
|
|
28
|
+
async function buildEntry(source: string, extra: Plugin[] = []): Promise<void> {
|
|
29
|
+
write(
|
|
30
|
+
"frontend/entry.ts",
|
|
31
|
+
`import "${source}";\nexport const frontend = 1;\n`,
|
|
32
|
+
);
|
|
33
|
+
await build({
|
|
34
|
+
root: join(root, "frontend"),
|
|
35
|
+
configFile: false,
|
|
36
|
+
logLevel: "silent",
|
|
37
|
+
plugins: [backendBoundary(), ...extra],
|
|
38
|
+
build: {
|
|
39
|
+
write: false,
|
|
40
|
+
lib: { entry: join(root, "frontend/entry.ts"), formats: ["es"] },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), "dms-ai-boundary-")));
|
|
47
|
+
manifest("backend", "@antelopejs/dms-ai");
|
|
48
|
+
write("backend/src/routes/status.ts", "export const status = 1;\n");
|
|
49
|
+
write("backend/dist/index.js", "export const built = 1;\n");
|
|
50
|
+
write("backend/srcfoo/index.ts", "export const sibling = 1;\n");
|
|
51
|
+
manifest("other", "another-package");
|
|
52
|
+
write("other/src/index.ts", "export const other = 1;\n");
|
|
53
|
+
manifest("frontend", "@antelopejs/dms-ai-frontend-vue");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
rmSync(root, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("the backend boundary of a real build", () => {
|
|
61
|
+
it("fails when the frontend imports a file under the backend's src", async () => {
|
|
62
|
+
await expect(buildEntry("../backend/src/routes/status.ts")).rejects.toThrow(
|
|
63
|
+
/frontend\/entry\.ts imports .*backend\/src\/routes\/status\.ts/,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("fails when the import carries a query", async () => {
|
|
68
|
+
await expect(
|
|
69
|
+
buildEntry("../backend/src/routes/status.ts?raw"),
|
|
70
|
+
).rejects.toThrow(/Backend boundary/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("follows a symlink a resolver returns to the file it points at", async () => {
|
|
74
|
+
symlinkSync(
|
|
75
|
+
join(root, "backend/src/routes"),
|
|
76
|
+
join(root, "frontend/routes"),
|
|
77
|
+
);
|
|
78
|
+
const linked: Plugin = {
|
|
79
|
+
name: "test:linked",
|
|
80
|
+
enforce: "pre",
|
|
81
|
+
resolveId: (source) =>
|
|
82
|
+
source === "@linked/status"
|
|
83
|
+
? join(root, "frontend/routes/status.ts")
|
|
84
|
+
: null,
|
|
85
|
+
};
|
|
86
|
+
await expect(buildEntry("@linked/status", [linked])).rejects.toThrow(
|
|
87
|
+
/backend\/src\/routes\/status\.ts/,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("sees through an alias another plugin resolves", async () => {
|
|
92
|
+
const alias: Plugin = {
|
|
93
|
+
name: "test:alias",
|
|
94
|
+
enforce: "pre",
|
|
95
|
+
resolveId: (source) =>
|
|
96
|
+
source === "@backend/status"
|
|
97
|
+
? join(root, "backend/src/routes/status.ts")
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
await expect(buildEntry("@backend/status", [alias])).rejects.toThrow(
|
|
101
|
+
/Backend boundary/,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("lets the backend's built output through", async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
buildEntry("../backend/dist/index.js"),
|
|
108
|
+
).resolves.toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("lets a sibling directory whose name starts with src through", async () => {
|
|
112
|
+
await expect(
|
|
113
|
+
buildEntry("../backend/srcfoo/index.ts"),
|
|
114
|
+
).resolves.toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("lets the src of another package through", async () => {
|
|
118
|
+
await expect(buildEntry("../other/src/index.ts")).resolves.toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("ignores a virtual module", async () => {
|
|
122
|
+
const virtual: Plugin = {
|
|
123
|
+
name: "test:virtual",
|
|
124
|
+
enforce: "pre",
|
|
125
|
+
resolveId: (source) => (source === "virtual:ok" ? "\0virtual:ok" : null),
|
|
126
|
+
load: (id) => (id === "\0virtual:ok" ? "export const v = 1;" : null),
|
|
127
|
+
};
|
|
128
|
+
await expect(buildEntry("virtual:ok", [virtual])).resolves.toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
buttonWithText,
|
|
6
|
+
CONVERSATION_ID,
|
|
7
|
+
createHarness,
|
|
8
|
+
deliver,
|
|
9
|
+
openPanel,
|
|
10
|
+
sentOfType,
|
|
11
|
+
unmountPanel,
|
|
12
|
+
} from "./support/panel-harness";
|
|
13
|
+
import {
|
|
14
|
+
COMMAND_REQUEST,
|
|
15
|
+
DELETE_REQUEST,
|
|
16
|
+
DIFF_REQUEST,
|
|
17
|
+
keydown,
|
|
18
|
+
} from "./support/panel-fixtures";
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
localStorage.setItem("dms-ai-conversation-id", CONVERSATION_ID);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
unmountPanel();
|
|
26
|
+
localStorage.clear();
|
|
27
|
+
vi.useRealTimers();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("the approvals dock", () => {
|
|
31
|
+
it("previews a diff with line numbers and allows a scope with ↵", async () => {
|
|
32
|
+
// The countdown reads the clock: frozen at the request's creation, it
|
|
33
|
+
// shows the full grace period however slow the run is.
|
|
34
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
35
|
+
vi.setSystemTime(DIFF_REQUEST.createdAtMs);
|
|
36
|
+
const harness = createHarness();
|
|
37
|
+
const panel = await openPanel(harness);
|
|
38
|
+
deliver(harness, DIFF_REQUEST);
|
|
39
|
+
deliver(harness, COMMAND_REQUEST);
|
|
40
|
+
await nextTick();
|
|
41
|
+
const dock = panel.find(".cb-approve");
|
|
42
|
+
expect(dock.attributes("role")).toBe("alertdialog");
|
|
43
|
+
expect(dock.text()).toContain("Needs your approval · 1 of 2");
|
|
44
|
+
expect(dock.text()).toContain("denies in 4:12");
|
|
45
|
+
expect(panel.find(".perm.is-focused .perm__title").text()).toBe(
|
|
46
|
+
"Edit customer.form.ts",
|
|
47
|
+
);
|
|
48
|
+
expect(panel.findAll(".ln").map((row) => row.text())).toEqual([
|
|
49
|
+
"@@ −42 +42 @@",
|
|
50
|
+
'42−label: "VAT",',
|
|
51
|
+
'42+label: "VAT number",',
|
|
52
|
+
]);
|
|
53
|
+
expect(dock.text()).toContain("Every edit under crm/customers/");
|
|
54
|
+
await panel.findAll(".perm.is-focused .scope-opt input")[2].setValue(true);
|
|
55
|
+
keydown(dock.element, "Enter");
|
|
56
|
+
await nextTick();
|
|
57
|
+
expect(sentOfType(harness, "permission_response")[0]).toEqual({
|
|
58
|
+
type: "permission_response",
|
|
59
|
+
conversationId: CONVERSATION_ID,
|
|
60
|
+
requestId: "r1",
|
|
61
|
+
decision: "allow_rule",
|
|
62
|
+
rule: { kind: "directory", value: "crm/customers/" },
|
|
63
|
+
});
|
|
64
|
+
expect(panel.find(".perm.is-focused .perm__title").text()).toBe(
|
|
65
|
+
"Run pnpm add jsvat",
|
|
66
|
+
);
|
|
67
|
+
expect(panel.find(".perm.is-focused").text()).toContain(
|
|
68
|
+
"adds a dependency · package.json, pnpm-lock.yaml",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("moves with J and K, denies with N, and denies everything at once", async () => {
|
|
73
|
+
const harness = createHarness();
|
|
74
|
+
const panel = await openPanel(harness);
|
|
75
|
+
deliver(harness, DIFF_REQUEST);
|
|
76
|
+
deliver(harness, COMMAND_REQUEST);
|
|
77
|
+
await nextTick();
|
|
78
|
+
const dock = panel.find(".cb-approve");
|
|
79
|
+
keydown(dock.element, "j");
|
|
80
|
+
await nextTick();
|
|
81
|
+
expect(dock.text()).toContain("2 of 2");
|
|
82
|
+
keydown(dock.element, "k");
|
|
83
|
+
await nextTick();
|
|
84
|
+
expect(dock.text()).toContain("1 of 2");
|
|
85
|
+
keydown(dock.element, "n");
|
|
86
|
+
await nextTick();
|
|
87
|
+
expect(sentOfType(harness, "permission_response")[0]).toMatchObject({
|
|
88
|
+
requestId: "r1",
|
|
89
|
+
decision: "deny",
|
|
90
|
+
});
|
|
91
|
+
deliver(harness, DIFF_REQUEST);
|
|
92
|
+
await nextTick();
|
|
93
|
+
await buttonWithText(
|
|
94
|
+
panel.find(".cb-approve"),
|
|
95
|
+
"Deny both and stop",
|
|
96
|
+
).trigger("click");
|
|
97
|
+
expect(sentOfType(harness, "permission_response").at(-1)).toMatchObject({
|
|
98
|
+
decision: "deny_all",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("denies with the user's suggestion", async () => {
|
|
103
|
+
const harness = createHarness();
|
|
104
|
+
const panel = await openPanel(harness);
|
|
105
|
+
deliver(harness, DIFF_REQUEST);
|
|
106
|
+
await nextTick();
|
|
107
|
+
await buttonWithText(panel, "Suggest a change").trigger("click");
|
|
108
|
+
await panel
|
|
109
|
+
.find(".u-textarea")
|
|
110
|
+
.setValue("Use the Builder's validation instead");
|
|
111
|
+
await buttonWithText(panel, "Deny and send").trigger("click");
|
|
112
|
+
expect(sentOfType(harness, "permission_response")[0]).toMatchObject({
|
|
113
|
+
decision: "deny",
|
|
114
|
+
feedback: "Use the Builder's validation instead",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("makes a data loss typed, offers to keep the data, and never a chat-wide rule", async () => {
|
|
119
|
+
const harness = createHarness();
|
|
120
|
+
const panel = await openPanel(harness);
|
|
121
|
+
deliver(harness, DELETE_REQUEST);
|
|
122
|
+
await nextTick();
|
|
123
|
+
const card = panel.find(".perm.is-danger");
|
|
124
|
+
expect(card.text()).toContain("Data loss");
|
|
125
|
+
expect(card.text()).not.toContain("this chat");
|
|
126
|
+
const confirm = buttonWithText(card, "Delete");
|
|
127
|
+
expect(confirm.attributes("disabled")).toBeDefined();
|
|
128
|
+
await card.find(".perm__confirm input").setValue("legacy_coupons");
|
|
129
|
+
expect(
|
|
130
|
+
buttonWithText(card, "Delete").attributes("disabled"),
|
|
131
|
+
).toBeUndefined();
|
|
132
|
+
await card.find('input[type="checkbox"]').setValue(true);
|
|
133
|
+
await buttonWithText(card, "Remove the code only").trigger("click");
|
|
134
|
+
expect(sentOfType(harness, "permission_response")[0]).toMatchObject({
|
|
135
|
+
decision: "allow_once",
|
|
136
|
+
keepData: true,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("turns an expired request into a card that asks again", async () => {
|
|
141
|
+
const harness = createHarness();
|
|
142
|
+
const panel = await openPanel(harness);
|
|
143
|
+
deliver(harness, DIFF_REQUEST);
|
|
144
|
+
deliver(harness, {
|
|
145
|
+
type: "permission_expired",
|
|
146
|
+
requestId: "r1",
|
|
147
|
+
toolName: "Edit",
|
|
148
|
+
summary: "Edit customer.form.ts",
|
|
149
|
+
expiredAtMs: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
await nextTick();
|
|
152
|
+
expect(panel.find(".cb-approve").exists()).toBe(false);
|
|
153
|
+
const card = panel.find(".cb-expired");
|
|
154
|
+
expect(card.text()).toContain("Edit file request expired");
|
|
155
|
+
await buttonWithText(card, "Ask again").trigger("click");
|
|
156
|
+
expect(sentOfType(harness, "user_message")[0].content).toContain(
|
|
157
|
+
"Edit customer.form.ts",
|
|
158
|
+
);
|
|
159
|
+
expect(panel.find(".cb-expired").exists()).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("lists the rules allowed in this chat and revokes one", async () => {
|
|
163
|
+
const harness = createHarness();
|
|
164
|
+
const panel = await openPanel(harness);
|
|
165
|
+
deliver(harness, DIFF_REQUEST);
|
|
166
|
+
deliver(harness, {
|
|
167
|
+
type: "rules_state",
|
|
168
|
+
rules: [
|
|
169
|
+
{
|
|
170
|
+
id: "rule-1",
|
|
171
|
+
kind: "command",
|
|
172
|
+
value: "pnpm test",
|
|
173
|
+
label: "Commands starting pnpm test",
|
|
174
|
+
createdAtMs: 1,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
await nextTick();
|
|
179
|
+
const rules = panel.find(".cb-approve .cb-rules");
|
|
180
|
+
expect(rules.text()).toContain("Commands starting pnpm test");
|
|
181
|
+
await buttonWithText(rules, "Revoke").trigger("click");
|
|
182
|
+
expect(sentOfType(harness, "revoke_rule")).toEqual([
|
|
183
|
+
{
|
|
184
|
+
type: "revoke_rule",
|
|
185
|
+
conversationId: CONVERSATION_ID,
|
|
186
|
+
ruleId: "rule-1",
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
buttonWithText,
|
|
6
|
+
CONVERSATION_ID,
|
|
7
|
+
createHarness,
|
|
8
|
+
deliver,
|
|
9
|
+
openPanel,
|
|
10
|
+
sendMessage,
|
|
11
|
+
sentOfType,
|
|
12
|
+
unmountPanel,
|
|
13
|
+
} from "./support/panel-harness";
|
|
14
|
+
import { keydown } from "./support/panel-fixtures";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
localStorage.setItem("dms-ai-conversation-id", CONVERSATION_ID);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
unmountPanel();
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("questions, queue and modes", () => {
|
|
27
|
+
const QUESTIONS = {
|
|
28
|
+
type: "ask_question",
|
|
29
|
+
requestId: "q1",
|
|
30
|
+
questions: [
|
|
31
|
+
{
|
|
32
|
+
question: "Where should the page live?",
|
|
33
|
+
header: "Placement",
|
|
34
|
+
options: [
|
|
35
|
+
{
|
|
36
|
+
label: "Finance › Overdue invoices",
|
|
37
|
+
description: "Next to Invoices",
|
|
38
|
+
},
|
|
39
|
+
{ label: "A tab on Invoices", description: "" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
question: "Default sort?",
|
|
44
|
+
header: "Sort",
|
|
45
|
+
options: [
|
|
46
|
+
{ label: "Days overdue", description: "" },
|
|
47
|
+
{ label: "Amount", description: "" },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("pages through questions with numbers, Other… and Skip, then keeps the answers", async () => {
|
|
54
|
+
const harness = createHarness();
|
|
55
|
+
const panel = await openPanel(harness);
|
|
56
|
+
deliver(harness, QUESTIONS);
|
|
57
|
+
await nextTick();
|
|
58
|
+
const dock = panel.find(".cb-ask");
|
|
59
|
+
expect(dock.text()).toContain("Question 1 of 2 · Placement");
|
|
60
|
+
expect(dock.find(".opt-card.is-selected").text()).toContain("Suggested");
|
|
61
|
+
keydown(dock.element, "3");
|
|
62
|
+
await nextTick();
|
|
63
|
+
await dock.find(".cb-ask__custom").setValue("Under Finance, as a tab");
|
|
64
|
+
await buttonWithText(dock.find(".cb-ask__foot"), "Next").trigger("click");
|
|
65
|
+
expect(dock.text()).toContain("Question 2 of 2 · Sort");
|
|
66
|
+
await buttonWithText(dock, "Skip, you decide").trigger("click");
|
|
67
|
+
expect(sentOfType(harness, "question_response")[0]).toEqual({
|
|
68
|
+
type: "question_response",
|
|
69
|
+
conversationId: CONVERSATION_ID,
|
|
70
|
+
requestId: "q1",
|
|
71
|
+
answers: ["Under Finance, as a tab", ""],
|
|
72
|
+
skipped: [false, true],
|
|
73
|
+
});
|
|
74
|
+
await nextTick();
|
|
75
|
+
const record = panel.find(".cb-answers");
|
|
76
|
+
expect(record.text()).toContain("“Under Finance, as a tab”");
|
|
77
|
+
expect(record.text()).toContain("Skipped: the assistant decides");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("edits, reorders and clears the queue", async () => {
|
|
81
|
+
const harness = createHarness();
|
|
82
|
+
const panel = await openPanel(harness);
|
|
83
|
+
deliver(harness, {
|
|
84
|
+
type: "queue_state",
|
|
85
|
+
items: [
|
|
86
|
+
{ id: "a", content: "Sort by order count" },
|
|
87
|
+
{ id: "b", content: "Limit to 3 orders" },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
await nextTick();
|
|
91
|
+
await panel
|
|
92
|
+
.find('button[aria-label="Edit queued message"]')
|
|
93
|
+
.trigger("click");
|
|
94
|
+
await panel.find(".cb-queued__editor").setValue("Sort by revenue");
|
|
95
|
+
await buttonWithText(panel.find(".cb-queue"), "Save").trigger("click");
|
|
96
|
+
keydown(panel.findAll(".cb-queued__handle")[1].element, "ArrowUp");
|
|
97
|
+
await nextTick();
|
|
98
|
+
expect(panel.findAll(".cb-queued__txt").map((item) => item.text())).toEqual(
|
|
99
|
+
["Limit to 3 orders", "Sort by revenue"],
|
|
100
|
+
);
|
|
101
|
+
await buttonWithText(panel.find(".cb-queue"), "Clear").trigger("click");
|
|
102
|
+
expect(
|
|
103
|
+
harness.sent
|
|
104
|
+
.map((msg) => msg.type)
|
|
105
|
+
.filter((type) => String(type).startsWith("queue_")),
|
|
106
|
+
).toEqual(["queue_update", "queue_move", "queue_clear"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("switches the chat's mode and scope, and confirms Full auto with a duration", async () => {
|
|
110
|
+
const harness = createHarness();
|
|
111
|
+
const panel = await openPanel(harness);
|
|
112
|
+
await buttonWithText(panel, "Plan only").trigger("click");
|
|
113
|
+
await buttonWithText(panel, "Full auto…").trigger("click");
|
|
114
|
+
await nextTick();
|
|
115
|
+
const modal = panel.find(".u-modal");
|
|
116
|
+
expect(modal.text()).toContain("Turn on Full auto?");
|
|
117
|
+
await buttonWithText(modal, "30 minutes").trigger("click");
|
|
118
|
+
await buttonWithText(modal, "Turn on Full auto").trigger("click");
|
|
119
|
+
expect(sentOfType(harness, "set_conversation_mode")).toEqual([
|
|
120
|
+
{
|
|
121
|
+
type: "set_conversation_mode",
|
|
122
|
+
conversationId: CONVERSATION_ID,
|
|
123
|
+
mode: "plan",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: "set_conversation_mode",
|
|
127
|
+
conversationId: CONVERSATION_ID,
|
|
128
|
+
fullAuto: { duration: "30m" },
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
await nextTick();
|
|
132
|
+
const banner = panel.find(".cb-banner-auto");
|
|
133
|
+
expect(banner.text()).toContain("Full auto · for 30 minutes");
|
|
134
|
+
await buttonWithText(banner, "Turn off").trigger("click");
|
|
135
|
+
expect(sentOfType(harness, "set_conversation_mode").at(-1)).toMatchObject({
|
|
136
|
+
fullAuto: null,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("runs a proposed plan in Ask first", async () => {
|
|
141
|
+
const harness = createHarness();
|
|
142
|
+
const panel = await openPanel(harness);
|
|
143
|
+
await sendMessage(panel, "plan a customer health page");
|
|
144
|
+
deliver(harness, {
|
|
145
|
+
type: "tool_call_start",
|
|
146
|
+
callId: "p1",
|
|
147
|
+
toolName: "ExitPlanMode",
|
|
148
|
+
args: { plan: "1. Create the page\n2. Add a query" },
|
|
149
|
+
});
|
|
150
|
+
deliver(harness, {
|
|
151
|
+
type: "tool_call_end",
|
|
152
|
+
callId: "p1",
|
|
153
|
+
status: "success",
|
|
154
|
+
result: "",
|
|
155
|
+
outcome: "done",
|
|
156
|
+
});
|
|
157
|
+
deliver(harness, { type: "run_done" });
|
|
158
|
+
await nextTick();
|
|
159
|
+
const plan = panel.find(".cb-plan");
|
|
160
|
+
expect(plan.text()).toContain("Proposed plan · 2 steps");
|
|
161
|
+
await buttonWithText(plan, "Run with Ask first").trigger("click");
|
|
162
|
+
expect(sentOfType(harness, "set_conversation_mode")[0]).toMatchObject({
|
|
163
|
+
mode: "normal",
|
|
164
|
+
});
|
|
165
|
+
expect(sentOfType(harness, "user_message").at(-1)).toMatchObject({
|
|
166
|
+
content: "Go ahead with the plan.",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|