@wix/himalaya-cli 0.1374.0 → 0.1376.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 (68) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +11 -3
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +5 -2
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/async-jobs/src/extensions.d.ts +1 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/async-jobs/src/index.d.ts +248 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +6 -1
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +3 -1
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +2 -1
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-async-jobs.mjs +34 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  11. package/dist/cli.mjs +1428 -637
  12. package/dist/preview-assets.generated.bin +0 -0
  13. package/guides/getting-started.md +23 -0
  14. package/guides/himi-json-output.md +42 -0
  15. package/guides/himi-skill.md +23 -0
  16. package/guides/iap-screen-adoption.md +7 -5
  17. package/guides/importable-sdks.md +1 -0
  18. package/guides/index.json +42 -0
  19. package/guides/one-app-business-fetch.md +81 -0
  20. package/guides/one-app-business-object.md +258 -0
  21. package/guides/one-app-reference.md +55 -0
  22. package/guides/one-app-section-mappings.md +381 -0
  23. package/guides/one-app-structure.md +453 -0
  24. package/guides/one-app-to-himi-app.md +305 -0
  25. package/guides/one-app-viewer.md +401 -0
  26. package/guides/release-notes.md +10 -0
  27. package/guides/sdk-reference.json +314 -4
  28. package/guides/skill.md +24 -1
  29. package/package.json +2 -2
  30. package/template-icons/base44-podcast.png +0 -0
  31. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +5 -5
  32. package/templates/beauty-salon-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  33. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +14 -3
  34. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  35. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +5 -5
  36. package/templates/community-club-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  37. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +14 -3
  38. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  39. package/templates/events-meetups-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  40. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +14 -3
  41. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  42. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +5 -5
  43. package/templates/gym-fitness-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  44. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +14 -3
  45. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  46. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  47. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +14 -3
  48. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  49. package/templates/owner-lite/tier5-src/_shared/aria-async-jobs.ts +10 -0
  50. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +14 -3
  51. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  52. package/templates/podcast-creator-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  53. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +14 -3
  54. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  55. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +5 -5
  56. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  57. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +14 -3
  58. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  59. package/templates/restaurant-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  60. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +14 -3
  61. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  62. package/templates/retail-store/tier5-src/himi-store-product/index.ts +5 -5
  63. package/templates/retail-store-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  64. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +14 -3
  65. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
  66. package/templates/yoga-studio-owner/tier5-src/_shared/aria-async-jobs.ts +10 -0
  67. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +14 -3
  68. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +2 -0
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -1,6 +1,6 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
2
  import { withIapPresentation } from "../_shared/iap-presentation.js";
