@wix/himalaya-cli 0.971.0 → 0.972.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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/genie-chat/src/index.d.ts +12 -0
- package/dist/cli.mjs +0 -1
- package/guides/sdk-reference.json +7 -1
- package/package.json +2 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +62 -5
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +62 -5
|
@@ -34,11 +34,23 @@ export declare function isGenieChatError(e: unknown): e is GenieChatError;
|
|
|
34
34
|
declare function wixCall<T>(method: string, path: string, body?: unknown): Promise<T>;
|
|
35
35
|
export type MessageRole = "UNKNOWN" | "USER" | "ASSISTANT";
|
|
36
36
|
export type CommunicationMode = "UNKNOWN" | "POLLING" | "REAL_TIME";
|
|
37
|
+
/** Where the service is with one message. `STATUS_PENDING` on an ASSISTANT row
|
|
38
|
+
* means the agent is still writing it: whatever `content` holds so far is a
|
|
39
|
+
* draft, not the answer. Open-ended because the wire may grow values.
|
|
40
|
+
*
|
|
41
|
+
* Taken from the service's published message entity (the generated
|
|
42
|
+
* `genie-chat-service-v1` client in `wix-private/aria-native-apps`), where the
|
|
43
|
+
* native Aria app ends a turn ONLY on a polled `STATUS_COMPLETED`, and resumes a
|
|
44
|
+
* conversation whose newest assistant row is still pending. Not observed on
|
|
45
|
+
* this repo's own captures, which is why it is optional and every consumer
|
|
46
|
+
* must treat an absent status as "settled". */
|
|
47
|
+
export type MessageStatus = "STATUS_UNSPECIFIED" | "STATUS_PENDING" | "STATUS_COMPLETED" | (string & {});
|
|
37
48
|
export interface ConversationMessage {
|
|
38
49
|
id?: string;
|
|
39
50
|
content?: string;
|
|
40
51
|
role?: MessageRole;
|
|
41
52
|
createdDate?: string;
|
|
53
|
+
status?: MessageStatus;
|
|
42
54
|
}
|
|
43
55
|
/** Which assistant the conversation talks to. The real dashboard sends a
|
|
44
56
|
* `unifiedAssistantReference`; helper `ariaAssistantConfig()` builds it. */
|
package/dist/cli.mjs
CHANGED
|
@@ -5290,7 +5290,6 @@ function resolveTranslation(value, catalog, sourceCatalog) {
|
|
|
5290
5290
|
var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
|
|
5291
5291
|
var init_bindings = __esm({
|
|
5292
5292
|
"../../core/ts/src/bindings.ts"() {
|
|
5293
|
-
"use strict";
|
|
5294
5293
|
BINDING_SIGILS = ["service", "flag", "item", "t"];
|
|
5295
5294
|
BINDING_SIGIL_RE = new RegExp(
|
|
5296
5295
|
`^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
|
|
@@ -16696,11 +16696,17 @@
|
|
|
16696
16696
|
"jsdoc": "",
|
|
16697
16697
|
"decl": "export type CommunicationMode = \"UNKNOWN\" | \"POLLING\" | \"REAL_TIME\";"
|
|
16698
16698
|
},
|
|
16699
|
+
{
|
|
16700
|
+
"name": "MessageStatus",
|
|
16701
|
+
"kind": "type",
|
|
16702
|
+
"jsdoc": "/** Where the service is with one message. `STATUS_PENDING` on an ASSISTANT row\n * means the agent is still writing it: whatever `content` holds so far is a\n * draft, not the answer. Open-ended because the wire may grow values.\n *\n * Taken from the service's published message entity (the generated\n * `genie-chat-service-v1` client in `wix-private/aria-native-apps`), where the\n * native Aria app ends a turn ONLY on a polled `STATUS_COMPLETED`, and resumes a\n * conversation whose newest assistant row is still pending. Not observed on\n * this repo's own captures, which is why it is optional and every consumer\n * must treat an absent status as \"settled\". */",
|
|
16703
|
+
"decl": "export type MessageStatus = \"STATUS_UNSPECIFIED\" | \"STATUS_PENDING\" | \"STATUS_COMPLETED\" | (string & {});"
|
|
16704
|
+
},
|
|
16699
16705
|
{
|
|
16700
16706
|
"name": "ConversationMessage",
|
|
16701
16707
|
"kind": "interface",
|
|
16702
16708
|
"jsdoc": "",
|
|
16703
|
-
"decl": "export interface ConversationMessage {\n id?: string;\n content?: string;\n role?: MessageRole;\n createdDate?: string;\n}"
|
|
16709
|
+
"decl": "export interface ConversationMessage {\n id?: string;\n content?: string;\n role?: MessageRole;\n createdDate?: string;\n status?: MessageStatus;\n}"
|
|
16704
16710
|
},
|
|
16705
16711
|
{
|
|
16706
16712
|
"name": "AssistantConfig",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.972.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"artifactId": "himalaya-cli"
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
-
"falconPackageHash": "
|
|
67
|
+
"falconPackageHash": "9d8d2a86205140b8694ab07b45d09c35b40bfb375a3baf1f123ffacf"
|
|
68
68
|
}
|
|
@@ -585,7 +585,10 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
585
585
|
// way, and an aborted turn consists of nothing else. Then envelopes, then
|
|
586
586
|
// the markdown stripper — each stage handing on only what a reader should
|
|
587
587
|
// actually see.
|
|
588
|
-
|
|
588
|
+
// A row the service is still writing is scanned as STREAMING: an
|
|
589
|
+
// envelope whose `>>` has not arrived yet is held back rather than
|
|
590
|
+
// painted as raw JSON until the row finishes.
|
|
591
|
+
const prose = role === "USER" ? raw : proseOfParts(scanEnvelopes(stripHidden(raw), 0, !stillWriting(m)).parts);
|
|
589
592
|
return {
|
|
590
593
|
id: m.id || `m${i}`,
|
|
591
594
|
role,
|
|
@@ -689,6 +692,29 @@ function answerText(m: ConversationMessage): string {
|
|
|
689
692
|
return stripHidden(messageText(m)).trim();
|
|
690
693
|
}
|
|
691
694
|
|
|
695
|
+
/** Is the service still WRITING this assistant row?
|
|
696
|
+
*
|
|
697
|
+
* The chat-service stamps every message with a status, and an ASSISTANT row
|
|
698
|
+
* that is `STATUS_PENDING` holds a draft: the agent has started the reply and
|
|
699
|
+
* not finished it. Whatever text it carries is worth PAINTING — it is the only
|
|
700
|
+
* view of a reply in progress a worker has, since the words themselves stream
|
|
701
|
+
* over a socket no host exposes (see the genie-chat client's header) — but it
|
|
702
|
+
* is not the answer, and ending the turn on it is the bug this exists for: the
|
|
703
|
+
* typing line clears, Stop disappears while the agent is still writing, and the
|
|
704
|
+
* ladder drops to its idle rung, so the rest of the reply sits unseen for a
|
|
705
|
+
* whole idle interval. The native Aria app ends a turn only on
|
|
706
|
+
* `STATUS_COMPLETED`, for the same reason.
|
|
707
|
+
*
|
|
708
|
+
* Only an explicit PENDING counts. An absent or unrecognised status is
|
|
709
|
+
* "settled": no capture in this repo has ever shown the field, and a gate that
|
|
710
|
+
* demanded `STATUS_COMPLETED` would hang every turn on a service that simply
|
|
711
|
+
* does not send it. Callers apply it to ASSISTANT rows only: a USER row is
|
|
712
|
+
* counted to retire the optimistic bubble, and holding one back would paint
|
|
713
|
+
* the owner's words twice. */
|
|
714
|
+
function stillWriting(m: ConversationMessage): boolean {
|
|
715
|
+
return m.status === "STATUS_PENDING";
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean | null {
|
|
693
719
|
if (!turnId) return null;
|
|
694
720
|
const at = msgs.findIndex((m) => m.id === turnId);
|
|
@@ -701,7 +727,9 @@ export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean
|
|
|
701
727
|
for (let i = at + 1; i < msgs.length; i++) {
|
|
702
728
|
const m = msgs[i];
|
|
703
729
|
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
704
|
-
|
|
730
|
+
// A row still being written is not the answer yet — keep looking, since
|
|
731
|
+
// the finished answer can land in a later row of the same turn.
|
|
732
|
+
if (answerText(m) && !stillWriting(m)) return true;
|
|
705
733
|
}
|
|
706
734
|
return false;
|
|
707
735
|
}
|
|
@@ -736,6 +764,26 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
736
764
|
return n;
|
|
737
765
|
}
|
|
738
766
|
|
|
767
|
+
/** How many FINISHED assistant answers the transcript holds — `countTurns`
|
|
768
|
+
* minus the rows the service is still writing (`stillWriting`).
|
|
769
|
+
*
|
|
770
|
+
* The counted path compares THIS against a mark taken with `countTurns`, and
|
|
771
|
+
* the asymmetry is the point. The mark includes drafts, so a follow-up sent
|
|
772
|
+
* while an earlier reply is still being written is marked AFTER that reply;
|
|
773
|
+
* the earlier reply finishing then leaves this count equal to the mark rather
|
|
774
|
+
* than past it, and cannot end the new turn. Were the mark taken without
|
|
775
|
+
* drafts too, that finish would read as the new turn's answer. And a draft
|
|
776
|
+
* of the turn's OWN answer is not counted here, so it does not end the turn
|
|
777
|
+
* either — the same rule `repliedTo` applies on the exact path. */
|
|
778
|
+
export function countSettledAnswers(msgs: ConversationMessage[]): number {
|
|
779
|
+
let n = 0;
|
|
780
|
+
for (const m of msgs) {
|
|
781
|
+
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
782
|
+
if (answerText(m) && !stillWriting(m)) n++;
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
|
|
739
787
|
/** Has the turn stored under `userMessageId` already been answered?
|
|
740
788
|
*
|
|
741
789
|
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
@@ -785,6 +833,12 @@ export function turnAnswered(msgs: ConversationMessage[], userMessageId: string)
|
|
|
785
833
|
// exactly these two, and the two scanners read the same transcript: if they
|
|
786
834
|
// disagree, one of them ends the turn while the other leaves the ladder
|
|
787
835
|
// running at the fast cadence.
|
|
836
|
+
//
|
|
837
|
+
// They DO part on one case, deliberately: a row the service is still
|
|
838
|
+
// writing (`stillWriting`). That does not end a turn, but it IS proof the
|
|
839
|
+
// trigger landed — the agent is answering — so re-triggering the stored id
|
|
840
|
+
// here would have it answer the same turn twice. The ladder never reads
|
|
841
|
+
// this scanner, so the split cannot pin it fast.
|
|
788
842
|
if (role === "ASSISTANT" && answerText(m)) return true;
|
|
789
843
|
}
|
|
790
844
|
return false;
|
|
@@ -1155,7 +1209,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1155
1209
|
// both; the widget path did neither.
|
|
1156
1210
|
widget: carrySelection(
|
|
1157
1211
|
activeWidgetOf(
|
|
1158
|
-
|
|
1212
|
+
// A draft offers no controls: the agent has not finished asking, and
|
|
1213
|
+
// an answer tapped now would start a second turn on top of this one.
|
|
1214
|
+
// An empty newest row is NO_WIDGET — the placeholder rule.
|
|
1215
|
+
msgs.map((m) => ({ role: m.role, content: stillWriting(m) ? "" : stripHidden(messageText(m)) })),
|
|
1159
1216
|
disclosedLabel,
|
|
1160
1217
|
o.subtree,
|
|
1161
1218
|
(url) => isWixMediaHost(hostOf(url)),
|
|
@@ -1220,7 +1277,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1220
1277
|
// widget-only reply is, even though it leaves no bubble to inspect. Asking
|
|
1221
1278
|
// the last BUBBLE stalled this loop on exactly that reply, the same way it
|
|
1222
1279
|
// stalled `tick`.
|
|
1223
|
-
if (repliedTo(raw, s._turnId) ??
|
|
1280
|
+
if (repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter) {
|
|
1224
1281
|
return { ...s, status: "ready", _convId: convId, messages: bubbles, prompts: [], input: "", statusText: "", error: "", _pending: "", ...observed(raw, s.widget), ok: false };
|
|
1225
1282
|
}
|
|
1226
1283
|
}
|
|
@@ -1804,7 +1861,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1804
1861
|
// Counted over RAW messages, so a widget-only answer still ends the turn
|
|
1805
1862
|
// instead of pinning the ladder to its fast rung forever (`countTurns`).
|
|
1806
1863
|
// Exact when the service named the turn, counted when it did not.
|
|
1807
|
-
const replied = repliedTo(raw, s._turnId) ??
|
|
1864
|
+
const replied = repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter;
|
|
1808
1865
|
const next: AriaChatState = {
|
|
1809
1866
|
...s,
|
|
1810
1867
|
status: replied ? "ready" : s.status,
|
|
@@ -585,7 +585,10 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
585
585
|
// way, and an aborted turn consists of nothing else. Then envelopes, then
|
|
586
586
|
// the markdown stripper — each stage handing on only what a reader should
|
|
587
587
|
// actually see.
|
|
588
|
-
|
|
588
|
+
// A row the service is still writing is scanned as STREAMING: an
|
|
589
|
+
// envelope whose `>>` has not arrived yet is held back rather than
|
|
590
|
+
// painted as raw JSON until the row finishes.
|
|
591
|
+
const prose = role === "USER" ? raw : proseOfParts(scanEnvelopes(stripHidden(raw), 0, !stillWriting(m)).parts);
|
|
589
592
|
return {
|
|
590
593
|
id: m.id || `m${i}`,
|
|
591
594
|
role,
|
|
@@ -689,6 +692,29 @@ function answerText(m: ConversationMessage): string {
|
|
|
689
692
|
return stripHidden(messageText(m)).trim();
|
|
690
693
|
}
|
|
691
694
|
|
|
695
|
+
/** Is the service still WRITING this assistant row?
|
|
696
|
+
*
|
|
697
|
+
* The chat-service stamps every message with a status, and an ASSISTANT row
|
|
698
|
+
* that is `STATUS_PENDING` holds a draft: the agent has started the reply and
|
|
699
|
+
* not finished it. Whatever text it carries is worth PAINTING — it is the only
|
|
700
|
+
* view of a reply in progress a worker has, since the words themselves stream
|
|
701
|
+
* over a socket no host exposes (see the genie-chat client's header) — but it
|
|
702
|
+
* is not the answer, and ending the turn on it is the bug this exists for: the
|
|
703
|
+
* typing line clears, Stop disappears while the agent is still writing, and the
|
|
704
|
+
* ladder drops to its idle rung, so the rest of the reply sits unseen for a
|
|
705
|
+
* whole idle interval. The native Aria app ends a turn only on
|
|
706
|
+
* `STATUS_COMPLETED`, for the same reason.
|
|
707
|
+
*
|
|
708
|
+
* Only an explicit PENDING counts. An absent or unrecognised status is
|
|
709
|
+
* "settled": no capture in this repo has ever shown the field, and a gate that
|
|
710
|
+
* demanded `STATUS_COMPLETED` would hang every turn on a service that simply
|
|
711
|
+
* does not send it. Callers apply it to ASSISTANT rows only: a USER row is
|
|
712
|
+
* counted to retire the optimistic bubble, and holding one back would paint
|
|
713
|
+
* the owner's words twice. */
|
|
714
|
+
function stillWriting(m: ConversationMessage): boolean {
|
|
715
|
+
return m.status === "STATUS_PENDING";
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean | null {
|
|
693
719
|
if (!turnId) return null;
|
|
694
720
|
const at = msgs.findIndex((m) => m.id === turnId);
|
|
@@ -701,7 +727,9 @@ export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean
|
|
|
701
727
|
for (let i = at + 1; i < msgs.length; i++) {
|
|
702
728
|
const m = msgs[i];
|
|
703
729
|
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
704
|
-
|
|
730
|
+
// A row still being written is not the answer yet — keep looking, since
|
|
731
|
+
// the finished answer can land in a later row of the same turn.
|
|
732
|
+
if (answerText(m) && !stillWriting(m)) return true;
|
|
705
733
|
}
|
|
706
734
|
return false;
|
|
707
735
|
}
|
|
@@ -736,6 +764,26 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
736
764
|
return n;
|
|
737
765
|
}
|
|
738
766
|
|
|
767
|
+
/** How many FINISHED assistant answers the transcript holds — `countTurns`
|
|
768
|
+
* minus the rows the service is still writing (`stillWriting`).
|
|
769
|
+
*
|
|
770
|
+
* The counted path compares THIS against a mark taken with `countTurns`, and
|
|
771
|
+
* the asymmetry is the point. The mark includes drafts, so a follow-up sent
|
|
772
|
+
* while an earlier reply is still being written is marked AFTER that reply;
|
|
773
|
+
* the earlier reply finishing then leaves this count equal to the mark rather
|
|
774
|
+
* than past it, and cannot end the new turn. Were the mark taken without
|
|
775
|
+
* drafts too, that finish would read as the new turn's answer. And a draft
|
|
776
|
+
* of the turn's OWN answer is not counted here, so it does not end the turn
|
|
777
|
+
* either — the same rule `repliedTo` applies on the exact path. */
|
|
778
|
+
export function countSettledAnswers(msgs: ConversationMessage[]): number {
|
|
779
|
+
let n = 0;
|
|
780
|
+
for (const m of msgs) {
|
|
781
|
+
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
782
|
+
if (answerText(m) && !stillWriting(m)) n++;
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
|
|
739
787
|
/** Has the turn stored under `userMessageId` already been answered?
|
|
740
788
|
*
|
|
741
789
|
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
@@ -785,6 +833,12 @@ export function turnAnswered(msgs: ConversationMessage[], userMessageId: string)
|
|
|
785
833
|
// exactly these two, and the two scanners read the same transcript: if they
|
|
786
834
|
// disagree, one of them ends the turn while the other leaves the ladder
|
|
787
835
|
// running at the fast cadence.
|
|
836
|
+
//
|
|
837
|
+
// They DO part on one case, deliberately: a row the service is still
|
|
838
|
+
// writing (`stillWriting`). That does not end a turn, but it IS proof the
|
|
839
|
+
// trigger landed — the agent is answering — so re-triggering the stored id
|
|
840
|
+
// here would have it answer the same turn twice. The ladder never reads
|
|
841
|
+
// this scanner, so the split cannot pin it fast.
|
|
788
842
|
if (role === "ASSISTANT" && answerText(m)) return true;
|
|
789
843
|
}
|
|
790
844
|
return false;
|
|
@@ -1155,7 +1209,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1155
1209
|
// both; the widget path did neither.
|
|
1156
1210
|
widget: carrySelection(
|
|
1157
1211
|
activeWidgetOf(
|
|
1158
|
-
|
|
1212
|
+
// A draft offers no controls: the agent has not finished asking, and
|
|
1213
|
+
// an answer tapped now would start a second turn on top of this one.
|
|
1214
|
+
// An empty newest row is NO_WIDGET — the placeholder rule.
|
|
1215
|
+
msgs.map((m) => ({ role: m.role, content: stillWriting(m) ? "" : stripHidden(messageText(m)) })),
|
|
1159
1216
|
disclosedLabel,
|
|
1160
1217
|
o.subtree,
|
|
1161
1218
|
(url) => isWixMediaHost(hostOf(url)),
|
|
@@ -1220,7 +1277,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1220
1277
|
// widget-only reply is, even though it leaves no bubble to inspect. Asking
|
|
1221
1278
|
// the last BUBBLE stalled this loop on exactly that reply, the same way it
|
|
1222
1279
|
// stalled `tick`.
|
|
1223
|
-
if (repliedTo(raw, s._turnId) ??
|
|
1280
|
+
if (repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter) {
|
|
1224
1281
|
return { ...s, status: "ready", _convId: convId, messages: bubbles, prompts: [], input: "", statusText: "", error: "", _pending: "", ...observed(raw, s.widget), ok: false };
|
|
1225
1282
|
}
|
|
1226
1283
|
}
|
|
@@ -1804,7 +1861,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1804
1861
|
// Counted over RAW messages, so a widget-only answer still ends the turn
|
|
1805
1862
|
// instead of pinning the ladder to its fast rung forever (`countTurns`).
|
|
1806
1863
|
// Exact when the service named the turn, counted when it did not.
|
|
1807
|
-
const replied = repliedTo(raw, s._turnId) ??
|
|
1864
|
+
const replied = repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter;
|
|
1808
1865
|
const next: AriaChatState = {
|
|
1809
1866
|
...s,
|
|
1810
1867
|
status: replied ? "ready" : s.status,
|
|
@@ -585,7 +585,10 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
585
585
|
// way, and an aborted turn consists of nothing else. Then envelopes, then
|
|
586
586
|
// the markdown stripper — each stage handing on only what a reader should
|
|
587
587
|
// actually see.
|
|
588
|
-
|
|
588
|
+
// A row the service is still writing is scanned as STREAMING: an
|
|
589
|
+
// envelope whose `>>` has not arrived yet is held back rather than
|
|
590
|
+
// painted as raw JSON until the row finishes.
|
|
591
|
+
const prose = role === "USER" ? raw : proseOfParts(scanEnvelopes(stripHidden(raw), 0, !stillWriting(m)).parts);
|
|
589
592
|
return {
|
|
590
593
|
id: m.id || `m${i}`,
|
|
591
594
|
role,
|
|
@@ -689,6 +692,29 @@ function answerText(m: ConversationMessage): string {
|
|
|
689
692
|
return stripHidden(messageText(m)).trim();
|
|
690
693
|
}
|
|
691
694
|
|
|
695
|
+
/** Is the service still WRITING this assistant row?
|
|
696
|
+
*
|
|
697
|
+
* The chat-service stamps every message with a status, and an ASSISTANT row
|
|
698
|
+
* that is `STATUS_PENDING` holds a draft: the agent has started the reply and
|
|
699
|
+
* not finished it. Whatever text it carries is worth PAINTING — it is the only
|
|
700
|
+
* view of a reply in progress a worker has, since the words themselves stream
|
|
701
|
+
* over a socket no host exposes (see the genie-chat client's header) — but it
|
|
702
|
+
* is not the answer, and ending the turn on it is the bug this exists for: the
|
|
703
|
+
* typing line clears, Stop disappears while the agent is still writing, and the
|
|
704
|
+
* ladder drops to its idle rung, so the rest of the reply sits unseen for a
|
|
705
|
+
* whole idle interval. The native Aria app ends a turn only on
|
|
706
|
+
* `STATUS_COMPLETED`, for the same reason.
|
|
707
|
+
*
|
|
708
|
+
* Only an explicit PENDING counts. An absent or unrecognised status is
|
|
709
|
+
* "settled": no capture in this repo has ever shown the field, and a gate that
|
|
710
|
+
* demanded `STATUS_COMPLETED` would hang every turn on a service that simply
|
|
711
|
+
* does not send it. Callers apply it to ASSISTANT rows only: a USER row is
|
|
712
|
+
* counted to retire the optimistic bubble, and holding one back would paint
|
|
713
|
+
* the owner's words twice. */
|
|
714
|
+
function stillWriting(m: ConversationMessage): boolean {
|
|
715
|
+
return m.status === "STATUS_PENDING";
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean | null {
|
|
693
719
|
if (!turnId) return null;
|
|
694
720
|
const at = msgs.findIndex((m) => m.id === turnId);
|
|
@@ -701,7 +727,9 @@ export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean
|
|
|
701
727
|
for (let i = at + 1; i < msgs.length; i++) {
|
|
702
728
|
const m = msgs[i];
|
|
703
729
|
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
704
|
-
|
|
730
|
+
// A row still being written is not the answer yet — keep looking, since
|
|
731
|
+
// the finished answer can land in a later row of the same turn.
|
|
732
|
+
if (answerText(m) && !stillWriting(m)) return true;
|
|
705
733
|
}
|
|
706
734
|
return false;
|
|
707
735
|
}
|
|
@@ -736,6 +764,26 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
736
764
|
return n;
|
|
737
765
|
}
|
|
738
766
|
|
|
767
|
+
/** How many FINISHED assistant answers the transcript holds — `countTurns`
|
|
768
|
+
* minus the rows the service is still writing (`stillWriting`).
|
|
769
|
+
*
|
|
770
|
+
* The counted path compares THIS against a mark taken with `countTurns`, and
|
|
771
|
+
* the asymmetry is the point. The mark includes drafts, so a follow-up sent
|
|
772
|
+
* while an earlier reply is still being written is marked AFTER that reply;
|
|
773
|
+
* the earlier reply finishing then leaves this count equal to the mark rather
|
|
774
|
+
* than past it, and cannot end the new turn. Were the mark taken without
|
|
775
|
+
* drafts too, that finish would read as the new turn's answer. And a draft
|
|
776
|
+
* of the turn's OWN answer is not counted here, so it does not end the turn
|
|
777
|
+
* either — the same rule `repliedTo` applies on the exact path. */
|
|
778
|
+
export function countSettledAnswers(msgs: ConversationMessage[]): number {
|
|
779
|
+
let n = 0;
|
|
780
|
+
for (const m of msgs) {
|
|
781
|
+
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
782
|
+
if (answerText(m) && !stillWriting(m)) n++;
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
|
|
739
787
|
/** Has the turn stored under `userMessageId` already been answered?
|
|
740
788
|
*
|
|
741
789
|
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
@@ -785,6 +833,12 @@ export function turnAnswered(msgs: ConversationMessage[], userMessageId: string)
|
|
|
785
833
|
// exactly these two, and the two scanners read the same transcript: if they
|
|
786
834
|
// disagree, one of them ends the turn while the other leaves the ladder
|
|
787
835
|
// running at the fast cadence.
|
|
836
|
+
//
|
|
837
|
+
// They DO part on one case, deliberately: a row the service is still
|
|
838
|
+
// writing (`stillWriting`). That does not end a turn, but it IS proof the
|
|
839
|
+
// trigger landed — the agent is answering — so re-triggering the stored id
|
|
840
|
+
// here would have it answer the same turn twice. The ladder never reads
|
|
841
|
+
// this scanner, so the split cannot pin it fast.
|
|
788
842
|
if (role === "ASSISTANT" && answerText(m)) return true;
|
|
789
843
|
}
|
|
790
844
|
return false;
|
|
@@ -1155,7 +1209,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1155
1209
|
// both; the widget path did neither.
|
|
1156
1210
|
widget: carrySelection(
|
|
1157
1211
|
activeWidgetOf(
|
|
1158
|
-
|
|
1212
|
+
// A draft offers no controls: the agent has not finished asking, and
|
|
1213
|
+
// an answer tapped now would start a second turn on top of this one.
|
|
1214
|
+
// An empty newest row is NO_WIDGET — the placeholder rule.
|
|
1215
|
+
msgs.map((m) => ({ role: m.role, content: stillWriting(m) ? "" : stripHidden(messageText(m)) })),
|
|
1159
1216
|
disclosedLabel,
|
|
1160
1217
|
o.subtree,
|
|
1161
1218
|
(url) => isWixMediaHost(hostOf(url)),
|
|
@@ -1220,7 +1277,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1220
1277
|
// widget-only reply is, even though it leaves no bubble to inspect. Asking
|
|
1221
1278
|
// the last BUBBLE stalled this loop on exactly that reply, the same way it
|
|
1222
1279
|
// stalled `tick`.
|
|
1223
|
-
if (repliedTo(raw, s._turnId) ??
|
|
1280
|
+
if (repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter) {
|
|
1224
1281
|
return { ...s, status: "ready", _convId: convId, messages: bubbles, prompts: [], input: "", statusText: "", error: "", _pending: "", ...observed(raw, s.widget), ok: false };
|
|
1225
1282
|
}
|
|
1226
1283
|
}
|
|
@@ -1804,7 +1861,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1804
1861
|
// Counted over RAW messages, so a widget-only answer still ends the turn
|
|
1805
1862
|
// instead of pinning the ladder to its fast rung forever (`countTurns`).
|
|
1806
1863
|
// Exact when the service named the turn, counted when it did not.
|
|
1807
|
-
const replied = repliedTo(raw, s._turnId) ??
|
|
1864
|
+
const replied = repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter;
|
|
1808
1865
|
const next: AriaChatState = {
|
|
1809
1866
|
...s,
|
|
1810
1867
|
status: replied ? "ready" : s.status,
|
|
@@ -585,7 +585,10 @@ export function toBubbles(msgs: ConversationMessage[], assistantName: string, yo
|
|
|
585
585
|
// way, and an aborted turn consists of nothing else. Then envelopes, then
|
|
586
586
|
// the markdown stripper — each stage handing on only what a reader should
|
|
587
587
|
// actually see.
|
|
588
|
-
|
|
588
|
+
// A row the service is still writing is scanned as STREAMING: an
|
|
589
|
+
// envelope whose `>>` has not arrived yet is held back rather than
|
|
590
|
+
// painted as raw JSON until the row finishes.
|
|
591
|
+
const prose = role === "USER" ? raw : proseOfParts(scanEnvelopes(stripHidden(raw), 0, !stillWriting(m)).parts);
|
|
589
592
|
return {
|
|
590
593
|
id: m.id || `m${i}`,
|
|
591
594
|
role,
|
|
@@ -689,6 +692,29 @@ function answerText(m: ConversationMessage): string {
|
|
|
689
692
|
return stripHidden(messageText(m)).trim();
|
|
690
693
|
}
|
|
691
694
|
|
|
695
|
+
/** Is the service still WRITING this assistant row?
|
|
696
|
+
*
|
|
697
|
+
* The chat-service stamps every message with a status, and an ASSISTANT row
|
|
698
|
+
* that is `STATUS_PENDING` holds a draft: the agent has started the reply and
|
|
699
|
+
* not finished it. Whatever text it carries is worth PAINTING — it is the only
|
|
700
|
+
* view of a reply in progress a worker has, since the words themselves stream
|
|
701
|
+
* over a socket no host exposes (see the genie-chat client's header) — but it
|
|
702
|
+
* is not the answer, and ending the turn on it is the bug this exists for: the
|
|
703
|
+
* typing line clears, Stop disappears while the agent is still writing, and the
|
|
704
|
+
* ladder drops to its idle rung, so the rest of the reply sits unseen for a
|
|
705
|
+
* whole idle interval. The native Aria app ends a turn only on
|
|
706
|
+
* `STATUS_COMPLETED`, for the same reason.
|
|
707
|
+
*
|
|
708
|
+
* Only an explicit PENDING counts. An absent or unrecognised status is
|
|
709
|
+
* "settled": no capture in this repo has ever shown the field, and a gate that
|
|
710
|
+
* demanded `STATUS_COMPLETED` would hang every turn on a service that simply
|
|
711
|
+
* does not send it. Callers apply it to ASSISTANT rows only: a USER row is
|
|
712
|
+
* counted to retire the optimistic bubble, and holding one back would paint
|
|
713
|
+
* the owner's words twice. */
|
|
714
|
+
function stillWriting(m: ConversationMessage): boolean {
|
|
715
|
+
return m.status === "STATUS_PENDING";
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean | null {
|
|
693
719
|
if (!turnId) return null;
|
|
694
720
|
const at = msgs.findIndex((m) => m.id === turnId);
|
|
@@ -701,7 +727,9 @@ export function repliedTo(msgs: ConversationMessage[], turnId: string): boolean
|
|
|
701
727
|
for (let i = at + 1; i < msgs.length; i++) {
|
|
702
728
|
const m = msgs[i];
|
|
703
729
|
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
704
|
-
|
|
730
|
+
// A row still being written is not the answer yet — keep looking, since
|
|
731
|
+
// the finished answer can land in a later row of the same turn.
|
|
732
|
+
if (answerText(m) && !stillWriting(m)) return true;
|
|
705
733
|
}
|
|
706
734
|
return false;
|
|
707
735
|
}
|
|
@@ -736,6 +764,26 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
736
764
|
return n;
|
|
737
765
|
}
|
|
738
766
|
|
|
767
|
+
/** How many FINISHED assistant answers the transcript holds — `countTurns`
|
|
768
|
+
* minus the rows the service is still writing (`stillWriting`).
|
|
769
|
+
*
|
|
770
|
+
* The counted path compares THIS against a mark taken with `countTurns`, and
|
|
771
|
+
* the asymmetry is the point. The mark includes drafts, so a follow-up sent
|
|
772
|
+
* while an earlier reply is still being written is marked AFTER that reply;
|
|
773
|
+
* the earlier reply finishing then leaves this count equal to the mark rather
|
|
774
|
+
* than past it, and cannot end the new turn. Were the mark taken without
|
|
775
|
+
* drafts too, that finish would read as the new turn's answer. And a draft
|
|
776
|
+
* of the turn's OWN answer is not counted here, so it does not end the turn
|
|
777
|
+
* either — the same rule `repliedTo` applies on the exact path. */
|
|
778
|
+
export function countSettledAnswers(msgs: ConversationMessage[]): number {
|
|
779
|
+
let n = 0;
|
|
780
|
+
for (const m of msgs) {
|
|
781
|
+
if ((m.role || "ASSISTANT") !== "ASSISTANT") continue;
|
|
782
|
+
if (answerText(m) && !stillWriting(m)) n++;
|
|
783
|
+
}
|
|
784
|
+
return n;
|
|
785
|
+
}
|
|
786
|
+
|
|
739
787
|
/** Has the turn stored under `userMessageId` already been answered?
|
|
740
788
|
*
|
|
741
789
|
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
@@ -785,6 +833,12 @@ export function turnAnswered(msgs: ConversationMessage[], userMessageId: string)
|
|
|
785
833
|
// exactly these two, and the two scanners read the same transcript: if they
|
|
786
834
|
// disagree, one of them ends the turn while the other leaves the ladder
|
|
787
835
|
// running at the fast cadence.
|
|
836
|
+
//
|
|
837
|
+
// They DO part on one case, deliberately: a row the service is still
|
|
838
|
+
// writing (`stillWriting`). That does not end a turn, but it IS proof the
|
|
839
|
+
// trigger landed — the agent is answering — so re-triggering the stored id
|
|
840
|
+
// here would have it answer the same turn twice. The ladder never reads
|
|
841
|
+
// this scanner, so the split cannot pin it fast.
|
|
788
842
|
if (role === "ASSISTANT" && answerText(m)) return true;
|
|
789
843
|
}
|
|
790
844
|
return false;
|
|
@@ -1155,7 +1209,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1155
1209
|
// both; the widget path did neither.
|
|
1156
1210
|
widget: carrySelection(
|
|
1157
1211
|
activeWidgetOf(
|
|
1158
|
-
|
|
1212
|
+
// A draft offers no controls: the agent has not finished asking, and
|
|
1213
|
+
// an answer tapped now would start a second turn on top of this one.
|
|
1214
|
+
// An empty newest row is NO_WIDGET — the placeholder rule.
|
|
1215
|
+
msgs.map((m) => ({ role: m.role, content: stillWriting(m) ? "" : stripHidden(messageText(m)) })),
|
|
1159
1216
|
disclosedLabel,
|
|
1160
1217
|
o.subtree,
|
|
1161
1218
|
(url) => isWixMediaHost(hostOf(url)),
|
|
@@ -1220,7 +1277,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1220
1277
|
// widget-only reply is, even though it leaves no bubble to inspect. Asking
|
|
1221
1278
|
// the last BUBBLE stalled this loop on exactly that reply, the same way it
|
|
1222
1279
|
// stalled `tick`.
|
|
1223
|
-
if (repliedTo(raw, s._turnId) ??
|
|
1280
|
+
if (repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter) {
|
|
1224
1281
|
return { ...s, status: "ready", _convId: convId, messages: bubbles, prompts: [], input: "", statusText: "", error: "", _pending: "", ...observed(raw, s.widget), ok: false };
|
|
1225
1282
|
}
|
|
1226
1283
|
}
|
|
@@ -1804,7 +1861,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1804
1861
|
// Counted over RAW messages, so a widget-only answer still ends the turn
|
|
1805
1862
|
// instead of pinning the ladder to its fast rung forever (`countTurns`).
|
|
1806
1863
|
// Exact when the service named the turn, counted when it did not.
|
|
1807
|
-
const replied = repliedTo(raw, s._turnId) ??
|
|
1864
|
+
const replied = repliedTo(raw, s._turnId) ?? countSettledAnswers(raw) > s._replyAfter;
|
|
1808
1865
|
const next: AriaChatState = {
|
|
1809
1866
|
...s,
|
|
1810
1867
|
status: replied ? "ready" : s.status,
|