@fiodos/web-core 0.1.10 → 0.1.12

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 (41) hide show
  1. package/dist/cjs/api/backendClient.js +20 -0
  2. package/dist/cjs/config/types.d.ts +24 -0
  3. package/dist/cjs/config/types.js +6 -1
  4. package/dist/cjs/controller/AgentController.d.ts +105 -2
  5. package/dist/cjs/controller/AgentController.js +572 -75
  6. package/dist/cjs/dropin/createFiodosAgent.d.ts +5 -0
  7. package/dist/cjs/dropin/createFiodosAgent.js +7 -0
  8. package/dist/cjs/index.d.ts +3 -3
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/orb/mountOrb.d.ts +6 -0
  11. package/dist/cjs/orb/mountOrb.js +229 -33
  12. package/dist/cjs/orb/orbView.d.ts +2 -0
  13. package/dist/cjs/orb/publishedConfig.d.ts +18 -0
  14. package/dist/cjs/orb/publishedConfig.js +6 -0
  15. package/dist/cjs/speech/bubbleDictation.d.ts +11 -0
  16. package/dist/cjs/speech/bubbleDictation.js +88 -0
  17. package/dist/cjs/ui/messages.d.ts +15 -0
  18. package/dist/cjs/ui/messages.js +12 -0
  19. package/dist/cjs/version.d.ts +1 -1
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/esm/api/backendClient.js +20 -0
  22. package/dist/esm/config/types.d.ts +24 -0
  23. package/dist/esm/config/types.js +5 -0
  24. package/dist/esm/controller/AgentController.d.ts +105 -2
  25. package/dist/esm/controller/AgentController.js +574 -77
  26. package/dist/esm/dropin/createFiodosAgent.d.ts +5 -0
  27. package/dist/esm/dropin/createFiodosAgent.js +7 -0
  28. package/dist/esm/index.d.ts +3 -3
  29. package/dist/esm/index.js +1 -1
  30. package/dist/esm/orb/mountOrb.d.ts +6 -0
  31. package/dist/esm/orb/mountOrb.js +230 -34
  32. package/dist/esm/orb/orbView.d.ts +2 -0
  33. package/dist/esm/orb/publishedConfig.d.ts +18 -0
  34. package/dist/esm/orb/publishedConfig.js +6 -0
  35. package/dist/esm/speech/bubbleDictation.d.ts +11 -0
  36. package/dist/esm/speech/bubbleDictation.js +84 -0
  37. package/dist/esm/ui/messages.d.ts +15 -0
  38. package/dist/esm/ui/messages.js +12 -0
  39. package/dist/esm/version.d.ts +1 -1
  40. package/dist/esm/version.js +1 -1
  41. package/package.json +2 -2
@@ -59,6 +59,11 @@ export interface FiodosAgentHandle {
59
59
  orb: MountedOrb | null;
60
60
  /** Resolves once the controller is ready (immediately on the manual path). */
61
61
  ready: Promise<AgentController | null>;
62
+ /**
63
+ * Wipes the current identity's conversation (live memory + persisted blob).
64
+ * Call it from the app's logout for a hard wipe; no-op until ready.
65
+ */
66
+ resetConversation(): void;
62
67
  destroy(): void;
63
68
  }
64
69
  export declare function createFiodosAgent(options: CreateFiodosAgentOptions): FiodosAgentHandle;
@@ -72,6 +72,7 @@ function assemble(options, manifest, baseUrl) {
72
72
  storage: createWebStorageAdapter(),
73
73
  telemetry,
74
74
  isAuthenticated: options.isAuthenticated,
75
+ getUserId: options.getUserId,
75
76
  ttsVoice: options.ttsVoice,
76
77
  ...options.configOverrides,
77
78
  };
