gsd-pi 2.7.1 → 2.8.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 +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/git-service.ts +3 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
|
@@ -176,7 +176,7 @@ export class InteractiveMode {
|
|
|
176
176
|
private pendingTools = new Map<string, ToolExecutionComponent>();
|
|
177
177
|
|
|
178
178
|
// Tool output expansion state
|
|
179
|
-
private toolOutputExpanded =
|
|
179
|
+
private toolOutputExpanded = true;
|
|
180
180
|
|
|
181
181
|
// Thinking block visibility state
|
|
182
182
|
private hideThinkingBlock = false;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-tools — page state capture
|
|
3
|
+
*
|
|
4
|
+
* Functions for capturing compact page state, screenshots, and summaries.
|
|
5
|
+
* Used by tool implementations for post-action feedback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Frame, Page } from "playwright";
|
|
9
|
+
import sharp from "sharp";
|
|
10
|
+
import type { CompactPageState, CompactSelectorState } from "./state.js";
|
|
11
|
+
import { formatCompactStateSummary } from "./utils.js";
|
|
12
|
+
|
|
13
|
+
// Anthropic API rejects images > 2000px in multi-image requests.
|
|
14
|
+
// Cap at 1568px (recommended optimal size) to stay well within limits.
|
|
15
|
+
const MAX_SCREENSHOT_DIM = 1568;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Compact page state capture
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export async function captureCompactPageState(
|
|
22
|
+
p: Page,
|
|
23
|
+
options: { selectors?: string[]; includeBodyText?: boolean; target?: Page | Frame } = {},
|
|
24
|
+
): Promise<CompactPageState> {
|
|
25
|
+
const selectors = Array.from(new Set((options.selectors ?? []).filter(Boolean)));
|
|
26
|
+
const target = options.target ?? p;
|
|
27
|
+
const domState = await target.evaluate(({ selectors, includeBodyText }) => {
|
|
28
|
+
const selectorStates: Record<string, {
|
|
29
|
+
exists: boolean;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
value: string;
|
|
32
|
+
checked: boolean | null;
|
|
33
|
+
text: string;
|
|
34
|
+
}> = {};
|
|
35
|
+
for (const selector of selectors) {
|
|
36
|
+
let el: Element | null = null;
|
|
37
|
+
try {
|
|
38
|
+
el = document.querySelector(selector);
|
|
39
|
+
} catch {
|
|
40
|
+
el = null;
|
|
41
|
+
}
|
|
42
|
+
if (!el) {
|
|
43
|
+
selectorStates[selector] = {
|
|
44
|
+
exists: false,
|
|
45
|
+
visible: false,
|
|
46
|
+
value: "",
|
|
47
|
+
checked: null,
|
|
48
|
+
text: "",
|
|
49
|
+
};
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const htmlEl = el as HTMLElement;
|
|
53
|
+
const style = window.getComputedStyle(htmlEl);
|
|
54
|
+
const rect = htmlEl.getBoundingClientRect();
|
|
55
|
+
const visible = style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
|
56
|
+
const input = el as HTMLInputElement;
|
|
57
|
+
selectorStates[selector] = {
|
|
58
|
+
exists: true,
|
|
59
|
+
visible,
|
|
60
|
+
value:
|
|
61
|
+
el instanceof HTMLInputElement ||
|
|
62
|
+
el instanceof HTMLTextAreaElement ||
|
|
63
|
+
el instanceof HTMLSelectElement
|
|
64
|
+
? el.value
|
|
65
|
+
: htmlEl.getAttribute("value") || "",
|
|
66
|
+
checked: el instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? input.checked : null,
|
|
67
|
+
text: (htmlEl.innerText || htmlEl.textContent || "").trim().replace(/\s+/g, " ").slice(0, 160),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const focused = document.activeElement as HTMLElement | null;
|
|
72
|
+
const focusedDesc = focused && focused !== document.body && focused !== document.documentElement
|
|
73
|
+
? `${focused.tagName.toLowerCase()}${focused.id ? '#' + focused.id : ''}${focused.getAttribute('aria-label') ? ' "' + focused.getAttribute('aria-label') + '"' : ''}`
|
|
74
|
+
: "";
|
|
75
|
+
const headings = Array.from(document.querySelectorAll('h1,h2,h3')).slice(0, 5).map((h) => (h.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80));
|
|
76
|
+
const dialog = document.querySelector('[role="dialog"]:not([hidden]),dialog[open]');
|
|
77
|
+
const dialogTitle = dialog?.querySelector('[role="heading"],[aria-label]')?.textContent?.trim().slice(0, 80) ?? "";
|
|
78
|
+
const bodyText = includeBodyText
|
|
79
|
+
? (document.body?.innerText || document.body?.textContent || "").trim().replace(/\s+/g, ' ').slice(0, 4000)
|
|
80
|
+
: "";
|
|
81
|
+
return {
|
|
82
|
+
url: window.location.href,
|
|
83
|
+
title: document.title,
|
|
84
|
+
focus: focusedDesc,
|
|
85
|
+
headings,
|
|
86
|
+
bodyText,
|
|
87
|
+
counts: {
|
|
88
|
+
landmarks: document.querySelectorAll('[role="main"],[role="banner"],[role="navigation"],[role="contentinfo"],[role="complementary"],[role="search"],[role="form"],[role="dialog"],[role="alert"],main,header,nav,footer,aside,section,form,dialog').length,
|
|
89
|
+
buttons: document.querySelectorAll('button,[role="button"]').length,
|
|
90
|
+
links: document.querySelectorAll('a[href]').length,
|
|
91
|
+
inputs: document.querySelectorAll('input,textarea,select').length,
|
|
92
|
+
},
|
|
93
|
+
dialog: {
|
|
94
|
+
count: document.querySelectorAll('[role="dialog"]:not([hidden]),dialog[open]').length,
|
|
95
|
+
title: dialogTitle,
|
|
96
|
+
},
|
|
97
|
+
selectorStates,
|
|
98
|
+
};
|
|
99
|
+
}, { selectors, includeBodyText: options.includeBodyText === true });
|
|
100
|
+
// URL and title always come from the Page, not the frame
|
|
101
|
+
return { ...domState, url: p.url(), title: await p.title() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Post-action summary
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/** Lightweight page summary after an action. Returns ~50-150 tokens instead of full tree. */
|
|
109
|
+
export async function postActionSummary(p: Page, target?: Page | Frame): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const state = await captureCompactPageState(p, { target });
|
|
112
|
+
return formatCompactStateSummary(state);
|
|
113
|
+
} catch {
|
|
114
|
+
return "[summary unavailable]";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Screenshot helpers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* If either dimension of the image buffer exceeds MAX_SCREENSHOT_DIM,
|
|
124
|
+
* downscale proportionally using sharp. Returns the original buffer
|
|
125
|
+
* unchanged if already within limits.
|
|
126
|
+
*
|
|
127
|
+
* `page` parameter is retained for ToolDeps signature stability (D008)
|
|
128
|
+
* but is no longer used — all processing is server-side via sharp.
|
|
129
|
+
*/
|
|
130
|
+
export async function constrainScreenshot(
|
|
131
|
+
_page: Page,
|
|
132
|
+
buffer: Buffer,
|
|
133
|
+
mimeType: string,
|
|
134
|
+
quality: number,
|
|
135
|
+
): Promise<Buffer> {
|
|
136
|
+
const { width, height } = await sharp(buffer).metadata();
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
width !== undefined &&
|
|
140
|
+
height !== undefined &&
|
|
141
|
+
width <= MAX_SCREENSHOT_DIM &&
|
|
142
|
+
height <= MAX_SCREENSHOT_DIM
|
|
143
|
+
) {
|
|
144
|
+
return buffer;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const resizer = sharp(buffer).resize(MAX_SCREENSHOT_DIM, MAX_SCREENSHOT_DIM, { fit: "inside" });
|
|
148
|
+
|
|
149
|
+
if (mimeType === "image/png") {
|
|
150
|
+
return Buffer.from(await resizer.png().toBuffer());
|
|
151
|
+
}
|
|
152
|
+
return Buffer.from(await resizer.jpeg({ quality }).toBuffer());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Capture a JPEG screenshot for error debugging. Returns base64 or null. */
|
|
156
|
+
export async function captureErrorScreenshot(p: Page | null): Promise<{ data: string; mimeType: string } | null> {
|
|
157
|
+
if (!p) return null;
|
|
158
|
+
try {
|
|
159
|
+
let buf = await p.screenshot({ type: "jpeg", quality: 60, scale: "css" });
|
|
160
|
+
buf = await constrainScreenshot(p, buf, "image/jpeg", 60);
|
|
161
|
+
return { data: buf.toString("base64"), mimeType: "image/jpeg" };
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-tools — browser-side evaluate helpers
|
|
3
|
+
*
|
|
4
|
+
* Exports a single string constant `EVALUATE_HELPERS_SOURCE` containing an IIFE
|
|
5
|
+
* that attaches utility functions to `window.__pi`. This is injected into every
|
|
6
|
+
* new BrowserContext via `context.addInitScript()` so that `page.evaluate()`
|
|
7
|
+
* callbacks can reference `window.__pi.cssPath(el)` etc. instead of redeclaring
|
|
8
|
+
* the same functions inline.
|
|
9
|
+
*
|
|
10
|
+
* The `simpleHash` function uses the djb2 algorithm identical to
|
|
11
|
+
* `computeContentHash` / `computeStructuralSignature` in `core.js`.
|
|
12
|
+
*
|
|
13
|
+
* Functions provided (9):
|
|
14
|
+
* cssPath, simpleHash, isVisible, isEnabled, inferRole,
|
|
15
|
+
* accessibleName, isInteractiveEl, domPath, selectorHints
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const EVALUATE_HELPERS_SOURCE = `(function() {
|
|
19
|
+
var pi = window.__pi = window.__pi || {};
|
|
20
|
+
|
|
21
|
+
// -----------------------------------------------------------------------
|
|
22
|
+
// 1. simpleHash — djb2 hash matching core.js computeContentHash
|
|
23
|
+
// -----------------------------------------------------------------------
|
|
24
|
+
pi.simpleHash = function simpleHash(str) {
|
|
25
|
+
if (!str) return "0";
|
|
26
|
+
var h = 5381;
|
|
27
|
+
for (var i = 0; i < str.length; i++) {
|
|
28
|
+
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
29
|
+
}
|
|
30
|
+
return (h >>> 0).toString(16);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
// 2. isVisible
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
pi.isVisible = function isVisible(el) {
|
|
37
|
+
var style = window.getComputedStyle(el);
|
|
38
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
39
|
+
var rect = el.getBoundingClientRect();
|
|
40
|
+
return rect.width > 0 && rect.height > 0;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// -----------------------------------------------------------------------
|
|
44
|
+
// 3. isEnabled
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
pi.isEnabled = function isEnabled(el) {
|
|
47
|
+
var disabledAttr = el.getAttribute("disabled") !== null;
|
|
48
|
+
var ariaDisabled = (el.getAttribute("aria-disabled") || "").toLowerCase() === "true";
|
|
49
|
+
return !disabledAttr && !ariaDisabled;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
// 4. inferRole
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
pi.inferRole = function inferRole(el) {
|
|
56
|
+
var explicit = (el.getAttribute("role") || "").trim();
|
|
57
|
+
if (explicit) return explicit;
|
|
58
|
+
var tag = el.tagName.toLowerCase();
|
|
59
|
+
if (tag === "a" && el.getAttribute("href")) return "link";
|
|
60
|
+
if (tag === "button") return "button";
|
|
61
|
+
if (tag === "select") return "combobox";
|
|
62
|
+
if (tag === "textarea") return "textbox";
|
|
63
|
+
if (tag === "input") {
|
|
64
|
+
var type = (el.getAttribute("type") || "text").toLowerCase();
|
|
65
|
+
if (["button", "submit", "reset"].indexOf(type) !== -1) return "button";
|
|
66
|
+
if (type === "checkbox") return "checkbox";
|
|
67
|
+
if (type === "radio") return "radio";
|
|
68
|
+
if (type === "search") return "searchbox";
|
|
69
|
+
return "textbox";
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// -----------------------------------------------------------------------
|
|
75
|
+
// 5. accessibleName
|
|
76
|
+
// -----------------------------------------------------------------------
|
|
77
|
+
pi.accessibleName = function accessibleName(el) {
|
|
78
|
+
var ariaLabel = el.getAttribute("aria-label");
|
|
79
|
+
if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim();
|
|
80
|
+
var labelledBy = el.getAttribute("aria-labelledby");
|
|
81
|
+
if (labelledBy && labelledBy.trim()) {
|
|
82
|
+
var text = labelledBy.trim().split(/\\s+/).map(function(id) {
|
|
83
|
+
var ref = document.getElementById(id);
|
|
84
|
+
return ref ? (ref.textContent || "").trim() : "";
|
|
85
|
+
}).join(" ").trim();
|
|
86
|
+
if (text) return text;
|
|
87
|
+
}
|
|
88
|
+
var placeholder = el.getAttribute("placeholder");
|
|
89
|
+
if (placeholder && placeholder.trim()) return placeholder.trim();
|
|
90
|
+
var alt = el.getAttribute("alt");
|
|
91
|
+
if (alt && alt.trim()) return alt.trim();
|
|
92
|
+
var value = el.value;
|
|
93
|
+
if (value && typeof value === "string" && value.trim()) return value.trim().slice(0, 80);
|
|
94
|
+
return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 80);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// 6. isInteractiveEl
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
var interactiveRoles = {
|
|
101
|
+
button: 1, link: 1, textbox: 1, searchbox: 1, combobox: 1,
|
|
102
|
+
checkbox: 1, radio: 1, "switch": 1, menuitem: 1,
|
|
103
|
+
menuitemcheckbox: 1, menuitemradio: 1, tab: 1, option: 1,
|
|
104
|
+
slider: 1, spinbutton: 1
|
|
105
|
+
};
|
|
106
|
+
pi.isInteractiveEl = function isInteractiveEl(el) {
|
|
107
|
+
var tag = el.tagName.toLowerCase();
|
|
108
|
+
var role = pi.inferRole(el);
|
|
109
|
+
if (["button", "input", "select", "textarea", "summary", "option"].indexOf(tag) !== -1) return true;
|
|
110
|
+
if (tag === "a" && !!el.getAttribute("href")) return true;
|
|
111
|
+
if (interactiveRoles[role]) return true;
|
|
112
|
+
if (el.tabIndex >= 0) return true;
|
|
113
|
+
if (el.isContentEditable) return true;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// 7. cssPath
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
pi.cssPath = function cssPath(el) {
|
|
121
|
+
if (el.id) return "#" + CSS.escape(el.id);
|
|
122
|
+
var parts = [];
|
|
123
|
+
var current = el;
|
|
124
|
+
while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
|
|
125
|
+
var tag = current.tagName.toLowerCase();
|
|
126
|
+
var part = tag;
|
|
127
|
+
var parent = current.parentElement;
|
|
128
|
+
if (parent) {
|
|
129
|
+
var siblings = Array.from(parent.children).filter(function(c) {
|
|
130
|
+
return c.tagName === current.tagName;
|
|
131
|
+
});
|
|
132
|
+
if (siblings.length > 1) {
|
|
133
|
+
var idx = siblings.indexOf(current) + 1;
|
|
134
|
+
part += ":nth-of-type(" + idx + ")";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
parts.unshift(part);
|
|
138
|
+
current = current.parentElement;
|
|
139
|
+
}
|
|
140
|
+
return "body > " + parts.join(" > ");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// -----------------------------------------------------------------------
|
|
144
|
+
// 8. domPath
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
pi.domPath = function domPath(el) {
|
|
147
|
+
var path = [];
|
|
148
|
+
var current = el;
|
|
149
|
+
while (current && current !== document.documentElement) {
|
|
150
|
+
var parent = current.parentElement;
|
|
151
|
+
if (!parent) break;
|
|
152
|
+
var idx = Array.from(parent.children).indexOf(current);
|
|
153
|
+
path.unshift(idx);
|
|
154
|
+
current = parent;
|
|
155
|
+
}
|
|
156
|
+
return path;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
// 9. selectorHints
|
|
161
|
+
// -----------------------------------------------------------------------
|
|
162
|
+
pi.selectorHints = function selectorHints(el) {
|
|
163
|
+
var hints = [];
|
|
164
|
+
if (el.id) hints.push("#" + CSS.escape(el.id));
|
|
165
|
+
var nameAttr = el.getAttribute("name");
|
|
166
|
+
if (nameAttr) hints.push(el.tagName.toLowerCase() + '[name="' + CSS.escape(nameAttr) + '"]');
|
|
167
|
+
var aria = el.getAttribute("aria-label");
|
|
168
|
+
if (aria) hints.push(el.tagName.toLowerCase() + '[aria-label="' + CSS.escape(aria) + '"]');
|
|
169
|
+
var placeholder = el.getAttribute("placeholder");
|
|
170
|
+
if (placeholder) hints.push(el.tagName.toLowerCase() + '[placeholder="' + CSS.escape(placeholder) + '"]');
|
|
171
|
+
var cls = Array.from(el.classList).slice(0, 2);
|
|
172
|
+
if (cls.length > 0) hints.push(el.tagName.toLowerCase() + "." + cls.map(function(c) { return CSS.escape(c); }).join("."));
|
|
173
|
+
hints.push(pi.cssPath(el));
|
|
174
|
+
var seen = {};
|
|
175
|
+
var unique = [];
|
|
176
|
+
for (var i = 0; i < hints.length; i++) {
|
|
177
|
+
if (!seen[hints[i]]) {
|
|
178
|
+
seen[hints[i]] = true;
|
|
179
|
+
unique.push(hints[i]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return unique.slice(0, 6);
|
|
183
|
+
};
|
|
184
|
+
})();`;
|