@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;
@@ -75,6 +75,7 @@ function assemble(options, manifest, baseUrl) {
75
75
  storage: (0, webStorageAdapter_1.createWebStorageAdapter)(),
76
76
  telemetry,
77
77
  isAuthenticated: options.isAuthenticated,
78
+ getUserId: options.getUserId,
78
79
  ttsVoice: options.ttsVoice,
79
80
  ...options.configOverrides,
80
81
  };
@@ -97,6 +98,9 @@ function createFiodosAgent(options) {
97
98
  controller,
98
99
  orb,
99
100
  ready: Promise.resolve(controller),
101
+ resetConversation() {
102
+ controller.resetConversation();
103
+ },
100
104
  destroy() {
101
105
  orb?.unmount();
102
106
  controller.dispose();
@@ -109,6 +113,9 @@ function createFiodosAgent(options) {
109
113
  controller: null,
110
114
  orb: null,
111
115
  ready: Promise.resolve(null),
116
+ resetConversation() {
117
+ handle.controller?.resetConversation();
118
+ },
112
119
  destroy() {
113
120
  cancelled = true;
114
121
  handle.orb?.unmount();
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { SDK_VERSION, SDK_PACKAGE } from './version';
11
11
  export { AgentController } from './controller/AgentController';
12
- export type { AgentState, AgentExchange } from './controller/AgentController';
12
+ export type { AgentState, AgentExchange, AgentActivity } from './controller/AgentController';
13
13
  export { createFiodosAgent } from './dropin/createFiodosAgent';
14
14
  export type { CreateFiodosAgentOptions, FiodosAgentHandle } from './dropin/createFiodosAgent';
15
15
  export { DEFAULT_WEB_API_URL, fetchClientManifest, sendOrbSeen, } from './api/clientBootstrap';
@@ -38,8 +38,8 @@ export { createScreenContextStore } from './context/screenContextStore';
38
38
  export type { ScreenContextStore, AgentScreenSnapshot } from './context/screenContextStore';
39
39
  export { createSpeechSession } from './speech/speechSession';
40
40
  export type { SpeechSession, SpeechSessionOptions, SpeechSessionState, SpeechSessionSnapshot, } from './speech/speechSession';
41
- export { DEFAULT_AGENT_TIMINGS } from './config/types';
42
- export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, TurnGateVerdict, } from './config/types';
41
+ export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types';
42
+ export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, AgentChaining, TurnGateVerdict, } from './config/types';
43
43
  export { resolveUiMessages } from './ui/messages';
44
44
  export type { WebUiMessages, ResolveUiMessagesOptions } from './ui/messages';
45
45
  export { decideAction, classifyPendingReply } from './core/turnEngine';
package/dist/cjs/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * behaviour and security. No framework imports live here.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.classifyPendingReply = exports.decideAction = exports.resolveUiMessages = exports.DEFAULT_AGENT_TIMINGS = exports.createSpeechSession = exports.createScreenContextStore = exports.createBridge = exports.isAgentApiError = exports.AgentApiError = exports.createFiodosTelemetry = exports.buildManifestPayload = exports.createFiodosBackendClient = exports.createWebVoiceAdapter = exports.BACK_ROUTE = exports.createWebNavigationAdapter = exports.createWebStorageAdapter = exports.watchPublishedConfig = exports.DEFAULT_ORB_APPEARANCE = exports.buildKeyboardChip = exports.createOrbVisual = exports.mountOrb = exports.sendOrbSeen = exports.fetchClientManifest = exports.DEFAULT_WEB_API_URL = exports.createFiodosAgent = exports.AgentController = exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
12
+ exports.classifyPendingReply = exports.decideAction = exports.resolveUiMessages = exports.DEFAULT_AGENT_CHAINING = exports.DEFAULT_AGENT_TIMINGS = exports.createSpeechSession = exports.createScreenContextStore = exports.createBridge = exports.isAgentApiError = exports.AgentApiError = exports.createFiodosTelemetry = exports.buildManifestPayload = exports.createFiodosBackendClient = exports.createWebVoiceAdapter = exports.BACK_ROUTE = exports.createWebNavigationAdapter = exports.createWebStorageAdapter = exports.watchPublishedConfig = exports.DEFAULT_ORB_APPEARANCE = exports.buildKeyboardChip = exports.createOrbVisual = exports.mountOrb = exports.sendOrbSeen = exports.fetchClientManifest = exports.DEFAULT_WEB_API_URL = exports.createFiodosAgent = exports.AgentController = exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
13
13
  // ── Version (reported by the orb heartbeat) ───────────────────────────────────
14
14
  var version_1 = require("./version");
15
15
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_1.SDK_VERSION; } });
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "createSpeechSession", { enumerable: true, get: f
63
63
  // ── Config / i18n ─────────────────────────────────────────────────────────────
64
64
  var types_1 = require("./config/types");
65
65
  Object.defineProperty(exports, "DEFAULT_AGENT_TIMINGS", { enumerable: true, get: function () { return types_1.DEFAULT_AGENT_TIMINGS; } });
66
+ Object.defineProperty(exports, "DEFAULT_AGENT_CHAINING", { enumerable: true, get: function () { return types_1.DEFAULT_AGENT_CHAINING; } });
66
67
  var messages_1 = require("./ui/messages");
67
68
  Object.defineProperty(exports, "resolveUiMessages", { enumerable: true, get: function () { return messages_1.resolveUiMessages; } });
68
69
  // ── Decision engine (shared, framework-free; advanced use / testing) ──────────
@@ -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;
@@ -22,6 +22,7 @@ exports.mountOrb = mountOrb;
22
22
  const core_1 = require("@fiodos/core");
23
23
  const orbView_1 = require("./orbView");
24
24
  const publishedConfig_1 = require("./publishedConfig");
25
+ const bubbleDictation_1 = require("../speech/bubbleDictation");
25
26
  const STYLE_ID = 'fyodos-orb-styles';
26
27
  const CSS = `
27
28
  .fyodos-orb-root{position:fixed;display:inline-flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;touch-action:none;}
@@ -30,17 +31,26 @@ const CSS = `
30
31
  .fyodos-orb-btn:focus-visible{outline:2px solid #8ab6ff;outline-offset:4px;border-radius:50%;}
31
32
  .fyodos-orb-anchor{position:relative;display:inline-flex;}
32
33
  .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;}
33
- .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;}
34
+ .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;}
34
35
  .fyodos-bubble-user-row{display:flex;flex-direction:row;align-items:flex-start;gap:6px;}
