@marketrix.ai/widget 3.8.492 → 3.8.494

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 (67) hide show
  1. package/README.md +4 -4
  2. package/dist/loader.js +8 -1
  3. package/dist/src/components/WidgetRoot.d.ts +19 -0
  4. package/dist/src/components/base/Avatar.d.ts +5 -2
  5. package/dist/src/components/base/Button.d.ts +1 -1
  6. package/dist/src/components/base/ErrorBoundary.d.ts +19 -0
  7. package/dist/src/components/base/Flex.d.ts +14 -0
  8. package/dist/src/components/base/Icon.d.ts +12 -1
  9. package/dist/src/components/base/IconButton.d.ts +4 -0
  10. package/dist/src/components/base/Spinner.d.ts +4 -0
  11. package/dist/src/components/base/Surface.d.ts +22 -5
  12. package/dist/src/components/base/Text.d.ts +17 -1
  13. package/dist/src/components/base/icons.d.ts +16 -0
  14. package/dist/src/components/base/layoutProps.d.ts +20 -6
  15. package/dist/src/components/blocks/ChatInput.d.ts +9 -0
  16. package/dist/src/components/blocks/HeaderBar.d.ts +4 -0
  17. package/dist/src/components/blocks/Notifications.d.ts +20 -10
  18. package/dist/src/components/blocks/WidgetDialog.d.ts +0 -5
  19. package/dist/src/components/blocks/WidgetFab.d.ts +10 -0
  20. package/dist/src/components/chat/MessageItem.d.ts +13 -0
  21. package/dist/src/components/chat/MessageList.d.ts +20 -0
  22. package/dist/src/components/chat/VideoStreamDisplay.d.ts +20 -0
  23. package/dist/src/components/navigation/ShellTabBar.d.ts +0 -1
  24. package/dist/src/components/views/ChatView.d.ts +20 -0
  25. package/dist/src/components/views/HomeView.d.ts +9 -0
  26. package/dist/src/context/ChatContext.d.ts +20 -0
  27. package/dist/src/context/UIStateContext.d.ts +5 -0
  28. package/dist/src/context/WidgetProviders.d.ts +20 -1
  29. package/dist/src/context/sseReducer.d.ts +20 -9
  30. package/dist/src/design-system/component-tokens.d.ts +27 -1
  31. package/dist/src/design-system/semantic-tokens.d.ts +18 -0
  32. package/dist/src/hooks/useDragSnap.d.ts +13 -0
  33. package/dist/src/hooks/useFocusTrap.d.ts +20 -0
  34. package/dist/src/hooks/useWidget.d.ts +11 -1
  35. package/dist/src/index.d.ts +20 -0
  36. package/dist/src/sdk/contracts/common.d.ts +0 -16
  37. package/dist/src/sdk/index.d.ts +0 -1
  38. package/dist/src/services/BrowserToolService.d.ts +26 -5
  39. package/dist/src/services/ChatService.d.ts +15 -1
  40. package/dist/src/services/ChatSessionManager.d.ts +0 -2
  41. package/dist/src/services/DomService.d.ts +18 -3
  42. package/dist/src/services/RrwebSessionRecorder.d.ts +0 -1
  43. package/dist/src/services/ShowModeService.d.ts +22 -5
  44. package/dist/src/services/StorageService.d.ts +20 -1
  45. package/dist/src/services/StreamClient.d.ts +21 -1
  46. package/dist/src/services/keySimulation.d.ts +3 -0
  47. package/dist/src/test/fixtures.d.ts +13 -2
  48. package/dist/src/test/renderWidget.d.ts +6 -0
  49. package/dist/src/test/setup.d.ts +14 -0
  50. package/dist/src/types/index.d.ts +20 -7
  51. package/dist/src/utils/chat.d.ts +21 -4
  52. package/dist/src/utils/color.d.ts +21 -0
  53. package/dist/src/utils/dom.d.ts +21 -0
  54. package/dist/src/utils/errors.d.ts +12 -0
  55. package/dist/src/utils/suggestedActions.d.ts +11 -0
  56. package/dist/src/utils/validation.d.ts +20 -7
  57. package/dist/src/utils/widgetPositioning.d.ts +20 -0
  58. package/dist/widget.mjs +63 -64
  59. package/package.json +6 -6
  60. package/dist/src/components/base/Stack.d.ts +0 -3
  61. package/dist/src/components/chat/MessageContent.d.ts +0 -8
  62. package/dist/src/components/chat/SuggestedActions.d.ts +0 -8
  63. package/dist/src/components/chat/TaskStatusIcon.d.ts +0 -5
  64. package/dist/src/components/chat/ThinkingIndicator.d.ts +0 -6
  65. package/dist/src/components/navigation/ResizeHandle.d.ts +0 -7
  66. package/dist/src/design-system/layers.d.ts +0 -7
  67. package/dist/src/design-system/shadows.d.ts +0 -9
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Pure state machine behind `ChatContext`: folds SSE `WidgetEvent`s and local transitions into `{messages,
3
+ * task}` (`SseState`; `TaskPhase`/`TaskState` carry the mode the run was dispatched in) and returns the tool
4
+ * runs the caller must perform (`SseEffect`, `ReduceResult`). No I/O, no React.
5
+ *
6
+ * `reduceToolProgress` writes a tool's progress line and sets the bubble's spinner, judging by the mode the
7
+ * run STARTED in, not what the composer shows now; `FINISH_TOOL` gets no line, only ending the run, though a
8
+ * failed finish is still stamped failed, and a `show`-mode DOM-mutating tool parks on "waiting-for-user".
9
+ * `reduceToolDone` stamps done and drops the progress parts, scaffolding for "still working" not answer.
10
+ * `reduceStop` stamps stopped and a stopped task STAYS stopped, so late `tool/call`s are ignored until
11
+ * `reduceDispatch` appends the next placeholder and returns to `idle`, reopening the task for the next turn.
12
+ *
13
+ * Terminal transitions clear `isPlaceholder` — the composer is disabled while any message is one, and
14
+ * `has_question` would otherwise ask for an answer the visitor could not type. `isTerminalTaskStatus` reads
15
+ * the very map that stamps the status, so a fourth reaches every caller at once. `reduceError` settles one
16
+ * pending message as a failed bubble, `reduceTransportFailure` does it to every placeholder since after a
17
+ * give-up no id-bearing event is coming, and `reduceStaleReply` covers a HEALTHY stream whose one reply
18
+ * never arrives and nothing else can see (a `waiting-for-user` pause is not stale). `reduceSse` switches:
19
+ * `chat/delta` accumulates, `chat/response` replaces, `tool/call` ACTIVATES the task before `task/status`.
20
+ */
1
21
  import type { WidgetEvent } from '../sdk';
