@k2b/cloud 0.41.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 (227) hide show
  1. package/package.json +6 -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-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. 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()}>
@@ -219,17 +229,25 @@ function MessagePreview(props: {
219
229
  readMore?: string;
220
230
  /** Only a question that sends says when the mail goes out; a draft that is created or deleted goes nowhere. */
221
231
  sends: boolean;
232
+ full?: boolean;
233
+ compact?: boolean;
222
234
  }) {
223
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
+ });
224
242
  const rows = () => {
225
243
  const p = props.preview;
226
244
  const more = (count?: number) => (count ? { value: <> {t().andMore({ count })}</> } : {});
227
245
  return [
228
- ...(p.from ? [{ label: t().from, people: [personRow(p.from)] }] : []),
229
- { label: t().to, people: p.to.map(personRow), ...more(p.more?.to) },
230
- ...(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) }] : []),
231
249
  ...(p.bcc?.length || p.more?.bcc
232
- ? [{ 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) }]
233
251
  : []),
234
252
  ];
235
253
  };
@@ -251,9 +269,15 @@ function MessagePreview(props: {
251
269
  return (
252
270
  <>
253
271
  <Chat.Preview.Header rows={rows()} note={note()} />
254
- <Chat.Preview.Title>{props.preview.subject || t().noSubject}</Chat.Preview.Title>
255
- <Show when={props.preview.body.trim()}>
256
- <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
+ />
257
281
  </Show>
258
282
  <For each={props.preview.attachments ?? []}>
259
283
  {(attachment) => (
@@ -262,7 +286,7 @@ function MessagePreview(props: {
262
286
  accent={attachmentLook(attachment.name).accent}
263
287
  meta={attachment.size !== undefined ? formatBytes(attachment.size, { locale: props.locale }) : undefined}
264
288
  >
265
- {attachment.name}
289
+ {text(attachment.name)}
266
290
  </Chat.Preview.Line>
267
291
  )}
268
292
  </For>
@@ -276,7 +300,7 @@ function MessagePreview(props: {
276
300
  <For each={props.preview.warnings ?? []}>
277
301
  {(warning) => (
278
302
  <Chat.Preview.Line icon="ti ti-alert-triangle" tone="warning">
279
- <WarningText text={warning} />
303
+ <WarningText text={text(warning)} />
280
304
  </Chat.Preview.Line>
281
305
  )}
282
306
  </For>
@@ -291,7 +315,14 @@ function MessagePreview(props: {
291
315
  );
292
316
  }
293
317
 
294
- 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
+ }) {
295
326
  const t = () => actionMessages(props.model.locale);
296
327
  const locale = () => props.model.locale;
297
328
  const tz = () => props.model.timeZone;
@@ -308,6 +339,9 @@ function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "
308
339
  const preview = () => props.model.preview;
309
340
  return (
310
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>
311
345
  <Switch>
312
346
  <Match when={preview()?.kind === "message" ? (preview() as Extract<CapabilityPreview, { kind: "message" }>) : undefined}>
313
347
  {(message) => (
@@ -317,6 +351,8 @@ function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "
317
351
  timeZone={tz()}
318
352
  readMore={props.readMore}
319
353
  sends={props.card.reason.code === "sends"}
354
+ full={props.full}
355
+ compact={props.compact}
320
356
  />
321
357
  )}
322
358
  </Match>
@@ -407,9 +443,17 @@ function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "
407
443
  <Match when={preview()?.kind === "page" ? (preview() as Extract<CapabilityPreview, { kind: "page" }>) : undefined}>
408
444
  {(page) => (
409
445
  <>
410
- <Chat.Preview.Title meta={page().place}>{page().title}</Chat.Preview.Title>
411
- <Show when={page().body.trim()}>
412
- <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
+ />
413
457
  </Show>
414
458
  <Show when={page().changes?.length ? page().changes : undefined}>
415
459
  {(changes) => (
@@ -567,15 +611,113 @@ function LegacyPreview(props: { model: Extract<ActionPreviewModel, { kind: "lega
567
611
  );
568
612
  }
569
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
+
570
705
  /** The thing the question is about, from its review, or an old review's rows and Markdown. */
571
- export function ActionPreview(props: { card: ActionCard; timeZone: string }) {
706
+ export function ActionPreview(props: { card: ActionCard; timeZone: string; onContentLoaded?: (key: string) => void }) {
572
707
  return (
573
708
  <>
574
709
  <Show when={props.card.preview}>
575
710
  {(model) => (
576
711
  <Switch>
577
712
  <Match when={model().kind === "review" ? (model() as Extract<ActionPreviewModel, { kind: "review" }>) : undefined}>
578
- {(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
+ )}
579
721
  </Match>
580
722
  <Match when={model().kind === "legacy" ? (model() as Extract<ActionPreviewModel, { kind: "legacy" }>) : undefined}>
581
723
  {(legacy) => <LegacyPreview model={legacy()} card={props.card} timeZone={props.timeZone} />}
@@ -583,6 +725,41 @@ export function ActionPreview(props: { card: ActionCard; timeZone: string }) {
583
725
  </Switch>
584
726
  )}
585
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>
586
763
  {/* The review's other links, such as editing the draft, stay one quiet click away before the answer. */}
587
764
  <Show when={props.card.links.length > 0 || (!props.card.preview && props.card.readMore)}>
588
765
  <Chat.Preview class="cloud-action-preview__links">
@@ -655,6 +832,9 @@ const confirmIcon = (card: ActionCard): string | undefined =>
655
832
 
656
833
  /** One Assistant question in card v4. An answer collapses it at once to its outcome line, which takes the focus. */
657
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);
658
838
  return (
659
839
  <LocaleProvider locale={props.card.locale}>
660
840
  <Chat.Decision
@@ -663,8 +843,8 @@ export function ActionQuestion(props: ActionQuestionProps) {
663
843
  anchorId={props.anchorId}
664
844
  sentence={<ActionTextView text={props.card.question} accent={props.card.app.accent} />}
665
845
  reason={{ text: props.card.reason.text, icon: props.card.reason.icon, tone: props.card.reason.tone }}
666
- unavailable={props.unavailable ? { text: props.unavailable, icon: "ti ti-player-pause", tone: "neutral" } : undefined}
667
- 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} />}
668
848
  confirm={{
669
849
  label: props.card.confirm.label,
670
850
  variant: props.card.confirm.variant,
@@ -107,7 +107,7 @@ export const actionNoteLabel = (locale: string): string => messages.resolve([loc
107
107
  export const actionCardText = (card: ActionCard): ActionQuestionText => {
108
108
  const t = messages.resolve([card.locale]).t;
109
109
  const rows: ActionTextRow[] = [];
110
- let incomplete = false;
110
+ let incomplete = Boolean(card.contentRef);
111
111
  const row = (label: string, value: string | undefined, extra: Pick<ActionTextRow, "block" | "format"> = {}) => {
112
112
  if (value !== undefined && value !== "") rows.push({ label, value, ...extra });
113
113
  };
@@ -31,6 +31,7 @@ import {
31
31
  chatTaskStopSentence,
32
32
  } from "./chat-task-reasons";
33
33
  import { buildBlocksFromMessages } from "./protocol";
34
+ import { compactReviewMetaSql } from "./review-content";
34
35
  import { withAiShortIdForDb } from "./short-id";
35
36
  import { aiConversations } from "./store";
36
37
  import { toolOutcome, toolWording } from "./tool-wording";
@@ -575,7 +576,7 @@ const deliverStoppedTurn = async (
575
576
  if (!claimed) return null;
576
577
  const locale = chatTaskLocale(config.locale ?? task?.locale, appLocale);
577
578
  const messages = await db<{ seq: number; message: Message; meta: AiStoredMessage["meta"] }[]>`
578
- SELECT seq, message, meta FROM ai.task_messages WHERE loop_id = ${turn.id} ORDER BY seq`;
579
+ SELECT seq, message, ${compactReviewMetaSql(sql`meta`)} AS meta FROM ai.task_messages WHERE loop_id = ${turn.id} ORDER BY seq`;
579
580
  const done = buildBlocksFromMessages(messages).flatMap((block) => {
580
581
  if (block.kind !== "tool" || block.status !== "completed" || block.isError) return [];
581
582
  // Web reads have chat wording too, but do not change anything for a stop receipt.