@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -4,15 +4,25 @@ import {
4
4
  Chat,
5
5
  type ChatDecisionState,
6
6
  type ChatOutcomeLine,
7
+ dialogCore,
7
8
  isStructuredDataValue,
8
9
  LocaleProvider,
9
10
  MarkdownView,
11
+ PanelDialog,
12
+ Placeholder,
13
+ panelDialogOptions,
10
14
  ReferenceChip,
11
15
  StructuredDataPreview,
12
16
  } from "@k2b/ui";
13
17
  import { Lexer, type Token, type Tokens } from "marked";
14
- import { createMemo, createSignal, createUniqueId, For, type JSX, Match, Show, Switch } from "solid-js";
15
- import type { CapabilityPerson, CapabilityPreview } from "../../contracts/capabilities";
18
+ import { createMemo, createSignal, createUniqueId, For, type JSX, Match, onCleanup, onMount, Show, Switch } from "solid-js";
19
+ import { readCapabilityReviewContent } from "../../capabilities/client";
20
+ import type {
21
+ CapabilityPerson,
22
+ CapabilityPreview,
23
+ CapabilityReviewContent,
24
+ CapabilityReviewContentRef,
25
+ } from "../../contracts/capabilities";
16
26
  import { formatBytes } from "../../shared/format";
17
27
  import type { ActionCard, ActionChip, ActionPreviewModel, ActionText } from "./action-card";
18
28
  import { actionMessages } from "./action-messages";
@@ -160,13 +170,13 @@ const mayOverflow = (text: string): boolean =>
160
170
  * not fit offers to show the rest in place, and any other text stands whole, so nothing a person approves stays out of
161
171
  * reach even where the estimate is short, such as for wide characters.
162
172
  */
