@fiodos/web-core 0.1.18 → 0.1.20
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/cjs/api/backendClient.js +7 -1
- package/dist/cjs/api/backendTelemetry.js +15 -0
- package/dist/cjs/config/types.d.ts +9 -3
- package/dist/cjs/config/types.js +1 -1
- package/dist/cjs/controller/AgentController.d.ts +13 -0
- package/dist/cjs/controller/AgentController.js +48 -0
- package/dist/cjs/core/turnEngine.d.ts +6 -0
- package/dist/cjs/core/turnEngine.js +2 -2
- package/dist/cjs/embed/global.d.ts +1 -1
- package/dist/cjs/orb/mountOrb.js +42 -8
- package/dist/cjs/speech/bubbleDictation.js +27 -3
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +6 -4
- package/dist/esm/api/backendClient.js +7 -1
- package/dist/esm/api/backendTelemetry.js +15 -0
- package/dist/esm/config/types.d.ts +9 -3
- package/dist/esm/config/types.js +1 -1
- package/dist/esm/controller/AgentController.d.ts +13 -0
- package/dist/esm/controller/AgentController.js +48 -0
- package/dist/esm/core/turnEngine.d.ts +6 -0
- package/dist/esm/core/turnEngine.js +2 -2
- package/dist/esm/embed/global.d.ts +1 -1
- package/dist/esm/orb/mountOrb.js +43 -9
- package/dist/esm/speech/bubbleDictation.js +27 -3
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -57,7 +57,13 @@ async function errorFromResponse(res) {
|
|
|
57
57
|
return new AgentApiError('unauthorized', detail, res.status);
|
|
58
58
|
}
|
|
59
59
|
if (res.status === 429) {
|
|
60
|
-
|
|
60
|
+
// The backend marks plan/budget 429s with error:"quota_exceeded" (rate
|
|
61
|
+
// limits carry error:"rate_limited"); the detail/remaining checks cover
|
|
62
|
+
// older backends. Getting this right matters: a quota 429 shown as "too
|
|
63
|
+
// many requests" sends the developer chasing rate limits that are fine.
|
|
64
|
+
const quotaSignal = body.error === 'quota_exceeded' ||
|
|
65
|
+
typeof body.remaining === 'number' ||
|
|
66
|
+
(detail ?? '').toLowerCase().includes('quota');
|
|
61
67
|
return new AgentApiError(quotaSignal ? 'quota_exceeded' : 'rate_limited', detail, res.status);
|
|
62
68
|
}
|
|
63
69
|
if (res.status === 404)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Bound on the technical error string shipped with action_failed events. */
|
|
2
|
+
const MAX_ERROR_DETAIL_CHARS = 200;
|
|
1
3
|
function mapEvent(params) {
|
|
2
4
|
const intent = params.intentDetected ?? null;
|
|
3
5
|
const sessionId = params.sessionId ?? null;
|
|
@@ -13,6 +15,19 @@ function mapEvent(params) {
|
|
|
13
15
|
}
|
|
14
16
|
return events;
|
|
15
17
|
}
|
|
18
|
+
// Failures are the events that make field reports diagnosable: without
|
|
19
|
+
// them, "the orb said it couldn't do it" leaves the developer blind. Only
|
|
20
|
+
// the technical error string travels (HTTP status + path, template
|
|
21
|
+
// param…), never any message content.
|
|
22
|
+
case 'action_failed':
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
type: 'action_failed',
|
|
26
|
+
intent,
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
error: (params.errorDetail ?? '').slice(0, MAX_ERROR_DETAIL_CHARS) || null,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
16
31
|
case 'action_confirmation_requested':
|
|
17
32
|
return [{ type: 'confirmation_requested', intent, session_id: sessionId }];
|
|
18
33
|
default:
|
|
@@ -18,14 +18,20 @@ export interface AgentTimings {
|
|
|
18
18
|
}
|
|
19
19
|
export declare const DEFAULT_AGENT_TIMINGS: AgentTimings;
|
|
20
20
|
/**
|
|
21
|
-
* Autonomous multi-step chaining.
|
|
22
|
-
* behaves exactly as before (one action per turn). When ON, after a successful
|
|
21
|
+
* Autonomous multi-step chaining. ON by default: after a successful
|
|
23
22
|
* navigate/execute whose backend `task_status` is "in_progress", the orb takes
|
|
24
23
|
* the next step on its own — capped at `maxSteps` and guarded by no-progress
|
|
25
24
|
* detection and sensitive-confirmation pauses (which always require the human).
|
|
25
|
+
*
|
|
26
|
+
* Default ON is a product invariant, not a tuning choice: data actions
|
|
27
|
+
* (search, get-cart…) are INVISIBLE — without the continuation turn the orb
|
|
28
|
+
* runs them and then goes silent ("I'll look for products…" and nothing
|
|
29
|
+
* happens). Every install (embeds included, which pass no config) must get
|
|
30
|
+
* the full act→observe→act loop. Pass `chaining: { enabled: false }` to opt
|
|
31
|
+
* out and return to one-action-per-turn.
|
|
26
32
|
*/
|
|
27
33
|
export interface AgentChaining {
|
|
28
|
-
/** Master switch. Default
|
|
34
|
+
/** Master switch. Default true (the agent completes multi-step tasks). */
|
|
29
35
|
enabled: boolean;
|
|
30
36
|
/** Hard cap on steps per user request (safety). Default 6. */
|
|
31
37
|
maxSteps: number;
|
package/dist/esm/config/types.js
CHANGED
|
@@ -104,6 +104,12 @@ export declare class AgentController {
|
|
|
104
104
|
private pendingConsentIntent;
|
|
105
105
|
/** Parked chain state (set when a chain pauses on a sensitive action). */
|
|
106
106
|
private chainState;
|
|
107
|
+
/**
|
|
108
|
+
* Intent whose showcase escort was DEFERRED mid-chain (suppressEscort):
|
|
109
|
+
* flushed when the chain ends so the user is walked to where the last
|
|
110
|
+
* action's effect is visible — never in the middle of a running chain.
|
|
111
|
+
*/
|
|
112
|
+
private deferredEscortIntent;
|
|
107
113
|
/** REAL live activity for the panel (replaces the old canned walk-through). */
|
|
108
114
|
private liveActivity;
|
|
109
115
|
private chainActive;
|
|
@@ -221,6 +227,13 @@ export declare class AgentController {
|
|
|
221
227
|
* maxSteps, with no-progress detection and sensitive-confirmation pauses.
|
|
222
228
|
*/
|
|
223
229
|
private runChainLoop;
|
|
230
|
+
/**
|
|
231
|
+
* Runs the showcase escort that was deferred while the chain was executing
|
|
232
|
+
* (see suppressEscort in applyOutcome). Skipped while a confirmation is
|
|
233
|
+
* pending: the chain may resume after the user answers, and the confirmed
|
|
234
|
+
* action escorts on its own.
|
|
235
|
+
*/
|
|
236
|
+
private flushDeferredEscort;
|
|
224
237
|
private handleTranscript;
|
|
225
238
|
private onTranscript;
|
|
226
239
|
private onSpeechError;
|
|
@@ -60,6 +60,12 @@ export class AgentController {
|
|
|
60
60
|
this.pendingConsentIntent = null;
|
|
61
61
|
/** Parked chain state (set when a chain pauses on a sensitive action). */
|
|
62
62
|
this.chainState = null;
|
|
63
|
+
/**
|
|
64
|
+
* Intent whose showcase escort was DEFERRED mid-chain (suppressEscort):
|
|
65
|
+
* flushed when the chain ends so the user is walked to where the last
|
|
66
|
+
* action's effect is visible — never in the middle of a running chain.
|
|
67
|
+
*/
|
|
68
|
+
this.deferredEscortIntent = null;
|
|
63
69
|
/** REAL live activity for the panel (replaces the old canned walk-through). */
|
|
64
70
|
this.liveActivity = null;
|
|
65
71
|
this.chainActive = false;
|
|
@@ -485,7 +491,11 @@ export class AgentController {
|
|
|
485
491
|
intentDetected: pending.intent,
|
|
486
492
|
sessionId: this.sessionId,
|
|
487
493
|
actionExecuted: { intent: pending.intent, confirmed: true },
|
|
494
|
+
...(!result.success ? { errorDetail: result.error ?? null } : {}),
|
|
488
495
|
});
|
|
496
|
+
if (!result.success) {
|
|
497
|
+
console.warn(`[fyodos] action "${pending.intent}" failed: ${result.error ?? 'unknown error'}`);
|
|
498
|
+
}
|
|
489
499
|
if (result.message) {
|
|
490
500
|
if (this.lastExchange)
|
|
491
501
|
this.lastExchange = { ...this.lastExchange, replyText: result.message };
|
|
@@ -539,6 +549,7 @@ export class AgentController {
|
|
|
539
549
|
// Declining a sensitive action stops any chain it was part of, cleanly.
|
|
540
550
|
const chainState = this.chainState;
|
|
541
551
|
this.chainState = null;
|
|
552
|
+
this.deferredEscortIntent = null;
|
|
542
553
|
if (chainState) {
|
|
543
554
|
this.telemetry.logEvent({
|
|
544
555
|
eventType: 'agent_chain_aborted',
|
|
@@ -658,6 +669,10 @@ export class AgentController {
|
|
|
658
669
|
let outAudio = turn.audioBase64;
|
|
659
670
|
if (outReply)
|
|
660
671
|
this.serverTtsUnavailable = !outAudio;
|
|
672
|
+
// Mid-chain: hold the showcase escort. On multi-page hosts (Shopify
|
|
673
|
+
// embeds) the escort is a full page load that would kill the chain
|
|
674
|
+
// before its continuation turn; the chain escorts once when it ends.
|
|
675
|
+
const escortDeferred = this.chaining.enabled && turn.taskStatus === 'in_progress';
|
|
661
676
|
const decision = await decideAction({
|
|
662
677
|
action: turn.action,
|
|
663
678
|
manifest: this.config.manifest,
|
|
@@ -665,6 +680,7 @@ export class AgentController {
|
|
|
665
680
|
context: snapshotContext,
|
|
666
681
|
messages: this.messages,
|
|
667
682
|
userMessage,
|
|
683
|
+
suppressEscort: escortDeferred,
|
|
668
684
|
});
|
|
669
685
|
let canContinue = false;
|
|
670
686
|
let lastStep;
|
|
@@ -707,6 +723,7 @@ export class AgentController {
|
|
|
707
723
|
intentDetected: turn.action.intent,
|
|
708
724
|
sessionId: this.sessionId,
|
|
709
725
|
actionExecuted: { intent: turn.action.intent, confirmed: false },
|
|
726
|
+
...(!r.success && !r.recoverable ? { errorDetail: r.error ?? null } : {}),
|
|
710
727
|
});
|
|
711
728
|
}
|
|
712
729
|
if (r.recoverable) {
|
|
@@ -729,12 +746,27 @@ export class AgentController {
|
|
|
729
746
|
outReply = this.messages.actionFailed;
|
|
730
747
|
outAudio = null;
|
|
731
748
|
}
|
|
749
|
+
if (!r.success) {
|
|
750
|
+
// Developer signal: the user only hears a generic failure phrase; the
|
|
751
|
+
// TECHNICAL cause (HTTP status + path, missing template param…) must
|
|
752
|
+
// be findable in the console or every field report is undebuggable.
|
|
753
|
+
console.warn(`[fyodos] action "${turn.action?.intent ?? '?'}" failed: ${r.error ?? 'unknown error'}`);
|
|
754
|
+
}
|
|
732
755
|
// Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
|
|
733
756
|
// and only when the backend asked to continue (task_status=in_progress).
|
|
734
757
|
if (turn.action &&
|
|
735
758
|
(turn.action.type === 'navigate' || turn.action.type === 'execute') &&
|
|
736
759
|
r.success &&
|
|
737
760
|
!r.recoverable) {
|
|
761
|
+
// Deferred-escort bookkeeping: remember the intent whose escort was
|
|
762
|
+
// held (flushed at chain end); a navigate — or an execute whose escort
|
|
763
|
+
// already ran inside the executor — supersedes any earlier deferral.
|
|
764
|
+
if (turn.action.type === 'execute') {
|
|
765
|
+
this.deferredEscortIntent = escortDeferred ? (turn.action.intent ?? null) : null;
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
this.deferredEscortIntent = null;
|
|
769
|
+
}
|
|
738
770
|
const intent = turn.action.intent ?? '';
|
|
739
771
|
// Feed the step's RESULT DATA back to the next chain turn (bounded):
|
|
740
772
|
// it is how the model reads real technical values (variant ids, line
|
|
@@ -938,8 +970,23 @@ export class AgentController {
|
|
|
938
970
|
}
|
|
939
971
|
finally {
|
|
940
972
|
this.setChainActive(false);
|
|
973
|
+
this.flushDeferredEscort();
|
|
941
974
|
}
|
|
942
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Runs the showcase escort that was deferred while the chain was executing
|
|
978
|
+
* (see suppressEscort in applyOutcome). Skipped while a confirmation is
|
|
979
|
+
* pending: the chain may resume after the user answers, and the confirmed
|
|
980
|
+
* action escorts on its own.
|
|
981
|
+
*/
|
|
982
|
+
flushDeferredEscort() {
|
|
983
|
+
if (this.pending != null)
|
|
984
|
+
return;
|
|
985
|
+
const intent = this.deferredEscortIntent;
|
|
986
|
+
this.deferredEscortIntent = null;
|
|
987
|
+
if (intent)
|
|
988
|
+
this.executor.escortForIntent(intent);
|
|
989
|
+
}
|
|
943
990
|
// ── Main turn ────────────────────────────────────────────────────────────────
|
|
944
991
|
async handleTranscript(rawText) {
|
|
945
992
|
const text = rawText.trim();
|
|
@@ -1223,6 +1270,7 @@ export class AgentController {
|
|
|
1223
1270
|
this.config.voice.stopPlayback();
|
|
1224
1271
|
this.pending = null;
|
|
1225
1272
|
this.chainState = null;
|
|
1273
|
+
this.deferredEscortIntent = null;
|
|
1226
1274
|
this.confirmationMessage = null;
|
|
1227
1275
|
this.confirmationVoiceHint = null;
|
|
1228
1276
|
this.liveActivity = null;
|
|
@@ -52,6 +52,12 @@ export interface DecideActionParams {
|
|
|
52
52
|
context: AgentScreenContext;
|
|
53
53
|
messages: AgentMessages;
|
|
54
54
|
userMessage: string;
|
|
55
|
+
/**
|
|
56
|
+
* Skip the post-action showcase escort (mid-chain steps): on multi-page
|
|
57
|
+
* hosts the escort is a full page load that would kill the running chain.
|
|
58
|
+
* The orchestrator escorts once at the END of the chain.
|
|
59
|
+
*/
|
|
60
|
+
suppressEscort?: boolean;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Decides what to do with a backend-proposed action. Sensitive actions are
|
|
@@ -27,7 +27,7 @@ function findManifestAction(manifest, intent) {
|
|
|
27
27
|
* never executed here — they return `needs-confirmation`.
|
|
28
28
|
*/
|
|
29
29
|
export async function decideAction(params) {
|
|
30
|
-
const { action, manifest, executor, context, messages, userMessage } = params;
|
|
30
|
+
const { action, manifest, executor, context, messages, userMessage, suppressEscort } = params;
|
|
31
31
|
if (!action || action.type === 'none') {
|
|
32
32
|
return { kind: 'noop' };
|
|
33
33
|
}
|
|
@@ -39,7 +39,7 @@ export async function decideAction(params) {
|
|
|
39
39
|
const manifestAction = findManifestAction(manifest, action.intent);
|
|
40
40
|
const needsConfirmation = manifestAction?.requireConfirmation === true;
|
|
41
41
|
if (!needsConfirmation) {
|
|
42
|
-
const result = await executor.execute(action, context);
|
|
42
|
+
const result = await executor.execute(action, context, { suppressEscort });
|
|
43
43
|
return { kind: 'executed', result };
|
|
44
44
|
}
|
|
45
45
|
// Precheck (context + idempotency) BEFORE asking: applying an already-applied
|
|
@@ -27,7 +27,7 @@ import { createScreenContextStore } from '../context/screenContextStore.js';
|
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics.js';
|
|
28
28
|
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
29
29
|
declare const api: {
|
|
30
|
-
readonly version: "0.1.
|
|
30
|
+
readonly version: "0.1.20";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - CONFIRMATIONS: the same security model (manifest decides; strict phrase /
|
|
17
17
|
* deliberate tap for payment-grade; loose "yes" never resolves strict).
|
|
18
18
|
*/
|
|
19
|
-
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt,
|
|
19
|
+
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, applyBubbleShapeCss, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, fiodosInputInkColor, fiodosInputMutedInkColor, shouldHideInternalOrb, } from '@fiodos/core';
|
|
20
20
|
import { buildKeyboardChip, createOrbVisual, DEFAULT_ORB_APPEARANCE, } from './orbView.js';
|
|
21
21
|
import { watchPublishedConfig } from './publishedConfig.js';
|
|
22
22
|
import { createBubbleDictation, scrollTextInputToEnd } from '../speech/bubbleDictation.js';
|
|
@@ -54,6 +54,8 @@ const CSS = `
|
|
|
54
54
|
.fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
|
|
55
55
|
.fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
|
|
56
56
|
.fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
|
|
57
|
+
.fyodos-bubble-greeting{font-size:15px;line-height:21px;white-space:pre-wrap;animation:fyodos-fade-in .18s ease-out;}
|
|
58
|
+
.fyodos-bubble--expanded .fyodos-bubble-greeting{font-size:17px;line-height:25px;}
|
|
57
59
|
.fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
|
|
58
60
|
.fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
|
|
59
61
|
.fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
|
|
@@ -207,13 +209,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
207
209
|
haptics.trigger('orbTap');
|
|
208
210
|
controller.primeAudio();
|
|
209
211
|
if (enableTextInput) {
|
|
210
|
-
|
|
211
|
-
openTextPanel
|
|
212
|
-
|
|
213
|
-
locale: currentBubbleLocale(),
|
|
214
|
-
messages: controller.messages,
|
|
215
|
-
})
|
|
216
|
-
: undefined);
|
|
212
|
+
// The bubble's question moves INTO the panel as an assistant greeting
|
|
213
|
+
// (openTextPanel shows it) — never pre-typed into the user's input.
|
|
214
|
+
openTextPanel();
|
|
217
215
|
return;
|
|
218
216
|
}
|
|
219
217
|
userTriedVoice = true;
|
|
@@ -455,7 +453,30 @@ export function mountOrb(controller, options = {}) {
|
|
|
455
453
|
loaderRow.style.display = 'none';
|
|
456
454
|
const exchangeList = document.createElement('div');
|
|
457
455
|
exchangeList.className = 'fyodos-bubble-exchanges';
|
|
458
|
-
|
|
456
|
+
// Screen-aware GREETING: the thought bubble's question, shown INSIDE the
|
|
457
|
+
// panel as an assistant message when it opens (tapping the bubble or opening
|
|
458
|
+
// the chat traditionally). Ephemeral by design: it never enters the
|
|
459
|
+
// conversation history and disappears the moment the user sends a message.
|
|
460
|
+
const greetingEl = document.createElement('div');
|
|
461
|
+
greetingEl.className = 'fyodos-bubble-greeting';
|
|
462
|
+
greetingEl.style.display = 'none';
|
|
463
|
+
exchangeScroll.append(loaderRow, exchangeList, greetingEl);
|
|
464
|
+
function showPanelGreeting() {
|
|
465
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
466
|
+
if (!prompt) {
|
|
467
|
+
greetingEl.style.display = 'none';
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const t = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) };
|
|
471
|
+
greetingEl.textContent = prompt;
|
|
472
|
+
greetingEl.style.color = t.titleColor;
|
|
473
|
+
greetingEl.style.display = '';
|
|
474
|
+
exchangeScroll.scrollTop = exchangeScroll.scrollHeight;
|
|
475
|
+
}
|
|
476
|
+
function hidePanelGreeting() {
|
|
477
|
+
if (greetingEl.style.display !== 'none')
|
|
478
|
+
greetingEl.style.display = 'none';
|
|
479
|
+
}
|
|
459
480
|
// Signature of the rendered list so state emits that do not change the
|
|
460
481
|
// transcript (volume, phase…) never re-render it nor fight the user's scroll.
|
|
461
482
|
let renderedSignature = '';
|
|
@@ -749,6 +770,8 @@ export function mountOrb(controller, options = {}) {
|
|
|
749
770
|
else {
|
|
750
771
|
bubbleDictation.stop();
|
|
751
772
|
syncMicButton();
|
|
773
|
+
// Ephemeral greeting never survives a close (recomputed on next open).
|
|
774
|
+
hidePanelGreeting();
|
|
752
775
|
// Collapse the paged history back to the initial page for the next open.
|
|
753
776
|
controller.resetBubbleWindow();
|
|
754
777
|
// Closing the panel returns the orb to idle (also clears mic fallback).
|
|
@@ -778,6 +801,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
778
801
|
syncMicButton();
|
|
779
802
|
haptics.trigger('send');
|
|
780
803
|
textInput.value = '';
|
|
804
|
+
// The greeting is a welcome, not part of the conversation: the user's
|
|
805
|
+
// first real message replaces it.
|
|
806
|
+
hidePanelGreeting();
|
|
781
807
|
// Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
|
|
782
808
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
783
809
|
void controller.submitTextTurn(value);
|
|
@@ -790,6 +816,11 @@ export function mountOrb(controller, options = {}) {
|
|
|
790
816
|
const draft = explicitSeed || cancelled || '';
|
|
791
817
|
textInput.value = draft;
|
|
792
818
|
setBubble(true);
|
|
819
|
+
// Welcome the user with this screen's bubble question as an assistant
|
|
820
|
+
// message (both open paths: bubble tap AND traditional open). It vanishes
|
|
821
|
+
// with the user's first message — never enters the history.
|
|
822
|
+
if (!draft)
|
|
823
|
+
showPanelGreeting();
|
|
793
824
|
// Only an explicit seed may auto-send; a restored cancelled message must
|
|
794
825
|
// wait for the user to edit/resend it.
|
|
795
826
|
if (autoSend && explicitSeed) {
|
|
@@ -1153,6 +1184,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
1153
1184
|
syncActivityStep(rowBusy, state);
|
|
1154
1185
|
// Text bubble: show recent exchanges + busy state.
|
|
1155
1186
|
if (bubbleOpen) {
|
|
1187
|
+
// A turn in flight (typed OR voice) replaces the ephemeral greeting.
|
|
1188
|
+
if (state.isBusy)
|
|
1189
|
+
hidePanelGreeting();
|
|
1156
1190
|
loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
|
|
1157
1191
|
renderBubbleExchanges(state.recentExchanges);
|
|
1158
1192
|
exchangeScroll.style.display = 'flex';
|
|
@@ -25,12 +25,29 @@ export function createBubbleDictation(locale) {
|
|
|
25
25
|
let listening = false;
|
|
26
26
|
let baseText = '';
|
|
27
27
|
const stop = () => {
|
|
28
|
-
recognition
|
|
28
|
+
const rec = recognition;
|
|
29
|
+
if (!rec)
|
|
30
|
+
return;
|
|
31
|
+
// DISCARD-ON-STOP: recognition engines fire one last consolidated
|
|
32
|
+
// `onresult` AFTER stop() is requested. If the user hits send while still
|
|
33
|
+
// dictating, that late result would rewrite the input we just cleared and
|
|
34
|
+
// the message would reappear (duplicate-send bug). Once a stop is
|
|
35
|
+
// requested, nothing may ever write to the input again.
|
|
36
|
+
rec.onresult = null;
|
|
37
|
+
recognition = null;
|
|
38
|
+
listening = false;
|
|
39
|
+
try {
|
|
40
|
+
if (typeof rec.abort === 'function')
|
|
41
|
+
rec.abort();
|
|
42
|
+
else
|
|
43
|
+
rec.stop();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* already stopped */
|
|
47
|
+
}
|
|
29
48
|
};
|
|
30
49
|
const dispose = () => {
|
|
31
50
|
stop();
|
|
32
|
-
recognition = null;
|
|
33
|
-
listening = false;
|
|
34
51
|
};
|
|
35
52
|
return {
|
|
36
53
|
isSupported: () => Ctor != null,
|
|
@@ -68,11 +85,18 @@ export function createBubbleDictation(locale) {
|
|
|
68
85
|
: dictated;
|
|
69
86
|
setValue(combined);
|
|
70
87
|
};
|
|
88
|
+
// Identity-guarded teardown: a session stopped via stop() may fire its
|
|
89
|
+
// onend AFTER the user already started a NEW dictation — it must never
|
|
90
|
+
// tear down the new session.
|
|
71
91
|
rec.onend = () => {
|
|
92
|
+
if (recognition !== rec)
|
|
93
|
+
return;
|
|
72
94
|
listening = false;
|
|
73
95
|
recognition = null;
|
|
74
96
|
};
|
|
75
97
|
rec.onerror = () => {
|
|
98
|
+
if (recognition !== rec)
|
|
99
|
+
return;
|
|
76
100
|
listening = false;
|
|
77
101
|
recognition = null;
|
|
78
102
|
};
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SDK_VERSION = "0.1.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.20";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/esm/version.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export const SDK_VERSION = '0.1.
|
|
8
|
+
export const SDK_VERSION = '0.1.20';
|
|
9
9
|
export const SDK_PACKAGE = '@fiodos/web-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|