2
22
  import { type ChatMessage, type InstructionType } from '../types';
3
23
  export type TaskPhase = 'idle' | 'running' | 'stopped';
@@ -32,17 +52,8 @@ declare const TASK_STATUS: {
32
52
  readonly stopped: "stopped";
33
53
  };
34
54
  export declare const isTerminalTaskStatus: (status: string) => status is keyof typeof TASK_STATUS;
35
- /** Settles a pending message into a plain error bubble — the one shape for both a failed POST and a chat/error. */
36
55
  export declare function reduceError(state: SseState, messageId: string, text: string): SseState;
37
- /** The transport gave up, so no id-bearing event is coming for whatever is still pending. */
38
56
  export declare function reduceTransportFailure(state: SseState, text: string): SseState;
39
- /**
40
- * The stream can stay healthy (no `StreamGaveUpError`) while a single dispatch's reply never arrives —
41
- * a dropped correlation, a silent backend failure before it ever acknowledges the request, or a reload
42
- * into the gap where the api has no tab to push to. `reduceTransportFailure` cannot see that, since
43
- * nothing told the transport it failed, so without this the bubble sits on "thinking" and the composer
44
- * stays disabled forever.
45
- */
46
57
  export declare function reduceStaleReply(state: SseState, messageId: string, text: string): SseState;
47
58
  export declare function reduceSse(state: SseState, event: WidgetEvent, currentMode: InstructionType): ReduceResult;
48
59
  export {};
@@ -1,5 +1,16 @@
1
+ /**
2
+ * Every fixed design token the widget's components read, in one home: the `RADIUS`, `TEXT_TONE`,
3
+ * `TEXT_LEADING` and `SHADOW` scales, `TAB_BAR_HEIGHT`, `LAYER_TOKENS`, the `notificationToneStyles`
4
+ * palette per notification tone, and `getElevationStyle`, which turns a `ShadowToken` into a `boxShadow`
5
+ * (none → undefined so a caller can spread it). Each radius value has exactly one name — a test pins that
6
+ * no two tokens are synonyms. Per-tenant values are NOT here: `semantic-tokens.ts` owns everything derived
7
+ * from widget settings, and this file is only what no tenant can change.
8
+ *
9
+ * `LAYER_TOKENS` is the z-index ladder (screen-edge glow < panel < dialog < toast), based just above the
10
+ * 2^31-ish ceiling most host pages use so the widget sits over everything without the values overflowing a
11
+ * 32-bit int.
12
+ */
1
13
  import type { CSSProperties } from 'react';
2
- import { type ShadowToken } from './shadows';
3
14
  export type RadiusToken = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'pill';
4
15
  export type TextTone = 'default' | 'muted' | 'faint' | 'primary' | 'inherit';
5
16
  export type TextLeading = 'tight' | 'snug' | 'normal' | 'relaxed';
@@ -7,7 +18,22 @@ export type NotificationTone = 'info' | 'error' | 'neutral';
7
18
  export declare const RADIUS: Record<RadiusToken, string>;
8
19
  export declare const TEXT_TONE: Record<TextTone, string>;
9
20
  export declare const TEXT_LEADING: Record<TextLeading, string>;
21
+ export declare const SHADOW: {
22
+ readonly none: "none";
23
+ readonly card: "0 1px 4px rgba(0,0,0,0.1)";
24
+ readonly section: "0 2px 8px rgba(0,0,0,0.06)";
25
+ readonly panel: "0 12px 40px rgba(0,0,0,0.16), 0 4px 12px rgba(0,0,0,0.08)";
26
+ readonly fab: "0 2px 8px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.04)";
27
+ readonly button: "0 4px 6px -1px rgba(0,0,0,0.1)";
28
+ };
29
+ export type ShadowToken = keyof typeof SHADOW;
10
30
  export declare const TAB_BAR_HEIGHT = 48;
