@antelopejs/dms-ai 0.7.0 → 0.9.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.
Files changed (190) hide show
  1. package/README.md +17 -7
  2. package/dist/lifecycle/skill-sources.js +17 -17
  3. package/dist/lifecycle/skill-sources.js.map +1 -1
  4. package/dist/pages/settings/availability.js.map +1 -1
  5. package/dist/pages/settings/sections.js +10 -0
  6. package/dist/pages/settings/sections.js.map +1 -1
  7. package/dist/routes/builder.d.ts +7 -1
  8. package/dist/routes/builder.js +3 -0
  9. package/dist/routes/builder.js.map +1 -1
  10. package/dist/routes/changes.d.ts +5 -4
  11. package/dist/routes/changes.js.map +1 -1
  12. package/dist/routes/codex-account.d.ts +3 -2
  13. package/dist/routes/codex-account.js.map +1 -1
  14. package/dist/routes/sidecar-results.d.ts +2 -2
  15. package/dist/routes/sidecar-results.js.map +1 -1
  16. package/dist/routes/status.js.map +1 -1
  17. package/dist/sidecar/http-client.js +3 -1
  18. package/dist/sidecar/http-client.js.map +1 -1
  19. package/dist/sidecar/index.d.ts +1 -0
  20. package/dist/sidecar/index.js +1 -0
  21. package/dist/sidecar/index.js.map +1 -1
  22. package/dist/sidecar/json-body.d.ts +4 -0
  23. package/dist/sidecar/json-body.js +33 -0
  24. package/dist/sidecar/json-body.js.map +1 -0
  25. package/dist/sidecar/settings-values.d.ts +5 -2
  26. package/dist/sidecar/settings-values.js.map +1 -1
  27. package/dist/vocabulary/tools.js +3 -0
  28. package/dist/vocabulary/tools.js.map +1 -1
  29. package/frontend-vue/app/chat/components/ChatView.vue +38 -37
  30. package/frontend-vue/app/chat/components/NoticeCard.vue +16 -0
  31. package/frontend-vue/app/chat/components/PanelHeader.vue +2 -6
  32. package/frontend-vue/app/chat/composables/conversation-events.ts +4 -0
  33. package/frontend-vue/app/chat/composables/useConversation.ts +6 -0
  34. package/frontend-vue/app/chat/types/protocol.ts +9 -1
  35. package/frontend-vue/app/chat/utils/status-label.ts +50 -0
  36. package/frontend-vue/app/chat/utils/tool-lexicon.ts +16 -0
  37. package/frontend-vue/app/components/SkillDetail.vue +1 -1
  38. package/frontend-vue/app/plugins/ai.client.ts +2 -2
  39. package/frontend-vue/app/runtime/assistant-actions.ts +4 -2
  40. package/frontend-vue/app/runtime/assistant-session.ts +5 -1
  41. package/frontend-vue/app/runtime/block-health.ts +135 -0
  42. package/frontend-vue/app/runtime/constants.ts +1 -0
  43. package/frontend-vue/app/runtime/navigation-complete.ts +47 -6
  44. package/frontend-vue/app/runtime/panel-intents.ts +6 -2
  45. package/frontend-vue/app/views/ChangeSetPanel.vue +1 -1
  46. package/frontend-vue/app/views/composables/useAiData.ts +10 -18
  47. package/frontend-vue/app/views/composables/useChangeSetActions.ts +5 -15
  48. package/frontend-vue/app/views/constants.ts +1 -1
  49. package/frontend-vue/app/views/types.ts +1 -1
  50. package/frontend-vue/build/backend-boundary.ts +54 -21
  51. package/frontend-vue/i18n/locales/dms-ai-pages-en-GB.json +7 -0
  52. package/frontend-vue/i18n/locales/dms-ai-pages-fr-FR.json +7 -0
  53. package/frontend-vue/i18n/locales/dms-ai-panel-en-GB.json +6 -1
  54. package/frontend-vue/i18n/locales/dms-ai-panel-fr-FR.json +6 -1
  55. package/frontend-vue/tests/assistant-actions.test.ts +37 -0
  56. package/frontend-vue/tests/block-health.test.ts +142 -0
  57. package/frontend-vue/tests/chat/conversation-events.test.ts +34 -0
  58. package/frontend-vue/tests/chat/tool-lexicon.test.ts +1 -0
  59. package/frontend-vue/tests/navigation.test.ts +67 -2
  60. package/frontend-vue/tests/support/api-error-presenter.ts +28 -0
  61. package/frontend-vue/tests/support/panel-harness.ts +2 -2
  62. package/frontend-vue/tests/views/api-data-reads.test.ts +2 -0
  63. package/frontend-vue/tests/views/api-error-presentation.test.ts +89 -0
  64. package/package.json +7 -6
  65. package/sidecar/dist/agent/provider.d.ts +5 -0
  66. package/sidecar/dist/agent/runner.d.ts +5 -0
  67. package/sidecar/dist/agent/runner.js +2 -0
  68. package/sidecar/dist/agent/runner.js.map +1 -1
  69. package/sidecar/dist/builder/builder-client.js +2 -2
  70. package/sidecar/dist/builder/builder-client.js.map +1 -1
  71. package/sidecar/dist/constants/builder.d.ts +2 -1
  72. package/sidecar/dist/constants/builder.js +12 -0
  73. package/sidecar/dist/constants/builder.js.map +1 -1
  74. package/sidecar/dist/constants/claude.d.ts +2 -0
  75. package/sidecar/dist/constants/claude.js +2 -0
  76. package/sidecar/dist/constants/claude.js.map +1 -1
  77. package/sidecar/dist/constants/mcp.d.ts +1 -1
  78. package/sidecar/dist/constants/mcp.js +1 -1
  79. package/sidecar/dist/constants/mcp.js.map +1 -1
  80. package/sidecar/dist/constants/native-binary.d.ts +5 -2
  81. package/sidecar/dist/constants/native-binary.js +5 -2
  82. package/sidecar/dist/constants/native-binary.js.map +1 -1
  83. package/sidecar/dist/constants/settings.d.ts +1 -0
  84. package/sidecar/dist/constants/settings.js +2 -0
  85. package/sidecar/dist/constants/settings.js.map +1 -1
  86. package/sidecar/dist/constants/timing.d.ts +1 -0
  87. package/sidecar/dist/constants/timing.js +1 -0
  88. package/sidecar/dist/constants/timing.js.map +1 -1
  89. package/sidecar/dist/constants/tool-kinds.js +1 -0
  90. package/sidecar/dist/constants/tool-kinds.js.map +1 -1
  91. package/sidecar/dist/logs/logs-client.js +2 -1
  92. package/sidecar/dist/logs/logs-client.js.map +1 -1
  93. package/sidecar/dist/logs/types.d.ts +2 -1
  94. package/sidecar/dist/mcp/define-tool.d.ts +1 -1
  95. package/sidecar/dist/mcp/define-tool.js.map +1 -1
  96. package/sidecar/dist/mcp/tools/builder-compose.d.ts +51 -0
  97. package/sidecar/dist/mcp/tools/builder-compose.js +96 -0
  98. package/sidecar/dist/mcp/tools/builder-compose.js.map +1 -0
  99. package/sidecar/dist/mcp/tools/builder-expr.d.ts +5 -0
  100. package/sidecar/dist/mcp/tools/builder-expr.js +25 -0
  101. package/sidecar/dist/mcp/tools/builder-expr.js.map +1 -0
  102. package/sidecar/dist/mcp/tools/builder-gate.js +2 -2
  103. package/sidecar/dist/mcp/tools/builder-gate.js.map +1 -1
  104. package/sidecar/dist/mcp/tools/builder-project.js +54 -1
  105. package/sidecar/dist/mcp/tools/builder-project.js.map +1 -1
  106. package/sidecar/dist/mcp/tools/builder-queries.d.ts +65 -0
  107. package/sidecar/dist/mcp/tools/builder-queries.js +73 -0
  108. package/sidecar/dist/mcp/tools/builder-queries.js.map +1 -0
  109. package/sidecar/dist/mcp/tools/builder-table-view-schema.d.ts +99 -0
  110. package/sidecar/dist/mcp/tools/builder-table-view-schema.js +118 -0
  111. package/sidecar/dist/mcp/tools/builder-table-view-schema.js.map +1 -0
  112. package/sidecar/dist/mcp/tools/builder-table-view.d.ts +102 -0
  113. package/sidecar/dist/mcp/tools/builder-table-view.js +24 -0
  114. package/sidecar/dist/mcp/tools/builder-table-view.js.map +1 -0
  115. package/sidecar/dist/mcp/tools/builder.d.ts +166 -20
  116. package/sidecar/dist/mcp/tools/builder.js +20 -73
  117. package/sidecar/dist/mcp/tools/builder.js.map +1 -1
  118. package/sidecar/dist/mcp/tools/navigate-to-page.js +12 -4
  119. package/sidecar/dist/mcp/tools/navigate-to-page.js.map +1 -1
  120. package/sidecar/dist/mcp/types.d.ts +2 -1
  121. package/sidecar/dist/pages/block-health.d.ts +14 -0
  122. package/sidecar/dist/pages/block-health.js +60 -0
  123. package/sidecar/dist/pages/block-health.js.map +1 -0
  124. package/sidecar/dist/prompts/agent.d.ts +1 -1
  125. package/sidecar/dist/prompts/agent.js +4 -3
  126. package/sidecar/dist/prompts/agent.js.map +1 -1
  127. package/sidecar/dist/protocol/events.d.ts +43 -0
  128. package/sidecar/dist/protocol/events.js +10 -0
  129. package/sidecar/dist/protocol/events.js.map +1 -1
  130. package/sidecar/dist/protocol/json.d.ts +4 -0
  131. package/sidecar/dist/protocol/json.js +29 -0
  132. package/sidecar/dist/protocol/json.js.map +1 -1
  133. package/sidecar/dist/protocol/messages.d.ts +57 -0
  134. package/sidecar/dist/protocol/messages.js +22 -0
  135. package/sidecar/dist/protocol/messages.js.map +1 -1
  136. package/sidecar/dist/providers/claude/config.d.ts +20 -3
  137. package/sidecar/dist/providers/claude/config.js +17 -5
  138. package/sidecar/dist/providers/claude/config.js.map +1 -1
  139. package/sidecar/dist/providers/claude/model-fallback.d.ts +9 -0
  140. package/sidecar/dist/providers/claude/model-fallback.js +37 -0
  141. package/sidecar/dist/providers/claude/model-fallback.js.map +1 -0
  142. package/sidecar/dist/providers/claude/provider.js +23 -22
  143. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  144. package/sidecar/dist/providers/claude/resolve-binary.js +18 -13
  145. package/sidecar/dist/providers/claude/resolve-binary.js.map +1 -1
  146. package/sidecar/dist/providers/claude/session-stream.d.ts +23 -0
  147. package/sidecar/dist/providers/claude/session-stream.js +58 -0
  148. package/sidecar/dist/providers/claude/session-stream.js.map +1 -0
  149. package/sidecar/dist/server/chat-events.d.ts +16 -3
  150. package/sidecar/dist/server/chat-events.js +1 -0
  151. package/sidecar/dist/server/chat-events.js.map +1 -1
  152. package/sidecar/dist/server/http-io.d.ts +2 -1
  153. package/sidecar/dist/server/http-io.js.map +1 -1
  154. package/sidecar/dist/server/http-routes.js +4 -7
  155. package/sidecar/dist/server/http-routes.js.map +1 -1
  156. package/sidecar/dist/server/navigation-completer.d.ts +15 -2
  157. package/sidecar/dist/server/navigation-completer.js +42 -41
  158. package/sidecar/dist/server/navigation-completer.js.map +1 -1
  159. package/sidecar/dist/server/permission-events.d.ts +8 -4
  160. package/sidecar/dist/server/permission-events.js.map +1 -1
  161. package/sidecar/dist/server/routing.js +15 -2
  162. package/sidecar/dist/server/routing.js.map +1 -1
  163. package/sidecar/dist/server/safety-net.js +2 -2
  164. package/sidecar/dist/server/safety-net.js.map +1 -1
  165. package/sidecar/dist/server/tool-calls.d.ts +13 -4
  166. package/sidecar/dist/server/tool-calls.js.map +1 -1
  167. package/sidecar/dist/server/turns.d.ts +16 -2
  168. package/sidecar/dist/server/turns.js +13 -3
  169. package/sidecar/dist/server/turns.js.map +1 -1
  170. package/sidecar/dist/state/change-sets.js +4 -1
  171. package/sidecar/dist/state/change-sets.js.map +1 -1
  172. package/sidecar/dist/state/conversations.d.ts +1 -0
  173. package/sidecar/dist/state/conversations.js +8 -0
  174. package/sidecar/dist/state/conversations.js.map +1 -1
  175. package/sidecar/dist/state/settings-store.d.ts +1 -0
  176. package/sidecar/dist/state/settings-store.js +1 -0
  177. package/sidecar/dist/state/settings-store.js.map +1 -1
  178. package/sidecar/dist/state/settings-types.d.ts +1 -0
  179. package/sidecar/dist/state/store.js +8 -2
  180. package/sidecar/dist/state/store.js.map +1 -1
  181. package/sidecar/dist/state/types.d.ts +8 -1
  182. package/sidecar/dist/state/types.js.map +1 -1
  183. package/sidecar/package.json +2 -2
  184. package/skills/dms-ai/SKILL.md +1 -1
  185. package/skills/dms-builder-safe/REFERENCE.md +5 -6
  186. package/skills/dms-builder-safe/SKILL.md +12 -10
  187. package/skills/dms-external-database/REFERENCE.md +20 -8
  188. package/skills/dms-external-database/SKILL.md +64 -18
  189. package/skills/dms-page-design/SKILL.md +192 -0
  190. package/skills/dms-page-design/cases.json +57 -0
