@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
@@ -8,8 +8,11 @@ import {
8
8
  watch,
9
9
  } from "vue";
10
10
  import type { ChatTransport } from "../../runtime/chat-transport";
11
- import type { ConversationStartMode } from "../../runtime/assistant-session";
12
- import type { PanelIntent, PanelIntents } from "../../runtime/panel-intents";
11
+ import type {
12
+ PanelIntent,
13
+ PanelIntents,
14
+ StartConversationIntent,
15
+ } from "../../runtime/panel-intents";
13
16
  import {
14
17
  useChangeSetActions,
15
18
  type ChatApi,
@@ -41,9 +44,10 @@ import type { ConversationSummary, CurrentPage } from "../types/conversation";
41
44
  import type { ExpiredRequest } from "../types/permission";
42
45
  import type { QuestionReply } from "../types/question";
43
46
  import type { PendingAttachment } from "../utils/attachments";
44
- import { formatClock, formatTokens } from "../utils/format";
47
+ import { formatTokens } from "../utils/format";
45
48
  import { newUuid } from "../utils/ids";
46
49
  import { isRunStalled, runElapsedMs } from "../utils/run-status";
50
+ import { describeStatus } from "../utils/status-label";
47
51
  import { bareToolName } from "../utils/tool-lexicon";
48
52
  import { latestTodos } from "../utils/todos";
49
53
  import ApprovalDock from "./ApprovalDock.vue";
@@ -176,27 +180,18 @@ const status = computed<PanelStatusKind>(() => {
176
180
  return isRunning.value ? "working" : "ready";
177
181
  });
178
182
 
179
- const statusLabel = computed<string>(() => {
180
- const labels: Record<PanelStatusKind, () => string> = {
181
- ready: () => t("dms_ai.panel.status.ready"),
182
- working: () =>
183
- t("dms_ai.panel.status.working", {
184
- time: formatClock(
185
- runElapsedMs(conversation.progress.value, nowMs.value),
186
- ),
187
- }),
188
- waiting: () =>
189
- t(
190
- "dms_ai.panel.status.waiting",
191
- { count: pendingCount.value },
192
- pendingCount.value,
193
- ),
194
- connecting: () => t("dms_ai.panel.status.connecting"),
195
- offline: () => t("dms_ai.panel.status.offline"),
196
- };
197
- const label = labels[status.value]();
198
- return status.value === "offline" ? label : `${label} · ${agentName.value}`;
199
- });
183
+ const statusLabel = computed<string>(() =>
184
+ describeStatus(
185
+ {
186
+ kind: status.value,
187
+ elapsedMs: runElapsedMs(conversation.progress.value, nowMs.value),
188
+ pendingCount: pendingCount.value,
189
+ agentName: agentName.value,
190
+ model: conversation.model.value,
191
+ },
192
+ t,
193
+ ),
194
+ );
200
195
 
201
196
  const usageLabel = computed<string>(() => {
202
197
  if (!isConnected.value) return t("dms_ai.panel.composer.usage_offline");
@@ -325,16 +320,13 @@ function setDraft(text: string): void {
325
320
  composerRef.value?.setDraft(text);
326
321
  }
327
322
 
328
- /** Sets the new conversation's mode as soon as the stream can carry it, unless the chat moved on. */
329
- function setModeWhenConnected(next: ConversationStartMode): void {
323
+ /** Runs `run` once the stream is up, if still here. */
324
+ function whenConnected(run: () => void): void {
330
325
  const conversationId = activeId.value;
331
326
  const apply = (): void => {
332
- if (activeId.value === conversationId) mode.setMode(next);
327
+ if (activeId.value === conversationId) run();
333
328
  };
334
- if (isConnected.value) {
335
- apply();
336
- return;
337
- }
329
+ if (isConnected.value) return apply();
338
330
  const stop = watch(isConnected, (connected) => {
339
331
  if (!connected) return;
340
332
  stop();
@@ -342,6 +334,20 @@ function setModeWhenConnected(next: ConversationStartMode): void {
342
334
  });
343
335
  }
344
336
 
337
+ function startConversation(intent: StartConversationIntent): void {
338
+ newConversation();
339
+ const { prompt = "", mode: next } = intent;
340
+ const sends = intent.send === true && prompt !== "";
341
+ if (next !== undefined || sends)
342
+ whenConnected(() => {
343
+ if (next !== undefined) mode.setMode(next);
344
+ if (sends) conversation.sendUserMessage(prompt);
345
+ });
346
+ if (sends) return;
347
+ if (prompt) void nextTick(() => setDraft(prompt));
348
+ else void nextTick(() => composerRef.value?.focus());
349
+ }
350
+
345
351
  const INTENT_HANDLERS: Record<
346
352
  PanelIntent["kind"],
347
353
  (intent: PanelIntent) => void
@@ -350,12 +356,7 @@ const INTENT_HANDLERS: Record<
350
356
  if (intent.kind === "open") switchConversation(intent.conversationId);
351
357
  },
352
358
  start: (intent) => {
353
- newConversation();
354
- if (intent.kind === "start" && intent.mode)
355
- setModeWhenConnected(intent.mode);
356
- if (intent.kind === "start" && intent.prompt)
357
- void nextTick(() => setDraft(intent.prompt ?? ""));
358
- else void nextTick(() => composerRef.value?.focus());
359
+ if (intent.kind === "start") startConversation(intent);
359
360
  },
360
361
  focus: () => void nextTick(() => composerRef.value?.focus()),
361
362
  approvals: () => {
@@ -30,6 +30,7 @@ const LOOKS: Record<Notice["kind"], NoticeLook> = {
30
30
  permission_expired: { tone: "neutral", icon: "i-ph-timer" },
31
31
  question_skipped: { tone: "neutral", icon: "i-ph-arrow-bend-down-right" },
32
32
  full_auto_ended: { tone: "neutral", icon: "i-ph-lightning-slash" },
33
+ model_fallback: { tone: "warning", icon: "i-ph-swap" },
33
34
  };
34
35
 
35
36
  const showErrors = ref(false);
@@ -116,6 +117,21 @@ const expiredTool = computed<string>(() => {
116
117
  t("dms_ai.panel.notice.question_skipped", { header: notice.header })
117
118
  }}
118
119
  </template>
120
+ <template v-else-if="notice.kind === 'model_fallback'">
121
+ <b>
122
+ {{
123
+ t("dms_ai.panel.notice.model_fallback_title", {
124
+ model: notice.usedModel,
125
+ })
126
+ }}
127
+ </b>
128
+ {{
129
+ t("dms_ai.panel.notice.model_fallback_text", {
130
+ requested: notice.requestedModel,
131
+ model: notice.usedModel,
132
+ })
133
+ }}
134
+ </template>
119
135
  <template v-else>
120
136
  {{ t("dms_ai.panel.notice.full_auto_ended", { time }) }}
121
137
  </template>
@@ -4,13 +4,9 @@ import { useChatI18n } from "../composables/useChatI18n";
4
4
  import type { ActiveRule } from "../types/protocol";
5
5
  import { ruleIcon } from "../utils/permission-view";
6
6
  import { shortcutLabel } from "../utils/platform";
7
+ import type { StatusKind } from "../utils/status-label";
7
8
 
8
- export type PanelStatusKind =
9
- | "ready"
10
- | "working"
11
- | "waiting"
12
- | "connecting"
13
- | "offline";
9
+ export type PanelStatusKind = StatusKind;
14
10
 
15
11
  interface Props {
16
12
  title: string;
@@ -73,6 +73,7 @@ interface SnapshotEvent {
73
73
  changeSets?: ChangeSetSummary[];
74
74
  mode?: SnapshotModeWire;
75
75
  totalTokens?: number;
76
+ model?: string;
76
77
  }
77
78
 
78
79
  interface QueueStateEvent {
@@ -99,6 +100,7 @@ interface BlockPreviewEvent {
99
100
 
100
101
  interface UsageEvent {
101
102
  totalTokens?: number;
103
+ model?: string;
102
104
  }
103
105
 
104
106
  type EventHandler = (state: ConversationState, event: never) => void;
@@ -290,6 +292,7 @@ function applySnapshot(state: ConversationState, event: SnapshotEvent): void {
290
292
  state.changeSets.value = byId(event.changeSets ?? []);
291
293
  state.mode.value = toModeState(event.mode) ?? state.mode.value;
292
294
  state.totalTokens.value = event.totalTokens ?? state.totalTokens.value;
295
+ state.model.value = event.model ?? null;
293
296
  state.isRunning.value = false;
294
297
  state.progress.value = null;
295
298
  state.isTurnInFlight.value = false;
@@ -379,6 +382,7 @@ function appendBlockPreview(
379
382
  }
380
383
 
381
384
  function applyUsage(state: ConversationState, event: UsageEvent): void {
385
+ if (event.model !== undefined) state.model.value = event.model;
382
386
  if (typeof event.totalTokens !== "number") return;
383
387
  state.totalTokens.value = event.totalTokens;
384
388
  }
@@ -49,6 +49,8 @@ export interface UseConversationResult {
49
49
  mode: Ref<ConversationModeState | null>;
50
50
  /** Tokens this chat has used so far; null until the sidecar says. */
51
51
  totalTokens: Ref<number | null>;
52
+ /** The model that answered last; null until a reply names it. */
53
+ model: Ref<string | null>;
52
54
  sendUserMessage: (
53
55
  content: string,
54
56
  attachments?: PendingAttachment[],
@@ -76,6 +78,7 @@ export interface ConversationState {
76
78
  changeSets: Ref<Record<string, ChangeSetSummary>>;
77
79
  mode: Ref<ConversationModeState | null>;
78
80
  totalTokens: Ref<number | null>;
81
+ model: Ref<string | null>;
79
82
  /**
80
83
  * When the calls of the stored transcript started and ended. A re-attach
81
84
  * replays the running turn's events, and the replayed calls take their
@@ -285,6 +288,7 @@ function reset(state: ConversationState): void {
285
288
  state.changeSets.value = {};
286
289
  state.mode.value = null;
287
290
  state.totalTokens.value = null;
291
+ state.model.value = null;
288
292
  state.callTimes.clear();
289
293
  }
290
294
 
@@ -302,6 +306,7 @@ function createConversationState(
302
306
  changeSets: ref<Record<string, ChangeSetSummary>>({}),
303
307
  mode: ref<ConversationModeState | null>(null),
304
308
  totalTokens: ref<number | null>(null),
309
+ model: ref<string | null>(null),
305
310
  callTimes: new Map(),
306
311
  };
307
312
  }
@@ -326,6 +331,7 @@ export function useConversation(
326
331
  changeSets: state.changeSets,
327
332
  mode: state.mode,
328
333
  totalTokens: state.totalTokens,
334
+ model: state.model,
329
335
  sendUserMessage: (content, attachments = [], extra = {}) =>
330
336
  sendUserMessage(state, content, attachments, extra),
331
337
  retry: () => retry(state),
@@ -165,11 +165,19 @@ export interface FullAutoEndedNotice {
165
165
  timestampMs: number;
166
166
  }
167
167
 
168
+ export interface ModelFallbackNotice {
169
+ kind: "model_fallback";
170
+ requestedModel: string;
171
+ usedModel: string;
172
+ timestampMs: number;
173
+ }
174
+
168
175
  export type Notice =
169
176
  | AutoFixNotice
170
177
  | PermissionExpiredNotice
171
178
  | QuestionSkippedNotice
172
- | FullAutoEndedNotice;
179
+ | FullAutoEndedNotice
180
+ | ModelFallbackNotice;
173
181
 
174
182
  export interface FullAutoState {
175
183
  duration: FullAutoDuration;
@@ -0,0 +1,50 @@
1
+ import type { Translate } from "../composables/useChatI18n";
2
+ import { formatClock } from "./format";
3
+
4
+ export type StatusKind =
5
+ | "ready"
6
+ | "working"
7
+ | "waiting"
8
+ | "connecting"
9
+ | "offline";
10
+
11
+ export interface StatusFacts {
12
+ kind: StatusKind;
13
+ elapsedMs: number;
14
+ pendingCount: number;
15
+ agentName: string;
16
+ /** The model that answered last; null until a reply names it. */
17
+ model: string | null;
18
+ }
19
+
20
+ const PART_SEPARATOR = " · ";
21
+
22
+ function stateLabel(facts: StatusFacts, t: Translate): string {
23
+ const labels: Record<StatusKind, () => string> = {
24
+ ready: () => t("dms_ai.panel.status.ready"),
25
+ working: () =>
26
+ t("dms_ai.panel.status.working", { time: formatClock(facts.elapsedMs) }),
27
+ waiting: () =>
28
+ t(
29
+ "dms_ai.panel.status.waiting",
30
+ { count: facts.pendingCount },
31
+ facts.pendingCount,
32
+ ),
33
+ connecting: () => t("dms_ai.panel.status.connecting"),
34
+ offline: () => t("dms_ai.panel.status.offline"),
35
+ };
36
+ return labels[facts.kind]();
37
+ }
38
+
39
+ /**
40
+ * The panel's status line: the state, then the agent, then the model that
41
+ * answered last ("Ready · Claude Code · claude-opus-5-5"). Offline has no
42
+ * agent to name.
43
+ */
44
+ export function describeStatus(facts: StatusFacts, t: Translate): string {
45
+ const label = stateLabel(facts, t);
46
+ if (facts.kind === "offline") return label;
47
+ const parts = [label, facts.agentName];
48
+ if (facts.model !== null) parts.push(facts.model);
49
+ return parts.join(PART_SEPARATOR);
50
+ }
@@ -374,6 +374,16 @@ const BUILDER_TOOLS: Record<string, ToolLexiconEntry> = {
374
374
  icon: ICONS.BLOCK,
375
375
  target: firstOf("path"),
376
376
  },
377
+ BuilderComposePage: {
378
+ verb: "compose_page",
379
+ icon: ICONS.BLOCK,
380
+ target: firstOf("pageRef"),
381
+ },
382
+ BuilderConfigureTableView: {
383
+ verb: "configure_table_view",
384
+ icon: ICONS.BLOCK,
385
+ target: firstOf("pageRef"),
386
+ },
377
387
  BuilderMoveBlock: {
378
388
  verb: "move_block",
379
389
  icon: ICONS.BLOCK,
@@ -475,6 +485,12 @@ const BUILDER_TOOLS: Record<string, ToolLexiconEntry> = {
475
485
  icon: ICONS.DATA,
476
486
  target: firstOf("name"),
477
487
  },
488
+ BuilderPreviewQuery: {
489
+ verb: "preview_query",
490
+ icon: ICONS.DATA,
491
+ target: firstOf("name"),
492
+ isReadOnly: true,
493
+ },
478
494
  BuilderConfigureQuery: {
479
495
  verb: "configure_query",
480
496
  icon: ICONS.DATA,
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { Tone } from "#dms-ui/app/types/tone";
2
+ import type { Tone } from "#dms-core/app/types/tone";
3
3
  import { computed } from "vue";
4
4
  import { renderDocument } from "../chat/utils/markdown";
5
5
  import { useAssistant } from "../views/composables/useAssistant";
@@ -268,8 +268,8 @@ function createSession(parts: SessionParts): AssistantSession {
268
268
  intents.push({ kind: "open", conversationId });
269
269
  panel.open();
270
270
  },
271
- startConversation: (prompt, mode) => {
272
- intents.push({ kind: "start", prompt, mode });
271
+ startConversation: (prompt, mode, send) => {
272
+ intents.push({ kind: "start", prompt, mode, send });
273
273
  panel.open();
274
274
  },
275
275
  holdStream: parts.holds.hold,
@@ -14,10 +14,12 @@ export interface PublicAssistantActions {
14
14
  /**
15
15
  * Opens the assistant panel on a new conversation with `prompt` in the composer, unsent. With
16
16
  * `mode`, the conversation is set to it first: `plan` makes the agent end with a plan to approve.
17
+ * With `send`, the prompt is sent as the first message once the stream is up, and the composer
18
+ * stays empty.
17
19
  */
18
20
  startConversation: (
19
21
  prompt: string,
20
- options?: { mode?: ConversationStartMode },
22
+ options?: { mode?: ConversationStartMode; send?: boolean },
21
23
  ) => void;
22
24
  }
23
25
 
@@ -36,6 +38,6 @@ export function publicAssistantActions(
36
38
  return session.value !== null;
37
39
  },
38
40
  startConversation: (prompt, options) =>
39
- session.value?.startConversation(prompt, options?.mode),
41
+ session.value?.startConversation(prompt, options?.mode, options?.send),
40
42
  };
41
43
  }
@@ -38,7 +38,11 @@ export interface AssistantSession {
38
38
  * Opens the panel on a new conversation, the composer optionally prefilled, and the
39
39
  * conversation set to `mode` first when one is given.
40
40
  */
41
- startConversation: (prompt?: string, mode?: ConversationStartMode) => void;
41
+ startConversation: (
42
+ prompt?: string,
43
+ mode?: ConversationStartMode,
44
+ send?: boolean,
45
+ ) => void;
42
46
  /**
43
47
  * Keeps the tab's stream open while the panel is closed, until the returned
44
48
  * function releases it: the command palette's answer streams through it.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * What the DMS data blocks (charts, KPIs, lists, tables) say about themselves.
3
+ * Each states its kind, its layout path and what its reader sees on its root
4
+ * element; a test harness reads the same attributes. A host that predates
5
+ * them states nothing, and the page reads as having no data block.
6
+ */
7
+ const BLOCK_SELECTOR = "[data-dms-block-state]";
8
+ const STATE_ATTRIBUTE = "data-dms-block-state";
9
+ const TYPE_ATTRIBUTE = "data-dms-block-type";
10
+ const PATH_ATTRIBUTE = "data-dms-block";
11
+ const UNKNOWN_TYPE = "Block";
12
+
13
+ /** How long the host waits for a page's blocks to stop loading. */
14
+ export const BLOCK_SETTLE_BUDGET_MS = 15_000;
15
+ /** How long the blocks of a page must stay unchanged before it counts as settled. */
16
+ export const BLOCK_QUIET_MS = 700;
17
+
18
+ const BLOCK_STATES = ["loading", "ok", "empty", "error"] as const;
19
+ type BlockState = (typeof BLOCK_STATES)[number];
20
+ type ReportedState = Exclude<BlockState, "ok">;
21
+
22
+ export interface BlockFact {
23
+ path?: string;
24
+ type: string;
25
+ state: BlockState;
26
+ }
27
+
28
+ export interface ReportedBlock extends BlockFact {
29
+ state: ReportedState;
30
+ }
31
+
32
+ export interface PageBlocks {
33
+ status: "settled" | "timeout" | "superseded";
34
+ checked: number;
35
+ /** The blocks showing no data: empty, failed, or still loading. */
36
+ blocks: ReportedBlock[];
37
+ }
38
+
39
+ function isBlockState(value: string | null): value is BlockState {
40
+ return BLOCK_STATES.some((state) => state === value);
41
+ }
42
+
43
+ function isReported(block: BlockFact): block is ReportedBlock {
44
+ return block.state !== "ok";
45
+ }
46
+
47
+ function readBlock(element: Element): BlockFact | null {
48
+ const state = element.getAttribute(STATE_ATTRIBUTE);
49
+ if (!isBlockState(state)) return null;
50
+ const path = element.getAttribute(PATH_ATTRIBUTE);
51
+ return {
52
+ ...(path === null ? {} : { path }),
53
+ type: element.getAttribute(TYPE_ATTRIBUTE) ?? UNKNOWN_TYPE,
54
+ state,
55
+ };
56
+ }
57
+
58
+ /** The data blocks under `root`, in document order. */
59
+ export function readBlocks(root: ParentNode): BlockFact[] {
60
+ return [...root.querySelectorAll(BLOCK_SELECTOR)].flatMap((element) => {
61
+ const block = readBlock(element);
62
+ return block === null ? [] : [block];
63
+ });
64
+ }
65
+
66
+ function holdsBlock(node: Node): boolean {
67
+ return (
68
+ node instanceof Element &&
69
+ (node.matches(BLOCK_SELECTOR) ||
70
+ node.querySelector(BLOCK_SELECTOR) !== null)
71
+ );
72
+ }
73
+
74
+ function changesBlocks(record: MutationRecord): boolean {
75
+ if (record.type === "attributes") return true;
76
+ return [...record.addedNodes, ...record.removedNodes].some(holdsBlock);
77
+ }
78
+
79
+ export interface AwaitSettledBlocksOptions {
80
+ root: Document;
81
+ /** Aborted when another navigation starts: the wait ends as `superseded`. */
82
+ signal: AbortSignal;
83
+ budgetMs?: number;
84
+ quietMs?: number;
85
+ }
86
+
87
+ /**
88
+ * Resolves when the blocks of the page have stopped changing for `quietMs`
89
+ * and none is loading, or at `budgetMs` with the blocks still loading named,
90
+ * or at once when `signal` aborts.
91
+ */
92
+ export function awaitSettledBlocks(
93
+ options: AwaitSettledBlocksOptions,
94
+ ): Promise<PageBlocks> {
95
+ const { root, signal } = options;
96
+ const budgetMs = options.budgetMs ?? BLOCK_SETTLE_BUDGET_MS;
97
+ const quietMs = options.quietMs ?? BLOCK_QUIET_MS;
98
+ return new Promise((resolve) => {
99
+ let quietTimer: ReturnType<typeof setTimeout> | undefined;
100
+ const finish = (status: PageBlocks["status"]): void => {
101
+ clearTimeout(quietTimer);
102
+ clearTimeout(budgetTimer);
103
+ observer.disconnect();
104
+ signal.removeEventListener("abort", onAbort);
105
+ const facts = readBlocks(root);
106
+ resolve({
107
+ status,
108
+ checked: facts.length,
109
+ blocks: facts.filter(isReported),
110
+ });
111
+ };
112
+ const onQuiet = (): void => {
113
+ if (readBlocks(root).every((block) => block.state !== "loading"))
114
+ finish("settled");
115
+ };
116
+ const restartQuiet = (): void => {
117
+ clearTimeout(quietTimer);
118
+ quietTimer = setTimeout(onQuiet, quietMs);
119
+ };
120
+ const onAbort = (): void => finish("superseded");
121
+ const observer = new MutationObserver((records) => {
122
+ if (records.some(changesBlocks)) restartQuiet();
123
+ });
124
+ const budgetTimer = setTimeout(() => finish("timeout"), budgetMs);
125
+ if (signal.aborted) return finish("superseded");
126
+ signal.addEventListener("abort", onAbort);
127
+ observer.observe(root, {
128
+ subtree: true,
129
+ childList: true,
130
+ attributes: true,
131
+ attributeFilter: [STATE_ATTRIBUTE],
132
+ });
133
+ restartQuiet();
134
+ });
135
+ }
@@ -40,6 +40,7 @@ export const HELLO_MESSAGE_TYPE = "hello";
40
40
  export const LOG_PREFIX = "[dms-ai]";
41
41
  export const HOST_STATE_UPDATE_TYPE = "host_state_update";
42
42
  export const HOST_NAVIGATION_COMPLETE_TYPE = "host_navigation_complete";
43
+ export const HOST_PAGE_BLOCKS_TYPE = "host_page_blocks";
43
44
  export const HOST_COMMAND_NAVIGATE_TYPE = "host_command_navigate";
44
45
  /** Frames of the tab's stream meant for the dashboard; every other one is the chat's. */
45
46
  export const HOST_COMMAND_TYPES: ReadonlySet<string> = new Set([
@@ -1,22 +1,63 @@
1
1
  import { router } from "@inertiajs/vue3";
2
- import { HOST_NAVIGATION_COMPLETE_TYPE } from "./constants";
2
+ import {
3
+ awaitSettledBlocks,
4
+ BLOCK_SETTLE_BUDGET_MS,
5
+ type PageBlocks,
6
+ } from "./block-health";
7
+ import {
8
+ HOST_NAVIGATION_COMPLETE_TYPE,
9
+ HOST_PAGE_BLOCKS_TYPE,
10
+ } from "./constants";
3
11
 
4
12
  interface HostNavigationCompleteMessage {
5
13
  type: typeof HOST_NAVIGATION_COMPLETE_TYPE;
6
14
  path: string;
15
+ /** A `host_page_blocks` message for this path follows within this long. */
16
+ blocksWithinMs: number;
7
17
  }
8
18
 
19
+ interface HostPageBlocksMessage extends PageBlocks {
20
+ type: typeof HOST_PAGE_BLOCKS_TYPE;
21
+ path: string;
22
+ }
23
+
24
+ type NavigationMessage = HostNavigationCompleteMessage | HostPageBlocksMessage;
25
+
9
26
  interface InstallNavigationCompleteEmitterOptions {
10
- send: (msg: HostNavigationCompleteMessage) => void;
27
+ send: (msg: NavigationMessage) => void;
11
28
  }
12
29
 
30
+ /**
31
+ * Reports each completed visit at once, then what the page's data blocks
32
+ * showed once they settled, so the assistant learns of empty charts without
33
+ * a screenshot.
34
+ */
13
35
  export function installNavigationCompleteEmitter(
14
36
  options: InstallNavigationCompleteEmitterOptions,
15
37
  ): () => void {
16
- return router.on("navigate", (event) =>
38
+ let settling = new AbortController();
39
+ let stopped = false;
40
+ const stopNavigation = router.on("navigate", (event) => {
41
+ const path = new URL(event.detail.page.url, window.location.origin)
42
+ .pathname;
43
+ settling.abort();
44
+ settling = new AbortController();
17
45
  options.send({
18
46
  type: HOST_NAVIGATION_COMPLETE_TYPE,
19
- path: new URL(event.detail.page.url, window.location.origin).pathname,
20
- }),
21
- );
47
+ path,
48
+ blocksWithinMs: BLOCK_SETTLE_BUDGET_MS,
49
+ });
50
+ void awaitSettledBlocks({
51
+ root: document,
52
+ signal: settling.signal,
53
+ }).then((blocks) => {
54
+ if (!stopped)
55
+ options.send({ type: HOST_PAGE_BLOCKS_TYPE, path, ...blocks });
56
+ });
57
+ });
58
+ return () => {
59
+ stopped = true;
60
+ settling.abort();
61
+ stopNavigation();
62
+ };
22
63
  }
@@ -7,11 +7,15 @@ interface OpenConversationIntent {
7
7
  conversationId: string;
8
8
  }
9
9
 
10
- /** Opens the panel on a new conversation, the composer optionally prefilled, in `mode` when given. */
11
- interface StartConversationIntent {
10
+ /**
11
+ * Opens the panel on a new conversation, in `mode` when given. The prompt is put in the composer,
12
+ * or sent as the first message when `send` is set.
13
+ */
14
+ export interface StartConversationIntent {
12
15
  kind: "start";
13
16
  prompt?: string;
14
17
  mode?: ConversationStartMode;
18
+ send?: boolean;
15
19
  }
16
20
 
17
21
  /** Puts the caret in the composer of the current conversation. */
@@ -7,7 +7,7 @@ import { useAssistant } from "./composables/useAssistant";
7
7
  import { useChangeSetActions } from "./composables/useChangeSetActions";
8
8
  import { useViewFormat } from "./composables/useViewFormat";
9
9
  import { AI_ROUTES } from "./constants";
10
- import type { Tone } from "#dms-ui/app/types/tone";
10
+ import type { Tone } from "#dms-core/app/types/tone";
11
11
  import type {
12
12
  ChangeSetDetail,
13
13
  ChangeSetSummary,