halo-agent 2.4.4 → 2.4.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/smartFill.js +63 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {
package/smartFill.js CHANGED
@@ -123,7 +123,7 @@ async function executePlanItem(page, item, fieldByMmid, ctx) {
123
123
  // it as click_option.
124
124
  if (field.role === 'combobox' || field.role === 'listbox') {
125
125
  const result = await openAndPickOption(root, locator, item.value, {
126
- config: ctx.config, jobId: ctx.jobId, label: field.label,
126
+ config: ctx.config, jobId: ctx.jobId, label: field.label, field,
127
127
  });
128
128
  if (result.ok) return result;
129
129
  // Fall through to text-fill if no option matched at all
@@ -151,7 +151,7 @@ async function executePlanItem(page, item, fieldByMmid, ctx) {
151
151
 
152
152
  case 'click_option': {
153
153
  return await openAndPickOption(root, locator, item.value, {
154
- config: ctx.config, jobId: ctx.jobId, label: field.label,
154
+ config: ctx.config, jobId: ctx.jobId, label: field.label, field,
155
155
  });
156
156
  }
157
157
 
@@ -636,6 +636,67 @@ async function openAndPickOption(root, triggerLocator, value, llmCtx) {
636
636
  // need the Page. For in-frame fields, option menus render inside the
637
637
  // frame so root.locator finds them.
638
638
  const page = (root && typeof root.page === 'function') ? root.page() : root;
639
+
640
+ // ── Async-listbox typeahead (shape-agnostic): try FIRST. ─────────────
641
+ // Any combobox where options are NOT pre-rendered — Ashby Location,
642
+ // Greenhouse Google Places, Workday city pickers, school/company
643
+ // autocompletes — must be typed into BEFORE options can appear. The
644
+ // generic "click and snapshot" path below opens the menu, waits 350ms,
645
+ // and bails on empty — but Ashby's geo fetch alone takes ~700ms.
646
+ //
647
+ // We probe DOM signals on the located element AND on its descendants AND
648
+ // on its closest field-group, because the planner's mmid may resolve to
649
+ // the hidden input, the wrapper div, or the toggle button — and Ashby's
650
+ // wrapper has no role/aria attrs of its own.
651
+ //
652
+ // Triggers:
653
+ // - role=combobox + (aria-haspopup=listbox | aria-autocomplete=list)
654
+ // - aria-autocomplete=list (canonical ARIA for async listbox)
655
+ // - scanAccessibility-tagged field.isTypeahead === true
656
+ // - Ashby's emotion-hashed _input_ class inside a _fieldEntry_ group
657
+ try {
658
+ const fieldIsTypeahead = !!(llmCtx && llmCtx.field && llmCtx.field.isTypeahead);
659
+ const domIsTypeahead = await triggerLocator.evaluate((el) => {
660
+ const probe = (n) => {
661
+ if (!n || n.nodeType !== 1) return false;
662
+ const role = n.getAttribute('role');
663
+ const ac = n.getAttribute('aria-autocomplete');
664
+ const hp = n.getAttribute('aria-haspopup');
665
+ if (ac === 'list' || ac === 'both') return true;
666
+ if (role === 'combobox' && (hp === 'listbox' || hp === 'true')) return true;
667
+ return false;
668
+ };
669
+ // 1. element itself
670
+ if (probe(el)) return true;
671
+ // 2. descendant input/combobox (when locator is wrapper or toggle button)
672
+ const innerSelectors = 'input[role="combobox"], [aria-autocomplete="list"], [aria-autocomplete="both"], [role="combobox"][aria-haspopup="listbox"]';
673
+ const inner = el.querySelector?.(innerSelectors);
674
+ if (probe(inner)) return true;
675
+ // 3. closest field-group, then re-descend (handles ATS-specific wrappers)
676
+ const groupSel = '[class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id], [class*="formField"], .field';
677
+ const group = el.closest?.(groupSel);
678
+ if (group) {
679
+ const groupInner = group.querySelector(innerSelectors);
680
+ if (probe(groupInner)) return true;
681
+ }
682
+ // 4. Ashby's emotion-hashed input class as a supplementary signal
683
+ const cls = String(el.className || '');
684
+ if (/_input_/.test(cls) && el.closest?.('[class*="_fieldEntry_"], .ashby-application-form-field-entry')) return true;
685
+ return false;
686
+ }).catch(() => false);
687
+
688
+ if (fieldIsTypeahead || domIsTypeahead) {
689
+ const why = [];
690
+ if (fieldIsTypeahead) why.push('field.isTypeahead');
691
+ if (domIsTypeahead) why.push('dom signals');
692
+ console.log(`[smartFill] openAndPickOption: detected typeahead (${why.join(', ')}) → typeAndPickSuggestion first`);
693
+ const r = await typeAndPickSuggestion(root, triggerLocator, value);
694
+ if (r && r.ok) return r;
695
+ // Fall through to specialized + generic paths on no-match.
696
+ console.log(`[smartFill] typeahead path did not land (${r && r.reason}); falling through to generic dispatch.`);
697
+ }
698
+ } catch {}
699
+
639
700
  try {
640
701
  const rs = await tryReactSelect(root, triggerLocator, value);
641
702
  if (rs !== null) {
@@ -652,29 +713,6 @@ async function openAndPickOption(root, triggerLocator, value, llmCtx) {
652
713
  if (itlResult !== null && itlResult.ok) return itlResult;
653
714
  } catch {}
654
715
 
655
- try {
656
- // Ashby async typeahead. Ashby's Location field is role="combobox" with
657
- // aria-haspopup="listbox" and an input class like `_input_v5ami_28`. It
658
- // renders NO options until you type, then fetches matches over the
659
- // network (~700ms) and shows them as [role="option"]. The generic
660
- // open-and-look path opened it, saw an empty menu, and bailed — that's
661
- // the Gen Digital "dropdown opened but no visible options found" failure.
662
- // Detect Ashby's widget and route to type-then-wait-for-async-options.
663
- const isAshbyCombo = await triggerLocator.evaluate((el) => {
664
- const role = el.getAttribute('role');
665
- const hasPopup = el.getAttribute('aria-haspopup');
666
- const cls = String(el.className || '');
667
- // Ashby's emotion-hashed input class, or the generic combobox+listbox shape.
668
- const ashbyClass = /_input_/.test(cls) && !!el.closest('[class*="_fieldEntry_"], .ashby-application-form-field-entry');
669
- return (role === 'combobox' && (hasPopup === 'listbox' || hasPopup === 'true')) || ashbyClass;
670
- }).catch(() => false);
671
- if (isAshbyCombo) {
672
- const r = await typeAndPickSuggestion(root, triggerLocator, value);
673
- if (r.ok) return r;
674
- // If typeahead didn't land, fall through to the generic path below.
675
- }
676
- } catch {}
677
-
678
716
  try {
679
717
  // Workday combobox / multi-select. Workday wraps every form widget in a
680
718
  // [data-automation-id^="formField-"] container and its dropdowns mount a