@@ -94,6 +95,9 @@ export function createFiodosAgent(options) {
94
95
  controller,
95
96
  orb,
96
97
  ready: Promise.resolve(controller),
98
+ resetConversation() {
99
+ controller.resetConversation();
100
+ },
97
101
  destroy() {
98
102
  orb?.unmount();
99
103
  controller.dispose();
@@ -106,6 +110,9 @@ export function createFiodosAgent(options) {
106
110
  controller: null,
107
111
  orb: null,
108
112
  ready: Promise.resolve(null),
113
+ resetConversation() {
114
+ handle.controller?.resetConversation();
115
+ },
109
116
  destroy() {
110
117
  cancelled = true;
111
118
  handle.orb?.unmount();
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { SDK_VERSION, SDK_PACKAGE } from './version.js';
11
11
  export { AgentController } from './controller/AgentController.js';
12
- export type { AgentState, AgentExchange } from './controller/AgentController.js';
12
+ export type { AgentState, AgentExchange, AgentActivity } from './controller/AgentController.js';
13
13
  export { createFiodosAgent } from './dropin/createFiodosAgent.js';
14
14
  export type { CreateFiodosAgentOptions, FiodosAgentHandle } from './dropin/createFiodosAgent.js';
15
15
  export { DEFAULT_WEB_API_URL, fetchClientManifest, sendOrbSeen, } from './api/clientBootstrap.js';
@@ -38,8 +38,8 @@ export { createScreenContextStore } from './context/screenContextStore.js';
38
38
  export type { ScreenContextStore, AgentScreenSnapshot } from './context/screenContextStore.js';
39
39
  export { createSpeechSession } from './speech/speechSession.js';
40
40
  export type { SpeechSession, SpeechSessionOptions, SpeechSessionState, SpeechSessionSnapshot, } from './speech/speechSession.js';
41
- export { DEFAULT_AGENT_TIMINGS } from './config/types.js';
42
- export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, TurnGateVerdict, } from './config/types.js';
41
+ export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types.js';
42
+ export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, AgentChaining, TurnGateVerdict, } from './config/types.js';
43
43
  export { resolveUiMessages } from './ui/messages.js';
44
44
  export type { WebUiMessages, ResolveUiMessagesOptions } from './ui/messages.js';
45
45
  export { decideAction, classifyPendingReply } from './core/turnEngine.js';
package/dist/esm/index.js CHANGED
@@ -34,7 +34,7 @@ export { createScreenContextStore } from './context/screenContextStore.js';
34
34
  // ── Speech session ──────────────────────────────────────────────────────────────
35
35
  export { createSpeechSession } from './speech/speechSession.js';
36
36
  // ── Config / i18n ─────────────────────────────────────────────────────────────
37
- export { DEFAULT_AGENT_TIMINGS } from './config/types.js';
37
+ export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types.js';
38
38
  export { resolveUiMessages } from './ui/messages.js';
39
39
  // ── Decision engine (shared, framework-free; advanced use / testing) ──────────
40
40
  export { decideAction, classifyPendingReply } from './core/turnEngine.js';
@@ -17,6 +17,12 @@ export interface MountOrbOptions {
17
17
  appearancePollMs?: number;
18
18
  /** Honour the dashboard's published screen position. Default: true. */
19
19
  usePublishedPosition?: boolean;
20
+ /**
21
+ * Internal-profile orbs only render on development hosts (localhost, LAN
22
+ * IPs, *.local). Set true to assert this page IS internal software (e.g. a
23
+ * hosted admin panel) so an internal orb may render here. Default: false.
24
+ */
25
+ allowInternalOrb?: boolean;
20
26
  }
21
27
  export interface MountedOrb {
22
28
  unmount(): void;
@@ -16,9 +16,10 @@
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, bubblePromptToAffirmative, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, } from '@fiodos/core';
19
+ import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt, bubblePromptToAffirmative, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, 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
+ import { createBubbleDictation, scrollTextInputToEnd } from '../speech/bubbleDictation.js';
22
23
  const STYLE_ID = 'fyodos-orb-styles';
23
24
  const CSS = `
24
25
  .fyodos-orb-root{position:fixed;display:inline-flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;touch-action:none;}
@@ -27,17 +28,26 @@ const CSS = `
27
28
  .fyodos-orb-btn:focus-visible{outline:2px solid #8ab6ff;outline-offset:4px;border-radius:50%;}
28
29
  .fyodos-orb-anchor{position:relative;display:inline-flex;}
29
30
  .fyodos-bubble{position:absolute;display:flex;flex-direction:column;width:min(320px,calc(100vw - 48px));max-height:340px;overflow:hidden;border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,0.28);border:1px solid rgba(160,180,220,0.22);animation:fyodos-fade-in .14s ease-out;}
30
- .fyodos-bubble-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;border:none;background:transparent;font-size:18px;line-height:18px;width:28px;height:28px;min-width:28px;min-height:28px;margin-top:-10px;margin-right:-10px;cursor:pointer;padding:0;opacity:.75;}
31
+ .fyodos-bubble-close{position:absolute;top:4px;right:6px;z-index:1;display:flex;align-items:center;justify-content:center;border:none;background:transparent;font-size:18px;line-height:18px;width:28px;height:28px;cursor:pointer;padding:0;opacity:.75;}
31
32
  .fyodos-bubble-user-row{display:flex;flex-direction:row;align-items:flex-start;gap:6px;}
32
- .fyodos-bubble-scroll{overflow-y:auto;padding:12px 14px 4px;display:flex;flex-direction:column;gap:6px;}
33
+ .fyodos-bubble-scroll{overflow-y:auto;padding:12px 14px 4px;display:flex;flex-direction:column;gap:6px;min-height:18px;scrollbar-width:none;-ms-overflow-style:none;}
34
+ .fyodos-bubble-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
35
+ .fyodos-bubble-loader{display:flex;justify-content:center;align-items:center;padding:2px 0 6px;flex-shrink:0;}
36
+ .fyodos-bubble-loader span{width:14px;height:14px;border-radius:50%;border:2px solid rgba(148,163,184,0.35);border-top-color:rgba(148,163,184,0.9);animation:fy-spin .7s linear infinite;}
37
+ .fyodos-bubble-exchanges{display:flex;flex-direction:column;gap:12px;}
38
+ .fyodos-bubble-exchange{display:flex;flex-direction:column;gap:6px;}
39
+ .fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
33
40
  .fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
34
41
  .fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
35
42
  .fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
36
43
  .fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
37
44
  .fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
38
45
  .fyodos-bubble-busy span:nth-child(3){animation-delay:.36s;}
46
+ .fyodos-busy-step{margin-right:6px;font-size:12.5px;line-height:18px;font-style:italic;animation:fyodos-fade-in .28s ease-out;}
39
47
  .fyodos-bubble-row{display:flex;gap:8px;align-items:flex-end;padding:8px 10px;border-top:1px solid rgba(160,180,220,0.18);}
40
- .fyodos-bubble-row input{flex:1;border:1px solid rgba(15,23,42,0.12);outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
48
+ .fyodos-bubble-row input{flex:1;min-width:0;border:none;outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
49
+ .fyodos-bubble-row input::placeholder{color:var(--fyodos-input-placeholder,#94a3b8);opacity:1;}
50
+ .fyodos-bubble-mic{appearance:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;border:1px solid rgba(148,163,184,0.4);border-radius:999px;background:rgba(255,255,255,0.06);color:inherit;cursor:pointer;}
41
51
  .fyodos-bubble-row button{appearance:none;border:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;font-size:17px;font-weight:700;line-height:1;color:#000;cursor:pointer;}
42
52
  .fyodos-thought{position:absolute;appearance:none;box-sizing:border-box;display:flex;align-items:center;width:max-content;max-width:min(280px,70vw);white-space:nowrap;overflow:hidden;text-align:left;line-height:1.25;font-weight:500;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.22);animation:fyodos-thought-in .18s ease-out;}
43
53
  @keyframes fyodos-thought-in{from{opacity:0;transform:translateY(4px) scale(0.96)}to{opacity:1;transform:none}}
@@ -127,7 +137,7 @@ export function mountOrb(controller, options = {}) {
127
137
  }
128
138
  // Subtle haptic feedback on every relevant orb interaction (silent no-op on
129
139
  // browsers without the Vibration API — desktop, iOS Safari).
130
- const haptics = createWebOrbHaptics();
140
+ const haptics = createWebOrbHaptics(() => effectiveAppearance().hapticIntensity);
131
141
  // ── Effective appearance: defaults ← published ← explicit option overrides ──
132
142
  let publishedAppearance = null;
133
143
  let chipTheme = null;
@@ -137,6 +147,9 @@ export function mountOrb(controller, options = {}) {
137
147
  // Dashboard VOICE switch. Default true (voice on). When false the orb never
138
148
  // listens/speaks: a tap opens the keyboard directly and text turns are silent.
139
149
  let voiceInputEnabled = true;
150
+ // Dashboard "thinking steps" switch. When true the busy row walks through
151
+ // what the agent is doing ("Analyzing your request…") instead of only dots.
152
+ let thinkingStepsEnabled = false;
140
153
  let currentRoute = null;
141
154
  let screenPosition = options.corner
142
155
  ? cornerToPosition(options.corner)
@@ -385,35 +398,168 @@ export function mountOrb(controller, options = {}) {
385
398
  }
386
399
  const exchangeScroll = document.createElement('div');
387
400
  exchangeScroll.className = 'fyodos-bubble-scroll';
388
- const userRow = document.createElement('div');
389
- userRow.className = 'fyodos-bubble-user-row';
390
- const userLine = document.createElement('div');
391
- userLine.className = 'fyodos-bubble-user';
401
+ // Close button overlaid at the bubble corner — in line with the first message,
402
+ // exactly like the dashboard preview (no extra header row/space).
392
403
  const closeBtn = document.createElement('button');
393
404
  closeBtn.type = 'button';
394
405
  closeBtn.className = 'fyodos-bubble-close';
395
406
  closeBtn.textContent = '×';
396
407
  closeBtn.setAttribute('aria-label', 'Close');
397
408
  closeBtn.addEventListener('click', () => setBubble(false));
398
- userRow.append(userLine, closeBtn);
399
- const replyLine = document.createElement('div');
400
- replyLine.className = 'fyodos-bubble-reply';
401
- exchangeScroll.append(userRow, replyLine);
409
+ // Loading row shown at the top while an older page of the session history
410
+ // streams in (scroll-to-top pagination).
411
+ const loaderRow = document.createElement('div');
412
+ loaderRow.className = 'fyodos-bubble-loader';
413
+ loaderRow.append(document.createElement('span'));
414
+ loaderRow.style.display = 'none';
415
+ const exchangeList = document.createElement('div');
416
+ exchangeList.className = 'fyodos-bubble-exchanges';
417
+ exchangeScroll.append(loaderRow, exchangeList);
418
+ // Signature of the rendered list so state emits that do not change the
419
+ // transcript (volume, phase…) never re-render it nor fight the user's scroll.
420
+ let renderedSignature = '';
421
+ function renderBubbleExchanges(exchanges) {
422
+ const signature = exchanges.map((ex) => `${ex.userText}\u0000${ex.replyText}`).join('\u0001');
423
+ if (signature === renderedSignature)
424
+ return;
425
+ // Prepend (older page loaded): the previous tail is intact at the end.
426
+ // Keep the user's reading position anchored instead of jumping to bottom.
427
+ const prepended = renderedSignature.length > 0 &&
428
+ signature.length > renderedSignature.length &&
429
+ signature.endsWith(renderedSignature);
430
+ const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
431
+ renderedSignature = signature;
432
+ exchangeList.replaceChildren();
433
+ for (const ex of exchanges) {
434
+ const block = document.createElement('div');
435
+ block.className = 'fyodos-bubble-exchange';
436
+ const user = document.createElement('div');
437
+ user.className = 'fyodos-bubble-user';
438
+ user.textContent = ex.userText;
439
+ block.append(user);
440
+ if (ex.replyText) {
441
+ const reply = document.createElement('div');
442
+ reply.className = 'fyodos-bubble-reply';
443
+ reply.textContent = ex.replyText;
444
+ block.append(reply);
445
+ }
446
+ exchangeList.append(block);
447
+ }
448
+ exchangeScroll.scrollTop = prepended
449
+ ? exchangeScroll.scrollHeight - fromBottom
450
+ : exchangeScroll.scrollHeight;
451
+ }
452
+ // Scroll-to-top pagination: reaching the top with older messages available
453
+ // pages the next block in (the controller shows the loader while it mounts).
454
+ exchangeScroll.addEventListener('scroll', () => {
455
+ if (!bubbleOpen || exchangeScroll.scrollTop > 12)
456
+ return;
457
+ const state = controller.getState();
458
+ if (state.hasOlderExchanges && !state.loadingOlderExchanges) {
459
+ controller.loadOlderExchanges();
460
+ }
461
+ });
402
462
  const busyRow = document.createElement('div');
403
463
  busyRow.className = 'fyodos-bubble-busy';
464
+ // REAL live activity FIRST in the row — flush left, aligned with the
465
+ // messages above (an <em> so the dot styling, which targets the spans,
466
+ // never applies to it). Hidden unless the dashboard advanced option is on.
467
+ const busyStep = document.createElement('em');
468
+ busyStep.className = 'fyodos-busy-step';
469
+ busyStep.style.display = 'none';
470
+ busyRow.appendChild(busyStep);
404
471
  busyRow.append(document.createElement('span'), document.createElement('span'), document.createElement('span'));
472
+ // ── REAL activity line (dashboard advanced option) ─────────────────────────
473
+ // Replaces the old canned walk-through: the controller reports what the
474
+ // agent is ACTUALLY doing (thinking / navigating / running an action /
475
+ // waiting for confirmation) and the line updates only on real changes.
476
+ let lastActivityText = null;
477
+ function activityText(state) {
478
+ if (!thinkingStepsEnabled || !state.liveActivity)
479
+ return null;
480
+ const { kind, label } = state.liveActivity;
481
+ switch (kind) {
482
+ case 'navigating':
483
+ return ui.activityNavigating.replace('{label}', label ?? '');
484
+ case 'executing':
485
+ return ui.activityExecuting.replace('{label}', label ?? '');
486
+ case 'waiting_confirmation':
487
+ return ui.activityWaitingConfirmation;
488
+ default:
489
+ return ui.activityThinking;
490
+ }
491
+ }
492
+ function syncActivityStep(show, state) {
493
+ const text = show ? activityText(state) : null;
494
+ if (text === lastActivityText)
495
+ return;
496
+ lastActivityText = text;
497
+ if (!text) {
498
+ busyStep.textContent = '';
499
+ busyStep.style.display = 'none';
500
+ return;
501
+ }
502
+ busyStep.style.display = '';
503
+ busyStep.textContent = text;
504
+ // Restart the fade-in for each new real step.
505
+ busyStep.style.animation = 'none';
506
+ void busyStep.offsetWidth;
507
+ busyStep.style.animation = '';
508
+ }
405
509
  const inputRow = document.createElement('div');
406
510
  inputRow.className = 'fyodos-bubble-row';
407
511
  const textInput = document.createElement('input');
408
512
  textInput.type = 'text';
409
513
  textInput.placeholder = ui.textPlaceholder;
514
+ const micBtn = document.createElement('button');
515
+ micBtn.type = 'button';
516
+ micBtn.className = 'fyodos-bubble-mic';
517
+ micBtn.innerHTML =
518
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"></rect><path d="M5 11a7 7 0 0014 0 M12 18v3"></path></svg>';
519
+ micBtn.style.display = 'none';
410
520
  const sendBtn = document.createElement('button');
411
521
  // Unified send affordance: an up-arrow glyph on the accent square (parity
412
522
  // with the dashboard preview and every native SDK), not a text label.
413
523
  sendBtn.textContent = '↑';
414
524
  sendBtn.setAttribute('aria-label', ui.sendLabel);
415
- inputRow.append(textInput, sendBtn);
416
- bubble.append(exchangeScroll, busyRow, inputRow);
525
+ inputRow.append(textInput, micBtn, sendBtn);
526
+ const bubbleDictation = createBubbleDictation(controller.config.locale);
527
+ if (bubbleDictation.isSupported()) {
528
+ micBtn.style.display = 'flex';
529
+ micBtn.setAttribute('aria-label', ui.micLabel);
530
+ micBtn.title = ui.micLabel;
531
+ micBtn.addEventListener('click', () => {
532
+ if (controller.getState().isBusy)
533
+ return;
534
+ bubbleDictation.toggle(() => textInput.value, (next) => {
535
+ textInput.value = next;
536
+ scrollTextInputToEnd(textInput);
537
+ });
538
+ syncMicButton();
539
+ });
540
+ }
541
+ function syncMicButton() {
542
+ const accent = modalTheme?.primaryButtonColor ?? effectiveAppearance().accentColor;
543
+ const sendRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
544
+ const bodyColor = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) }.bodyColor;
545
+ const active = bubbleDictation.isListening();
546
+ micBtn.classList.toggle('is-listening', active);
547
+ if (active) {
548
+ micBtn.style.background = accent;
549
+ micBtn.style.border = 'none';
550
+ micBtn.style.color = '#000';
551
+ micBtn.style.borderRadius = sendRadius;
552
+ }
553
+ else {
554
+ micBtn.style.background = 'rgba(255,255,255,0.06)';
555
+ micBtn.style.border = '1px solid rgba(148,163,184,0.4)';
556
+ micBtn.style.color = bodyColor;
557
+ micBtn.style.borderRadius = '999px';
558
+ }
559
+ micBtn.setAttribute('aria-label', active ? ui.micStopLabel : ui.micLabel);
560
+ micBtn.title = active ? ui.micStopLabel : ui.micLabel;
561
+ }
562
+ bubble.append(closeBtn, exchangeScroll, busyRow, inputRow);
417
563
  anchor.appendChild(bubble);
418
564
  // Initial placement (after the bubble exists, so applyDeployment can lay it out).
419
565
  reposition();
@@ -424,16 +570,25 @@ export function mountOrb(controller, options = {}) {
424
570
  bubble.style.borderColor = t.cardBorderColor;
425
571
  if (modalTheme?.borderRadius != null)
426
572
  bubble.style.borderRadius = `${modalTheme.borderRadius}px`;
427
- userLine.style.color = t.bodyColor;
428
- replyLine.style.color = t.titleColor;
429
573
  closeBtn.style.color = t.bodyColor;
430
- textInput.style.background = '#fff';
431
- textInput.style.color = '#0f172a';
574
+ exchangeList.querySelectorAll('.fyodos-bubble-user').forEach((node) => {
575
+ node.style.color = t.bodyColor;
576
+ });
577
+ exchangeList.querySelectorAll('.fyodos-bubble-reply').forEach((node) => {
578
+ node.style.color = t.titleColor;
579
+ });
580
+ const inputBg = modalTheme?.inputBackgroundColor ?? '#ffffff';
581
+ textInput.style.background = inputBg;
582
+ textInput.style.color = fiodosInputInkColor(inputBg);
583
+ textInput.style.setProperty('--fyodos-input-placeholder', fiodosInputMutedInkColor(inputBg));
432
584
  sendBtn.style.background = accent;
433
585
  sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
586
+ syncMicButton();
434
587
  busyRow.querySelectorAll('span').forEach((s) => {
435
588
  s.style.background = accent;
436
589
  });
590
+ // Live-activity line: white (reply color), italic — not the faint gray.
591
+ busyStep.style.color = t.titleColor;
437
592
  }
438
593
  function setBubble(open) {
439
594
  bubbleOpen = open;
@@ -444,6 +599,10 @@ export function mountOrb(controller, options = {}) {
444
599
  textInput.focus();
445
600
  }
446
601
  else {
602
+ bubbleDictation.stop();
603
+ syncMicButton();
604
+ // Collapse the paged history back to the initial page for the next open.
605
+ controller.resetBubbleWindow();
447
606
  // Closing the panel returns the orb to idle (also clears mic fallback).
448
607
  micFallbackActive = false;
449
608
  }
@@ -457,6 +616,8 @@ export function mountOrb(controller, options = {}) {
457
616
  const value = textInput.value.trim();
458
617
  if (!value)
459
618
  return;
619
+ bubbleDictation.stop();
620
+ syncMicButton();
460
621
  haptics.trigger('send');
461
622
  textInput.value = '';
462
623
  // Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
@@ -464,11 +625,16 @@ export function mountOrb(controller, options = {}) {
464
625
  void controller.submitTextTurn(value);
465
626
  }
466
627
  function openTextPanel(seed, autoSend = false) {
467
- controller.cancelAll();
468
- const draft = seed?.trim() ?? '';
628
+ // Opening the keyboard cancels any in-flight turn; a cancelled message
629
+ // becomes the editable input content unless an explicit seed was given.
630
+ const cancelled = controller.cancelAll();
631
+ const explicitSeed = seed?.trim() ?? '';
632
+ const draft = explicitSeed || cancelled || '';
469
633
  textInput.value = draft;
470
634
  setBubble(true);
471
- if (autoSend && draft) {
635
+ // Only an explicit seed may auto-send; a restored cancelled message must
636
+ // wait for the user to edit/resend it.
637
+ if (autoSend && explicitSeed) {
472
638
  queueMicrotask(() => submitText());
473
639
  }
474
640
  }
@@ -704,7 +870,12 @@ export function mountOrb(controller, options = {}) {
704
870
  // (aborts the request, no reply is delivered) and returns the orb to idle.
705
871
  if (state.showThinking) {
706
872
  haptics.trigger('orbTap');
707
- controller.cancelAll();
873
+ const draft = controller.cancelAll();
874
+ // The cancelled message goes back into the input for editing.
875
+ if (draft && bubbleOpen) {
876
+ textInput.value = draft;
877
+ scrollTextInputToEnd(textInput);
878
+ }
708
879
  return;
709
880
  }
710
881
  haptics.trigger('orbTap');
@@ -738,10 +909,17 @@ export function mountOrb(controller, options = {}) {
738
909
  };
739
910
  sendBtn.onclick = () => {
740
911
  // Send (arrow) while idle; Stop (square) while a turn is processing — tapping
741
- // it cancels the in-flight turn so the user can type/send another message.
742
- if (controller.getState().isBusy) {
912
+ // it cancels the in-flight turn (or running chain) so the user can type again.
913
+ const st = controller.getState();
914
+ if (st.isBusy || st.chainActive) {
743
915
  haptics.trigger('orbTap');
744
- controller.cancelAll();
916
+ // STOP: the cancelled message leaves the panel and goes back into the
917
+ // input so the user can edit and resend it.
918
+ const draft = controller.cancelAll();
919
+ if (draft) {
920
+ textInput.value = draft;
921
+ scrollTextInputToEnd(textInput);
922
+ }
745
923
  textInput.focus();
746
924
  return;
747
925
  }
@@ -753,6 +931,7 @@ export function mountOrb(controller, options = {}) {
753
931
  if (e.key === 'Escape')
754
932
  setBubble(false);
755
933
  };
934
+ textInput.addEventListener('input', () => scrollTextInputToEnd(textInput));
756
935
  if (!enableTextInput) {
757
936
  if (chipEl)
758
937
  chipEl.style.display = 'none';
@@ -795,20 +974,28 @@ export function mountOrb(controller, options = {}) {
795
974
  ? 'speaking'
796
975
  : 'idle';
797
976
  orbVisual.applyState(visual, state.volumeLevel);
798
- // Text bubble: show the current exchange + busy state.
977
+ // Busy through the WHOLE task: single turns and every autonomous chain
978
+ // step (so the real narration never flickers between steps).
979
+ const rowBusy = state.showThinking || state.chainActive;
980
+ // REAL activity line: updates when the agent actually does something new,
981
+ // and clears when the reply lands.
982
+ syncActivityStep(rowBusy, state);
983
+ // Text bubble: show recent exchanges + busy state.
799
984
  if (bubbleOpen) {
800
- const ex = state.lastExchange;
985
+ loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
986
+ renderBubbleExchanges(state.recentExchanges);
801
987
  exchangeScroll.style.display = 'flex';
802
- userLine.textContent = ex?.userText ?? '';
803
- replyLine.textContent = ex?.replyText ?? '';
804
- replyLine.style.display = ex?.replyText ? 'block' : 'none';
805
- busyRow.style.display = state.showThinking ? 'flex' : 'none';
988
+ busyRow.style.display = rowBusy ? 'flex' : 'none';
806
989
  // Send (arrow) ↔ Stop (square): while busy the action becomes "cancel".
807
- const busy = state.isBusy;
990
+ // A running autonomous chain counts as busy so the user can interrupt it.
991
+ const busy = state.isBusy || state.chainActive;
808
992
  sendBtn.textContent = busy ? '◼' : '↑';
809
993
  sendBtn.setAttribute('aria-label', busy ? ui.cancelLabel : ui.sendLabel);
810
994
  sendBtn.style.opacity = '1';
811
995
  sendBtn.disabled = false;
996
+ micBtn.style.opacity = busy ? '0.45' : '1';
997
+ micBtn.disabled = busy;
998
+ syncMicButton();
812
999
  }
813
1000
  syncChip(state);
814
1001
  syncThoughtBubble(state);
@@ -856,7 +1043,11 @@ export function mountOrb(controller, options = {}) {
856
1043
  // Dashboard master switch: hide the whole orb (button + chip + bubble live
857
1044
  // inside `root`) when explicitly turned off, show it otherwise. No host
858
1045
  // code change — it flips on the next poll and reappears when re-enabled.
859
- root.style.display = config && config.enabled === false ? 'none' : '';
1046
+ // Internal-profile orbs additionally hide on public (non-development)
1047
+ // hosts: they are for the developer/company, never for end users.
1048
+ const hiddenByProfile = config != null &&
1049
+ shouldHideInternalOrb(config.internalProfile, options.allowInternalOrb === true);
1050
+ root.style.display = (config && config.enabled === false) || hiddenByProfile ? 'none' : '';
860
1051
  if (config) {
861
1052
  publishedAppearance = config.appearance;
862
1053
  chipTheme = config.keyboardChip;
@@ -864,6 +1055,10 @@ export function mountOrb(controller, options = {}) {
864
1055
  bubbleTheme = config.bubbleTheme;
865
1056
  bubbleEnabled = config.bubbleEnabled;
866
1057
  voiceInputEnabled = config.voiceInputEnabled;
1058
+ thinkingStepsEnabled = config.thinkingStepsEnabled;
1059
+ // Idle context retention chosen in the dashboard (0 = never clear) +
1060
+ // expanded memory window when the project's orb profile is 'internal'.
1061
+ controller.setConversationRetention(config.conversationRetentionMinutes, config.internalProfile);
867
1062
  if (usePublishedPosition && config.screenPosition)
868
1063
  screenPosition = config.screenPosition;
869
1064
  }
@@ -881,6 +1076,7 @@ export function mountOrb(controller, options = {}) {
881
1076
  });
882
1077
  return {
883
1078
  unmount() {
1079
+ bubbleDictation.dispose();
884
1080
  unsubscribe();
885
1081
  stopWatch();
886
1082
  window.clearInterval(routeTimer);
@@ -16,6 +16,8 @@ export interface OrbAppearance {
16
16
  glowColor?: string;
17
17
  colorKey?: string;
18
18
  glowIntensity?: number;
19
+ /** 0–100 orb vibration/haptic strength (dashboard advanced option). */
20
+ hapticIntensity?: number;
19
21
  innerBorderColor?: string;
20
22
  innerBorderWidth?: number;
21
23
  size: number;
@@ -27,12 +27,30 @@ export interface ResolvedOrbConfig {
27
27
  * directly on tap and never plays TTS. Default true (absent = voice on).
28
28
  */
29
29
  voiceInputEnabled: boolean;
30
+ /**
31
+ * Dashboard on/off for the agent's live "thinking steps" in the chat panel
32
+ * (`agentThinkingStepsEnabled`). True ONLY when the developer turned it on;
33
+ * then the busy row shows what the agent is doing instead of only the dots.
34
+ * Default false (absent = dots only).
35
+ */
36
+ thinkingStepsEnabled: boolean;
30
37
  /**
31
38
  * Dashboard master visibility switch (`orbEnabled`). False ONLY when the
32
39
  * developer explicitly turned the orb off. The orb is hidden when false and
33
40
  * shown otherwise (including when nothing is published / on error).
34
41
  */
35
42
  enabled: boolean;
43
+ /**
44
+ * Idle context retention in minutes (`conversationRetentionMinutes`).
45
+ * 0 = never clear; null = not published (the SDK's 30-minute default).
46
+ */
47
+ conversationRetentionMinutes: number | null;
48
+ /**
49
+ * True when the project's orb profile is 'internal' (internal/developer
50
+ * software): the agent uses the EXPANDED conversation memory window.
51
+ * Default false ('public' — today's behavior unchanged).
52
+ */
53
+ internalProfile: boolean;
36
54
  }
37
55
  export interface WatchPublishedConfigOptions {
38
56
  /** Poll interval (ms) to pick up dashboard changes in real time. Default 12000. */
@@ -20,6 +20,7 @@ function toResolved(props, published) {
20
20
  glowColor: theme.glowColor,
21
21
  colorKey: theme.colorKey,
22
22
  glowIntensity: theme.glowIntensity,
23
+ hapticIntensity: theme.hapticIntensity,
23
24
  innerBorderColor: theme.innerBorderColor,
24
25
  innerBorderWidth: theme.innerBorderWidth,
25
26
  size: theme.size ?? DEFAULT_ORB_APPEARANCE.size,
@@ -34,7 +35,12 @@ function toResolved(props, published) {
34
35
  bubbleTheme: resolveBubbleTheme(published.bubbleTheme),
35
36
  bubbleEnabled: published.bubbleEnabled !== false,
36
37
  voiceInputEnabled: published.voiceInputEnabled !== false,
38
+ thinkingStepsEnabled: published.agentThinkingStepsEnabled === true,
37
39
  enabled,
40
+ conversationRetentionMinutes: typeof published.conversationRetentionMinutes === 'number'
41
+ ? published.conversationRetentionMinutes
42
+ : null,
43
+ internalProfile: published.orbProfile === 'internal',
38
44
  };
39
45
  }
40
46
  /**
@@ -0,0 +1,11 @@
1
+ /** Push-to-talk dictation for the text bubble input (Web Speech API). */
2
+ /** Keep the trailing dictation visible inside a single-line text input. */
3
+ export declare function scrollTextInputToEnd(input: HTMLInputElement): void;
4
+ export interface BubbleDictation {
5
+ isSupported(): boolean;
6
+ isListening(): boolean;
7
+ toggle(getValue: () => string, setValue: (value: string) => void): void;
8
+ stop(): void;
9
+ dispose(): void;
10
+ }
11
+ export declare function createBubbleDictation(locale: string): BubbleDictation;