3
- import { itemPaymentEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
3
+ import { storesAppleSalesEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
4
4
  import { iapPriceCopy, iapPurchaseCopy } from "../_shared/iap-worker.js";
5
5
  import { share as shareSheet } from "@himalaya/share";
6
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
@@ -317,10 +317,10 @@ function derive(s: S): S {
317
317
  // read answering late — must not replace the badge the add set (#3468 review).
318
318
  let cartBadgeEpoch = 0;
319
319
 
320
- // A digital product with no options sells through the App Store once the platform pays for Wix
321
- // Stores items with wix-iap StartItemPayment (`itemPaymentEnabled`). Until then, and for any other
322
- // product, an App Store route only blocks the regular checkout, as before.
323
- const SELLS_THROUGH_APPLE = itemPaymentEnabled();
320
+ // A digital product with no options sells through the App Store once the platform sells Wix Stores
321
+ // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
322
+ // Until then, and for any other product, an App Store route only blocks the regular checkout.
323
+ const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
324
324
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
325
325
  const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
326
326
 
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -1,6 +1,6 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
2
  import { withIapPresentation } from "../_shared/iap-presentation.js";
3
- import { itemPaymentEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
3
+ import { storesAppleSalesEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
4
4
  import { iapPriceCopy, iapPurchaseCopy } from "../_shared/iap-worker.js";
5
5
  import { share as shareSheet } from "@himalaya/share";
6
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
@@ -317,10 +317,10 @@ function derive(s: S): S {
317
317
  // read answering late — must not replace the badge the add set (#3468 review).
318
318
  let cartBadgeEpoch = 0;
319
319
 
320
- // A digital product with no options sells through the App Store once the platform pays for Wix
321
- // Stores items with wix-iap StartItemPayment (`itemPaymentEnabled`). Until then, and for any other
322
- // product, an App Store route only blocks the regular checkout, as before.
323
- const SELLS_THROUGH_APPLE = itemPaymentEnabled();
320
+ // A digital product with no options sells through the App Store once the platform sells Wix Stores
321
+ // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
322
+ // Until then, and for any other product, an App Store route only blocks the regular checkout.
323
+ const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
324
324
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
325
325
  const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
326
326
 
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",
@@ -24,6 +24,7 @@
24
24
  // question is put to the owner in a real modal (`@himalaya/ui` alert) and
25
25
  // their tap is the answer — see `confirmWithOwner`.
26
26
 
27
+ import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
27
28
  import { defineService } from "@himalaya/state";
28
29
  import { open } from "@himalaya/open-url";
29
30
  import { writeText } from "@himalaya/clipboard";
@@ -182,6 +183,7 @@ function userAgent(): string {
182
183
  }
183
184
 
184
185
  const core = createAriaChat({
186
+ asyncJobState: readAsyncJobState,
185
187
  // Mount the generic card for an unrecognised widget kind. This screen is
186
188
  // listed in `workerSubtreeScreens`; a screen that is not would have the node
187
189
  // rejected by the descriptor validator, so omitting this is the correct
@@ -1,6 +1,6 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
2
  import { withIapPresentation } from "../_shared/iap-presentation.js";
3
- import { itemPaymentEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
3
+ import { storesAppleSalesEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
4
4
  import { iapPriceCopy, iapPurchaseCopy } from "../_shared/iap-worker.js";
5
5
  import { share as shareSheet } from "@himalaya/share";
6
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
@@ -317,10 +317,10 @@ function derive(s: S): S {
317
317
  // read answering late — must not replace the badge the add set (#3468 review).
318
318
  let cartBadgeEpoch = 0;
319
319
 
320
- // A digital product with no options sells through the App Store once the platform pays for Wix
321
- // Stores items with wix-iap StartItemPayment (`itemPaymentEnabled`). Until then, and for any other
322
- // product, an App Store route only blocks the regular checkout, as before.
323
- const SELLS_THROUGH_APPLE = itemPaymentEnabled();
320
+ // A digital product with no options sells through the App Store once the platform sells Wix Stores
321
+ // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
322
+ // Until then, and for any other product, an App Store route only blocks the regular checkout.
323
+ const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
324
324
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
325
325
  const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
326
326
 
@@ -0,0 +1,10 @@
1
+ import { getAsyncJob } from "@himalaya/wix-async-jobs";
2
+
3
+ /** First-party async-jobs status read, using the generated Wix gateway client. */
4
+ export async function readAsyncJobState(jobId: string): Promise<string> {
5
+ const response = await getAsyncJob(jobId);
6
+ const status = response.job?.status;
7
+ if (typeof status !== "string") return "";
8
+ const normalized = status.toLowerCase();
9
+ return ["initialized", "processing", "finished", "failed"].includes(normalized) ? normalized : "";
10
+ }
@@ -2522,6 +2522,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2522
2522
  // transcript row itself is unchanged.
2523
2523
  if (!next.sourceMessageId || prev.sourceMessageId !== next.sourceMessageId) return next;
2524
2524
  if (prev.kind !== next.kind) return next;
2525
+ // Keep the latest service status while this exact job and transcript row
2526
+ // remain visible. Rebuilding from the envelope must not restart a terminal
2527
+ // job or replace progress with its initial status message.
2528
+ if (next.kind === "ASYNC_TOOL" && next.asyncJobId && prev.asyncJobId === next.asyncJobId) {
2529
+ return asyncAdvanced(next, prev.asyncState);
2530
+ }
2525
2531
  // A questionnaire carries MORE than a selection — every answer, typed text
2526
2532
  // and which question is on screen — and its `options` are only the CURRENT
2527
2533
  // question's choices, so the comparison below would call an owner on
@@ -3457,10 +3463,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3457
3463
  // `copyNote: ""` — a resumed transcript ends in a reply, so the row is
3458
3464
  // shown at once; a note carried over would say "Copied" beside an
3459
3465
  // answer from a different conversation that nobody copied.
3460
- return leaving(await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3466
+ const restored = await withWidgetItems(await withSuggestions({ ...s, status: "ready", _convId: resumeId, _siteId: site, _pending: "", statusText: "", error: "", copyNote: "",
3461
3467
  messages: resumed, ...observed(raw, s.widget), _suggested: [], _suggestToken: "", prompts: promptsFor(resumed, promptList()),
3462
3468
  // Transcript scroll — open a resumed conversation at its latest turn, pinning nothing.
3463
- ...openedAtLatest(s, resumed) }, site)));
3469
+ ...openedAtLatest(s, resumed) }, site));
3470
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3471
+ ? cadence(restored, o.poll.idleMs, null) : leaving(restored);
3464
3472
  } catch { /* expired or unreachable — fall through to a fresh one */ }
3465
3473
  }
3466
3474
  // Opening a screen runs `load` more than once — the bundle self-kicks at
@@ -3498,7 +3506,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3498
3506
  // bundle from before the suggestions existed never asked at all — the
3499
3507
  // update every owner's last conversation goes through — so it asks now.
3500
3508
  const orphaned = !!s._suggestToken && !suggestOutcomes.has(s._suggestToken);
3501
- return orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3509
+ const restored = orphaned || (!!o.suggestions && !s._suggestAsked) ? await withSuggestions(reused, site) : reused;
3510
+ // A saved, still-running async job resumes its status polling on rehydration.
3511
+ return restored.widget.kind === "ASYNC_TOOL" && restored.widget.asyncRunning && restored.widget.asyncJobId && o.poll
3512
+ ? cadence(restored, o.poll.idleMs, null) : restored;
3502
3513
  }
3503
3514
  try {
3504
3515
  return leaving(await withSuggestions({ ...s, status: "ready", _convId: await createOnce(site), _siteId: site, _pending: "", statusText: "",