yunti-browser-runtime 0.2.0 → 0.2.1

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.
@@ -534,7 +534,30 @@ export function createToolDispatcher({
534
534
  async function clickByUid(tabId, session, args = {}) {
535
535
  const uid = String(args.uid || "").trim()
536
536
  if (uid) {
537
- return clickViaSnapshotUid(tabId, session, uid)
537
+ const point = resolveObservedUidCenter(session, uid)
538
+ if (!point.ok) return buildUidPointFailureResult(session, uid, "click", point.error)
539
+ const result = await chrome.tabs.sendMessage(tabId, {
540
+ type: "yunti_execute_tool",
541
+ tool: "yunti_click",
542
+ arguments: { ...args, x: point.x, y: point.y },
543
+ })
544
+ if (!result || typeof result !== "object" || result.clicked !== true) {
545
+ return buildUidPointFailureResult(session, uid, "click", result?.error || "Uid click failed")
546
+ }
547
+ const roundedX = Math.round(point.x)
548
+ const roundedY = Math.round(point.y)
549
+ return {
550
+ ...result,
551
+ uid,
552
+ x: roundedX,
553
+ y: roundedY,
554
+ browserSessionId: session.browserSessionId,
555
+ action: "click",
556
+ target: { uid, x: roundedX, y: roundedY },
557
+ ok: true,
558
+ recoverable: false,
559
+ nextStepHint: "Click dispatched without attaching Chrome debugger. Observe again or read page state to verify the intended change.",
560
+ }
538
561
  }
539
562
  const x = Number(args.x)
540
563
  const y = Number(args.y)
@@ -542,7 +565,26 @@ export function createToolDispatcher({
542
565
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
543
566
  throw new Error("yunti_click coordinate mode requires both x and y. Use yunti_click_at with x/y, or call yunti_take_snapshot and pass uid.")
544
567
  }
545
- return clickAtCoordinate(tabId, session, x, y)
568
+ const result = await chrome.tabs.sendMessage(tabId, {
569
+ type: "yunti_execute_tool",
570
+ tool: "yunti_click",
571
+ arguments: { ...args, x, y },
572
+ })
573
+ if (!result || typeof result !== "object" || result.clicked !== true) return result
574
+ const roundedX = Math.round(x)
575
+ const roundedY = Math.round(y)
576
+ return {
577
+ ...result,
578
+ x: roundedX,
579
+ y: roundedY,
580
+ browserSessionId: session.browserSessionId,
581
+ method: "coordinate",
582
+ action: "click",
583
+ target: { method: "coordinate", x: roundedX, y: roundedY },
584
+ ok: true,
585
+ recoverable: false,
586
+ nextStepHint: "Coordinate click dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify the intended change.",
587
+ }
546
588
  }
547
589
  const selector = String(args.selector || "").trim()
548
590
  if (!selector) {
@@ -573,7 +615,30 @@ export function createToolDispatcher({
573
615
  async function hoverByUid(tabId, session, args = {}) {
574
616
  const uid = String(args.uid || "").trim()
575
617
  if (uid) {
576
- return hoverViaSnapshotUid(tabId, session, uid)
618
+ const point = resolveObservedUidCenter(session, uid)
619
+ if (!point.ok) return buildUidPointFailureResult(session, uid, "hover", point.error)
620
+ const result = await chrome.tabs.sendMessage(tabId, {
621
+ type: "yunti_execute_tool",
622
+ tool: "yunti_hover",
623
+ arguments: { ...args, x: point.x, y: point.y },
624
+ })
625
+ if (!result || typeof result !== "object" || result.hovered !== true) {
626
+ return buildUidPointFailureResult(session, uid, "hover", result?.error || "Uid hover failed")
627
+ }
628
+ const roundedX = Math.round(point.x)
629
+ const roundedY = Math.round(point.y)
630
+ return {
631
+ ...result,
632
+ uid,
633
+ x: roundedX,
634
+ y: roundedY,
635
+ browserSessionId: session.browserSessionId,
636
+ action: "hover",
637
+ target: { uid, x: roundedX, y: roundedY },
638
+ ok: true,
639
+ recoverable: false,
640
+ nextStepHint: "Hover dispatched without attaching Chrome debugger. Observe again or read page state to verify menus, tooltips, or hover-only controls.",
641
+ }
577
642
  }
578
643
  const x = Number(args.x)
579
644
  const y = Number(args.y)
@@ -581,10 +646,16 @@ export function createToolDispatcher({
581
646
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
582
647
  throw new Error("yunti_hover coordinate mode requires both x and y. Call yunti_take_snapshot to get uid, pass selector, or provide both coordinates.")
583
648
  }
584
- await mouseMove(tabId, x, y)
649
+ const result = await chrome.tabs.sendMessage(tabId, {
650
+ type: "yunti_execute_tool",
651
+ tool: "yunti_hover",
652
+ arguments: { ...args, x, y },
653
+ })
654
+ if (!result || typeof result !== "object" || result.hovered !== true) return result
585
655
  const roundedX = Math.round(x)
586
656
  const roundedY = Math.round(y)
587
657
  return {
658
+ ...result,
588
659
  hovered: true,
589
660
  x: roundedX,
590
661
  y: roundedY,
@@ -594,29 +665,30 @@ export function createToolDispatcher({
594
665
  target: { method: "coordinate", x: roundedX, y: roundedY },
595
666
  ok: true,
596
667
  recoverable: false,
597
- nextStepHint: "Coordinate hover dispatched. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
668
+ nextStepHint: "Coordinate hover dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
598
669
  }
599
670
  }
600
671
  const selector = String(args.selector || "").trim()
601
672
  if (!selector) {
602
673
  throw new Error("yunti_hover requires uid, selector, or both x and y. Call yunti_take_snapshot to get uid or pass a CSS selector.")
603
674
  }
604
- const point = await resolveSelectorCenter(tabId, selector)
605
- await mouseMove(tabId, point.x, point.y)
606
- const roundedX = Math.round(point.x)
607
- const roundedY = Math.round(point.y)
675
+ const result = await chrome.tabs.sendMessage(tabId, {
676
+ type: "yunti_execute_tool",
677
+ tool: "yunti_hover",
678
+ arguments: args,
679
+ })
680
+ if (!result || typeof result !== "object" || result.hovered !== true) return result
608
681
  return {
682
+ ...result,
609
683
  hovered: true,
610
684
  selector,
611
- x: roundedX,
612
- y: roundedY,
613
685
  browserSessionId: session.browserSessionId,
614
686
  method: "selector",
615
687
  action: "hover",
616
- target: { selector, method: "selector", x: roundedX, y: roundedY },
688
+ target: { selector, method: "selector" },
617
689
  ok: true,
618
690
  recoverable: false,
619
- nextStepHint: "Selector hover dispatched. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
691
+ nextStepHint: "Selector hover dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
620
692
  }
621
693
  }
622
694
 
@@ -626,187 +698,45 @@ export function createToolDispatcher({
626
698
  const value = String(args.value)
627
699
  if (uid) {
628
700
  try {
629
- const { x, y } = await resolveUidCenter(tabId, session, uid)
630
- await mouseClick(tabId, x, y, 1)
631
- await delayCdp(100)
632
- // Clear existing value
633
- const elements = await chromeDebuggerSendCommand(
634
- { tabId },
635
- "Runtime.evaluate",
636
- { expression: `(() => {
637
- const el = document.elementFromPoint(${x}, ${y});
638
- if (!el) return 'not found';
639
- const tag = el.tagName.toLowerCase();
640
- const rect = el.getBoundingClientRect();
641
- const style = getComputedStyle(el);
642
- const type = String(el.type || '').toLowerCase();
643
- const disabled = Boolean(el.disabled || el.getAttribute('aria-disabled') === 'true');
644
- const readOnly = Boolean(el.readOnly || el.getAttribute('aria-readonly') === 'true');
645
- const hidden = Boolean(el.hidden || style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0 || rect.width <= 0 || rect.height <= 0);
646
- const valueEditable = tag === 'input' || tag === 'textarea';
647
- const blockedInputType = tag === 'input' && ['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(type);
648
- const editable = el.isContentEditable || tag === 'select' || (valueEditable && !blockedInputType);
649
- if (tag === 'select') {
650
- const opts = Array.from(el.options);
651
- return { tag: 'select', type, disabled, readOnly, hidden, editable, options: opts.map(o => ({ value: o.value, text: o.text.slice(0, 80) })), selectedIndex: el.selectedIndex };
652
- }
653
- const beforeText = el.isContentEditable ? String(el.textContent || '') : '';
654
- if (el.isContentEditable) {
655
- el.focus();
656
- el.textContent = '';
657
- } else if (valueEditable && !blockedInputType && !disabled && !readOnly && !hidden) {
658
- el.focus();
659
- el.select();
660
- }
661
- return {
662
- tag,
663
- type,
664
- contentEditable: el.isContentEditable,
665
- disabled,
666
- readOnly,
667
- hidden,
668
- editable,
669
- before: el.isContentEditable ? { textLength: beforeText.length } : undefined,
670
- };
671
- })()`,
672
- returnByValue: true,
673
- }
674
- )
675
- const elInfo = elements?.result?.value
676
-
677
- if (elInfo === "not found") {
678
- return buildUidFillFailureResult(session, uid, {
679
- code: "ELEMENT_NOT_FOUND",
680
- error: `No element found at uid ${uid} coordinates`,
681
- })
682
- }
683
-
684
- if (elInfo?.hidden || elInfo?.disabled || elInfo?.readOnly || elInfo?.editable === false) {
685
- return buildUidFillFailureResult(session, uid, {
686
- code: "TARGET_NOT_EDITABLE",
687
- error: buildFillEditabilityError(uid, elInfo),
688
- element: summarizeFillTarget(elInfo),
689
- })
690
- }
691
-
692
- // Handle select element
693
- if (elInfo?.tag === "select") {
694
- const targetOption = elInfo.options?.find(
695
- o => o.value === value || o.text === value
696
- )
697
- if (targetOption) {
698
- await chromeDebuggerSendCommand(
699
- { tabId },
700
- "Runtime.evaluate",
701
- {
702
- expression: `(() => {
703
- const el = document.elementFromPoint(${x}, ${y});
704
- if (el) el.value = ${JSON.stringify(targetOption.value)};
705
- el.dispatchEvent(new Event('change', { bubbles: true }));
706
- el.dispatchEvent(new Event('input', { bubbles: true }));
707
- })()`,
708
- }
709
- )
710
- return {
711
- filled: true,
712
- uid,
713
- method: "select",
714
- value: targetOption.value,
715
- browserSessionId: session.browserSessionId,
716
- action: "fill",
717
- target: { uid, method: "select" },
718
- ok: true,
719
- recoverable: false,
720
- nextStepHint: "Select value dispatched. Observe again, read page state, or evaluate the select value to verify the intended change.",
721
- }
722
- }
701
+ const point = resolveObservedUidCenter(session, uid)
702
+ if (!point.ok) {
723
703
  return buildUidFillFailureResult(session, uid, {
724
- code: "OPTION_NOT_FOUND",
725
- error: `Option '${value}' not found in select at uid ${uid}`,
726
- availableValues: elInfo.options?.map((option) => option.value).slice(0, 50),
727
- availableTexts: elInfo.options?.map((option) => option.text).slice(0, 50),
704
+ code: "UID_COORDINATES_UNAVAILABLE",
705
+ error: point.error,
728
706
  })
729
707
  }
730
-
731
- // Type text via CDP Input.dispatchKeyEvent
732
- const text = value
733
- for (const char of text) {
734
- await chromeDebuggerSendCommand(
735
- { tabId },
736
- "Input.dispatchKeyEvent",
737
- { type: "char", text: char, unmodifiedText: char }
738
- )
708
+ const result = await chrome.tabs.sendMessage(tabId, {
709
+ type: "yunti_execute_tool",
710
+ tool: "yunti_fill",
711
+ arguments: { ...args, x: point.x, y: point.y },
712
+ })
713
+ if (!result || typeof result !== "object" || result.filled !== true) {
714
+ return buildUidFillFailureResult(session, uid, result)
739
715
  }
740
- const method = elInfo?.contentEditable ? "contenteditable" : "keyboard"
741
- const before = elInfo?.before
742
- await delayCdp(50)
743
- const verification = await chromeDebuggerSendCommand(
744
- { tabId },
745
- "Runtime.evaluate",
746
- {
747
- expression: `(() => {
748
- const el = document.elementFromPoint(${x}, ${y});
749
- if (!el) return { found: false };
750
- const tag = el.tagName.toLowerCase();
751
- const type = String(el.type || '').toLowerCase();
752
- const expected = ${JSON.stringify(text)};
753
- const actual = el.isContentEditable
754
- ? String(el.textContent || '')
755
- : (tag === 'input' || tag === 'textarea')
756
- ? String(el.value || '')
757
- : undefined;
758
- if (actual === undefined) {
759
- return { found: true, tag, type, contentEditable: el.isContentEditable };
760
- }
761
- return {
762
- found: true,
763
- tag,
764
- type,
765
- contentEditable: el.isContentEditable,
766
- valueMatches: actual === expected,
767
- valueLength: actual.length,
768
- expectedLength: expected.length,
769
- };
770
- })()`,
771
- returnByValue: true,
772
- }
773
- )
774
- const verificationInfo = verification?.result?.value
775
- const verifiedAfter = verificationInfo?.found && Number.isFinite(Number(verificationInfo.valueLength))
776
- ? { textLength: Number(verificationInfo.valueLength) }
777
- : undefined
778
- const after = verifiedAfter || (elInfo?.contentEditable ? { textLength: text.length } : undefined)
779
- if (verificationInfo?.found && verificationInfo.valueMatches === false) {
716
+ const method = result.method || "dom"
717
+ if (result.valueApplied === false) {
780
718
  return buildUidFillFailureResult(session, uid, {
719
+ ...result,
781
720
  code: "VALUE_NOT_APPLIED",
782
721
  error: `Filled value did not remain on uid ${uid}`,
783
722
  method,
784
- expectedValueLength: Number(verificationInfo.expectedLength),
785
- actualValueLength: Number(verificationInfo.valueLength),
786
- ...(before ? { before } : {}),
787
- after,
788
- element: summarizeFillTarget({
789
- tag: verificationInfo.tag,
790
- type: verificationInfo.type,
791
- contentEditable: verificationInfo.contentEditable,
792
- }),
723
+ expectedValueLength: value.length,
793
724
  })
794
725
  }
795
726
  return {
727
+ ...result,
796
728
  filled: true,
797
729
  uid,
798
730
  method,
799
- value: text,
800
- ...(before ? { before } : {}),
801
- ...(after ? { after } : {}),
731
+ value,
802
732
  browserSessionId: session.browserSessionId,
803
733
  action: "fill",
804
734
  target: { uid, method },
805
735
  ok: true,
806
736
  recoverable: false,
807
- nextStepHint: elInfo?.contentEditable
737
+ nextStepHint: method === "contenteditable"
808
738
  ? "Contenteditable fill dispatched. Observe again, read page text, or evaluate textContent to verify the intended change."
809
- : "Fill dispatched. Observe again, read page state, or evaluate the field value to verify the intended change.",
739
+ : "Fill dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended change.",
810
740
  }
811
741
  } catch (error) {
812
742
  return buildUidFillFailureResult(session, uid, {
@@ -1208,65 +1138,27 @@ export function createToolDispatcher({
1208
1138
  if (uid) {
1209
1139
  const matchMode = text ? "text" : "value"
1210
1140
  const targetOption = text || value
1211
- const { x, y } = await resolveUidCenter(tabId, session, uid)
1212
- await ensureCdpAttached(tabId, "1.3")
1213
- const result = await chromeDebuggerSendCommand(
1214
- { tabId },
1215
- "Runtime.evaluate",
1216
- {
1217
- expression: `(() => {
1218
- const el = document.elementFromPoint(${x}, ${y});
1219
- if (!el) return { ok: false, code: "ELEMENT_NOT_FOUND", error: "No element found at uid coordinates" };
1220
- if (el.tagName.toLowerCase() !== "select") {
1221
- return { ok: false, code: "NOT_SELECT", error: "Element at uid is not a select element" };
1222
- }
1223
- const options = Array.from(el.options);
1224
- const option = options.find((item) => {
1225
- if (${JSON.stringify(matchMode)} === "text") return item.text.trim() === ${JSON.stringify(targetOption)};
1226
- return item.value === ${JSON.stringify(targetOption)};
1227
- });
1228
- if (!option) {
1229
- return {
1230
- ok: false,
1231
- code: "OPTION_NOT_FOUND",
1232
- error: ${JSON.stringify(matchMode)} === "text" ? "Option text not found" : "Option value not found",
1233
- availableValues: options.map((item) => item.value).slice(0, 50),
1234
- availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
1235
- };
1236
- }
1237
- if (option.disabled) {
1238
- return {
1239
- ok: false,
1240
- code: "OPTION_DISABLED",
1241
- error: ${JSON.stringify(matchMode)} === "text" ? "Option text is disabled" : "Option value is disabled",
1242
- disabledValue: option.value,
1243
- disabledText: option.text.trim(),
1244
- availableValues: options.map((item) => item.value).slice(0, 50),
1245
- availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
1246
- };
1247
- }
1248
- el.value = option.value;
1249
- el.dispatchEvent(new Event("change", { bubbles: true }));
1250
- el.dispatchEvent(new Event("input", { bubbles: true }));
1251
- return {
1252
- ok: true,
1253
- value: option.value,
1254
- selectedIndex: el.selectedIndex,
1255
- optionText: option.text.slice(0, 80),
1256
- };
1257
- })()`,
1258
- returnByValue: true,
1259
- }
1260
- )
1261
- const selectResult = result?.result?.value
1262
- if (!selectResult?.ok) {
1141
+ const point = resolveObservedUidCenter(session, uid)
1142
+ if (!point.ok) {
1143
+ return buildUidSelectFailureResult(session, uid, matchMode, targetOption, {
1144
+ code: "UID_COORDINATES_UNAVAILABLE",
1145
+ error: point.error,
1146
+ })
1147
+ }
1148
+ const selectResult = await chrome.tabs.sendMessage(tabId, {
1149
+ type: "yunti_execute_tool",
1150
+ tool: "yunti_select",
1151
+ arguments: { ...args, x: point.x, y: point.y },
1152
+ })
1153
+ if (!selectResult?.selected) {
1263
1154
  return buildUidSelectFailureResult(session, uid, matchMode, targetOption, selectResult)
1264
1155
  }
1265
1156
  return {
1157
+ ...selectResult,
1266
1158
  selected: true,
1267
1159
  uid,
1268
1160
  value: selectResult.value,
1269
- text: selectResult.optionText,
1161
+ text: selectResult.text,
1270
1162
  selectedIndex: selectResult.selectedIndex,
1271
1163
  browserSessionId: session.browserSessionId,
1272
1164
  action: "select",
@@ -1274,8 +1166,8 @@ export function createToolDispatcher({
1274
1166
  ok: true,
1275
1167
  recoverable: false,
1276
1168
  nextStepHint: matchMode === "text"
1277
- ? "Uid select dispatched by visible option text. Observe again, read page state, or evaluate the select value to verify the intended change."
1278
- : "Uid select dispatched by option value. Observe again, read page state, or evaluate the select value to verify the intended change.",
1169
+ ? "Uid select dispatched by visible option text without attaching Chrome debugger. Observe again, read page state, or evaluate the select value to verify the intended change."
1170
+ : "Uid select dispatched by option value without attaching Chrome debugger. Observe again, read page state, or evaluate the select value to verify the intended change.",
1279
1171
  }
1280
1172
  }
1281
1173
 
@@ -1529,39 +1421,72 @@ export function createToolDispatcher({
1529
1421
  }
1530
1422
 
1531
1423
  async function resolveUidCenter(tabId, session, uid) {
1424
+ const point = resolveObservedUidCenter(session, uid)
1425
+ if (point.ok) return { x: point.x, y: point.y }
1426
+ throw new Error(point.error)
1427
+ }
1428
+
1429
+ function resolveObservedUidCenter(session, uid) {
1532
1430
  const uidState = pageUidStore.get(session.browserSessionId)
1533
1431
  const uidMap = uidState?.uidMap
1534
1432
  if (!uidMap || !uidMap[uid]) {
1535
- throw new Error(
1536
- `uid ${uid} not found in the latest page uid map. Run yunti_observe_page or yunti_take_snapshot again before retrying.`
1537
- )
1433
+ return {
1434
+ ok: false,
1435
+ error: `uid ${uid} not found in the latest page uid map. Run yunti_observe_page again before retrying.`,
1436
+ }
1538
1437
  }
1539
1438
  const el = uidMap[uid]
1540
1439
 
1541
1440
  if (el.rect && el.rect.width > 0 && el.rect.height > 0) {
1542
1441
  return {
1442
+ ok: true,
1543
1443
  x: el.rect.x + el.rect.width / 2,
1544
1444
  y: el.rect.y + el.rect.height / 2,
1545
1445
  }
1546
1446
  }
1547
-
1548
- // Resolve coordinates via CDP DOM.getBoxModel using backendNodeId
1549
- if (el.backendNodeId) {
1550
- await ensureCdpAttached(tabId, "1.3")
1551
- const boxModel = await chromeDebuggerSendCommand(
1552
- { tabId },
1553
- "DOM.getBoxModel",
1554
- { backendNodeId: el.backendNodeId }
1555
- )
1556
- const content = boxModel?.model?.content
1557
- if (content && content.length >= 4) {
1558
- const x = (content[0] + content[4]) / 2
1559
- const y = (content[1] + content[5]) / 2
1560
- return { x, y }
1561
- }
1447
+
1448
+ return {
1449
+ ok: false,
1450
+ error: `Cannot resolve coordinates for uid ${uid} without Chrome debugger. Run yunti_observe_page with includeRects enabled, scroll the element into view, or use an explicit selector fallback.`,
1451
+ }
1452
+ }
1453
+
1454
+ function buildUidPointFailureResult(session, uid, action, error) {
1455
+ const code = /not found|latest page uid map/i.test(String(error || "")) ? "UID_NOT_FOUND" : "UID_COORDINATES_UNAVAILABLE"
1456
+ const resultFieldByAction = {
1457
+ click: "clicked",
1458
+ hover: "hovered",
1459
+ type: "typed",
1460
+ press: "pressed",
1461
+ }
1462
+ const resultField = resultFieldByAction[action] || `${action}ed`
1463
+ const recommendedToolByAction = {
1464
+ click: "yunti_click",
1465
+ hover: "yunti_hover",
1466
+ type: "yunti_type_text",
1467
+ press: "yunti_press_key",
1468
+ }
1469
+ const recommendedTool = recommendedToolByAction[action] || "yunti_observe_page"
1470
+ return {
1471
+ [resultField]: false,
1472
+ uid,
1473
+ browserSessionId: session.browserSessionId,
1474
+ action,
1475
+ target: { uid, method: "uid" },
1476
+ ok: false,
1477
+ recoverable: true,
1478
+ code,
1479
+ error: error || `Cannot ${action} uid ${uid}`,
1480
+ recoveryHint: {
1481
+ reason: `uid-${action}-failed`,
1482
+ recommendedTools: ["yunti_observe_page", recommendedTool],
1483
+ nextAction: "observe-again",
1484
+ decision: "refresh-observation-before-retry",
1485
+ uid,
1486
+ message: "The uid could not be resolved without attaching Chrome debugger. Observe again with rects enabled, scroll the target into view, or retry with selector fallback.",
1487
+ },
1488
+ nextStepHint: `Uid ${action} failed without attaching Chrome debugger. Observe again for a fresh visible uid before retrying.`,
1562
1489
  }
1563
-
1564
- throw new Error(`Cannot resolve coordinates for uid ${uid}. Element may be off-screen, hidden, or stale. Run yunti_observe_page again before retrying.`)
1565
1490
  }
1566
1491
 
1567
1492
  async function mouseClick(tabId, x, y, clickCount = 1) {
@@ -2034,31 +1959,27 @@ export function createToolDispatcher({
2034
1959
  const text = String(args.text || "")
2035
1960
  if (!text) throw new Error("text is required")
2036
1961
 
2037
- // Click the uid element to focus it
2038
- const { x, y } = await resolveUidCenter(tabId, session, uid)
2039
- await mouseClick(tabId, x, y, 1)
2040
- await delayCdp(100)
2041
-
2042
- // Type each character via CDP
2043
- for (const char of text) {
2044
- await chromeDebuggerSendCommand(
2045
- { tabId },
2046
- "Input.dispatchKeyEvent",
2047
- { type: "char", text: char, unmodifiedText: char }
2048
- )
2049
- }
1962
+ const point = resolveObservedUidCenter(session, uid)
1963
+ if (!point.ok) return buildUidPointFailureResult(session, uid, "type", point.error)
1964
+ const result = await chrome.tabs.sendMessage(tabId, {
1965
+ type: "yunti_execute_tool",
1966
+ tool: "yunti_type_text",
1967
+ arguments: { ...args, x: point.x, y: point.y },
1968
+ })
1969
+ if (!result || typeof result !== "object" || result.typed !== true) return result
2050
1970
 
2051
1971
  return {
1972
+ ...result,
2052
1973
  typed: true,
2053
1974
  uid,
2054
1975
  text,
2055
- method: "cdp.keyboard",
1976
+ method: result.method || "dom",
2056
1977
  browserSessionId: session.browserSessionId,
2057
1978
  action: "type_text",
2058
- target: { uid, method: "cdp.keyboard" },
1979
+ target: { uid, method: result.method || "dom" },
2059
1980
  ok: true,
2060
1981
  recoverable: false,
2061
- nextStepHint: "Type text dispatched. Observe again, read page state, or evaluate the field value to verify the intended change.",
1982
+ nextStepHint: "Type text dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended change.",
2062
1983
  }
2063
1984
  }
2064
1985
 
@@ -2099,39 +2020,26 @@ export function createToolDispatcher({
2099
2020
  const key = String(args.key || "").trim()
2100
2021
  if (!key) throw new Error("key is required")
2101
2022
 
2102
- // Focus the uid element
2103
- const { x, y } = await resolveUidCenter(tabId, session, uid)
2104
- await mouseClick(tabId, x, y, 1)
2105
- await delayCdp(50)
2106
-
2107
- // Map common key names to CDP key events
2108
- const keyDef = normalizeKey(key)
2109
-
2110
- await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
2111
- type: "keyDown",
2112
- key: keyDef.key,
2113
- code: keyDef.code,
2114
- windowsVirtualKeyCode: keyDef.keyCode,
2115
- nativeVirtualKeyCode: keyDef.keyCode,
2116
- })
2117
- await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
2118
- type: "keyUp",
2119
- key: keyDef.key,
2120
- code: keyDef.code,
2121
- windowsVirtualKeyCode: keyDef.keyCode,
2122
- nativeVirtualKeyCode: keyDef.keyCode,
2023
+ const point = resolveObservedUidCenter(session, uid)
2024
+ if (!point.ok) return buildUidPointFailureResult(session, uid, "press", point.error)
2025
+ const result = await chrome.tabs.sendMessage(tabId, {
2026
+ type: "yunti_execute_tool",
2027
+ tool: "yunti_press_key",
2028
+ arguments: { ...args, x: point.x, y: point.y },
2123
2029
  })
2030
+ if (!result || typeof result !== "object" || result.pressed !== true) return result
2124
2031
 
2125
2032
  return {
2033
+ ...result,
2126
2034
  pressed: true,
2127
2035
  uid,
2128
2036
  key,
2129
2037
  browserSessionId: session.browserSessionId,
2130
2038
  action: "press_key",
2131
- target: { uid, method: "cdp.keyboard" },
2039
+ target: { uid, method: "dom" },
2132
2040
  ok: true,
2133
2041
  recoverable: false,
2134
- nextStepHint: "Key press dispatched. Observe again, read page state, or evaluate the field value to verify the intended effect.",
2042
+ nextStepHint: "Key press dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended effect.",
2135
2043
  }
2136
2044
  }
2137
2045
 
package/mcp/bridge-hub.js CHANGED
@@ -810,11 +810,11 @@ export class BridgeHub {
810
810
  }),
811
811
  guidance: {
812
812
  noSessions:
813
- "Keep this bridge running, load or reload the extension, open an http/https page, refresh that page, then run yunti-browser-runtime doctor.",
813
+ "Keep this bridge running, load or reload the extension, and open an http/https page. Yunti will auto-register accessible tabs; refresh the target page only if it remains invisible.",
814
814
  staleSession:
815
- "Refresh the page or reload the extension, then call yunti_list_browser_targets before retrying browser tools.",
815
+ "Call yunti_list_browser_targets to refresh live routes; Yunti will try to auto-register accessible tabs before listing. Refresh the page only as a fallback.",
816
816
  versionMismatch:
817
- "Reload the unpacked extension from the current package directory, then refresh open http/https pages.",
817
+ "Reload the unpacked extension from the current package directory. It will auto-register accessible open http/https pages; refresh only if a page remains invisible.",
818
818
  cancellation:
819
819
  "Cancel only clears runtime pending/queued requests; it does not undo browser-side effects that already happened.",
820
820
  },
@@ -829,7 +829,7 @@ function consoleWarnings(sessions, { expectedExtensionVersion = "" } = {}) {
829
829
  code: "NO_CONNECTED_PAGES",
830
830
  severity: "warning",
831
831
  message:
832
- "No browser pages are connected. Load or reload the extension, open an http/https page, and refresh the page.",
832
+ "No browser pages are connected. Load or reload the extension and open an http/https page; Yunti will auto-register accessible pages. Refresh only if the page remains invisible.",
833
833
  })
834
834
  return warnings
835
835
  }