31
+ export declare const LAYER_TOKENS: {
32
+ screenEdgeGlow: number;
33
+ panel: number;
34
+ dialog: number;
35
+ toast: number;
36
+ };
11
37
  export declare const notificationToneStyles: Record<NotificationTone, {
12
38
  background: string;
13
39
  border: string;
@@ -1,3 +1,21 @@
1
+ /**
2
+ * The one home for turning per-tenant widget settings into semantic design tokens and the CSS custom
3
+ * properties inlined on the widget root — the widget's entire theming mechanism (no dark mode, no
4
+ * class-based themes).
5
+ *
6
+ * Contents: the `SemanticTokens` shape (color / radius / motion); `WIDGET_RADIUS_PX`, exported because
7
+ * `WidgetFab` needs the raw number for its SVG `rx`/`ry`, not a CSS string; `DURATION_ANIMATION` and
8
+ * `DURATION_FADE`; `WidgetStyleSettingsDefaults` and `WIDGET_STYLE_SETTINGS_DEFAULTS`, the five colour
9
+ * settings this file consumes and their fallbacks; `createSemanticTokens`, which resolves a partial
10
+ * settings object against those defaults and derives the muted/faint/hover/contrast variants;
11
+ * `semanticTokensToCssCustomProperties`, the token → `--var` map.
12
+ *
13
+ * Radius and both durations are fixed rather than per-tenant: every widget row in production holds these
14
+ * values and no surface writes them. Settings are filtered for explicit `undefined` before merging —
15
+ * a plain spread would let an `undefined` key shadow its default instead of falling back to it. The
16
+ * `--var` map must cover every variable `index.css` `:host` declares, or that hardcoded fallback palette
17
+ * shows through on the widget root.
18
+ */
1
19
  import type { WidgetSettingsData } from '../sdk';
2
20
  type SemanticTokens = {
3
21
  color: {
@@ -1,3 +1,16 @@
1
+ /**
2
+ * `useDragSnap` — drag the launcher (FAB) and snap it to the nearest corner. Pointer events are tracked
3
+ * in a ref; movement under DRAG_THRESHOLD_PX stays a click, beyond it the wrapper is translated on a
4
+ * rAF loop with velocity sampled so a flick lands where it was heading. On release
5
+ * `getNearestCornerByTranslation` picks the corner, the wrapper animates there for SNAP_DURATION_MS via
6
+ * `left`/`top` transitions, and `commitPositionAfterAnimation` calls `onPositionCommit` on
7
+ * `transitionend` (with a timeout fallback, since a hidden tab fires no transition events) — the
8
+ * committed corner is the one being animated TO, so two snaps in flight cannot commit the abandoned
9
+ * one (`abandonSnapRef`). `suppressUntilRef` stamps a time after which a click may open the widget
10
+ * again, so the pointer-up that ends a drag is not read as a tap. The wrapper is measured with a
11
+ * ResizeObserver in a layout effect so the pixel position is right on the first paint; preview mode
12
+ * disables everything.
13
+ */
1
14
  import React from 'react';
2
15
  import type { WidgetPosition } from '../types';
3
16
  export interface UseDragSnapOptions {
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Focus trap for the messenger panel: while `isActive`, focus starts inside `containerRef`, Tab cycles
3
+ * within it, Escape calls `onEscape`, and on deactivation focus returns to whatever held it before.
4
+ *
5
+ * The tabbable-candidate query is `utils/dom`'s shared `TABBABLE_SELECTOR`; `activeElementIn` reads the
6
+ * focused element as seen from a container's own root; `getFocusables` lists a container's visible tabbable elements; and
7
+ * `useFocusTrap(containerRef, isActive, {onEscape, focusTargetRef})` focuses `focusTargetRef` (else the
8
+ * first focusable), installs one capture-phase `keydown` listener on `document`, and restores focus on
9
+ * the active→inactive edge.
10
+ *
11
+ * Inside the widget's closed shadow root `document.activeElement` retargets to the HOST, never naming
12
+ * an element of the widget's own tree; `activeElementIn` reads through `container.getRootNode()`
13
+ * instead and is the ONE home for that retargeting — eslint's `no-restricted-properties` bans the bare
14
+ * read everywhere else. Hand-rolled on purpose: `MessengerShell` is a NON-modal panel, not a Dialog,
15
+ * and Base UI exposes no standalone focus trap; reaching it by making the panel a Dialog would inert
16
+ * the customer's page. Both key arms bail unless focus is currently inside the container, since the
17
+ * listener sits on `document` ahead of host-page handlers and an unguarded Escape would close the
18
+ * widget mid-typing. Tab `preventDefault`s only at the two ends; `previousActiveRef` edge-triggers the
19
+ * restore once on close, and `getFocusables` drops hidden/`aria-hidden` elements the selector can't express.
20
+ */
1
21
  export declare function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, isActive: boolean, options?: {
2
22
  onEscape?: () => void;
3
23
  focusTargetRef?: React.RefObject<HTMLElement | null>;
@@ -1,5 +1,15 @@
1
+ /**
2
+ * The two context hooks every widget component reads: `useWidgetConfig` for settings, `useWidget` for the store.
3
+ *
4
+ * `WidgetConfigContext` publishes every setting resolved — API settings plus the position and z-index `WidgetRoot`
5
+ * layers on top — and `useWidgetConfig` reads it, throwing outside `WidgetRoot` rather than defaulting, so nothing
6
+ * below ever touches the raw config prop or threads it down as props. `useWidget` folds the two independent stores
7
+ * (`UIStateContext`, `ChatContext`) into one memoized `{state, actions}`: `isTaskRunning` reads the canonical *wire*
8
+ * status `'running'`, never the UI-only `ChatMessage.taskStatus` vocabulary, and `isAwaitingReply` the placeholder
9
+ * message held open while a reply streams. `clearChatHistory` resets messages, task and the UI error together — an
10
+ * error left standing would outlive the chat it described.
11
+ */
1
12
  import type { ValidWidgetConfig, WidgetState } from '../types';
2
- /** Every setting resolved: API settings plus the position and script-tag overrides WidgetRoot layers on top. */
3
13
  export declare const WidgetConfigContext: import("react").Context<ValidWidgetConfig | null>;
4
14
  export declare const useWidgetConfig: () => ValidWidgetConfig;
5
15
  export declare const useWidget: () => {
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Public entry point of `@marketrix.ai/widget`: the imperative lifecycle API, the `MarketrixWidgetPreview`
3
+ * dashboard wrapper and the script-tag auto-init hook. `README.md` is the customer-facing surface these
4
+ * exports make up, named and default alike. `initWidget` is the guarded, coalescing production entry over
5
+ * `initWidgetInternal`; `mount` builds the shadow-DOM container; `unmountWidget` tears down stream,
6
+ * recorder, screen share and tree;
7
+ * `updateMarketrixConfig` re-mounts with client settings merged in, on whichever path the current mount
8
+ * came from; `mountWidget` dispatches on shape — `settings` → preview (no network), credentials → live.
9
+ *
10
+ * `previewConfig` is the one home for the preview path's validate-then-resolve, shared by the React
11
+ * component and the imperative `mountWidget`; a null return means it has already reported why. There is
12
+ * no default api host, so an unconfigured SDK would resolve every request against the HOST PAGE's origin
13
+ * and `configureSdk` runs before anything else on the production path. `widget_enabled` is the kill switch:
14
+ * unlike `show_widget`/`widget_appearance`, hidden but initialized, disabled means no chat id, stream or
15
+ * recording. `window.__mtx` guards the singleton because it survives ES-module re-execution, and every init
16
+ * and unmount bumps `lifecycleGeneration` so in-flight work abandons itself rather than mounting over a
17
+ * newer one. Preview with no `container` mounts into the div it renders, never the parent, which would size
18
+ * the widget to the wrong box. Auto-init runs on import, browser-guarded so the package stays importable from
19
+ * a server render, deferred a tick so every export exists before the tag scan.
20
+ */
1
21
  declare global {
2
22
  interface Window {
3
23
  __mtx?: {
@@ -33,14 +33,6 @@ export declare const PaginationSchema: z.ZodObject<{
33
33
  offset: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
34
34
  }, z.core.$strip>;
35
35
  type StripDefault<T> = T extends z.ZodDefault<infer Inner> ? Inner : T;
36
- /**
37
- * The ONE way to build a PATCH schema from an entity/input schema. Zod's `.partial()` makes every
38
- * field optional but does NOT stop a `.default(...)` field from being backfilled when omitted — an
39
- * update caller who sends only `{ title }` gets `options: []`/`config: {}` re-applied by parsing, so a
40
- * `field !== undefined` merge guard downstream never sees the omission and silently wipes the column.
41
- * This strips each field's default before making it optional, so "not sent" really does parse to
42
- * `undefined`.
43
- */
44
36
  export declare function partialPatch<Shape extends z.ZodRawShape>(schema: z.ZodObject<Shape>): z.ZodObject<{
45
37
  [K in keyof Shape]: z.ZodOptional<StripDefault<Shape[K]>>;
46
38
  }>;
@@ -128,14 +120,6 @@ export declare const GraphSchema: z.ZodObject<{
128
120
  }, z.core.$loose>>;
129
121
  }, z.core.$strip>;
130
122
  export type GraphData = z.infer<typeof GraphSchema>;
131
- export declare const NotificationResolvedReasonSchema: z.ZodEnum<{
132
- answered: "answered";
133
- dismissed: "dismissed";
134
- cancelled: "cancelled";
135
- }>;
136
- export type NotificationResolvedReason = z.infer<typeof NotificationResolvedReasonSchema>;
137
- /** Every live-progress stream the api opens. The app keys `useAgentProgress` on these, so a kind it
138
- * cannot name is a generator whose progress never renders. */
139
123
  export declare const AGENT_PROGRESS_KINDS: readonly ["study_plan", "qa_generate_journeys", "qa_prefill_values", "persona_generate", "persona_from_description", "persona_autocomplete"];
140
124
  export declare const AgentProgressKindSchema: z.ZodEnum<{
141
125
  study_plan: "study_plan";
@@ -1,4 +1,3 @@
1
- /** Must be called before any SDK operation — the widget has no baked-in API host. */
2
1
  export declare const configureSdk: (apiUrl: string) => void;
3
2
  export declare const sdk: {
4
3
  activityLogCreate: import("@orpc/contract").ContractProcedureClient<Record<never, never>, import("zod").ZodObject<{
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Every browser action the agent can ask the widget to take on the HOST page, as one name → handler
3
+ * registry: `tools` holds label, wait-for-user flag and handler together so no parallel list can drift, and
4
+ * `executeTool` in `show` mode highlights the target and waits for the visitor before running the handler.
5
+ *
6
+ * `typeText` trails a `blur` because some frameworks validate only on it. `extract` truncates at 10k while
7
+ * `getHtml` is uncapped — the agent's parser indexes by `data-id`, and a trimmed tree loses elements the
8
+ * loop then cannot click. `getScreenshot` reads the EXISTING share, since a fresh prompt bypasses an
9
+ * earlier Deny. `FINISH_TOOL` is one named export so dispatch, progress lines and the settlement check read
10
+ * one string. `sendKeys` dispatches the KeyboardEvent and delegates the default action the browser
11
+ * withholds to `keySimulation`. `deferred` reports an action DISPATCHED, never completed: a click or
12
+ * navigation can tear the page down before the report is read.
13
+ *
14
+ * `element` / `selectElement` are the ONE way a handler reaches a host-page node: they resolve an index
15
+ * through `domService` and THROW. `executeTool`'s catch is this file's ONLY error handler and no handler
16
+ * may add a second — a throw already reaches the agent verbatim, so a local try/catch just rewrites the
17
+ * same failure. `httpUrl` admits only http(s) — `extract` feeds the model page-controlled hrefs, so a raw
18
+ * target would let `javascript:` run in the HOST origin; its bare catch is the same verdict as a rejected
19
+ * protocol, an unparseable string being exactly a value that is not a URL.
20
+ */
1
21
  import type { InstructionType } from '../types';
2
22
  export interface TextData {
3
23
  text: string;
@@ -26,7 +46,13 @@ export type ToolExecutionResult<T = TextData> = {
26
46
  data: T;
27
47
  afterResponseAttempt?: () => void;
28
48
  } | ToolFailure;
49
+ export declare const FINISH_TOOL = "finish";
29
50
  export declare class BrowserToolService {
51
+ private readonly tools;
52
+ private element;
53
+ private selectElement;
54
+ getFriendlyToolName(browserToolName: string): string;
55
+ isWaitForUserTool(browserToolName: string): boolean;
30
56
  executeTool(browserToolName: string, args: Record<string, unknown>, mode: InstructionType, explanation?: string): Promise<ToolExecutionResult<unknown>>;
31
57
  private navigate;
32
58
  private search;
@@ -40,11 +66,6 @@ export declare class BrowserToolService {
40
66
  private selectDropdownOption;
41
67
  private getDropdownOptions;
42
68
  private sendKeys;
43
- /** Programmatic KeyboardEvents aren't "trusted", so manually reproduce each key's expected behavior. */
44
- private simulateKeyAction;
45
- /** Native value setter so React/Vue controlled inputs pick up the change. */
46
- private setNativeValue;
47
- private setValueAndCaret;
48
69
  private closeTab;
49
70
  private done;
50
71
  private getHtml;
@@ -1,4 +1,18 @@
1
+ /**
2
+ * Sends one visitor turn to the api. `chatPost` mints or reuses the chat id, files a `widget_question`
3
+ * activity-log row, then POSTs a `chat/${mode}` `WidgetCommand` over the stream.
4
+ *
5
+ * The reply never comes back from here — it arrives asynchronously on the SSE stream as `chat/delta`
6
+ * fragments and a final `chat/response`, matched by `request_id` (the caller's placeholder message id).
7
+ * That is why `StreamClient.ready` must resolve BEFORE the POST: registration is what gives the reply
8
+ * somewhere to land.
9
+ *
10
+ * The logged `chat_id` is read back from `storageService` rather than passed, so it is only correct
11
+ * after `getOrCreateChatId()` has resolved and written it. `user_id` is omitted, never sent null, when
12
+ * the host supplied no visitor identity. The activity log is telemetry and is deliberately not awaited:
13
+ * it can neither delay nor fail the send, and a rejection warns and stops there — whereas a failed
14
+ * `send` propagates to `ChatContext`, which renders the error turn.
15
+ */
1
16
  import { type InstructionType } from '../sdk';
2
17
  import { type CredentialedConfig } from './StorageService';
3
- /** The reply does not come back from here — it arrives asynchronously as a chat/response event on the stream. */
4
18
  export declare function chatPost(config: CredentialedConfig, message: string, mode: InstructionType, requestId: string): Promise<void>;
@@ -1,8 +1,6 @@
1
1
  declare class ChatSessionManager {
2
2
  private creation;
3
- /** Promise-based lock: concurrent callers all await the same creation. */
4
3
  getOrCreateChatId(): Promise<string>;
5
- private createChatId;
6
4
  }
7
5
  export declare const chatSessionManager: ChatSessionManager;
8
6
  export {};
@@ -1,3 +1,21 @@
1
+ /**
2
+ * The numbered address space the agent drives the host page by: `reindexAndSnapshot` walks the live document and
3
+ * returns a clone stamped with `data-id="<n>"` on every interactive element; `getSequenceForElement` is the reverse
4
+ * lookup; `getValidatedElement` resolves an index back to a live element or a `ValidatedElementResult` error;
5
+ * `notInteractableReason` phrases why an element cannot be acted on; `generateAnchoredSelector` and `indexElements`
6
+ * build the walk and its body-anchored selectors. `domService` is the process-wide singleton.
7
+ *
8
+ * `data-id` is the whole contract with the agent, so the clone is tagged by re-querying each stored selector rather
9
+ * than walking the two trees in step — a synced walk breaks on modals and fixed elements. Indices are addresses the
10
+ * agent holds across turns: each entry snapshots IDENTITY_ATTRIBUTES so a changed element reads as DOM_CHANGED
11
+ * rather than silently acting on a control the agent no longer means.
12
+ *
13
+ * The tree walker keeps an element with a null `offsetParent` when it or an ancestor is fixed/sticky, since the
14
+ * browser reports no offsetParent for those even when visible. The obscured test ignores Marketrix's own chrome
15
+ * (Show-mode highlight/popup, the widget's shadow host) since those legitimately sit over the element they point
16
+ * at. `notInteractableReason`'s `document.body.contains` check must stay first: ShowModeService uses it as its
17
+ * removal watchdog and keeps no identity snapshot of its own.
18
+ */
1
19
  export interface ValidatedElementResult {
2
20
  element: HTMLElement | null;
3
21
  error?: string;
@@ -6,12 +24,9 @@ export declare class DomService {
6
24
  private index;
7
25
  private elementToSequence;
8
26
  private generateAnchoredSelector;
9
- private staleReason;
10
27
  private indexElements;
11
- /** `data-id` is the whole contract: the agent parses the snapshot for `[data-id]` and reads nothing else off it. */
12
28
  reindexAndSnapshot(): string;
13
29
  getSequenceForElement(element: Element): number | undefined;
14
- private clearIndex;
15
30
  notInteractableReason(element: HTMLElement, index: number): string | null;
16
31
  getValidatedElement(index: number): ValidatedElementResult;
17
32
  }
@@ -10,6 +10,5 @@ export declare class RrwebSessionRecorder {
10
10
  constructor(chatId: string, applicationId: number);
11
11
  start(): Promise<void>;
12
12
  stop(): void;
13
- private buffer;
14
13
  private flush;
15
14
  }
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Show mode's on-page coaching overlay: highlights one host-page element, explains the step beside it and
3
+ * returns a promise that settles when the visitor acts, so `BrowserToolService` can await a `show`-mode tool
4
+ * call before running the tool. `showToolAction` mounts highlight and popup and returns the pending promise,
5
+ * or the in-flight one for an identical restage (a duplicate tool call); `cleanup` cancels and unwinds;
6
+ * `showModeService` is the singleton every caller uses.
7
+ *
8
+ * Exactly one settle: the element click, the Continue button and both watchdog branches race, so both
9
+ * settlers are DETACHED before either fires and the off-screen branch returns rather than falling through.
10
+ * The highlight is `pointer-events:none` so the visitor's click reaches the real element; click and
11
+ * reposition handlers sit on `document` in the CAPTURE phase — the former tests `composedPath`, which sees
12
+ * through Shadow DOM retargeting, and prevents default so navigation cannot precede the result; the latter
13
+ * tracks a scrolling container, not just the window. `#marketrix-show-highlight` and `#marketrix-show-popup`
14
+ * are load-bearing ids: `DomService.notInteractableReason` allowlists them so the overlay never reads as an
15
+ * obscuring modal, and cleanup re-finds them because an interrupted node outlives its handle. The highlight's
16
+ * cssText is one line because template-literal whitespace is not minified. `trackElement` is the one place the
17
+ * highlight is sized and placed over its element, so the first paint and every reposition run through it;
18
+ * placement takes the first of right/left/above/below that fits then clamps, its 120px height an assumption,
19
+ * and the watchdog tests `document.body.contains` first, covering removal as well as occlusion.
20
+ */
1
21
  export interface ShowModeOptions {
2
22
  element: HTMLElement;
3
23
  explanation: string;
@@ -17,18 +37,15 @@ export declare class ShowModeService {
17
37
  private visibilityCheckInterval;
18
38
  showToolAction(options: ShowModeOptions): Promise<void>;
19
39
  cleanup(): void;
20
- private completeAction;
21
- /** Detach both settlers before calling one — the click handler, the Continue button and the two watchdogs race. */
40
+ private settle;
22
41
  private takeSettlers;
23
42
  private createHighlight;
43
+ private trackElement;
24
44
  private createPopup;
25
45
  private setupPositionUpdates;
26
46
  private updatePopupPosition;
27
47
  private setupClickHandler;
28
- private setupContinueButton;
29
- /** notInteractableReason's first test is `document.body.contains`, so this one watchdog also covers removal. */
30
48
  private setupVisibilityMonitoring;
31
- private failShowAction;
32
49
  private escapeHtml;
33
50
  }
34
51
  export declare const showModeService: ShowModeService;
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Browser-local persistence for the widget: the one door to `localStorage`, and the per-tenant chat context
3
+ * (chat_id, transcript, composer mode, open state, resolved config) behind the `storageService` singleton.
4
+ * `tenantScope` is the scope suffix for every browser-local key — the credential id, else the application id, else
5
+ * `default` — shared with the drag-position and resize keys so all of them partition the same way. `readLocal` /
6
+ * `writeLocal` are the only `localStorage` access in `src/`; a host page can deny storage outright (third-party
7
+ * cookies off, sandboxed iframe) where even reading throws, so both degrade to a warn and the widget keeps working
8
+ * unpersisted. `loadContext` parses one key merged over `DEFAULT_CONTEXT`, so a payload from an older widget
9
+ * version reads as incomplete rather than corrupt, and discards anything older than `CONTEXT_EXPIRY_MS` (7 days).
10
+ * `StorageService`'s `getContext`, `updateContext` (merges, restamps `timestamp`, writes through), `getChatId` /
11
+ * `setChatId`, `getCredentialedConfig` (null unless both `mtxId` and `mtxKey` are present, so callers cannot
12
+ * dispatch half-credentialed) and `setConfig` sit behind the singleton; `readChatSnapshot` / `writeChatSnapshot`
13
+ * are the UI-facing view of the context. `setConfig` re-keys storage to `${STORAGE_KEY}_${tenantScope}` and
14
+ * reloads from that key, since without it two applications embedded on one origin would share a stored chat_id and
15
+ * one tenant's transcript would leak into another's.
16
+ *
17
+ * The snapshot is `{messages, currentMode, isOpen}` — chat_id, config and timestamp are deliberately excluded.
18
+ * Reading revives `timestamp` to a `Date` and backfills a text part for messages stored before `parts` existed;
19
+ * writing drops `videoStream` (unserializable, dead on reload), rewriting it as `Screen sharing ended`.
20
+ */
1
21
  import type { ChatMessage, InstructionType, MarketrixConfig, ValidWidgetConfig } from '../types';
2
22
  export type StoredMessage = Omit<ChatMessage, 'videoStream' | 'timestamp'> & {
3
23
  timestamp: string;
@@ -17,7 +37,6 @@ export type MarketrixChatContext = Omit<ChatSnapshot, 'messages'> & {
17
37
  config: MarketrixConfig | null;
18
38
  timestamp: number;
19
39
  };
20
- /** Per-tenant scope for browser-local keys: the credential id, else the application id. */
21
40
  export declare function tenantScope(config: MarketrixConfig): string;
22
41
  export declare function readLocal(key: string): string | null;
23
42
  export declare function writeLocal(key: string, value: string): void;
@@ -1,5 +1,24 @@
1
+ /**
2
+ * Singleton SSE transport between the widget and the api: `StreamClient.getInstance` drains one `widgetStream`
3
+ * iterator in the background, `send` posts via `widgetMessagePost`, `ready` connects then waits,
4
+ * `waitUntilRegistered` parks a caller, `canReconnect`/`reconnectNow` back the Retry affordance, `disconnect`
5
+ * tears down and rejects parked callers, and `StreamGaveUpError` marks a stream that has stopped retrying.
6
+ *
7
+ * `open` is the transport, `registered` is the chat: only the latter can carry a reply, so `isConnected` reads
8
+ * `registered` and nothing waits on `open`. Backoff counters reset only on `registered` — resetting at `open` would
9
+ * defeat the max-attempts cap if registration never lands and the stream flaps open→closed. Tabs share the
10
+ * localStorage chat id, so the server keys SSE by (chat_id, tab_id) and `tabId` stops tabs evicting each other's
11
+ * stream. Credentials are read at connect time, not captured at init, so a reconnect after `updateMarketrixConfig`
12
+ * dials with the current ones.
13
+ *
14
+ * A `chat/error` whose `request_id === 'auth'` is non-retriable: `chat/error` otherwise settles the message whose
15
+ * id is the request id, and no message is ever id `'auth'`, so without the explicit `giveUp` here the widget went
16
+ * permanently silent — no toast, no bubble, and (console dropped by terser) no trace. Both give-up messages are
17
+ * read by a visitor on a customer's page, so they name the state and the way out rather than the counter, and
18
+ * `giveUp` needs no console line of its own for the same reason. A dial or stream that will be retried warns;
19
+ * only the rejected credential is an error, being the one failure nothing here recovers from.
20
+ */
1
21
  import { type WidgetCommand, type WidgetEvent } from '../sdk';
2
- /** The stream has stopped retrying, so nothing further will arrive for anything still in flight. */
3
22
  export declare class StreamGaveUpError extends Error {
4
23
  }
5
24
  export interface StreamClientCallbacks {
@@ -36,6 +55,7 @@ export declare class StreamClient {
36
55
  private consumeEvents;
37
56
  disconnect(): void;
38
57
  send(command: WidgetCommand): Promise<void>;
58
+ private resetBackoff;
39
59
  private notifyError;
40
60
  private settleWaiters;
41
61
  private giveUp;
@@ -0,0 +1,3 @@
1
+ export declare const isTextField: (el: Element) => el is HTMLInputElement | HTMLTextAreaElement;
2
+ export declare function simulateKeyAction(element: HTMLElement, key: string): string | null;
3
+ export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): void;
@@ -1,4 +1,15 @@
1
- import type { MarketrixConfig, WidgetSettingsData } from '../types';
2
- type MockWidgetConfig = MarketrixConfig & Pick<WidgetSettingsData, 'widget_border_radius' | 'widget_font_size' | 'widget_animation_duration' | 'widget_fade_duration'>;
1
+ /**
2
+ * The shared widget-settings fixture the config-driven tests build from: `getMockWidgetConfig(overrides)`
3
+ * returns a complete, schema-valid tenant config — every rendered setting plus the `mtxId`/`mtxKey` credential,
4
+ * api host and preview flag a mounted widget needs — shallow-merged with `overrides`.
5
+ *
6
+ * `MockWidgetConfig` widens `ValidWidgetConfig` with the four render constants (`widget_border_radius`,
7
+ * `widget_font_size`, `widget_animation_duration`, `widget_fade_duration`). `ValidWidgetConfig` is built on
8
+ * `WidgetRenderedSettings`, which omits them, but callers hand this fixture straight to
9
+ * `WidgetSettingsDataSchema.parse()`, which demands the whole wire shape. It is `Valid`, not partial, so
10
+ * `renderWidget` can hand it to `WidgetRoot` without a cast — the fixture really does set every field.
11
+ */
12
+ import type { ValidWidgetConfig, WidgetSettingsData } from '../types';
13
+ type MockWidgetConfig = ValidWidgetConfig & Pick<WidgetSettingsData, 'widget_border_radius' | 'widget_font_size' | 'widget_animation_duration' | 'widget_fade_duration'>;
3
14
  export declare function getMockWidgetConfig(overrides?: Partial<MockWidgetConfig>): MockWidgetConfig;
4
15
  export {};
@@ -0,0 +1,6 @@
1
+ import { getMockWidgetConfig } from './fixtures';
2
+ export declare function renderWidget(overrides?: Parameters<typeof getMockWidgetConfig>[0], { previewMode }?: {
3
+ previewMode?: boolean;
4
+ }): import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries"), HTMLElement, HTMLElement>;
5
+ export declare const openWidget: () => void;
6
+ export declare const openChatTab: () => void;
@@ -1 +1,15 @@
1
+ /**
2
+ * Vitest global setup (`setupFiles` in `vitest.config.ts`), run once per test file before any suite:
3
+ * registers the jest-dom matchers and fills the three browser globals jsdom does not usably provide here.
4
+ *
5
+ * `localStorage` — a Map-backed `Storage` (getItem/setItem/removeItem/clear/length/key). jsdom defines it
6
+ * on the prototype, and that does not transfer to vitest's `globalThis`, leaving `StorageService` — the one
7
+ * door to it in `src/` — an object with no `setItem`; hence the guard probes for the method, not the property.
8
+ * `matchMedia` — a never-matching `MediaQueryList` with inert listener registration, required by
9
+ * `useScrollLock`. `ResizeObserver` — an inert observe/unobserve/disconnect class, required by
10
+ * `useDragSnap`.
11
+ *
12
+ * Every fill is conditional, so a real implementation — a future jsdom, or a per-test override
13
+ * installed before this file — wins.
14
+ */
1
15
  import '@testing-library/jest-dom/vitest';
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Widget-wide shared types: the config shapes a host supplies, the chat message/part model, the UI
3
+ * store shape, and `messageText` — the one place a message's displayed text is derived.
4
+ *
5
+ * `ClientOwnedConfig` holds host-supplied knobs the api never sends (`show_widget: false` still
6
+ * initializes the widget fully and only hides its UI; `use_screenshare: false` auto-denies
7
+ * screen-access requests and hides the Share Screen button; both default true). `MarketrixConfig` is
8
+ * deliberately FLAT so api settings spread straight in — `mtxId`+`mtxKey` is the credential, while
9
+ * `mtxApp` is stamped internally after validation and never an input, since an application id is
10
+ * guessable and authenticates nothing. `ValidWidgetConfig` is a `MarketrixConfig` run through
11
+ * `parseWidgetSettings`, so every rendered setting is present. `ChatMessage`+`MessagePart` are the chat
12
+ * model; `WidgetState` is the flattened read model `useWidget()` folds from `UIStateContext`+
13
+ * `ChatContext`; `WidgetPosition` re-aliases `widget_position` as the drag-snap/resize corner
14
+ * vocabulary; `AddWidgetConfig`'s union makes `settings` and `mtxId`/`mtxKey` mutually exclusive.
15
+ * `ChatMessage.pendingContent` queues a message behind an open screen-access request, sent once it
16
+ * resolves. A `streaming` `MessagePart` accumulates `chat/delta` fragments until the final
17
+ * `chat/response` replaces it. `taskStatus`/`MessagePart.status` are presentational only, not the wire
18
+ * vocabulary (`task/status.status`). `messageText` joins text parts and IS the text; `content` is kept
19
+ * equal to it by every writer.
20
+ */
1
21
  import type { InstructionType, WidgetSettingsData } from '../sdk';
2
22
  import type { WidgetRenderedSettings } from '../utils/validation';
3
23
  export type { InstructionType, WidgetSettingsData } from '../sdk';
@@ -5,9 +25,7 @@ export interface ClientOwnedConfig {
5
25
  mtxApiHost?: string;
6
26
  userId?: number;
7
27
  widget_position_z_index?: number;
8
- /** When false, widget initializes fully but UI is hidden. Default: true */
9
28
  show_widget?: boolean;
10
- /** When false, screen access requests are auto-denied and Share Screen button is hidden. Default: true */
11
29
  use_screenshare?: boolean;
12
30
  }
13
31
  export type MarketrixConfig = Partial<WidgetRenderedSettings> & ClientOwnedConfig & {
@@ -16,7 +34,6 @@ export type MarketrixConfig = Partial<WidgetRenderedSettings> & ClientOwnedConfi
16
34
  mtxApp?: number;
17
35
  isPreviewMode?: boolean;
18
36
  };
19
- /** A MarketrixConfig that has been through parseWidgetSettings — every rendered setting present. */
20
37
  export type ValidWidgetConfig = MarketrixConfig & Required<Pick<MarketrixConfig, keyof WidgetRenderedSettings>>;
21
38
  export interface ChatMessage {
22
39
  id: string;
@@ -27,7 +44,6 @@ export interface ChatMessage {
27
44
  videoStream?: MediaStream;
28
45
  isScreenAccessRequest?: boolean;
29
46
  screenShareStatus?: 'allowed' | 'denied';
30
- /** The message queued behind an open screen-access request, sent once it resolves. */
31
47
  pendingContent?: string;
32
48
  isSystemMessage?: boolean;
33
49
  isPlaceholder?: boolean;
@@ -40,11 +56,8 @@ export interface MessagePart {
40
56
  content: string;
41
57
  status?: 'in_progress' | 'completed' | 'failed';
42
58
  browserToolName?: string;
43
- /** chat/delta fragments accumulate into this part; the final chat/response replaces it. */
44
59
  streaming?: boolean;
45
60
  }
46
- /** A message's text is the text it shows: its text parts joined. `content` is this value, kept by the
47
- * writers, so a reader never has to know which of the two fields is authoritative. */
48
61
  export declare const messageText: (parts: MessagePart[]) => string;
49
62
  export type WidgetView = 'home' | 'chat';
50
63
  export interface WidgetState {
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Pure helpers for the chat message list — the mode label, the hh:mm timestamp, the newest-first
3
+ * `lastIndexWhere` (also used by `useScreenShare`), and `addProgressLine` / `markProgressLineComplete` /
4
+ * `markProgressLineFailed`, which append or settle the open progress part for one `browserToolName` via
5
+ * `openLineFor` and the shared `patchPart` copy-on-write. `createMessage` and its per-sender constructors are
6
+ * the ONLY way a `ChatMessage` is built: ids are `<prefix>-<uuid>` since two messages minted in one millisecond
7
+ * used to collide, and empty content yields no `text` part, the screen-share bubble rendering from `videoStream`
8
+ * alone and a placeholder having nothing to say yet. `SCREEN_ACCESS_PROMPT` is the one wording of the
9
+ * screen-access ask, the transcript card and the toolbar dialog being two renderings of one question.
10
+ *
11
+ * `findMessageForProgress` picks the agent reply a `tool/call` or progress event renders into, by ranked
12
+ * predicates: the first rank matching anything wins, within a rank the newest message, and no match at all is a
13
+ * legitimate outcome, warned not thrown. Every rank is bounded to messages after the last agent message carrying
14
+ * a `taskStatus`, since a terminal stamp means that run already ended — unbounded, a late event reaches back past
15
+ * the stamp onto an already-settled reply and a late `completed` overwrites a `stopped` icon. Placeholders with
16
+ * `mode` still undefined match leniently and a mode-agnostic rank is always appended, since a `tool/call` can
17
+ * arrive before the mode is set. `filterCancellationText` strips "cancelled by cleanup" from progress content
18
+ * and error text — chatter from a torn-down run a visitor should never see.
19
+ */
1
20
  import type { ChatMessage, InstructionType } from '../types';
2
21
  export declare const getModeDisplayName: (mode: InstructionType) => string;
3
22
  export declare const formatMessageTime: (date: Date | undefined) => string;
@@ -7,19 +26,17 @@ export interface FindMessageOptions {
7
26
  currentMode: InstructionType;
8
27
  }
9
28
  export declare function lastIndexWhere<T>(items: T[], matches: (item: T) => boolean): number;
10
- /** Ranked predicates: the first rank matching anything wins, and within it the newest message. */
11
29
  export declare function findMessageForProgress({ messages, isTaskRunning, currentMode, }: FindMessageOptions): {
12
30
  index: number;
13
31
  message: ChatMessage;
14
32
  } | null;
15
- export declare const WAIT_FOR_USER_TOOLS: Set<string>;
16
33
  export declare function addProgressLine(message: ChatMessage, browserToolName: string, explanation: string): ChatMessage;
17
34
  export declare const markProgressLineComplete: (message: ChatMessage, browserToolName: string) => ChatMessage;
18
35
  export declare function markProgressLineFailed(message: ChatMessage, browserToolName: string, error: string): ChatMessage;
19
- export declare const TOOL_LABELS: Map<string, string>;
20
- export declare const getFriendlyToolName: (browserToolName: string) => string;
21
36
  export declare const createUserMessage: (content: string, mode?: InstructionType, idPrefix?: string) => ChatMessage;
22
37
  export declare const createAgentMessage: (content: string) => ChatMessage;
23
38
  export declare const createSystemMessage: (content: string, idPrefix: string) => ChatMessage;
39
+ export declare const SCREEN_ACCESS_PROMPT = "Can I take a look at your screen?";
24
40
  export declare const createScreenAccessRequestMessage: (mode: InstructionType | undefined, pendingContent?: string) => ChatMessage;
25
41
  export declare const createScreenshareMessage: (stream: MediaStream, mode?: InstructionType) => ChatMessage;
42
+ export declare const createPlaceholderMessage: (mode: InstructionType) => ChatMessage;