35
- .fyodos-bubble-scroll{overflow-y:auto;padding:12px 14px 4px;display:flex;flex-direction:column;gap:6px;}
36
+ .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;}
37
+ .fyodos-bubble-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
38
+ .fyodos-bubble-loader{display:flex;justify-content:center;align-items:center;padding:2px 0 6px;flex-shrink:0;}
39
+ .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;}
40
+ .fyodos-bubble-exchanges{display:flex;flex-direction:column;gap:12px;}
41
+ .fyodos-bubble-exchange{display:flex;flex-direction:column;gap:6px;}
42
+ .fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
36
43
  .fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
37
44
  .fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
38
45
  .fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
39
46
  .fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
40
47
  .fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
41
48
  .fyodos-bubble-busy span:nth-child(3){animation-delay:.36s;}
49
+ .fyodos-busy-step{margin-right:6px;font-size:12.5px;line-height:18px;font-style:italic;animation:fyodos-fade-in .28s ease-out;}
42
50
  .fyodos-bubble-row{display:flex;gap:8px;align-items:flex-end;padding:8px 10px;border-top:1px solid rgba(160,180,220,0.18);}
43
- .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;}
51
+ .fyodos-bubble-row input{flex:1;min-width:0;border:none;outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
52
+ .fyodos-bubble-row input::placeholder{color:var(--fyodos-input-placeholder,#94a3b8);opacity:1;}
53
+ .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;}
44
54
  .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;}
45
55
  .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;}
46
56
  @keyframes fyodos-thought-in{from{opacity:0;transform:translateY(4px) scale(0.96)}to{opacity:1;transform:none}}
@@ -130,7 +140,7 @@ function mountOrb(controller, options = {}) {
130
140
  }
131
141
  // Subtle haptic feedback on every relevant orb interaction (silent no-op on
132
142
  // browsers without the Vibration API — desktop, iOS Safari).
133
- const haptics = (0, core_1.createWebOrbHaptics)();
143
+ const haptics = (0, core_1.createWebOrbHaptics)(() => effectiveAppearance().hapticIntensity);
134
144
  // ── Effective appearance: defaults ← published ← explicit option overrides ──
135
145
  let publishedAppearance = null;
136
146
  let chipTheme = null;
@@ -140,6 +150,9 @@ function mountOrb(controller, options = {}) {
140
150
  // Dashboard VOICE switch. Default true (voice on). When false the orb never
141
151
  // listens/speaks: a tap opens the keyboard directly and text turns are silent.
142
152
  let voiceInputEnabled = true;
153
+ // Dashboard "thinking steps" switch. When true the busy row walks through
154
+ // what the agent is doing ("Analyzing your request…") instead of only dots.
155
+ let thinkingStepsEnabled = false;
143
156
  let currentRoute = null;
144
157
  let screenPosition = options.corner
145
158
  ? cornerToPosition(options.corner)
@@ -388,35 +401,168 @@ function mountOrb(controller, options = {}) {
388
401
  }
389
402
  const exchangeScroll = document.createElement('div');
390
403
  exchangeScroll.className = 'fyodos-bubble-scroll';
391
- const userRow = document.createElement('div');
392
- userRow.className = 'fyodos-bubble-user-row';
393
- const userLine = document.createElement('div');
394
- userLine.className = 'fyodos-bubble-user';
404
+ // Close button overlaid at the bubble corner — in line with the first message,
405
+ // exactly like the dashboard preview (no extra header row/space).
395
406
  const closeBtn = document.createElement('button');
396
407
  closeBtn.type = 'button';
397
408
  closeBtn.className = 'fyodos-bubble-close';
398
409
  closeBtn.textContent = '×';
399
410
  closeBtn.setAttribute('aria-label', 'Close');
400
411
  closeBtn.addEventListener('click', () => setBubble(false));
401
- userRow.append(userLine, closeBtn);
402
- const replyLine = document.createElement('div');
403
- replyLine.className = 'fyodos-bubble-reply';
404
- exchangeScroll.append(userRow, replyLine);
412
+ // Loading row shown at the top while an older page of the session history
413
+ // streams in (scroll-to-top pagination).
414
+ const loaderRow = document.createElement('div');
415
+ loaderRow.className = 'fyodos-bubble-loader';
416
+ loaderRow.append(document.createElement('span'));
417
+ loaderRow.style.display = 'none';
418
+ const exchangeList = document.createElement('div');
419
+ exchangeList.className = 'fyodos-bubble-exchanges';
420
+ exchangeScroll.append(loaderRow, exchangeList);
421
+ // Signature of the rendered list so state emits that do not change the
422
+ // transcript (volume, phase…) never re-render it nor fight the user's scroll.
423
+ let renderedSignature = '';
424
+ function renderBubbleExchanges(exchanges) {
425
+ const signature = exchanges.map((ex) => `${ex.userText}\u0000${ex.replyText}`).join('\u0001');
426
+ if (signature === renderedSignature)
427
+ return;
428
+ // Prepend (older page loaded): the previous tail is intact at the end.
429
+ // Keep the user's reading position anchored instead of jumping to bottom.
430
+ const prepended = renderedSignature.length > 0 &&
431
+ signature.length > renderedSignature.length &&
432
+ signature.endsWith(renderedSignature);
433
+ const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
434
+ renderedSignature = signature;
435
+ exchangeList.replaceChildren();
436
+ for (const ex of exchanges) {
437
+ const block = document.createElement('div');
438
+ block.className = 'fyodos-bubble-exchange';
439
+ const user = document.createElement('div');
440
+ user.className = 'fyodos-bubble-user';
441
+ user.textContent = ex.userText;
442
+ block.append(user);
443
+ if (ex.replyText) {
444
+ const reply = document.createElement('div');
445
+ reply.className = 'fyodos-bubble-reply';
446
+ reply.textContent = ex.replyText;
447
+ block.append(reply);
448
+ }
449
+ exchangeList.append(block);
450
+ }
451
+ exchangeScroll.scrollTop = prepended
452
+ ? exchangeScroll.scrollHeight - fromBottom
453
+ : exchangeScroll.scrollHeight;
454
+ }
455
+ // Scroll-to-top pagination: reaching the top with older messages available
456
+ // pages the next block in (the controller shows the loader while it mounts).
457
+ exchangeScroll.addEventListener('scroll', () => {
458
+ if (!bubbleOpen || exchangeScroll.scrollTop > 12)
459
+ return;
460
+ const state = controller.getState();
461
+ if (state.hasOlderExchanges && !state.loadingOlderExchanges) {
462
+ controller.loadOlderExchanges();
463
+ }
464
+ });
405
465
  const busyRow = document.createElement('div');
406
466
  busyRow.className = 'fyodos-bubble-busy';
467
+ // REAL live activity FIRST in the row — flush left, aligned with the
468
+ // messages above (an <em> so the dot styling, which targets the spans,
469
+ // never applies to it). Hidden unless the dashboard advanced option is on.
470
+ const busyStep = document.createElement('em');
471
+ busyStep.className = 'fyodos-busy-step';
472
+ busyStep.style.display = 'none';
473
+ busyRow.appendChild(busyStep);
407
474
  busyRow.append(document.createElement('span'), document.createElement('span'), document.createElement('span'));
475
+ // ── REAL activity line (dashboard advanced option) ─────────────────────────
476
+ // Replaces the old canned walk-through: the controller reports what the
477
+ // agent is ACTUALLY doing (thinking / navigating / running an action /
478
+ // waiting for confirmation) and the line updates only on real changes.
479
+ let lastActivityText = null;
480
+ function activityText(state) {
481
+ if (!thinkingStepsEnabled || !state.liveActivity)
482
+ return null;
483
+ const { kind, label } = state.liveActivity;
484
+ switch (kind) {
485
+ case 'navigating':
486
+ return ui.activityNavigating.replace('{label}', label ?? '');
487
+ case 'executing':
488
+ return ui.activityExecuting.replace('{label}', label ?? '');
489
+ case 'waiting_confirmation':
490
+ return ui.activityWaitingConfirmation;
491
+ default:
492
+ return ui.activityThinking;
493
+ }
494
+ }
495
+ function syncActivityStep(show, state) {
496
+ const text = show ? activityText(state) : null;
497
+ if (text === lastActivityText)
498
+ return;
499
+ lastActivityText = text;
500
+ if (!text) {
501
+ busyStep.textContent = '';
502
+ busyStep.style.display = 'none';
503
+ return;
504
+ }
505
+ busyStep.style.display = '';
506
+ busyStep.textContent = text;
507
+ // Restart the fade-in for each new real step.
508
+ busyStep.style.animation = 'none';
509
+ void busyStep.offsetWidth;
510
+ busyStep.style.animation = '';
511
+ }
408
512
  const inputRow = document.createElement('div');
409
513
  inputRow.className = 'fyodos-bubble-row';
410
514
  const textInput = document.createElement('input');
411
515
  textInput.type = 'text';
412
516
  textInput.placeholder = ui.textPlaceholder;
517
+ const micBtn = document.createElement('button');
518
+ micBtn.type = 'button';
519
+ micBtn.className = 'fyodos-bubble-mic';
520
+ micBtn.innerHTML =
521
+ '<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>';
522
+ micBtn.style.display = 'none';
413
523
  const sendBtn = document.createElement('button');
414
524
  // Unified send affordance: an up-arrow glyph on the accent square (parity
415
525
  // with the dashboard preview and every native SDK), not a text label.
416
526
  sendBtn.textContent = '↑';
417
527
  sendBtn.setAttribute('aria-label', ui.sendLabel);
418
- inputRow.append(textInput, sendBtn);
419
- bubble.append(exchangeScroll, busyRow, inputRow);
528
+ inputRow.append(textInput, micBtn, sendBtn);
529
+ const bubbleDictation = (0, bubbleDictation_1.createBubbleDictation)(controller.config.locale);
530
+ if (bubbleDictation.isSupported()) {
531
+ micBtn.style.display = 'flex';
532
+ micBtn.setAttribute('aria-label', ui.micLabel);
533
+ micBtn.title = ui.micLabel;
534
+ micBtn.addEventListener('click', () => {
535
+ if (controller.getState().isBusy)
536
+ return;
537
+ bubbleDictation.toggle(() => textInput.value, (next) => {
538
+ textInput.value = next;
539
+ (0, bubbleDictation_1.scrollTextInputToEnd)(textInput);
540
+ });
541
+ syncMicButton();
542
+ });
543
+ }
544
+ function syncMicButton() {
545
+ const accent = modalTheme?.primaryButtonColor ?? effectiveAppearance().accentColor;
546
+ const sendRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
547
+ const bodyColor = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) }.bodyColor;
548
+ const active = bubbleDictation.isListening();
549
+ micBtn.classList.toggle('is-listening', active);
550
+ if (active) {
551
+ micBtn.style.background = accent;
552
+ micBtn.style.border = 'none';
553
+ micBtn.style.color = '#000';
554
+ micBtn.style.borderRadius = sendRadius;
555
+ }
556
+ else {
557
+ micBtn.style.background = 'rgba(255,255,255,0.06)';
558
+ micBtn.style.border = '1px solid rgba(148,163,184,0.4)';
559
+ micBtn.style.color = bodyColor;
560
+ micBtn.style.borderRadius = '999px';
561
+ }
562
+ micBtn.setAttribute('aria-label', active ? ui.micStopLabel : ui.micLabel);
563
+ micBtn.title = active ? ui.micStopLabel : ui.micLabel;
564
+ }
565
+ bubble.append(closeBtn, exchangeScroll, busyRow, inputRow);
420
566
  anchor.appendChild(bubble);
421
567
  // Initial placement (after the bubble exists, so applyDeployment can lay it out).
422
568
  reposition();
@@ -427,16 +573,25 @@ function mountOrb(controller, options = {}) {
427
573
  bubble.style.borderColor = t.cardBorderColor;
428
574
  if (modalTheme?.borderRadius != null)
429
575
  bubble.style.borderRadius = `${modalTheme.borderRadius}px`;
430
- userLine.style.color = t.bodyColor;
431
- replyLine.style.color = t.titleColor;
432
576
  closeBtn.style.color = t.bodyColor;
433
- textInput.style.background = '#fff';
434
- textInput.style.color = '#0f172a';
577
+ exchangeList.querySelectorAll('.fyodos-bubble-user').forEach((node) => {
578
+ node.style.color = t.bodyColor;
579
+ });
580
+ exchangeList.querySelectorAll('.fyodos-bubble-reply').forEach((node) => {
581
+ node.style.color = t.titleColor;
582
+ });
583
+ const inputBg = modalTheme?.inputBackgroundColor ?? '#ffffff';
584
+ textInput.style.background = inputBg;
585
+ textInput.style.color = (0, core_1.fiodosInputInkColor)(inputBg);
586
+ textInput.style.setProperty('--fyodos-input-placeholder', (0, core_1.fiodosInputMutedInkColor)(inputBg));
435
587
  sendBtn.style.background = accent;
436
588
  sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
589
+ syncMicButton();
437
590
  busyRow.querySelectorAll('span').forEach((s) => {
438
591
  s.style.background = accent;
439
592
  });
593
+ // Live-activity line: white (reply color), italic — not the faint gray.
594
+ busyStep.style.color = t.titleColor;
440
595
  }