@@ -40,16 +40,10 @@ interface FetchErrorResponse {
40
40
  status?: number;
41
41
  }
42
42
 
43
- interface FetchErrorBody {
44
- message?: unknown;
45
- }
46
-
47
43
  interface FetchErrorLike {
48
44
  status?: number;
49
45
  statusCode?: number;
50
46
  response?: FetchErrorResponse;
51
- data?: FetchErrorBody;
52
- message?: string;
53
47
  }
54
48
 
55
49
  /** The HTTP status of a `$authFetch` rejection, if it got an answer. */
@@ -60,14 +54,6 @@ function errorStatus(error: unknown): number | undefined {
60
54
  );
61
55
  }
62
56
 
63
- /** The server's `{ message }`, else the error's own text. */
64
- export function errorMessage(error: unknown): string {
65
- const candidate = error as FetchErrorLike | null;
66
- const serverMessage = candidate?.data?.message;
67
- if (typeof serverMessage === "string") return serverMessage;
68
- return candidate?.message ?? String(error);
69
- }
70
-
71
57
  export function classifyFailure(error: unknown): RequestFailure {
72
58
  return errorStatus(error) === HTTP_SERVICE_UNAVAILABLE
73
59
  ? "unavailable"
@@ -80,15 +66,21 @@ export function classifyFailure(error: unknown): RequestFailure {
80
66
  * whether the assistant isn't running (503) rather than an error.
81
67
  */
82
68
  function describeFailure(result: FailingRead): FailureDescription {
69
+ const { resolveError } = useApiErrorPresenter();
70
+ const notice = computed(() =>
71
+ result.error.value
72
+ ? resolveError(result.error.value, {
73
+ fallbackKey: `dms_ai.views.${classifyFailure(result.error.value)}.description`,
74
+ })
75
+ : null,
76
+ );
83
77
  return {
84
78
  failure: computed(() =>
85
- result.error.value && result.status.value !== "pending"
79
+ notice.value && result.status.value !== "pending"
86
80
  ? classifyFailure(result.error.value)
87
81
  : null,
88
82
  ),
89
- message: computed(() =>
90
- result.error.value ? errorMessage(result.error.value) : "",
91
- ),
83
+ message: computed(() => notice.value?.message ?? ""),
92
84
  };
93
85
  }
94
86
 
@@ -1,7 +1,7 @@
1
1
  import { ref, type Ref } from "vue";
2
2
  import { AI_ROUTES } from "../constants";
3
3
  import type { ChangeSetSummary } from "../types";
4
- import { classifyFailure, errorMessage } from "./useAiData";
4
+ import { classifyFailure } from "./useAiData";
5
5
 
6
6
  export interface ChangeSetActions {
7
7
  isBusy: Ref<boolean>;
@@ -28,23 +28,11 @@ function changeSetPath(changeSetId: string, action: string): string {
28
28
  export function useChangeSetActions(): ChangeSetActions {
29
29
  const { t } = useI18n();
30
30
  const toast = useToast();
31
+ const { presentError } = useApiErrorPresenter();
31
32
  const { $authFetch } = useAuthFetch();
32
33
  const { confirmFrom } = useConfirmFrom();
33
34
  const isBusy = ref(false);
34
35
 
35
- function failureToast(error: unknown): void {
36
- const failure = classifyFailure(error);
37
- toast.add({
38
- title: t(`dms_ai.views.${failure}.title`),
39
- description:
40
- failure === "error"
41
- ? errorMessage(error)
42
- : t("dms_ai.views.unavailable.description"),
43
- color: "error",
44
- icon: "i-ph-warning-circle",
45
- });
46
- }
47
-
48
36
  async function redo(changeSet: ChangeSetSummary): Promise<void> {
49
37
  isBusy.value = true;
50
38
  try {
@@ -59,7 +47,9 @@ export function useChangeSetActions(): ChangeSetActions {
59
47
  });
60
48
  await refreshPageBlocks();
61
49
  } catch (error) {
62
- failureToast(error);
50
+ presentError(error, {
51
+ fallbackKey: `dms_ai.views.${classifyFailure(error)}.description`,
52
+ });
63
53
  } finally {
64
54
  isBusy.value = false;
65
55
  }
@@ -1,5 +1,5 @@
1
1
  import type { ActivityResult, AllowedBy } from "./types";
2
- import type { Tone } from "#dms-ui/app/types/tone";
2
+ import type { Tone } from "#dms-core/app/types/tone";
3
3
 
4
4
  export const AI_ROUTES = {
5
5
  status: "/ai/status",
@@ -1,4 +1,4 @@
1
- import type { Tone } from "#dms-ui/app/types/tone";
1
+ import type { Tone } from "#dms-core/app/types/tone";
2
2
 
3
3
  type AgentId = "claude" | "codex";
4
4
  export type ScopeId = "safe" | "vibe";
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
3
- import type { Plugin } from "vite";
3
+ import type { DmsVitePluginEntry } from "#dms/frontend-build";
4
+
5
+ type VitePlugin = Extract<DmsVitePluginEntry, { name: string }>;
4
6
 
5
7
  const BACKEND_PACKAGE = "@antelopejs/dms-ai";
6
8
  const BACKEND_SOURCE = "src";
@@ -8,20 +10,32 @@ const QUERY_OR_HASH = /[?#].*$/;
8
10
 
9
11
  const backendRoots = new Map<string, string | null>();
10
12
 
13
+ /** Whether a parsed manifest names its package. */
14
+ function isNamedManifest(value: unknown): value is { name: string } {
15
+ return (
16
+ typeof value === "object" &&
17
+ value !== null &&
18
+ "name" in value &&
19
+ typeof value.name === "string"
20
+ );
21
+ }
22
+
23
+ /** The package name a `package.json` declares, or `undefined`. */
11
24
  function packageName(manifest: string): string | undefined {
12
25
  try {
13
- const parsed: { name?: string } = JSON.parse(
14
- readFileSync(manifest, "utf8"),
15
- );
16
- return parsed.name;
26
+ const parsed: unknown = JSON.parse(readFileSync(manifest, "utf8"));
27
+ return isNamedManifest(parsed) ? parsed.name : undefined;
17
28
  } catch {
18
29
  return undefined;
19
30
  }
20
31
  }
21
32
 
33
+ /** The directory of the backend package that holds `directory`, or null. */
22
34
  function backendRoot(directory: string): string | null {
23
35
  const known = backendRoots.get(directory);
24
- if (known !== undefined) return known;
36
+ if (known !== undefined) {
37
+ return known;
38
+ }
25
39
  const manifest = join(directory, "package.json");
26
40
  const parent = dirname(directory);
27
41
  let root: string | null = null;
@@ -38,12 +52,13 @@ function backendRoot(directory: string): string | null {
38
52
  * The real path of a resolved module id when it lies under the `src`
39
53
  * directory of the backend package, or `undefined`. The package that owns the
40
54
  * 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.
55
+ * the build workspace. Virtual ids and ids that are not files are ignored.
43
56
  */
44
57
  function backendSourceFile(id: string): string | undefined {
45
58
  const path = id.replace(QUERY_OR_HASH, "");
46
- if (!isAbsolute(path)) return undefined;
59
+ if (!isAbsolute(path)) {
60
+ return undefined;
61
+ }
47
62
  let real: string;
48
63
  try {
49
64
  real = realpathSync(path);
@@ -51,38 +66,56 @@ function backendSourceFile(id: string): string | undefined {
51
66
  return undefined;
52
67
  }
53
68
  const root = backendRoot(dirname(real));
54
- if (root === null) return undefined;
69
+ if (root === null) {
70
+ return undefined;
71
+ }
55
72
  return relative(root, real).split(sep)[0] === BACKEND_SOURCE
56
73
  ? real
57
74
  : undefined;
58
75
  }
59
76
 
60
77
  /**
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"`).
78
+ * Fails the build and the dev server when a module of the frontend resolves to
79
+ * a file under the `src` of the backend package: the browser reaches the
80
+ * backend only through its HTTP interface. It checks what the whole resolution
81
+ * chain returns, an id marked external included, so an alias is seen through
82
+ * and a symlink the resolver returns is followed to its real path, and it runs
83
+ * before the other plugins (`enforce: "pre"`). Vite builds workers with their
84
+ * own plugin list, so the plugin adds itself there too.
85
+ *
86
+ * antelopejs-sync writes this file from `@antelopejs/tooling-configs` and
87
+ * restores it on every run: change the plugin in the pack, not here.
67
88
  */
68
- export function backendBoundary(): Plugin {
89
+ export function backendBoundary(): VitePlugin {
90
+ return {
91
+ ...boundaryCheck(),
92
+ config: () => ({ worker: { plugins: () => [boundaryCheck()] } }),
93
+ };
94
+ }
95
+
96
+ /** The check alone, as a worker build runs it. */
97
+ function boundaryCheck(): VitePlugin {
69
98
  return {
70
- name: "dms-ai:backend-boundary",
99
+ name: `${BACKEND_PACKAGE}:backend-boundary`,
71
100
  enforce: "pre",
72
101
  async resolveId(source, importer, options) {
73
102
  const resolved = await this.resolve(source, importer, {
74
103
  ...options,
75
104
  skipSelf: true,
76
105
  });
77
- if (resolved === null || resolved.external) return resolved;
106
+ if (resolved === null) {
107
+ return resolved;
108
+ }
78
109
  const file = backendSourceFile(resolved.id);
79
- if (file === undefined) return resolved;
110
+ if (file === undefined) {
111
+ return resolved;
112
+ }
80
113
  const from =
81
114
  importer === undefined
82
115
  ? "an entry point"
83
116
  : importer.replace(QUERY_OR_HASH, "");
84
117
  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.`,
118
+ `Backend boundary: ${from} imports ${file}, which is under the src of ${BACKEND_PACKAGE}. The frontend reaches the backend only through its HTTP interface.`,
86
119
  );
87
120
  },
88
121
  };
@@ -314,6 +314,8 @@
314
314
  "BuilderPageStructure": "Read page",
315
315
  "BuilderCreatePage": "Create page",
316
316
  "BuilderConfigurePage": "Configure page",
317
+ "BuilderComposePage": "Compose page",
318
+ "BuilderConfigureTableView": "Configure table view",
317
319
  "BuilderDeletePage": "Delete page",
318
320
  "BuilderAddBlock": "Add block",
319
321
  "BuilderConfigureBlock": "Configure block",
@@ -332,6 +334,7 @@
332
334
  "BuilderRemoveField": "Remove field",
333
335
  "BuilderQueryTemplates": "Read query templates",
334
336
  "BuilderAddQuery": "Add query",
337
+ "BuilderPreviewQuery": "Preview query",
335
338
  "BuilderConfigureQuery": "Configure query",
336
339
  "BuilderRemoveQuery": "Remove query",
337
340
  "UndoChangeSet": "Undo change set",
@@ -462,6 +465,10 @@
462
465
  "high": "High"
463
466
  }
464
467
  },
468
+ "claude_model": {
469
+ "label": "Claude model",
470
+ "description": "The model Claude Code runs on. Thinking sets its reasoning effort."
471
+ },
465
472
  "mode": {
466
473
  "label": "Approval mode",
467
474
  "description": "What the assistant may do without asking you first.",
@@ -314,6 +314,8 @@
314
314
  "BuilderPageStructure": "Lire une page",
315
315
  "BuilderCreatePage": "Créer une page",
316
316
  "BuilderConfigurePage": "Configurer une page",
317
+ "BuilderComposePage": "Composer une page",
318
+ "BuilderConfigureTableView": "Configurer une vue tableau",
317
319
  "BuilderDeletePage": "Supprimer une page",
318
320
  "BuilderAddBlock": "Ajouter un bloc",
319
321
  "BuilderConfigureBlock": "Configurer un bloc",
@@ -332,6 +334,7 @@
332
334
  "BuilderRemoveField": "Retirer un champ",
333
335
  "BuilderQueryTemplates": "Lire les modèles de requêtes",
334
336
  "BuilderAddQuery": "Ajouter une requête",
337
+ "BuilderPreviewQuery": "Aperçu de la requête",
335
338
  "BuilderConfigureQuery": "Configurer une requête",
336
339
  "BuilderRemoveQuery": "Retirer une requête",
337
340
  "UndoChangeSet": "Annuler un lot de modifications",
@@ -462,6 +465,10 @@
462
465
  "high": "Élevée"
463
466
  }
464
467
  },
468
+ "claude_model": {
469
+ "label": "Modèle Claude",
470
+ "description": "Le modèle sur lequel tourne Claude Code. La réflexion règle son niveau d'effort."
471
+ },
465
472
  "mode": {
466
473
  "label": "Mode de validation",
467
474
  "description": "Ce que l'assistant peut faire sans vous demander d'abord.",
@@ -142,7 +142,9 @@
142
142
  "expired_title": "{tool} request expired.",
143
143
  "expired_text": "“{summary}” was not answered in time, so it was denied at {time} and the assistant was told why.",
144
144
  "question_skipped": "You let the assistant decide “{header}”.",
145
- "full_auto_ended": "Full auto ended at {time}. The assistant asks first again."
145
+ "full_auto_ended": "Full auto ended at {time}. The assistant asks first again.",
146
+ "model_fallback_title": "Switched to {model}.",
147
+ "model_fallback_text": "The requested model, {requested}, was refused (unknown, or no access on this plan). This answer comes from {model}, the default model."
146
148
  },
147
149
  "errors": {
148
150
  "turn_title": "The turn stopped.",
@@ -430,6 +432,8 @@
430
432
  "configure_page": "Configure page",
431
433
  "delete_page": "Delete page",
432
434
  "add_block": "Add block",
435
+ "compose_page": "Compose page",
436
+ "configure_table_view": "Configure table view",
433
437
  "configure_block": "Configure block",
434
438
  "move_block": "Move block",
435
439
  "remove_block": "Remove block",
@@ -446,6 +450,7 @@
446
450
  "remove_field": "Remove field",
447
451
  "browse_queries": "Browse query templates",
448
452
  "add_query": "Add query",
453
+ "preview_query": "Preview query",
449
454
  "configure_query": "Configure query",
450
455
  "remove_query": "Remove query",
451
456
  "search_tools": "Load tools",
@@ -142,7 +142,9 @@
142
142
  "expired_title": "Demande « {tool} » expirée.",
143
143
  "expired_text": "« {summary} » est restée sans réponse : elle a été refusée à {time} et l'assistant en a été informé.",
144
144
  "question_skipped": "Vous avez laissé l'assistant décider pour « {header} ».",
145
- "full_auto_ended": "Le mode automatique complet a pris fin à {time}. L'assistant redemande avant d'agir."
145
+ "full_auto_ended": "Le mode automatique complet a pris fin à {time}. L'assistant redemande avant d'agir.",
146
+ "model_fallback_title": "Passage à {model}.",
147
+ "model_fallback_text": "Le modèle demandé, {requested}, a été refusé (inconnu, ou sans accès avec cet abonnement). Cette réponse vient de {model}, le modèle par défaut."
146
148
  },
147
149
  "errors": {
148
150
  "turn_title": "Le tour s'est arrêté.",
@@ -430,6 +432,8 @@
430
432
  "configure_page": "Configurer la page",
431
433
  "delete_page": "Supprimer la page",
432
434
  "add_block": "Ajouter un bloc",
435
+ "compose_page": "Composer une page",
436
+ "configure_table_view": "Configurer la vue de tableau",
433
437
  "configure_block": "Configurer le bloc",
434
438
  "move_block": "Déplacer le bloc",
435
439
  "remove_block": "Retirer le bloc",
@@ -446,6 +450,7 @@
446
450
  "remove_field": "Retirer le champ",
447
451
  "browse_queries": "Parcourir les modèles de requête",
448
452
  "add_query": "Ajouter une requête",
453
+ "preview_query": "Aperçu de la requête",
449
454
  "configure_query": "Configurer la requête",
450
455
  "remove_query": "Retirer la requête",
451
456
  "search_tools": "Charger des outils",
@@ -64,6 +64,43 @@ describe("the assistant actions another module's layer injects", () => {
64
64
  ]);
65
65
  });
66
66
 
67
+ it("send the prompt as the first message, in plan mode, when asked to", async () => {
68
+ const harness = createHarness();
69
+ const panel = await openPanel(harness);
70
+ publicAssistantActions(shallowRef(harness.session)).startConversation(
71
+ PROMPT,
72
+ { mode: "plan", send: true },
73
+ );
74
+ await flushPromises();
75
+ const started = sentOfType(harness, "hello").at(-1)?.conversationId;
76
+ expect(sentOfType(harness, "set_conversation_mode")).toEqual([
77
+ expect.objectContaining({ conversationId: started, mode: "plan" }),
78
+ ]);
79
+ expect(sentOfType(harness, "user_message")).toEqual([
80
+ expect.objectContaining({ content: PROMPT }),
81
+ ]);
82
+ expect((panel.find("textarea").element as HTMLTextAreaElement).value).toBe(
83
+ "",
84
+ );
85
+ });
86
+
87
+ it("send the prompt once the stream is up, when the panel opens before it", async () => {
88
+ const harness = createHarness();
89
+ await openPanel(harness);
90
+ harness.setChannelStatus("disconnected");
91
+ publicAssistantActions(shallowRef(harness.session)).startConversation(
92
+ PROMPT,
93
+ { send: true },
94
+ );
95
+ await flushPromises();
96
+ expect(sentOfType(harness, "user_message")).toEqual([]);
97
+ harness.setChannelStatus("connected");
98
+ await flushPromises();
99
+ expect(sentOfType(harness, "user_message")).toEqual([
100
+ expect.objectContaining({ content: PROMPT }),
101
+ ]);
102
+ });
103
+
67
104
  it("are unavailable without a session, and starting then does nothing", () => {
68
105
  const actions = publicAssistantActions(shallowRef(null));
69
106
  expect(actions.isAvailable).toBe(false);
@@ -0,0 +1,142 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ BLOCK_SETTLE_BUDGET_MS,
5
+ awaitSettledBlocks,
6
+ readBlocks,
7
+ } from "../app/runtime/block-health";
8
+
9
+ function addBlock(
10
+ state: string,
11
+ type: string,
12
+ path?: string,
13
+ parent: Element = document.body,
14
+ ): HTMLElement {
15
+ const element = document.createElement("div");
16
+ element.setAttribute("data-dms-block-state", state);
17
+ element.setAttribute("data-dms-block-type", type);
18
+ if (path !== undefined) element.setAttribute("data-dms-block", path);
19
+ parent.append(element);
20
+ return element;
21
+ }
22
+
23
+ const never = new AbortController().signal;
24
+
25
+ describe("readBlocks", () => {
26
+ beforeEach(() => {
27
+ document.body.replaceChildren();
28
+ });
29
+
30
+ it("reads the state, kind and layout path each block states on its root", () => {
31
+ addBlock("empty", "ChartCard", "revenue");
32
+ addBlock("ok", "KpiCard");
33
+ document.body.append(document.createElement("section"));
34
+ expect(readBlocks(document)).toEqual([
35
+ { path: "revenue", type: "ChartCard", state: "empty" },
36
+ { type: "KpiCard", state: "ok" },
37
+ ]);
38
+ });
39
+
40
+ it("finds none on a host that states nothing", () => {
41
+ document.body.append(document.createElement("section"));
42
+ expect(readBlocks(document)).toEqual([]);
43
+ });
44
+ });
45
+
46
+ describe("awaitSettledBlocks", () => {
47
+ beforeEach(() => {
48
+ document.body.replaceChildren();
49
+ vi.useFakeTimers();
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.useRealTimers();
54
+ });
55
+
56
+ it("reports the blocks showing no data once the page stopped changing", async () => {
57
+ addBlock("ok", "KpiCard", "orders");
58
+ addBlock("empty", "ChartCard", "revenue");
59
+ addBlock("error", "TopListCard", "top");
60
+ const settled = awaitSettledBlocks({ root: document, signal: never });
61
+ await vi.advanceTimersByTimeAsync(BLOCK_SETTLE_BUDGET_MS);
62
+ await expect(settled).resolves.toEqual({
63
+ status: "settled",
64
+ checked: 3,
65
+ blocks: [
66
+ { path: "revenue", type: "ChartCard", state: "empty" },
67
+ { path: "top", type: "TopListCard", state: "error" },
68
+ ],
69
+ });
70
+ });
71
+
72
+ it("waits for a loading block and reports what it turned into", async () => {
73
+ const chart = addBlock("loading", "ChartCard", "revenue");
74
+ let result: Awaited<ReturnType<typeof awaitSettledBlocks>> | undefined;
75
+ void awaitSettledBlocks({ root: document, signal: never }).then((value) => {
76
+ result = value;
77
+ });
78
+ await vi.advanceTimersByTimeAsync(4_000);
79
+ expect(result).toBeUndefined();
80
+ chart.setAttribute("data-dms-block-state", "empty");
81
+ await vi.advanceTimersByTimeAsync(4_000);
82
+ expect(result).toEqual({
83
+ status: "settled",
84
+ checked: 1,
85
+ blocks: [{ path: "revenue", type: "ChartCard", state: "empty" }],
86
+ });
87
+ });
88
+
89
+ it("waits for a block that mounts after the others settled", async () => {
90
+ addBlock("ok", "KpiCard", "orders");
91
+ const settled = awaitSettledBlocks({ root: document, signal: never });
92
+ await vi.advanceTimersByTimeAsync(300);
93
+ addBlock("loading", "ChartCard", "late");
94
+ await vi.advanceTimersByTimeAsync(BLOCK_SETTLE_BUDGET_MS);
95
+ await expect(settled).resolves.toMatchObject({
96
+ status: "timeout",
97
+ checked: 2,
98
+ blocks: [{ path: "late", type: "ChartCard", state: "loading" }],
99
+ });
100
+ });
101
+
102
+ it("gives up at the budget and names the blocks still loading", async () => {
103
+ addBlock("loading", "Chart", "slow");
104
+ let finishedAt = -1;
105
+ const started = Date.now();
106
+ const settled = awaitSettledBlocks({ root: document, signal: never });
107
+ void settled.then(() => {
108
+ finishedAt = Date.now() - started;
109
+ });
110
+ await vi.advanceTimersByTimeAsync(BLOCK_SETTLE_BUDGET_MS - 1);
111
+ expect(finishedAt).toBe(-1);
112
+ await vi.advanceTimersByTimeAsync(1);
113
+ await expect(settled).resolves.toEqual({
114
+ status: "timeout",
115
+ checked: 1,
116
+ blocks: [{ path: "slow", type: "Chart", state: "loading" }],
117
+ });
118
+ expect(finishedAt).toBe(BLOCK_SETTLE_BUDGET_MS);
119
+ });
120
+
121
+ it("reports a page without data blocks as having none", async () => {
122
+ const settled = awaitSettledBlocks({ root: document, signal: never });
123
+ await vi.advanceTimersByTimeAsync(BLOCK_SETTLE_BUDGET_MS);
124
+ await expect(settled).resolves.toEqual({
125
+ status: "settled",
126
+ checked: 0,
127
+ blocks: [],
128
+ });
129
+ });
130
+
131
+ it("answers at once as superseded when another navigation starts", async () => {
132
+ addBlock("loading", "Chart", "slow");
133
+ const controller = new AbortController();
134
+ const settled = awaitSettledBlocks({
135
+ root: document,
136
+ signal: controller.signal,
137
+ });
138
+ await vi.advanceTimersByTimeAsync(100);
139
+ controller.abort();
140
+ await expect(settled).resolves.toMatchObject({ status: "superseded" });
141
+ });
142
+ });
@@ -220,6 +220,40 @@ describe("the conversation folds the redesign's events", () => {
220
220
  expect(conversation.totalTokens.value).toBe(1_200);
221
221
  });
222
222
 
223
+ it("shows the model that answered, from a usage event or a snapshot", () => {
224
+ const b = bus();
225
+ const conversation = useConversation(b.options as never);
226
+ expect(conversation.model.value).toBeNull();
227
+ b.receive({ type: "usage", totalTokens: 10, model: "claude-opus-5-5" });
228
+ expect(conversation.model.value).toBe("claude-opus-5-5");
229
+ b.receive({
230
+ type: "conversation_snapshot",
231
+ messages: [],
232
+ changeSets: [],
233
+ totalTokens: 10,
234
+ model: "claude-sonnet-4-6",
235
+ });
236
+ expect(conversation.model.value).toBe("claude-sonnet-4-6");
237
+ });
238
+
239
+ it("keeps the notice that the configured model was refused", () => {
240
+ const b = bus();
241
+ const conversation = useConversation(b.options as never);
242
+ b.receive({
243
+ type: "notice",
244
+ notice: {
245
+ kind: "model_fallback",
246
+ requestedModel: "claude-opus-5-5",
247
+ usedModel: "claude-sonnet-4-6",
248
+ timestampMs: 7,
249
+ },
250
+ });
251
+ expect(conversation.messages.value[0]).toMatchObject({
252
+ role: "notice",
253
+ notice: { kind: "model_fallback", usedModel: "claude-sonnet-4-6" },
254
+ });
255
+ });
256
+
223
257
  it("ignores another conversation's events", () => {
224
258
  const b = bus();
225
259
  const conversation = useConversation(b.options as never);
@@ -68,6 +68,7 @@ describe("the tool lexicon names a call by what it does", () => {
68
68
  "BuilderRemoveField",
69
69
  "BuilderRemoveQuery",
70
70
  "BuilderQueryTemplates",
71
+ "BuilderPreviewQuery",
71
72
  ];
72
73
  for (const name of [...builder, "AskUser", "Typecheck", "NavigateToPage"])
73
74
  expect(describeTool(`mcp__dms-ai__${name}`, {}).verbKey).not.toBeNull();