@magicx-eng/ai-autocomplete-vanilla 0.5.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -323,7 +323,19 @@ The object passed to `onSubmit`:
323
323
  |---|---|---|
324
324
  | `query` | `string` | Plain text as the user sees it. |
325
325
  | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
326
- | `completed_params` | `CompletedParam[]` | Array of filled parameter values. |
326
+ | `completed_params` | `CompletedParam[]` | Array of filled parameter values, followed by any the user skipped (see below). |
327
+
328
+ #### Skipped parameters
329
+
330
+ Pressing <kbd>→</kbd> at the end of the input dismisses the active pill. The dismissal is reported to the server — and included in `completed_params` here — as an entry with no placeholder and the sentinel text `"skipped"`:
331
+
332
+ ```ts
333
+ { placeholder: "", type: "goal", text: "skipped", kind: null }
334
+ ```
335
+
336
+ Skipped entries are appended after the filled params (they have no position in the query) and are deduped by type. A skip is dropped if a param of the same type ends up filled anyway. Skipping the last available pill triggers an immediate request so the server can suggest something else; earlier skips ride along on the next request. `reset()` clears them.
337
+
338
+ > **Reading `state.skippedParams` directly:** the array is append-only until `reset()`. The "drop a skip whose type got filled" rule is applied when the payload is built, not by pruning the array — so if the user skips `goal` and later fills one, the raw array still holds the `goal` entry. That's deliberate: the filter self-heals if they then delete that param's text, where pruning would discard the signal for good. Apply the same rule yourself with the exported `withSkippedParams(completedParams, skippedParams)`.
327
339
 
328
340
  ### Event Subscription
329
341
 
package/dist/index.d.mts CHANGED
@@ -23,11 +23,28 @@ interface InputItem {
23
23
  type: string;
24
24
  text: string;
25
25
  state: "completed" | "in_progress";
26
+ /** Absent or "selected" = echo of client params; "identified" = LLM-inferred. */
27
+ source?: "selected" | "identified";
28
+ }
29
+ /** Wire shape for an LLM-identified param echoed back to the server. */
30
+ interface IdentifiedParam {
31
+ type: string;
32
+ value: string;
33
+ }
34
+ /**
35
+ * Wire shape for a recently-suggested hint entry: a suggestion that was on
36
+ * screen when the user started typing the current unresolved trailing text.
37
+ */
38
+ interface RecentlySuggested {
39
+ type: string;
40
+ text: string;
26
41
  }