441
596
  function setBubble(open) {
442
597
  bubbleOpen = open;
@@ -447,6 +602,10 @@ function mountOrb(controller, options = {}) {
447
602
  textInput.focus();
448
603
  }
449
604
  else {
605
+ bubbleDictation.stop();
606
+ syncMicButton();
607
+ // Collapse the paged history back to the initial page for the next open.
608
+ controller.resetBubbleWindow();
450
609
  // Closing the panel returns the orb to idle (also clears mic fallback).
451
610
  micFallbackActive = false;
452
611
  }
@@ -460,6 +619,8 @@ function mountOrb(controller, options = {}) {
460
619
  const value = textInput.value.trim();
461
620
  if (!value)
462
621
  return;
622
+ bubbleDictation.stop();
623
+ syncMicButton();
463
624
  haptics.trigger('send');
464
625
  textInput.value = '';
465
626
  // Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
@@ -467,11 +628,16 @@ function mountOrb(controller, options = {}) {
467
628
  void controller.submitTextTurn(value);
468
629
  }
469
630
  function openTextPanel(seed, autoSend = false) {
470
- controller.cancelAll();
471
- const draft = seed?.trim() ?? '';
631
+ // Opening the keyboard cancels any in-flight turn; a cancelled message
632
+ // becomes the editable input content unless an explicit seed was given.
633
+ const cancelled = controller.cancelAll();
634
+ const explicitSeed = seed?.trim() ?? '';
635
+ const draft = explicitSeed || cancelled || '';
472
636
  textInput.value = draft;
473
637
  setBubble(true);
474
- if (autoSend && draft) {
638
+ // Only an explicit seed may auto-send; a restored cancelled message must
639
+ // wait for the user to edit/resend it.
640
+ if (autoSend && explicitSeed) {
475
641
  queueMicrotask(() => submitText());
476
642
  }
477
643
  }
@@ -707,7 +873,12 @@ function mountOrb(controller, options = {}) {
707
873
  // (aborts the request, no reply is delivered) and returns the orb to idle.
708
874
  if (state.showThinking) {
709
875
  haptics.trigger('orbTap');
710
- controller.cancelAll();
876
+ const draft = controller.cancelAll();
877
+ // The cancelled message goes back into the input for editing.
878
+ if (draft && bubbleOpen) {
879
+ textInput.value = draft;
880
+ (0, bubbleDictation_1.scrollTextInputToEnd)(textInput);
881
+ }
711
882
  return;
712
883
  }
713
884
  haptics.trigger('orbTap');
@@ -741,10 +912,17 @@ function mountOrb(controller, options = {}) {
741
912
  };
742
913
  sendBtn.onclick = () => {
743
914
  // Send (arrow) while idle; Stop (square) while a turn is processing — tapping
744
- // it cancels the in-flight turn so the user can type/send another message.
745
- if (controller.getState().isBusy) {
915
+ // it cancels the in-flight turn (or running chain) so the user can type again.
916
+ const st = controller.getState();
917
+ if (st.isBusy || st.chainActive) {
746
918
  haptics.trigger('orbTap');
747
- controller.cancelAll();
919
+ // STOP: the cancelled message leaves the panel and goes back into the
920
+ // input so the user can edit and resend it.
921
+ const draft = controller.cancelAll();
922
+ if (draft) {
923
+ textInput.value = draft;
924
+ (0, bubbleDictation_1.scrollTextInputToEnd)(textInput);
925
+ }
748
926
  textInput.focus();
749
927
  return;
750
928
  }
@@ -756,6 +934,7 @@ function mountOrb(controller, options = {}) {
756
934
  if (e.key === 'Escape')
757
935
  setBubble(false);
758
936
  };
937
+ textInput.addEventListener('input', () => (0, bubbleDictation_1.scrollTextInputToEnd)(textInput));
759
938
  if (!enableTextInput) {
760
939
  if (chipEl)
761
940
  chipEl.style.display = 'none';
@@ -798,20 +977,28 @@ function mountOrb(controller, options = {}) {
798
977
  ? 'speaking'
799
978
  : 'idle';
800
979
  orbVisual.applyState(visual, state.volumeLevel);
801
- // Text bubble: show the current exchange + busy state.
980
+ // Busy through the WHOLE task: single turns and every autonomous chain
981
+ // step (so the real narration never flickers between steps).
982
+ const rowBusy = state.showThinking || state.chainActive;
983
+ // REAL activity line: updates when the agent actually does something new,
984
+ // and clears when the reply lands.
985
+ syncActivityStep(rowBusy, state);
986
+ // Text bubble: show recent exchanges + busy state.
802
987
  if (bubbleOpen) {
803
- const ex = state.lastExchange;
988
+ loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
989
+ renderBubbleExchanges(state.recentExchanges);
804
990
  exchangeScroll.style.display = 'flex';
805
- userLine.textContent = ex?.userText ?? '';
806
- replyLine.textContent = ex?.replyText ?? '';
807
- replyLine.style.display = ex?.replyText ? 'block' : 'none';
808
- busyRow.style.display = state.showThinking ? 'flex' : 'none';
991
+ busyRow.style.display = rowBusy ? 'flex' : 'none';
809
992
  // Send (arrow) ↔ Stop (square): while busy the action becomes "cancel".
810
- const busy = state.isBusy;
993
+ // A running autonomous chain counts as busy so the user can interrupt it.
994
+ const busy = state.isBusy || state.chainActive;
811
995
  sendBtn.textContent = busy ? '◼' : '↑';
812
996
  sendBtn.setAttribute('aria-label', busy ? ui.cancelLabel : ui.sendLabel);
813
997
  sendBtn.style.opacity = '1';
814
998
  sendBtn.disabled = false;
999
+ micBtn.style.opacity = busy ? '0.45' : '1';
1000
+ micBtn.disabled = busy;
1001
+ syncMicButton();
815
1002
  }
816
1003
  syncChip(state);
817
1004
  syncThoughtBubble(state);
@@ -859,7 +1046,11 @@ function mountOrb(controller, options = {}) {
859
1046
  // Dashboard master switch: hide the whole orb (button + chip + bubble live
860
1047
  // inside `root`) when explicitly turned off, show it otherwise. No host
861
1048
  // code change — it flips on the next poll and reappears when re-enabled.
862
- root.style.display = config && config.enabled === false ? 'none' : '';
1049
+ // Internal-profile orbs additionally hide on public (non-development)
1050
+ // hosts: they are for the developer/company, never for end users.
1051
+ const hiddenByProfile = config != null &&
1052
+ (0, core_1.shouldHideInternalOrb)(config.internalProfile, options.allowInternalOrb === true);
1053
+ root.style.display = (config && config.enabled === false) || hiddenByProfile ? 'none' : '';
863
1054
  if (config) {
864
1055
  publishedAppearance = config.appearance;
865
1056
  chipTheme = config.keyboardChip;
@@ -867,6 +1058,10 @@ function mountOrb(controller, options = {}) {
867
1058
  bubbleTheme = config.bubbleTheme;
868
1059
  bubbleEnabled = config.bubbleEnabled;
869
1060
  voiceInputEnabled = config.voiceInputEnabled;
1061
+ thinkingStepsEnabled = config.thinkingStepsEnabled;
1062
+ // Idle context retention chosen in the dashboard (0 = never clear) +
1063
+ // expanded memory window when the project's orb profile is 'internal'.
1064
+ controller.setConversationRetention(config.conversationRetentionMinutes, config.internalProfile);
870
1065
  if (usePublishedPosition && config.screenPosition)
871
1066
  screenPosition = config.screenPosition;
872
1067
  }
@@ -884,6 +1079,7 @@ function mountOrb(controller, options = {}) {
884
1079
  });
885
1080
  return {
886
1081
  unmount() {
1082
+ bubbleDictation.dispose();
887
1083
  unsubscribe();
888
1084
  stopWatch();
889
1085
  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. */
@@ -23,6 +23,7 @@ function toResolved(props, published) {
23
23
  glowColor: theme.glowColor,
24
24
  colorKey: theme.colorKey,
25
25
  glowIntensity: theme.glowIntensity,
26
+ hapticIntensity: theme.hapticIntensity,
26
27
  innerBorderColor: theme.innerBorderColor,
27
28
  innerBorderWidth: theme.innerBorderWidth,
28
29
  size: theme.size ?? orbView_1.DEFAULT_ORB_APPEARANCE.size,
@@ -37,7 +38,12 @@ function toResolved(props, published) {
37
38
  bubbleTheme: (0, core_1.resolveBubbleTheme)(published.bubbleTheme),
38
39
  bubbleEnabled: published.bubbleEnabled !== false,
39
40
  voiceInputEnabled: published.voiceInputEnabled !== false,
41
+ thinkingStepsEnabled: published.agentThinkingStepsEnabled === true,
40
42
  enabled,
43
+ conversationRetentionMinutes: typeof published.conversationRetentionMinutes === 'number'
44
+ ? published.conversationRetentionMinutes
45
+ : null,
46
+ internalProfile: published.orbProfile === 'internal',
41
47
  };
42
48
  }
43
49
  /**
@@ -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;