163
- function PreviewExcerpt(props: { text: string; readMore?: string; label: string }) {
173
+ function PreviewExcerpt(props: { text: string; readMore?: string; label: string; full?: boolean; compact?: boolean }) {
164
174
  const id = createUniqueId();
165
175
  const [expanded, setExpanded] = createSignal(false);
166
- const offer = () => !props.readMore && mayOverflow(props.text);
176
+ const offer = () => !props.full && !props.compact && !props.readMore && mayOverflow(props.text);
167
177
  return (
168
178
  <>
169
- <Chat.Preview.Excerpt id={id} expanded={expanded() || (!props.readMore && !offer())}>
179
+ <Chat.Preview.Excerpt id={id} expanded={props.full || expanded() || (!props.compact && !props.readMore && !offer())}>
170
180
  {props.text}
171
181
  </Chat.Preview.Excerpt>
172
182
  <Show when={offer() && !expanded()}>
@@ -217,22 +227,33 @@ function MessagePreview(props: {
217
227
  locale: string;
218
228
  timeZone: string;
219
229
  readMore?: string;
230
+ /** Only a question that sends says when the mail goes out; a draft that is created or deleted goes nowhere. */
231
+ sends: boolean;
232
+ full?: boolean;
233
+ compact?: boolean;
220
234
  }) {
221
235
  const t = () => actionMessages(props.locale);
236
+ const text = (value: string) => (props.full ? visibleReviewText(value) : value);
237
+ const person = (value: CapabilityPerson) => ({
238
+ ...personRow(value),
239
+ ...(value.name !== undefined ? { name: text(value.name) } : {}),
240
+ ...(value.address !== undefined ? { address: text(value.address) } : {}),
241
+ });
222
242
  const rows = () => {
223
243
  const p = props.preview;
224
244
  const more = (count?: number) => (count ? { value: <> {t().andMore({ count })}</> } : {});
225
245
  return [
226
- ...(p.from ? [{ label: t().from, people: [personRow(p.from)] }] : []),
227
- { label: t().to, people: p.to.map(personRow), ...more(p.more?.to) },
228
- ...(p.cc?.length || p.more?.cc ? [{ label: t().cc, people: (p.cc ?? []).map(personRow), ...more(p.more?.cc) }] : []),
246
+ ...(p.from ? [{ label: t().from, people: [person(p.from)] }] : []),
247
+ { label: t().to, people: p.to.map(person), ...more(p.more?.to) },
248
+ ...(p.cc?.length || p.more?.cc ? [{ label: t().cc, people: (p.cc ?? []).map(person), ...more(p.more?.cc) }] : []),
229
249
  ...(p.bcc?.length || p.more?.bcc
230
- ? [{ label: t().bcc, people: (p.bcc ?? []).map(personRow), emphasis: true, ...more(p.more?.bcc) }]
250
+ ? [{ label: t().bcc, people: (p.bcc ?? []).map(person), emphasis: true, ...more(p.more?.bcc) }]
231
251
  : []),
232
252
  ];
233
253
  };
234
254
  const note = () => {
235
255
  const p = props.preview;
256
+ if (!props.sends) return undefined;
236
257
  if (p.sendAt) {
237
258
  const [before, after] = t().sendsAt({ at: "\u0000" }).split("\u0000");
238
259
  return (
@@ -248,9 +269,15 @@ function MessagePreview(props: {
248
269
  return (
249
270
  <>
250
271
  <Chat.Preview.Header rows={rows()} note={note()} />
251
- <Chat.Preview.Title>{props.preview.subject || t().noSubject}</Chat.Preview.Title>
252
- <Show when={props.preview.body.trim()}>
253
- <PreviewExcerpt text={excerptText(props.preview.body, props.preview.format)} readMore={props.readMore} label={t().readMail} />
272
+ <Chat.Preview.Title>{text(props.preview.subject) || t().noSubject}</Chat.Preview.Title>
273
+ <Show when={props.full || props.preview.body.trim()}>
274
+ <PreviewExcerpt
275
+ text={props.full ? visibleReviewText(props.preview.body) : excerptText(props.preview.body, props.preview.format)}
276
+ readMore={props.readMore}
277
+ label={t().readMail}
278
+ full={props.full}
279
+ compact={props.compact}
280
+ />
254
281
  </Show>
255
282
  <For each={props.preview.attachments ?? []}>
256
283
  {(attachment) => (
@@ -259,7 +286,7 @@ function MessagePreview(props: {
259
286
  accent={attachmentLook(attachment.name).accent}
260
287
  meta={attachment.size !== undefined ? formatBytes(attachment.size, { locale: props.locale }) : undefined}
261
288
  >
262
- {attachment.name}
289
+ {text(attachment.name)}
263
290
  </Chat.Preview.Line>
264
291
  )}
265
292
  </For>
@@ -273,7 +300,7 @@ function MessagePreview(props: {
273
300
  <For each={props.preview.warnings ?? []}>
274
301
  {(warning) => (
275
302
  <Chat.Preview.Line icon="ti ti-alert-triangle" tone="warning">
276
- <WarningText text={warning} />
303
+ <WarningText text={text(warning)} />
277
304
  </Chat.Preview.Line>
278
305
  )}
279
306
  </For>
@@ -288,7 +315,14 @@ function MessagePreview(props: {
288
315
  );
289
316
  }
290
317
 
291
- function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "review" }>; card: ActionCard; readMore?: string }) {
318
+ function ReviewPreviewBody(props: {
319
+ model: Extract<ActionPreviewModel, { kind: "review" }>;
320
+ card: ActionCard;
321
+ readMore?: string;
322
+ full?: boolean;
323
+ baseRevision?: number;
324
+ compact?: boolean;
325
+ }) {
292
326
  const t = () => actionMessages(props.model.locale);
293
327
  const locale = () => props.model.locale;
294
328
  const tz = () => props.model.timeZone;
@@ -305,9 +339,22 @@ function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "
305
339
  const preview = () => props.model.preview;
306
340
  return (
307
341
  <Chat.Preview>
342
+ <Show when={props.baseRevision !== undefined}>
343
+ <Chat.Preview.Line icon="ti ti-history">{t().baseRevision({ revision: props.baseRevision! })}</Chat.Preview.Line>
344
+ </Show>
308
345
  <Switch>
309
346
  <Match when={preview()?.kind === "message" ? (preview() as Extract<CapabilityPreview, { kind: "message" }>) : undefined}>
310
- {(message) => <MessagePreview preview={message()} locale={locale()} timeZone={tz()} readMore={props.readMore} />}
347
+ {(message) => (
348
+ <MessagePreview
349
+ preview={message()}
350
+ locale={locale()}
351
+ timeZone={tz()}
352
+ readMore={props.readMore}
353
+ sends={props.card.reason.code === "sends"}
354
+ full={props.full}
355
+ compact={props.compact}
356
+ />
357
+ )}
311
358
  </Match>
312
359
  <Match when={preview()?.kind === "event" ? (preview() as Extract<CapabilityPreview, { kind: "event" }>) : undefined}>
313
360
  {(event) => (
@@ -396,9 +443,17 @@ function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "
396
443
  <Match when={preview()?.kind === "page" ? (preview() as Extract<CapabilityPreview, { kind: "page" }>) : undefined}>
397
444
  {(page) => (
398
445
  <>
399
- <Chat.Preview.Title meta={page().place}>{page().title}</Chat.Preview.Title>
400
- <Show when={page().body.trim()}>
401
- <PreviewExcerpt text={excerptText(page().body, page().format)} readMore={props.readMore} label={t().readPage} />
446
+ <Chat.Preview.Title meta={props.full && page().place ? visibleReviewText(page().place!) : page().place}>
447
+ {props.full ? visibleReviewText(page().title) : page().title}
448
+ </Chat.Preview.Title>
449
+ <Show when={props.full || page().body.trim()}>
450
+ <PreviewExcerpt
451
+ text={props.full ? visibleReviewText(page().body) : excerptText(page().body, page().format)}
452
+ readMore={props.readMore}
453
+ label={t().readPage}
454
+ full={props.full}
455
+ compact={props.compact}
456
+ />
402
457
  </Show>
403
458
  <Show when={page().changes?.length ? page().changes : undefined}>
404
459
  {(changes) => (
@@ -556,15 +611,113 @@ function LegacyPreview(props: { model: Extract<ActionPreviewModel, { kind: "lega
556
611
  );
557
612
  }
558
613
 
614
+ /** Controls remain inspectable instead of being removed or changing reading order. */
615
+ export const visibleReviewText = (text: string): string =>
616
+ text.replace(/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/gu, (character) =>
617
+ character === "\n" || character === "\t" ? character : `⟦U+${character.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0")}⟧`,
618
+ );
619
+
620
+ function ReviewContentPanel(props: {
621
+ card: ActionCard;
622
+ source: CapabilityReviewContentRef;
623
+ timeZone: string;
624
+ close: () => void;
625
+ onLoaded: () => void;
626
+ }) {
627
+ const t = () => actionMessages(props.card.locale);
628
+ const [content, setContent] = createSignal<CapabilityReviewContent>();
629
+ const [failed, setFailed] = createSignal(false);
630
+ const [loading, setLoading] = createSignal(false);
631
+ let request: AbortController | undefined;
632
+ let disposed = false;
633
+ onCleanup(() => {
634
+ disposed = true;
635
+ request?.abort();
636
+ });
637
+ const load = async () => {
638
+ if (loading() || disposed) return;
639
+ request = new AbortController();
640
+ setLoading(true);
641
+ setFailed(false);
642
+ try {
643
+ const result = await readCapabilityReviewContent(props.source, { signal: request.signal });
644
+ if (!result.ok) throw new Error(result.error.message);
645
+ if (disposed) return;
646
+ setContent(result.data);
647
+ props.onLoaded();
648
+ } catch {
649
+ if (!disposed) setFailed(true);
650
+ } finally {
651
+ if (!disposed) setLoading(false);
652
+ }
653
+ };
654
+ onMount(() => void load());
655
+ return (
656
+ <LocaleProvider locale={props.card.locale}>
657
+ <PanelDialog>
658
+ <PanelDialog.Header
659
+ icon={props.card.app.icon}
660
+ title={t().fullReview}
661
+ subtitle={props.card.questionText}
662
+ close={props.close}
663
+ closeLabel={t().fullReviewClose}
664
+ />
665
+ <PanelDialog.Body>
666
+ <Show
667
+ when={content()}
668
+ fallback={
669
+ <Show when={failed()} fallback={<Placeholder state="loading" title={t().fullReviewLoading} variant="panel" />}>
670
+ <Placeholder
671
+ state="error"
672
+ title={t().fullReviewFailed}
673
+ variant="panel"
674
+ action={
675
+ <Button variant="secondary" onClick={() => void load()}>
676
+ {t().fullReviewRetry}
677
+ </Button>
678
+ }
679
+ />
680
+ </Show>
681
+ }
682
+ >
683
+ {(value) => (
684
+ <ReviewPreviewBody
685
+ card={props.card}
686
+ full
687
+ baseRevision={value().baseRevision}
688
+ model={{
689
+ kind: "review",
690
+ locale: props.card.locale,
691
+ timeZone: props.timeZone,
692
+ preview: value(),
693
+ slots: {},
694
+ consequences: [],
695
+ }}
696
+ />
697
+ )}
698
+ </Show>
699
+ </PanelDialog.Body>
700
+ </PanelDialog>
701
+ </LocaleProvider>
702
+ );
703
+ }
704
+
559
705
  /** The thing the question is about, from its review, or an old review's rows and Markdown. */
560
- export function ActionPreview(props: { card: ActionCard; timeZone: string }) {
706
+ export function ActionPreview(props: { card: ActionCard; timeZone: string; onContentLoaded?: (key: string) => void }) {
561
707
  return (
562
708
  <>
563
709
  <Show when={props.card.preview}>
564
710
  {(model) => (
565
711
  <Switch>
566
712
  <Match when={model().kind === "review" ? (model() as Extract<ActionPreviewModel, { kind: "review" }>) : undefined}>
567
- {(review) => <ReviewPreviewBody model={review()} card={props.card} readMore={props.card.readMore} />}
713
+ {(review) => (
714
+ <ReviewPreviewBody
715
+ model={review()}
716
+ card={props.card}
717
+ compact={!!props.card.contentRef}
718
+ readMore={props.card.contentRef ? undefined : props.card.readMore}
719
+ />
720
+ )}
568
721
  </Match>
569
722
  <Match when={model().kind === "legacy" ? (model() as Extract<ActionPreviewModel, { kind: "legacy" }>) : undefined}>
570
723
  {(legacy) => <LegacyPreview model={legacy()} card={props.card} timeZone={props.timeZone} />}
@@ -572,6 +725,41 @@ export function ActionPreview(props: { card: ActionCard; timeZone: string }) {
572
725
  </Switch>
573
726
  )}
574
727
  </Show>
728
+ <Show when={props.card.contentRef}>
729
+ {(reference) => (
730
+ <Chat.Preview>
731
+ <Button
732
+ variant="ghost"
733
+ size="sm"
734
+ onClick={() => {
735
+ const card = props.card;
736
+ const source = reference();
737
+ void dialogCore.open<void>(
738
+ (close) => (
739
+ <ReviewContentPanel
740
+ card={card}
741
+ source={source}
742
+ timeZone={props.timeZone}
743
+ close={close}
744
+ onLoaded={() => props.onContentLoaded?.(`${source.href}:${source.digest}`)}
745
+ />
746
+ ),
747
+ { ...panelDialogOptions, history: true, ariaLabel: actionMessages(card.locale).fullReview },
748
+ );
749
+ }}
750
+ >
751
+ {actionMessages(props.card.locale).fullReview}
752
+ </Button>
753
+ <Show when={props.card.readMore}>
754
+ {(href) => (
755
+ <Chat.Preview.ReadMore href={href()}>
756
+ {actionMessages(props.card.locale).openIn({ app: props.card.app.name })}
757
+ </Chat.Preview.ReadMore>
758
+ )}
759
+ </Show>
760
+ </Chat.Preview>
761
+ )}
762
+ </Show>
575
763
  {/* The review's other links, such as editing the draft, stay one quiet click away before the answer. */}
576
764
  <Show when={props.card.links.length > 0 || (!props.card.preview && props.card.readMore)}>
577
765
  <Chat.Preview class="cloud-action-preview__links">
@@ -644,6 +832,9 @@ const confirmIcon = (card: ActionCard): string | undefined =>
644
832
 
645
833
  /** One Assistant question in card v4. An answer collapses it at once to its outcome line, which takes the focus. */
646
834
  export function ActionQuestion(props: ActionQuestionProps) {
835
+ const [loadedContent, setLoadedContent] = createSignal<string>();
836
+ const needsContent = () => props.card.contentRef && loadedContent() !== `${props.card.contentRef.href}:${props.card.contentRef.digest}`;
837
+ const unavailable = () => props.unavailable ?? (needsContent() ? actionMessages(props.card.locale).fullReviewRequired : undefined);
647
838
  return (
648
839
  <LocaleProvider locale={props.card.locale}>
649
840
  <Chat.Decision
@@ -652,8 +843,8 @@ export function ActionQuestion(props: ActionQuestionProps) {
652
843
  anchorId={props.anchorId}
653
844
  sentence={<ActionTextView text={props.card.question} accent={props.card.app.accent} />}
654
845
  reason={{ text: props.card.reason.text, icon: props.card.reason.icon, tone: props.card.reason.tone }}
655
- unavailable={props.unavailable ? { text: props.unavailable, icon: "ti ti-player-pause", tone: "neutral" } : undefined}
656
- preview={<ActionPreview card={props.card} timeZone={props.timeZone} />}
846
+ unavailable={unavailable() ? { text: unavailable()!, icon: "ti ti-player-pause", tone: "neutral" } : undefined}
847
+ preview={<ActionPreview card={props.card} timeZone={props.timeZone} onContentLoaded={setLoadedContent} />}
657
848
  confirm={{
658
849
  label: props.card.confirm.label,
659
850
  variant: props.card.confirm.variant,
@@ -0,0 +1,288 @@
1
+ import { dates, i18n } from "@k2b/stdlib";
2
+ import { capabilityPersonText, capabilitySentenceText } from "../../_internal/capability-sentences";
3
+ import type { CapabilityAskReason, CapabilityPerson, CapabilitySentence } from "../../contracts/capabilities";
4
+ import { formatBytes } from "../../shared/format";
5
+ import type { ActionCard } from "./action-card";
6
+ import { actionMessages } from "./action-messages";
7
+
8
+ export type ActionTextRow = { label: string; value: string; format?: "date" | "date-time"; block?: true };
9
+ export type ActionQuestionText = {
10
+ locale: string;
11
+ question: string;
12
+ reason: string;
13
+ reasonCode: CapabilityAskReason;
14
+ rows: ActionTextRow[];
15
+ readMore?: string;
16
+ confirm: string;
17
+ decline: string;
18
+ incomplete: boolean;
19
+ };
20
+
21
+ const messages = i18n.define({
22
+ baseLocale: "en",
23
+ messages: {
24
+ en: {
25
+ from: "From",
26
+ to: "To",
27
+ cc: "Cc",
28
+ bcc: "Bcc",
29
+ subject: "Subject",
30
+ sendAt: "Send at",
31
+ warning: "Warning",
32
+ attachments: "Attachments",
33
+ text: "Text",
34
+ when: "When",
35
+ until: "Until",
36
+ where: "Where",
37
+ with: "With",
38
+ repeats: "Repeats",
39
+ due: "Due",
40
+ assignees: "Assignees",
41
+ status: "Status",
42
+ checklist: "Checklist",
43
+ tags: "Tags",
44
+ folder: "Folder",
45
+ size: "Size",
46
+ target: "To",
47
+ place: "Place",
48
+ added: "Added",
49
+ removed: "Removed",
50
+ starts: "Starts",
51
+ may: "May",
52
+ mayAdded: "May (added)",
53
+ mayRemoved: "May (removed)",
54
+ also: "Also",
55
+ readAll: "Read all",
56
+ title: "Title",
57
+ name: "Name",
58
+ note: "Note",
59
+ before: ({ label }: { label: string }) => `${label} (before)`,
60
+ more: ({ count }: { count: number }) => `${count.toLocaleString("en")} more`,
61
+ },
62
+ de: {
63
+ from: "Von",
64
+ to: "An",
65
+ cc: "Cc",
66
+ bcc: "Bcc",
67
+ subject: "Betreff",
68
+ sendAt: "Senden am",
69
+ warning: "Warnung",
70
+ attachments: "Anhänge",
71
+ text: "Text",
72
+ when: "Wann",
73
+ until: "Bis",
74
+ where: "Wo",
75
+ with: "Mit",
76
+ repeats: "Wiederholung",
77
+ due: "Fällig",
78
+ assignees: "Zuständig",
79
+ status: "Status",
80
+ checklist: "Checkliste",
81
+ tags: "Tags",
82
+ folder: "Ordner",
83
+ size: "Größe",
84
+ target: "Ziel",
85
+ place: "Ort",
86
+ added: "Neu",
87
+ removed: "Entfernt",
88
+ starts: "Startet",
89
+ may: "Darf",
90
+ mayAdded: "Darf (neu)",
91
+ mayRemoved: "Darf (entfernt)",
92
+ also: "Folge",
93
+ readAll: "Ganz lesen",
94
+ title: "Titel",
95
+ name: "Name",
96
+ note: "Hinweis",
97
+ before: ({ label }) => `${label} (vorher)`,
98
+ more: ({ count }) => `${count.toLocaleString("de")} weitere`,
99
+ },
100
+ },
101
+ });
102
+ export const checkActionTextMessages = () => messages.check();
103
+ /** The label for a row an old review left without one, where a transport requires labels. */
104
+ export const actionNoteLabel = (locale: string): string => messages.resolve([locale]).t.note;
105
+
106
+ /** Flatten the question card once. Transport limits and terminal escaping belong to its callers. */
107
+ export const actionCardText = (card: ActionCard): ActionQuestionText => {
108
+ const t = messages.resolve([card.locale]).t;
109
+ const rows: ActionTextRow[] = [];
110
+ let incomplete = Boolean(card.contentRef);
111
+ const row = (label: string, value: string | undefined, extra: Pick<ActionTextRow, "block" | "format"> = {}) => {
112
+ if (value !== undefined && value !== "") rows.push({ label, value, ...extra });
113
+ };
114
+ const list = (values: string[], more = 0) => {
115
+ if (more > 0) {
116
+ incomplete = true;
117
+ values.push(t.more({ count: more }));
118
+ }
119
+ return new Intl.ListFormat(card.locale, { type: "conjunction" }).format(values);
120
+ };
121
+ const people = (values: CapabilityPerson[] | undefined, more = 0) => list((values ?? []).map(capabilityPersonText), more);
122
+ // The thing's own name, unless the question already says it.
123
+ const named = (label: string, value: string) => {
124
+ if (!card.questionText.includes(value)) row(label, value);
125
+ };
126
+ const model = card.preview;
127
+ if (model?.kind === "legacy") {
128
+ rows.push(...model.rows);
129
+ row(t.text, model.markdown, { block: true });
130
+ } else if (model?.kind === "review") {
131
+ const context = { locale: card.locale, timeZone: model.timeZone };
132
+ const sentence = (value: CapabilitySentence) => capabilitySentenceText(value.text, value.slots ?? model.slots ?? {}, context);
133
+ const preview = model.preview;
134
+ if (preview && "truncated" in preview && preview.truncated) incomplete = true;
135
+ switch (preview?.kind) {
136
+ case "message":
137
+ row(t.from, preview.from && capabilityPersonText(preview.from));
138
+ row(t.to, people(preview.to, preview.more?.to));
139
+ row(t.cc, people(preview.cc, preview.more?.cc));
140
+ row(t.bcc, people(preview.bcc, preview.more?.bcc));
141
+ row(t.subject, preview.subject);
142
+ row(t.sendAt, preview.sendAt, { format: "date-time" });
143
+ row(t.warning, preview.warnings?.join(" · "));
144
+ row(
145
+ t.attachments,
146
+ list(
147
+ (preview.attachments ?? []).map((file) =>
148
+ file.size === undefined ? file.name : `${file.name} (${formatBytes(file.size, { locale: card.locale })})`,
149
+ ),
150
+ preview.moreAttachments,
151
+ ),
152
+ );
153
+ row(t.text, preview.body, { block: true });
154
+ break;
155
+ case "event": {
156
+ named(t.title, preview.title);
157
+ const dateOnly = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value);
158
+ const allDay = preview.allDay === true || dateOnly(preview.start);
159
+ // Both ends stay typed, so a device shows them in its own zone. An all-day instant becomes its calendar day
160
+ // in the card's zone first, so no reader's zone moves it to another day.
161
+ const day = (value: string) =>
162
+ dateOnly(value)
163
+ ? value
164
+ : new Intl.DateTimeFormat("en-CA", { timeZone: model.timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(
165
+ new Date(value),
166
+ );
167
+ const format = allDay ? "date" : "date-time";
168
+ row(t.when, allDay ? day(preview.start) : preview.start, { format });
169
+ if (preview.end) row(t.until, allDay ? day(preview.end) : preview.end, { format });
170
+ row(t.where, [preview.place, preview.location].filter(Boolean).join(" · "));
171
+ row(t.with, people(preview.attendees));
172
+ row(t.repeats, preview.recurrence);
173
+ row(t.text, preview.description, { block: true });
174
+ break;
175
+ }
176
+ case "task":
177
+ named(t.title, preview.title);
178
+ row(t.place, preview.place);
179
+ row(t.due, preview.due, { format: /^\d{4}-\d{2}-\d{2}$/.test(preview.due ?? "") ? "date" : "date-time" });
180
+ row(t.assignees, people(preview.assignees));
181
+ row(t.status, preview.status);
182
+ row(t.checklist, preview.checklist?.map((item) => `${item.done ? "☑" : "☐"} ${item.label}`).join(" · "));
183
+ row(t.tags, preview.tags?.join(", "));
184
+ row(t.text, preview.description, { block: true });
185
+ break;
186
+ case "file":
187
+ named(t.name, preview.name);
188
+ row(t.folder, preview.place ?? preview.path);
189
+ row(t.size, preview.size === undefined ? undefined : formatBytes(preview.size, { locale: card.locale }));
190
+ row(t.target, preview.to);
191
+ break;
192
+ case "page":
193
+ named(t.title, preview.title);
194
+ row(t.place, preview.place);
195
+ row(t.text, preview.body, { block: true });
196
+ for (const change of preview.changes ?? []) row(change.change === "added" ? t.added : t.removed, change.text);
197
+ break;
198
+ case "rule":
199
+ row(t.starts, sentence(preview.when));
200
+ row(t.text, preview.does, { block: true });
201
+ for (const may of preview.may)
202
+ row(may.mark === "added" ? t.mayAdded : may.mark === "removed" ? t.mayRemoved : t.may, sentence(may));
203
+ // The card's fixed line: a routine knows only its instruction, never this chat.
204
+ if (!preview.seesChat) row(t.note, actionMessages(card.locale).ruleBlindToChat);
205
+ break;
206
+ case "fields":
207
+ for (const field of preview.rows) {
208
+ rows.push({ label: field.label, value: field.value, ...(field.format ? { format: field.format } : {}) });
209
+ if (field.before !== undefined)
210
+ rows.push({ label: t.before({ label: field.label }), value: field.before, ...(field.format ? { format: field.format } : {}) });
211
+ }
212
+ break;
213
+ }
214
+ for (const consequence of model.consequences)
215
+ row(consequence.mark === "added" ? t.added : consequence.mark === "removed" ? t.removed : t.also, consequence.plain);
216
+ }
217
+ // A long question shortens slot values and names at most three people of a slot, then "N more". The text is
218
+ // complete only when the question or a row shows each value whole; people the app left out are never shown.
219
+ const shown = [card.questionText, ...rows.map((entry) => entry.value)];
220
+ const whole = (value: string) => shown.some((text) => text.includes(value));
221
+ const typed = (value: string, format: "date" | "date-time") =>
222
+ rows.some((entry) => entry.value === value && (entry.format === "date-time" || entry.format === format));
223
+ if (
224
+ card.slotValues?.unlisted ||
225
+ card.slotValues?.values.some((value) => !whole(value)) ||
226
+ card.slotValues?.times.some(
227
+ (time) => !whole(time.text) && !(typed(time.start, time.format) && (!time.end || typed(time.end, time.format))),
228
+ )
229
+ )
230
+ incomplete = true;
231
+ return {
232
+ locale: card.locale,
233
+ question: card.questionText,
234
+ reason: card.reason.text,
235
+ reasonCode: card.reason.code,
236
+ rows,
237
+ ...(card.readMore ? { readMore: card.readMore } : {}),
238
+ confirm: card.confirm.label,
239
+ decline: card.decline,
240
+ incomplete,
241
+ };
242
+ };
243
+
244
+ /** The CLI's plain question block, before terminal-safe sanitizing at the CLI boundary. */
245
+ export const actionQuestionLines = (
246
+ text: ActionQuestionText,
247
+ extra: { origin?: string; commands?: { approve: string; reject: string }; timeZone: string },
248
+ ): string[] => {
249
+ const t = messages.resolve([text.locale]).t;
250
+ const lines = [`? ${text.question}`, ` ${text.reason}`];
251
+ if (extra.origin) lines.push(` ${extra.origin}`);
252
+ const width = Math.max(0, ...text.rows.filter((row) => !row.block && row.label).map((row) => Array.from(row.label).length + 1));
253
+ let markedIncomplete = false;
254
+ for (const row of text.rows) {
255
+ if (row.block) {
256
+ const body = row.value.split(/\r?\n/);
257
+ // Cut a long text only when "Read all" can open the whole thing; otherwise the terminal shows all of it.
258
+ const shown = text.readMore ? body.slice(0, 8) : body;
259
+ lines.push(...shown.map((line) => ` ${line}`));
260
+ if (shown.length < body.length || (text.incomplete && !markedIncomplete)) {
261
+ lines.push(" …");
262
+ markedIncomplete = true;
263
+ }
264
+ continue;
265
+ }
266
+ const options = { locale: text.locale, timeZone: extra.timeZone };
267
+ const value =
268
+ row.format === "date-time"
269
+ ? dates.formatDateTime(row.value, options)
270
+ : row.format === "date"
271
+ ? dates.formatDate(/^\d{4}-\d{2}-\d{2}$/.test(row.value) ? `${row.value}T12:00:00.000Z` : row.value, {
272
+ ...options,
273
+ ...(/^\d{4}-\d{2}-\d{2}$/.test(row.value) ? { timeZone: "UTC" } : {}),
274
+ })
275
+ : row.value;
276
+ lines.push(row.label ? ` ${(row.label + ":").padEnd(width)} ${value}` : ` ${value}`);
277
+ }
278
+ if (text.incomplete && !markedIncomplete) lines.push(" …");
279
+ if (text.readMore) lines.push(` ${t.readAll}: ${text.readMore}`);
280
+ if (extra.commands) {
281
+ const width = Math.max(Array.from(text.confirm).length, Array.from(text.decline).length) + 1;
282
+ lines.push(
283
+ ` ${(text.confirm + ":").padEnd(width)} ${extra.commands.approve}`,
284
+ ` ${(text.decline + ":").padEnd(width)} ${extra.commands.reject}`,
285
+ );
286
+ }
287
+ return lines;
288
+ };
@@ -0,0 +1,31 @@
1
+ import { capabilityUnknownText } from "../../_internal/capability-sentences";
2
+ import type { AiTurnBlock } from "../protocol";
3
+ import { toolOutcome } from "../tool-wording";
4
+ import { isRecord } from "./message-utils";
5
+ import { aiChatMessages } from "./messages";
6
+
7
+ type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
8
+
9
+ /** The application's own short summary of a result, if it fits on one line. */
10
+ const resultSummary = (block: ToolBlock): string | undefined => {
11
+ const value = isRecord(block.result) ? block.result.summary : undefined;
12
+ return typeof value === "string" && value.trim() && value.trim().length <= 500 ? value.trim() : undefined;
13
+ };
14
+
15
+ /** The words of a call without an app sentence: Cloud's frames around its catalog sentence or title. */
16
+ export const legacyOutcomeWords = (block: ToolBlock, locale: string, timeZone: string) => (subject: string) => {
17
+ const t = aiChatMessages(locale);
18
+ const capability = block.presentation?.kind === "capability";
19
+ return {
20
+ running: t.receiptRunning({ title: subject }),
21
+ // The app's done sentence, else its own summary of the call, else Cloud's frame.
22
+ done:
23
+ toolOutcome(block, "done", locale, timeZone) ??
24
+ resultSummary(block) ??
25
+ (capability ? t.receiptDone({ title: subject }) : t.receiptApproved({ title: subject })),
26
+ rejected: toolOutcome(block, "rejected", locale, timeZone) ?? t.receiptRejected({ title: subject }),
27
+ notRun: toolOutcome(block, "notRun", locale, timeZone) ?? t.receiptNotRun({ title: subject, stopped: false }),
28
+ unknown: capabilityUnknownText(subject, locale),
29
+ unknownStill: capabilityUnknownText(subject, locale, true),
30
+ };
31
+ };