27
42
  interface AutocompleteRequest {
28
43
  data: {
29
44
  raw_query: string;
30
45
  completed_params: CompletedParam[];
46
+ identified_params?: IdentifiedParam[];
47
+ recently_suggested?: RecentlySuggested[];
31
48
  contact_account_count?: number;
32
49
  };
33
50
  meta: {
@@ -61,6 +78,40 @@ interface CompletedParamState extends CompletedParam {
61
78
  options: SuggestionOption[];
62
79
  metadata?: Record<string, unknown>;
63
80
  }
81
+ /**
82
+ * A suggestion the user dismissed with the skip key (→) instead of filling.
83
+ *
84
+ * Client-only bookkeeping. A skipped suggestion has no text in the input, so
85
+ * it can't live in `completedParams` — that array is reconciled against the
86
+ * input on every keystroke and anything missing from the text is dropped.
87
+ * Skipped entries are folded into the wire `completed_params` array (with
88
+ * `text: "skipped"`, no placeholder) only when a request or a submit result is
89
+ * built. See `withSkippedParams`.
90
+ *
91
+ * The array is append-only until `reset()` — a skip is filtered out at
92
+ * build time when a param of its type ended up filled, not pruned here.
93
+ * That's deliberate: the filter self-heals if the user later deletes that
94
+ * param's text (the skip reappears in the payload), where pruning would have
95
+ * discarded the signal permanently. Consumers reading this array directly
96
+ * should apply the same filter — `withSkippedParams` is exported for it.
97
+ */
98
+ interface SkippedParamState {
99
+ id: string;
100
+ /** The skipped suggestion's `type` (e.g. "goal"). */
101
+ type: string;
102
+ /** The suggestion's display text at skip time. Introspection only — never sent. */
103
+ suggestionPlaceholder: string;
104
+ }
105
+ /**
106
+ * Client-side state for an LLM-identified param. Tentative — replaced
107
+ * wholesale from each response (latest wins) and dropped when its text no
108
+ * longer matches. Never placeholder-substituted in raw_query.
109
+ */
110
+ interface IdentifiedParamState {
111
+ id: string;
112
+ type: string;
113
+ text: string;
114
+ }
64
115
  type Segment = {
65
116
  type: "text";
66
117
  value: string;
@@ -68,6 +119,10 @@ type Segment = {
68
119
  type: "completed";
69
120
  value: string;
70
121
  param: CompletedParamState;
122
+ } | {
123
+ type: "identified";
124
+ value: string;
125
+ param: IdentifiedParamState;
71
126
  };
72
127
  type AppearanceMode = "light" | "dark" | "auto";
73
128
  interface APIConfigBase {
@@ -105,6 +160,33 @@ interface AutocompleteResult {
105
160
  interface CoreInputState {
106
161
  text: string;
107
162
  completedParams: CompletedParamState[];
163
+ /**
164
+ * LLM-identified params from the latest response. Revisable: replaced
165
+ * wholesale per response, dropped when their text is edited away. Never
166
+ * override or overlap completed params.
167
+ */
168
+ identifiedParams: IdentifiedParamState[];
169
+ /**
170
+ * Suggestions the user dismissed with the skip key (→). Held apart from
171
+ * `completedParams` because they have no text in the input; folded into the
172
+ * wire `completed_params` array (as `text: "skipped"`) on every request and
173
+ * on the submit result. Append-only until `reset()` — see
174
+ * {@link SkippedParamState} for why skips of a since-filled type are
175
+ * filtered at build time rather than pruned here.
176
+ */
177
+ skippedParams: SkippedParamState[];
178
+ /**
179
+ * Open while the user has unresolved trailing text: anchored at the covered
180
+ * offset where they started typing, snapshotting the actionable suggestions
181
+ * that were on screen. While open, fetches carry `recently_suggested`
182
+ * (snapshot ∪ current on-screen suggestions). Closes when the trailing text
183
+ * becomes covered by a completed/identified param, is deleted back to the
184
+ * anchor, is consumed by an option selection, or on reset().
185
+ */
186
+ pendingSpan: {
187
+ anchor: number;
188
+ snapshot: Suggestion[];
189
+ } | null;
108
190
  suggestions: Suggestion[];
109
191
  activeDropdownIndex: number;
110
192
  newParamId: string | null;
@@ -325,6 +407,14 @@ declare class AIAutocomplete {
325
407
  /** Auto-clear newParamId after shimmer animation. */
326
408
  private subscribeNewParamTimer;
327
409
  private handleChange;
410
+ /**
411
+ * Opens the pending span on the first keystroke past covered text: there is
412
+ * unresolved trailing text beyond the covered offset (filterBase / last pill
413
+ * end), actionable suggestions are on screen, and no span is already open.
414
+ * The span snapshots those suggestions so `recently_suggested` can carry
415
+ * them even after later responses replace what's on screen.
416
+ */
417
+ private maybeOpenPendingSpan;
328
418
  /**
329
419
  * In re-edit mode, once the user has typed enough that no *tappable* options
330
420
  * still match (non-tappable options are kept by filterOptions regardless of
@@ -336,6 +426,8 @@ declare class AIAutocomplete {
336
426
  * subscription early-returns on subsequent fires.
337
427
  */
338
428
  private maybeExitReEditOnNoMatch;
429
+ /** Fire an immediate (undebounced) fetch for the current text + params. */
430
+ private fetchNow;
339
431
  /**
340
432
  * When the user has typed text that exactly matches (case-insensitive) one
341
433
  * of the active suggestion's options, promote it to a completed param right
@@ -439,7 +531,12 @@ interface BuildQueryResult {
439
531
  * replaces each completed param's text in the string with a {{TYPE_N}} token,
440
532
  * and returns the transformed query + params with placeholders filled in.
441
533
  *
442
- * Replacements happen left-to-right, first occurrence only per param.
534
+ * Replacements advance a position cursor (params are appended in text order in
535
+ * normal flows), so a short param value (e.g. quantity "1") can never match
536
+ * INSIDE an already-inserted placeholder (e.g. the "1" in "{{SIZE_1}}") and
537
+ * splice into it. Out-of-order params fall back to a from-zero rescan that
538
+ * rejects any match overlapping a previously inserted placeholder; with no
539
+ * clean match the param is left unreplaced (same as the text-not-found path).
443
540
  * Counter is per-type (e.g. {{TASK_1}}, {{GOAL_1}}, {{GOAL_2}}).
444
541
  */
445
542
  declare function buildQuery(text: string, completedParams: CompletedParamState[]): BuildQueryResult;
@@ -474,4 +571,36 @@ declare class ModeController {
474
571
  private detachListener;
475
572
  }
476
573
 
477
- export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type InputItem, ModeController, type OptionOverrides, type RenderMode, type Segment, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset };
574
+ /**
575
+ * Sentinel `text` marking a `completed_params` entry the user skipped (→)
576
+ * rather than filled. Sent regardless of `maskCompletedText` — it's a fixed
577
+ * marker, never user-entered content.
578
+ */
579
+ declare const SKIPPED_PARAM_TEXT = "skipped";
580
+ /**
581
+ * Folds skipped suggestions into a wire `completed_params` array so the server
582
+ * learns which parameters the user dismissed and can stop re-suggesting them.
583
+ *
584
+ * Skipped entries carry no placeholder: nothing was substituted into
585
+ * `raw_query`, so a `{{TYPE_N}}` token would point at text that doesn't exist.
586
+ * They're appended after the real params for the same reason — they have no
587
+ * position in the query.
588
+ *
589
+ * A skip is dropped when a param of the same type ends up filled anyway (the
590
+ * user skipped `goal`, then typed one): sending both would tell the server the
591
+ * parameter is simultaneously answered and declined.
592
+ */
593
+ declare function withSkippedParams(completed: CompletedParam[], skipped: SkippedParamState[]): CompletedParam[];
594
+
595
+ /**
596
+ * Builds the `AutocompleteResult` handed to `onSubmit`: the placeholder-
597
+ * tokenized raw query plus the completed params, with skipped suggestions
598
+ * folded in (see {@link withSkippedParams}).
599
+ *
600
+ * Shared by every submit path — vanilla Enter / submit button, the React Tier 1
601
+ * component, the Angular Tier 1 component — so they can't drift on what a
602
+ * result contains.
603
+ */
604
+ declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
605
+
606
+ export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
package/dist/index.d.ts CHANGED
@@ -23,11 +23,28 @@ interface InputItem {
23
23
  type: string;
24
24
  text: string;
25
25
  state: "completed" | "in_progress";
26
+ /** Absent or "selected" = echo of client params; "identified" = LLM-inferred. */
27
+ source?: "selected" | "identified";
28
+ }
29
+ /** Wire shape for an LLM-identified param echoed back to the server. */
30
+ interface IdentifiedParam {
31
+ type: string;
32
+ value: string;
33
+ }
34
+ /**
35
+ * Wire shape for a recently-suggested hint entry: a suggestion that was on
36
+ * screen when the user started typing the current unresolved trailing text.
37
+ */
38
+ interface RecentlySuggested {
39
+ type: string;
40
+ text: string;
26
41
  }
27
42
  interface AutocompleteRequest {
28
43
  data: {
29
44
  raw_query: string;
30
45
  completed_params: CompletedParam[];
46
+ identified_params?: IdentifiedParam[];
47
+ recently_suggested?: RecentlySuggested[];
31
48
  contact_account_count?: number;
32
49
  };
33
50
  meta: {
@@ -61,6 +78,40 @@ interface CompletedParamState extends CompletedParam {
61
78
  options: SuggestionOption[];
62
79
  metadata?: Record<string, unknown>;
63
80
  }
81
+ /**
82
+ * A suggestion the user dismissed with the skip key (→) instead of filling.
83
+ *
84
+ * Client-only bookkeeping. A skipped suggestion has no text in the input, so
85
+ * it can't live in `completedParams` — that array is reconciled against the
86
+ * input on every keystroke and anything missing from the text is dropped.
87
+ * Skipped entries are folded into the wire `completed_params` array (with
88
+ * `text: "skipped"`, no placeholder) only when a request or a submit result is
89
+ * built. See `withSkippedParams`.
90
+ *
91
+ * The array is append-only until `reset()` — a skip is filtered out at
92
+ * build time when a param of its type ended up filled, not pruned here.
93
+ * That's deliberate: the filter self-heals if the user later deletes that
94
+ * param's text (the skip reappears in the payload), where pruning would have
95
+ * discarded the signal permanently. Consumers reading this array directly
96
+ * should apply the same filter — `withSkippedParams` is exported for it.
97
+ */
98
+ interface SkippedParamState {
99
+ id: string;
100
+ /** The skipped suggestion's `type` (e.g. "goal"). */
101
+ type: string;
102
+ /** The suggestion's display text at skip time. Introspection only — never sent. */
103
+ suggestionPlaceholder: string;
104
+ }
105
+ /**
106
+ * Client-side state for an LLM-identified param. Tentative — replaced
107
+ * wholesale from each response (latest wins) and dropped when its text no
108
+ * longer matches. Never placeholder-substituted in raw_query.
109
+ */
110
+ interface IdentifiedParamState {
111
+ id: string;
112
+ type: string;
113
+ text: string;
114
+ }
64
115
  type Segment = {
65
116
  type: "text";
66
117
  value: string;
@@ -68,6 +119,10 @@ type Segment = {
68
119
  type: "completed";
69
120
  value: string;
70
121
  param: CompletedParamState;
122
+ } | {
123
+ type: "identified";
124
+ value: string;
125
+ param: IdentifiedParamState;
71
126
  };
72
127
  type AppearanceMode = "light" | "dark" | "auto";
73
128
  interface APIConfigBase {
@@ -105,6 +160,33 @@ interface AutocompleteResult {
105
160
  interface CoreInputState {
106
161
  text: string;
107
162
  completedParams: CompletedParamState[];
163
+ /**
164
+ * LLM-identified params from the latest response. Revisable: replaced
165
+ * wholesale per response, dropped when their text is edited away. Never
166
+ * override or overlap completed params.
167
+ */
168
+ identifiedParams: IdentifiedParamState[];
169
+ /**
170
+ * Suggestions the user dismissed with the skip key (→). Held apart from
171
+ * `completedParams` because they have no text in the input; folded into the
172
+ * wire `completed_params` array (as `text: "skipped"`) on every request and
173
+ * on the submit result. Append-only until `reset()` — see
174
+ * {@link SkippedParamState} for why skips of a since-filled type are
175
+ * filtered at build time rather than pruned here.
176
+ */
177
+ skippedParams: SkippedParamState[];
178
+ /**
179
+ * Open while the user has unresolved trailing text: anchored at the covered
180
+ * offset where they started typing, snapshotting the actionable suggestions
181
+ * that were on screen. While open, fetches carry `recently_suggested`
182
+ * (snapshot ∪ current on-screen suggestions). Closes when the trailing text
183
+ * becomes covered by a completed/identified param, is deleted back to the
184
+ * anchor, is consumed by an option selection, or on reset().
185
+ */
186
+ pendingSpan: {
187
+ anchor: number;
188
+ snapshot: Suggestion[];
189
+ } | null;
108
190
  suggestions: Suggestion[];
109
191
  activeDropdownIndex: number;
110
192
  newParamId: string | null;
@@ -325,6 +407,14 @@ declare class AIAutocomplete {
325
407
  /** Auto-clear newParamId after shimmer animation. */
326
408
  private subscribeNewParamTimer;
327
409
  private handleChange;
410
+ /**
411
+ * Opens the pending span on the first keystroke past covered text: there is
412
+ * unresolved trailing text beyond the covered offset (filterBase / last pill
413
+ * end), actionable suggestions are on screen, and no span is already open.
414
+ * The span snapshots those suggestions so `recently_suggested` can carry
415
+ * them even after later responses replace what's on screen.
416
+ */
417
+ private maybeOpenPendingSpan;
328
418
  /**
329
419
  * In re-edit mode, once the user has typed enough that no *tappable* options
330
420
  * still match (non-tappable options are kept by filterOptions regardless of
@@ -336,6 +426,8 @@ declare class AIAutocomplete {
336
426
  * subscription early-returns on subsequent fires.
337
427
  */
338
428
  private maybeExitReEditOnNoMatch;
429
+ /** Fire an immediate (undebounced) fetch for the current text + params. */
430
+ private fetchNow;
339
431
  /**
340
432
  * When the user has typed text that exactly matches (case-insensitive) one
341
433
  * of the active suggestion's options, promote it to a completed param right
@@ -439,7 +531,12 @@ interface BuildQueryResult {
439
531
  * replaces each completed param's text in the string with a {{TYPE_N}} token,
440
532
  * and returns the transformed query + params with placeholders filled in.
441
533
  *
442
- * Replacements happen left-to-right, first occurrence only per param.
534
+ * Replacements advance a position cursor (params are appended in text order in
535
+ * normal flows), so a short param value (e.g. quantity "1") can never match
536
+ * INSIDE an already-inserted placeholder (e.g. the "1" in "{{SIZE_1}}") and
537
+ * splice into it. Out-of-order params fall back to a from-zero rescan that
538
+ * rejects any match overlapping a previously inserted placeholder; with no
539
+ * clean match the param is left unreplaced (same as the text-not-found path).
443
540
  * Counter is per-type (e.g. {{TASK_1}}, {{GOAL_1}}, {{GOAL_2}}).
444
541
  */
445
542
  declare function buildQuery(text: string, completedParams: CompletedParamState[]): BuildQueryResult;
@@ -474,4 +571,36 @@ declare class ModeController {
474
571
  private detachListener;
475
572
  }
476
573
 
477
- export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type InputItem, ModeController, type OptionOverrides, type RenderMode, type Segment, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset };
574
+ /**
575
+ * Sentinel `text` marking a `completed_params` entry the user skipped (→)
576
+ * rather than filled. Sent regardless of `maskCompletedText` — it's a fixed
577
+ * marker, never user-entered content.
578
+ */
579
+ declare const SKIPPED_PARAM_TEXT = "skipped";
580
+ /**
581
+ * Folds skipped suggestions into a wire `completed_params` array so the server
582
+ * learns which parameters the user dismissed and can stop re-suggesting them.
583
+ *
584
+ * Skipped entries carry no placeholder: nothing was substituted into
585
+ * `raw_query`, so a `{{TYPE_N}}` token would point at text that doesn't exist.
586
+ * They're appended after the real params for the same reason — they have no
587
+ * position in the query.
588
+ *
589
+ * A skip is dropped when a param of the same type ends up filled anyway (the
590
+ * user skipped `goal`, then typed one): sending both would tell the server the
591
+ * parameter is simultaneously answered and declined.
592
+ */
593
+ declare function withSkippedParams(completed: CompletedParam[], skipped: SkippedParamState[]): CompletedParam[];
594
+
595
+ /**
596
+ * Builds the `AutocompleteResult` handed to `onSubmit`: the placeholder-
597
+ * tokenized raw query plus the completed params, with skipped suggestions
598
+ * folded in (see {@link withSkippedParams}).
599
+ *
600
+ * Shared by every submit path — vanilla Enter / submit button, the React Tier 1
601
+ * component, the Angular Tier 1 component — so they can't drift on what a
602
+ * result contains.
603
+ */
604
+ declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
605
+
606
+ export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };