pinpoints 0.1.4
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/LICENSE +22 -0
- package/README.md +187 -0
- package/out/browser/browserSessionManager.d.ts +19 -0
- package/out/browser/browserSessionManager.d.ts.map +1 -0
- package/out/browser/browserSessionManager.js +169 -0
- package/out/browser/browserSessionManager.js.map +1 -0
- package/out/cli/index.d.ts +3 -0
- package/out/cli/index.d.ts.map +1 -0
- package/out/cli/index.js +564 -0
- package/out/cli/index.js.map +1 -0
- package/out/core/pickerToolbar.d.ts +9 -0
- package/out/core/pickerToolbar.d.ts.map +1 -0
- package/out/core/pickerToolbar.js +475 -0
- package/out/core/pickerToolbar.js.map +1 -0
- package/out/export/contextFormatter.d.ts +9 -0
- package/out/export/contextFormatter.d.ts.map +1 -0
- package/out/export/contextFormatter.js +151 -0
- package/out/export/contextFormatter.js.map +1 -0
- package/out/extension.d.ts +4 -0
- package/out/extension.d.ts.map +1 -0
- package/out/extension.js +95 -0
- package/out/extension.js.map +1 -0
- package/out/extraction/domExtractor.d.ts +10 -0
- package/out/extraction/domExtractor.d.ts.map +1 -0
- package/out/extraction/domExtractor.js +72 -0
- package/out/extraction/domExtractor.js.map +1 -0
- package/out/extraction/layoutExtractor.d.ts +11 -0
- package/out/extraction/layoutExtractor.d.ts.map +1 -0
- package/out/extraction/layoutExtractor.js +107 -0
- package/out/extraction/layoutExtractor.js.map +1 -0
- package/out/extraction/redactor.d.ts +9 -0
- package/out/extraction/redactor.d.ts.map +1 -0
- package/out/extraction/redactor.js +78 -0
- package/out/extraction/redactor.js.map +1 -0
- package/out/extraction/screenshotExtractor.d.ts +7 -0
- package/out/extraction/screenshotExtractor.d.ts.map +1 -0
- package/out/extraction/screenshotExtractor.js +104 -0
- package/out/extraction/screenshotExtractor.js.map +1 -0
- package/out/extraction/selectorExtractor.d.ts +16 -0
- package/out/extraction/selectorExtractor.d.ts.map +1 -0
- package/out/extraction/selectorExtractor.js +172 -0
- package/out/extraction/selectorExtractor.js.map +1 -0
- package/out/extraction/styleExtractor.d.ts +10 -0
- package/out/extraction/styleExtractor.d.ts.map +1 -0
- package/out/extraction/styleExtractor.js +96 -0
- package/out/extraction/styleExtractor.js.map +1 -0
- package/out/picker/pickerController.d.ts +33 -0
- package/out/picker/pickerController.d.ts.map +1 -0
- package/out/picker/pickerController.js +979 -0
- package/out/picker/pickerController.js.map +1 -0
- package/out/schemas/index.d.ts +418 -0
- package/out/schemas/index.d.ts.map +1 -0
- package/out/schemas/index.js +128 -0
- package/out/schemas/index.js.map +1 -0
- package/out/source/sourceLocator.d.ts +7 -0
- package/out/source/sourceLocator.d.ts.map +1 -0
- package/out/source/sourceLocator.js +37 -0
- package/out/source/sourceLocator.js.map +1 -0
- package/out/source/sourceMapResolver.d.ts +17 -0
- package/out/source/sourceMapResolver.d.ts.map +1 -0
- package/out/source/sourceMapResolver.js +204 -0
- package/out/source/sourceMapResolver.js.map +1 -0
- package/out/source/workspaceGrep.d.ts +30 -0
- package/out/source/workspaceGrep.d.ts.map +1 -0
- package/out/source/workspaceGrep.js +237 -0
- package/out/source/workspaceGrep.js.map +1 -0
- package/out/testPaste.d.ts +1 -0
- package/out/testPaste.d.ts.map +1 -0
- package/out/testPaste.js +3 -0
- package/out/testPaste.js.map +1 -0
- package/out/ui/statusBarManager.d.ts +14 -0
- package/out/ui/statusBarManager.d.ts.map +1 -0
- package/out/ui/statusBarManager.js +89 -0
- package/out/ui/statusBarManager.js.map +1 -0
- package/package.json +132 -0
- package/resources/fonts/icons.css +19 -0
- package/resources/fonts/icons.html +69 -0
- package/resources/fonts/icons.json +3 -0
- package/resources/fonts/icons.ts +13 -0
- package/resources/fonts/icons.woff +0 -0
- package/resources/icon.png +0 -0
- package/resources/icons/pinpoint-logo.svg +4 -0
- package/resources/logo.svg +97 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PickerController = void 0;
|
|
37
|
+
const vscode = __importStar(require("vscode"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const net = __importStar(require("net"));
|
|
41
|
+
const browserSessionManager_1 = require("../browser/browserSessionManager");
|
|
42
|
+
const selectorExtractor_1 = require("../extraction/selectorExtractor");
|
|
43
|
+
const domExtractor_1 = require("../extraction/domExtractor");
|
|
44
|
+
const styleExtractor_1 = require("../extraction/styleExtractor");
|
|
45
|
+
const layoutExtractor_1 = require("../extraction/layoutExtractor");
|
|
46
|
+
const screenshotExtractor_1 = require("../extraction/screenshotExtractor");
|
|
47
|
+
const redactor_1 = require("../extraction/redactor");
|
|
48
|
+
const contextFormatter_1 = require("../export/contextFormatter");
|
|
49
|
+
const sourceLocator_1 = require("../source/sourceLocator");
|
|
50
|
+
const pickerToolbar_1 = require("../core/pickerToolbar");
|
|
51
|
+
class PickerController {
|
|
52
|
+
constructor(context) {
|
|
53
|
+
this.context = context;
|
|
54
|
+
this.browserSession = null;
|
|
55
|
+
this.currentMode = 'pick';
|
|
56
|
+
this.screenshotEnabled = false;
|
|
57
|
+
this.contextRadius = 1;
|
|
58
|
+
this.injectionTarget = 'claude-code';
|
|
59
|
+
this.capturedElements = [];
|
|
60
|
+
this.tempDir = null;
|
|
61
|
+
this.isPickerActive = false;
|
|
62
|
+
this.workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
|
63
|
+
this.loadSettings();
|
|
64
|
+
}
|
|
65
|
+
loadSettings() {
|
|
66
|
+
const config = vscode.workspace.getConfiguration('pinpoint');
|
|
67
|
+
this.currentMode = (config.get('defaultMode') || 'pick');
|
|
68
|
+
this.screenshotEnabled = config.get('screenshotEnabled', false);
|
|
69
|
+
this.contextRadius = config.get('contextRadius', 1);
|
|
70
|
+
this.injectionTarget = config.get('injectionTarget', 'claude-code');
|
|
71
|
+
}
|
|
72
|
+
async startPicker() {
|
|
73
|
+
try {
|
|
74
|
+
// Initialize temp directory
|
|
75
|
+
if (!this.workspaceRoot) {
|
|
76
|
+
throw new Error('No workspace folder open. Please open a folder first.');
|
|
77
|
+
}
|
|
78
|
+
this.tempDir = path.join(this.workspaceRoot, '.pinpoint', 'temp');
|
|
79
|
+
if (!fs.existsSync(this.tempDir)) {
|
|
80
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
// Ensure .gitignore includes .pinpoint/
|
|
83
|
+
this.ensureGitignore();
|
|
84
|
+
// Detect local servers and let user pick or enter custom URL
|
|
85
|
+
const url = await this.promptForUrl();
|
|
86
|
+
if (!url) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Launch browser and navigate directly to the URL
|
|
90
|
+
this.browserSession = new browserSessionManager_1.BrowserSessionManager();
|
|
91
|
+
try {
|
|
92
|
+
const page = await vscode.window.withProgress({
|
|
93
|
+
location: vscode.ProgressLocation.Notification,
|
|
94
|
+
title: `PinPoint: Loading ${url}...`,
|
|
95
|
+
}, async () => {
|
|
96
|
+
const p = await this.browserSession.launch({ url });
|
|
97
|
+
return p;
|
|
98
|
+
});
|
|
99
|
+
vscode.window.showInformationMessage('Page loaded. Injecting picker...');
|
|
100
|
+
// Enable inspect mode
|
|
101
|
+
await this.enableInspectMode(page);
|
|
102
|
+
// Load logo SVG from file (single source of truth)
|
|
103
|
+
const logoSvgPath = path.join(this.context.extensionPath, 'resources', 'logo.svg');
|
|
104
|
+
const logoSvgContent = fs.readFileSync(logoSvgPath, 'utf-8');
|
|
105
|
+
// Inject picker UI and handlers
|
|
106
|
+
await this.injectPickerUI(page, logoSvgContent, this.currentMode, this.injectionTarget);
|
|
107
|
+
// Re-inject on navigation/refresh to make persistent
|
|
108
|
+
const attemptReinject = async () => {
|
|
109
|
+
if (this.isPickerActive) {
|
|
110
|
+
try {
|
|
111
|
+
// Wait for body to be available just in case
|
|
112
|
+
await page.waitForSelector('body', { timeout: 2000 }).catch(() => { });
|
|
113
|
+
await this.injectPickerUI(page, logoSvgContent, this.currentMode, this.injectionTarget);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.error('Failed to reinject picker UI:', e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
page.on('domcontentloaded', attemptReinject);
|
|
121
|
+
page.on('load', attemptReinject);
|
|
122
|
+
page.on('framenavigated', (frame) => {
|
|
123
|
+
if (frame === page.mainFrame()) {
|
|
124
|
+
attemptReinject();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
this.isPickerActive = true;
|
|
128
|
+
vscode.window.showInformationMessage('Picker active: Hover and click elements. Toolbar visible at bottom of page.');
|
|
129
|
+
}
|
|
130
|
+
catch (innerError) {
|
|
131
|
+
vscode.window.showErrorMessage(`Error during picker setup: ${innerError}`);
|
|
132
|
+
throw innerError;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
vscode.window.showErrorMessage(`Failed to start picker: ${error}`);
|
|
137
|
+
await this.stopPicker();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async injectPickerUI(page, logoSvg, initialMode, initialTarget) {
|
|
141
|
+
await (0, pickerToolbar_1.injectPickerToolbar)(page, { logoSvg, initialMode, initialTarget });
|
|
142
|
+
return;
|
|
143
|
+
await page.evaluate((args) => {
|
|
144
|
+
const { logoSvg, initialMode, initialTarget } = args;
|
|
145
|
+
if (document.getElementById('pinpoint-module'))
|
|
146
|
+
return;
|
|
147
|
+
const btnBase = `
|
|
148
|
+
height: 36px;
|
|
149
|
+
padding: 0;
|
|
150
|
+
background: transparent;
|
|
151
|
+
border: none;
|
|
152
|
+
border-radius: 9999px;
|
|
153
|
+
color: rgba(255, 255, 255, 0.6);
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
transition: color 0.2s ease;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
position: relative;
|
|
160
|
+
z-index: 1;
|
|
161
|
+
width: 36px;
|
|
162
|
+
flex-shrink: 0;
|
|
163
|
+
`;
|
|
164
|
+
// Create floating toolbar that doesn't affect page layout
|
|
165
|
+
const moduleContainer = document.createElement('div');
|
|
166
|
+
moduleContainer.id = 'pinpoint-module';
|
|
167
|
+
moduleContainer.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 0; pointer-events: none; overflow: visible; z-index: 2147483647;';
|
|
168
|
+
moduleContainer.innerHTML = `
|
|
169
|
+
<div id="pinpoint-tooltip" style="
|
|
170
|
+
position: fixed;
|
|
171
|
+
z-index: 9999999999;
|
|
172
|
+
background: rgba(0, 0, 0, 0.85);
|
|
173
|
+
color: #fff;
|
|
174
|
+
font-size: 11px;
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
padding: 4px 10px;
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
white-space: nowrap;
|
|
180
|
+
opacity: 0;
|
|
181
|
+
transition: opacity 0.15s ease;
|
|
182
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
183
|
+
"></div>
|
|
184
|
+
<div id="pinpoint-toolbar" style="
|
|
185
|
+
pointer-events: auto;
|
|
186
|
+
position: fixed;
|
|
187
|
+
bottom: 24px;
|
|
188
|
+
left: 50%;
|
|
189
|
+
transform: translateX(-50%);
|
|
190
|
+
z-index: 999999999;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: row;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 2px;
|
|
195
|
+
padding: 6px 10px;
|
|
196
|
+
background: rgba(30, 30, 30, 0.92);
|
|
197
|
+
backdrop-filter: blur(16px);
|
|
198
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
199
|
+
border-radius: 9999px;
|
|
200
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
201
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
202
|
+
user-select: none;
|
|
203
|
+
transition: padding 0.3s cubic-bezier(0.4, 0, 0.2, 1), gap 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
204
|
+
">
|
|
205
|
+
<!-- Toggle button (always visible) -->
|
|
206
|
+
<button id="pinpoint-toggle" title="Capture mode active (click or Esc to interact)" style="
|
|
207
|
+
height: 36px;
|
|
208
|
+
width: 36px;
|
|
209
|
+
padding: 0;
|
|
210
|
+
background: rgba(14, 165, 233, 0.2);
|
|
211
|
+
border: none;
|
|
212
|
+
border-radius: 9999px;
|
|
213
|
+
color: #0ea5e9;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: center;
|
|
219
|
+
flex-shrink: 0;
|
|
220
|
+
">
|
|
221
|
+
<span id="pinpoint-logo-slot" style="display:flex;align-items:center;justify-content:center;pointer-events:none;"></span>
|
|
222
|
+
</button>
|
|
223
|
+
|
|
224
|
+
<!-- Collapsible content -->
|
|
225
|
+
<div id="pinpoint-toolbar-content" style="
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: row;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 2px;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
transition: max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
|
|
232
|
+
max-width: 600px;
|
|
233
|
+
opacity: 1;
|
|
234
|
+
">
|
|
235
|
+
|
|
236
|
+
<!-- Drag handle -->
|
|
237
|
+
<div id="pinpoint-drag" title="Drag to reposition" style="
|
|
238
|
+
width: 28px;
|
|
239
|
+
height: 36px;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
cursor: grab;
|
|
244
|
+
color: rgba(255, 255, 255, 0.3);
|
|
245
|
+
flex-shrink: 0;
|
|
246
|
+
">
|
|
247
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
248
|
+
<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/>
|
|
249
|
+
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
|
|
250
|
+
<circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>
|
|
251
|
+
</svg>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<!-- Mode buttons -->
|
|
255
|
+
<div id="pinpoint-modes" style="
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: row;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 2px;
|
|
260
|
+
position: relative;
|
|
261
|
+
">
|
|
262
|
+
<div id="pinpoint-mode-slider" style="
|
|
263
|
+
position: absolute;
|
|
264
|
+
top: 0;
|
|
265
|
+
left: 0;
|
|
266
|
+
height: 100%;
|
|
267
|
+
background: rgba(255, 255, 255, 0.15);
|
|
268
|
+
border-radius: 9999px;
|
|
269
|
+
z-index: 0;
|
|
270
|
+
pointer-events: none;
|
|
271
|
+
width: 36px;
|
|
272
|
+
transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
273
|
+
"></div>
|
|
274
|
+
<button data-mode="pick" title="Quick Fix" style="${btnBase}">
|
|
275
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;">
|
|
276
|
+
<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/><circle cx="5" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>
|
|
277
|
+
</svg>
|
|
278
|
+
</button>
|
|
279
|
+
<button data-mode="full" title="Full" style="${btnBase}">
|
|
280
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;">
|
|
281
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/>
|
|
282
|
+
</svg>
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- Divider -->
|
|
287
|
+
<div style="width: 1px; height: 20px; background: rgba(255, 255, 255, 0.15); margin: 0 6px; flex-shrink: 0;"></div>
|
|
288
|
+
|
|
289
|
+
<!-- Target buttons -->
|
|
290
|
+
<div id="pinpoint-targets" style="
|
|
291
|
+
display: flex;
|
|
292
|
+
flex-direction: row;
|
|
293
|
+
align-items: center;
|
|
294
|
+
gap: 2px;
|
|
295
|
+
position: relative;
|
|
296
|
+
">
|
|
297
|
+
<div id="pinpoint-target-slider" style="
|
|
298
|
+
position: absolute;
|
|
299
|
+
top: 0;
|
|
300
|
+
left: 0;
|
|
301
|
+
height: 100%;
|
|
302
|
+
background: rgba(255, 255, 255, 0.15);
|
|
303
|
+
border-radius: 9999px;
|
|
304
|
+
z-index: 0;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
width: 36px;
|
|
307
|
+
transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
308
|
+
"></div>
|
|
309
|
+
<button data-target="claude-code" title="Claude Code" style="${btnBase}">
|
|
310
|
+
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink:0;">
|
|
311
|
+
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
|
312
|
+
</svg>
|
|
313
|
+
</button>
|
|
314
|
+
<button data-target="copilot-chat" title="Copilot Chat" style="${btnBase}">
|
|
315
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;">
|
|
316
|
+
<path d="M4 18v-5.5c0 -.667 .167 -1.333 .5 -2"/>
|
|
317
|
+
<path d="M12 7.5c0 -1 -.01 -4.07 -4 -3.5c-3.5 .5 -4 2.5 -4 3.5c0 1.5 0 4 3 4c4 0 5 -2.5 5 -4"/>
|
|
318
|
+
<path d="M4 12c-1.333 .667 -2 1.333 -2 2c0 1 0 3 1.5 4c3 2 6.5 3 8.5 3s5.499 -1 8.5 -3c1.5 -1 1.5 -3 1.5 -4c0 -.667 -.667 -1.333 -2 -2"/>
|
|
319
|
+
<path d="M20 18v-5.5c0 -.667 -.167 -1.333 -.5 -2"/>
|
|
320
|
+
<path d="M12 7.5l0 -.297l.01 -.269l.027 -.298l.013 -.105l.033 -.215c.014 -.073 .029 -.146 .046 -.22l.06 -.223c.336 -1.118 1.262 -2.237 3.808 -1.873c2.838 .405 3.703 1.797 3.93 2.842l.036 .204c0 .033 .01 .066 .013 .098l.016 .185l0 .171l0 .49l-.015 .394l-.02 .271c-.122 1.366 -.655 2.845 -2.962 2.845c-3.256 0 -4.524 -1.656 -4.883 -3.081l-.053 -.242a3.865 3.865 0 0 1 -.036 -.235l-.021 -.227a3.518 3.518 0 0 1 -.007 -.215l.005 0"/>
|
|
321
|
+
<path d="M10 15v2"/><path d="M14 15v2"/>
|
|
322
|
+
</svg>
|
|
323
|
+
</button>
|
|
324
|
+
<button data-target="clipboard" title="Clipboard" style="${btnBase}">
|
|
325
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;">
|
|
326
|
+
<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/>
|
|
327
|
+
</svg>
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
`;
|
|
333
|
+
document.body.appendChild(moduleContainer);
|
|
334
|
+
// Inject logo SVG from file and size it to fit the button
|
|
335
|
+
const logoSlot = document.getElementById('pinpoint-logo-slot');
|
|
336
|
+
logoSlot.innerHTML = logoSvg;
|
|
337
|
+
const svgEl = logoSlot.querySelector('svg');
|
|
338
|
+
if (svgEl) {
|
|
339
|
+
svgEl.setAttribute('width', '22');
|
|
340
|
+
svgEl.setAttribute('height', '22');
|
|
341
|
+
svgEl.style.flexShrink = '0';
|
|
342
|
+
svgEl.style.pointerEvents = 'none';
|
|
343
|
+
const paths = svgEl.querySelectorAll('path');
|
|
344
|
+
paths.forEach((p, i) => {
|
|
345
|
+
if (i === 0) {
|
|
346
|
+
// Outer pin silhouette — fills with currentColor (mode-driven)
|
|
347
|
+
p.setAttribute('fill', 'currentColor');
|
|
348
|
+
p.removeAttribute('stroke');
|
|
349
|
+
p.removeAttribute('stroke-width');
|
|
350
|
+
p.removeAttribute('stroke-linejoin');
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Inner details — dark cutout so they read against the colored fill
|
|
354
|
+
const cutout = 'rgba(20,20,20,0.92)';
|
|
355
|
+
if (p.getAttribute('fill') === 'none') {
|
|
356
|
+
p.setAttribute('stroke', cutout);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
p.setAttribute('fill', cutout);
|
|
360
|
+
p.removeAttribute('stroke');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const toolbar = document.getElementById('pinpoint-toolbar');
|
|
366
|
+
const modes = document.getElementById('pinpoint-modes');
|
|
367
|
+
const targets = document.getElementById('pinpoint-targets');
|
|
368
|
+
const modeSlider = document.getElementById('pinpoint-mode-slider');
|
|
369
|
+
const targetSlider = document.getElementById('pinpoint-target-slider');
|
|
370
|
+
const tooltip = document.getElementById('pinpoint-tooltip');
|
|
371
|
+
// Slider update helper — sets position; CSS transition handles the animation
|
|
372
|
+
function updateSlider(slider, activeBtn) {
|
|
373
|
+
slider.style.left = activeBtn.offsetLeft + 'px';
|
|
374
|
+
slider.style.width = activeBtn.offsetWidth + 'px';
|
|
375
|
+
}
|
|
376
|
+
// Tooltip helper
|
|
377
|
+
function showTooltip(btn, text) {
|
|
378
|
+
tooltip.textContent = text;
|
|
379
|
+
tooltip.style.opacity = '1';
|
|
380
|
+
const btnRect = btn.getBoundingClientRect();
|
|
381
|
+
const tipWidth = tooltip.offsetWidth;
|
|
382
|
+
tooltip.style.left = (btnRect.left + btnRect.width / 2 - tipWidth / 2) + 'px';
|
|
383
|
+
tooltip.style.top = (btnRect.top - 32) + 'px';
|
|
384
|
+
}
|
|
385
|
+
function hideTooltip() {
|
|
386
|
+
tooltip.style.opacity = '0';
|
|
387
|
+
}
|
|
388
|
+
// Shared state
|
|
389
|
+
let lastEl = null;
|
|
390
|
+
// Interact/Capture toggle
|
|
391
|
+
let isInteractMode = false;
|
|
392
|
+
const toggleBtn = document.getElementById('pinpoint-toggle');
|
|
393
|
+
const toolbarContent = document.getElementById('pinpoint-toolbar-content');
|
|
394
|
+
const shortcutLabel = 'Esc';
|
|
395
|
+
function setInteractMode(interact) {
|
|
396
|
+
isInteractMode = interact;
|
|
397
|
+
if (interact) {
|
|
398
|
+
// Collapsed: hide content, compact circle
|
|
399
|
+
toolbarContent.style.maxWidth = '0';
|
|
400
|
+
toolbarContent.style.opacity = '0';
|
|
401
|
+
toolbar.style.padding = '6px';
|
|
402
|
+
toolbar.style.gap = '0';
|
|
403
|
+
// Collapsed: main logo color
|
|
404
|
+
toggleBtn.style.background = '#ABFF06';
|
|
405
|
+
// Always black icon
|
|
406
|
+
toggleBtn.style.color = '#000000';
|
|
407
|
+
// Clear any hover highlight
|
|
408
|
+
if (lastEl) {
|
|
409
|
+
lastEl.style.outline = '';
|
|
410
|
+
lastEl = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Expanded: show content, restore padding
|
|
415
|
+
toolbarContent.style.maxWidth = '600px';
|
|
416
|
+
toolbarContent.style.opacity = '1';
|
|
417
|
+
toolbar.style.padding = '6px 10px';
|
|
418
|
+
toolbar.style.gap = '2px';
|
|
419
|
+
// Expanded: use main logo color
|
|
420
|
+
toggleBtn.style.background = '#ABFF06';
|
|
421
|
+
// Always black icon
|
|
422
|
+
toggleBtn.style.color = '#000000';
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Apply initial visual state
|
|
426
|
+
setInteractMode(isInteractMode);
|
|
427
|
+
toggleBtn.addEventListener('click', (e) => {
|
|
428
|
+
e.stopPropagation();
|
|
429
|
+
setInteractMode(!isInteractMode);
|
|
430
|
+
});
|
|
431
|
+
toggleBtn.addEventListener('mouseenter', () => {
|
|
432
|
+
const label = isInteractMode
|
|
433
|
+
? `Switch to Capture (${shortcutLabel})`
|
|
434
|
+
: `Switch to Interact (${shortcutLabel})`;
|
|
435
|
+
showTooltip(toggleBtn, label);
|
|
436
|
+
});
|
|
437
|
+
toggleBtn.addEventListener('mouseleave', () => {
|
|
438
|
+
hideTooltip();
|
|
439
|
+
});
|
|
440
|
+
document.addEventListener('keydown', (e) => {
|
|
441
|
+
const ke = e;
|
|
442
|
+
if (ke.key === 'Escape') {
|
|
443
|
+
ke.preventDefault();
|
|
444
|
+
ke.stopPropagation();
|
|
445
|
+
setInteractMode(!isInteractMode);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
// Drag logic
|
|
449
|
+
const dragHandle = document.getElementById('pinpoint-drag');
|
|
450
|
+
let isDragging = false;
|
|
451
|
+
let dragOffsetX = 0;
|
|
452
|
+
let dragOffsetY = 0;
|
|
453
|
+
dragHandle.addEventListener('mousedown', (e) => {
|
|
454
|
+
const me = e;
|
|
455
|
+
isDragging = true;
|
|
456
|
+
dragHandle.style.cursor = 'grabbing';
|
|
457
|
+
const rect = toolbar.getBoundingClientRect();
|
|
458
|
+
dragOffsetX = me.clientX - rect.left;
|
|
459
|
+
dragOffsetY = me.clientY - rect.top;
|
|
460
|
+
toolbar.style.left = rect.left + 'px';
|
|
461
|
+
toolbar.style.bottom = 'auto';
|
|
462
|
+
toolbar.style.top = rect.top + 'px';
|
|
463
|
+
toolbar.style.transform = 'none';
|
|
464
|
+
me.preventDefault();
|
|
465
|
+
me.stopPropagation();
|
|
466
|
+
});
|
|
467
|
+
document.addEventListener('mousemove', (e) => {
|
|
468
|
+
if (!isDragging)
|
|
469
|
+
return;
|
|
470
|
+
const me = e;
|
|
471
|
+
let newX = me.clientX - dragOffsetX;
|
|
472
|
+
let newY = me.clientY - dragOffsetY;
|
|
473
|
+
const rect = toolbar.getBoundingClientRect();
|
|
474
|
+
newX = Math.max(0, Math.min(window.innerWidth - rect.width, newX));
|
|
475
|
+
newY = Math.max(0, Math.min(window.innerHeight - rect.height, newY));
|
|
476
|
+
toolbar.style.left = newX + 'px';
|
|
477
|
+
toolbar.style.top = newY + 'px';
|
|
478
|
+
me.preventDefault();
|
|
479
|
+
me.stopPropagation();
|
|
480
|
+
});
|
|
481
|
+
document.addEventListener('mouseup', (e) => {
|
|
482
|
+
if (isDragging) {
|
|
483
|
+
isDragging = false;
|
|
484
|
+
dragHandle.style.cursor = 'grab';
|
|
485
|
+
e.preventDefault();
|
|
486
|
+
e.stopPropagation();
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
// Mode buttons
|
|
490
|
+
window.pinPointMode = initialMode || 'pick';
|
|
491
|
+
const modeButtons = modes.querySelectorAll('button');
|
|
492
|
+
let activeModeBtn = Array.from(modeButtons).find(btn => btn.getAttribute('data-mode') === window.pinPointMode) || modeButtons[0];
|
|
493
|
+
// Initialize active mode button
|
|
494
|
+
activeModeBtn.style.color = '#ffffff';
|
|
495
|
+
updateSlider(modeSlider, activeModeBtn);
|
|
496
|
+
modeButtons.forEach((btn) => {
|
|
497
|
+
btn.addEventListener('click', (e) => {
|
|
498
|
+
e.stopPropagation();
|
|
499
|
+
const mode = btn.getAttribute('data-mode');
|
|
500
|
+
window.pinPointMode = mode;
|
|
501
|
+
console.log('PINPOINT_MODE_CHANGED:', mode);
|
|
502
|
+
if (activeModeBtn !== btn) {
|
|
503
|
+
activeModeBtn.style.color = 'rgba(255, 255, 255, 0.6)';
|
|
504
|
+
activeModeBtn = btn;
|
|
505
|
+
activeModeBtn.style.color = '#ffffff';
|
|
506
|
+
updateSlider(modeSlider, activeModeBtn);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
btn.addEventListener('mouseenter', () => {
|
|
510
|
+
if (btn !== activeModeBtn) {
|
|
511
|
+
btn.style.color = 'rgba(255, 255, 255, 0.9)';
|
|
512
|
+
showTooltip(btn, btn.getAttribute('title') || '');
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
btn.addEventListener('mouseleave', () => {
|
|
516
|
+
if (btn !== activeModeBtn) {
|
|
517
|
+
btn.style.color = 'rgba(255, 255, 255, 0.6)';
|
|
518
|
+
}
|
|
519
|
+
hideTooltip();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
// Target buttons
|
|
523
|
+
const targetButtons = targets.querySelectorAll('button');
|
|
524
|
+
let activeTargetBtn = Array.from(targetButtons).find(btn => btn.getAttribute('data-target') === initialTarget) || targetButtons[0];
|
|
525
|
+
// Initialize active target button
|
|
526
|
+
activeTargetBtn.style.color = '#ffffff';
|
|
527
|
+
updateSlider(targetSlider, activeTargetBtn);
|
|
528
|
+
targetButtons.forEach((btn) => {
|
|
529
|
+
btn.addEventListener('click', (e) => {
|
|
530
|
+
e.stopPropagation();
|
|
531
|
+
const target = btn.getAttribute('data-target');
|
|
532
|
+
console.log('PINPOINT_TARGET_CHANGED:', target);
|
|
533
|
+
if (activeTargetBtn !== btn) {
|
|
534
|
+
activeTargetBtn.style.color = 'rgba(255, 255, 255, 0.6)';
|
|
535
|
+
activeTargetBtn = btn;
|
|
536
|
+
activeTargetBtn.style.color = '#ffffff';
|
|
537
|
+
updateSlider(targetSlider, activeTargetBtn);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
btn.addEventListener('mouseenter', () => {
|
|
541
|
+
if (btn !== activeTargetBtn) {
|
|
542
|
+
btn.style.color = 'rgba(255, 255, 255, 0.9)';
|
|
543
|
+
showTooltip(btn, btn.getAttribute('title') || '');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
btn.addEventListener('mouseleave', () => {
|
|
547
|
+
if (btn !== activeTargetBtn) {
|
|
548
|
+
btn.style.color = 'rgba(255, 255, 255, 0.6)';
|
|
549
|
+
}
|
|
550
|
+
hideTooltip();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
// Hover highlight + click handler
|
|
554
|
+
document.addEventListener('mousemove', (e) => {
|
|
555
|
+
if (isDragging)
|
|
556
|
+
return;
|
|
557
|
+
if (isInteractMode)
|
|
558
|
+
return;
|
|
559
|
+
// Use elementFromPoint instead of e.target for more reliable hit testing,
|
|
560
|
+
// especially with complex layouts or overlays
|
|
561
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
562
|
+
if (!target)
|
|
563
|
+
return;
|
|
564
|
+
const el = target;
|
|
565
|
+
// Skip highlighting the module itself
|
|
566
|
+
if (el.closest('#pinpoint-module'))
|
|
567
|
+
return;
|
|
568
|
+
if (lastEl && lastEl !== el && lastEl !== window.__pinpoint_clicked) {
|
|
569
|
+
lastEl.style.outline = '';
|
|
570
|
+
}
|
|
571
|
+
el.style.outline = '3px solid #0ea5e9';
|
|
572
|
+
el.style.outlineOffset = '2px';
|
|
573
|
+
lastEl = el;
|
|
574
|
+
}, true);
|
|
575
|
+
document.addEventListener('click', (e) => {
|
|
576
|
+
if (isDragging)
|
|
577
|
+
return;
|
|
578
|
+
if (isInteractMode)
|
|
579
|
+
return;
|
|
580
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
581
|
+
if (!target)
|
|
582
|
+
return;
|
|
583
|
+
const el = target;
|
|
584
|
+
// Don't capture if clicking the module
|
|
585
|
+
if (el.closest('#pinpoint-module'))
|
|
586
|
+
return;
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
e.stopPropagation();
|
|
589
|
+
window.__pinpoint_clicked = el;
|
|
590
|
+
console.log('PINPOINT_SELECTED:', JSON.stringify({
|
|
591
|
+
tag: el.tagName,
|
|
592
|
+
class: el.className,
|
|
593
|
+
id: el.id,
|
|
594
|
+
}));
|
|
595
|
+
}, true);
|
|
596
|
+
}, { logoSvg, initialMode, initialTarget });
|
|
597
|
+
}
|
|
598
|
+
async promptForUrl() {
|
|
599
|
+
const CUSTOM_URL_LABEL = '$(edit) Enter custom URL...';
|
|
600
|
+
const HISTORY_KEY = 'pinpoint.urlHistory';
|
|
601
|
+
const MAX_HISTORY = 10;
|
|
602
|
+
const history = this.context.globalState.get(HISTORY_KEY, []);
|
|
603
|
+
const detectedServers = await this.detectLocalServers();
|
|
604
|
+
const items = [];
|
|
605
|
+
if (detectedServers.length > 0) {
|
|
606
|
+
items.push({ label: 'Detected Servers', kind: vscode.QuickPickItemKind.Separator });
|
|
607
|
+
for (const server of detectedServers) {
|
|
608
|
+
items.push({
|
|
609
|
+
label: `$(radio-tower) ${server.url}`,
|
|
610
|
+
description: server.label,
|
|
611
|
+
detail: server.url,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (history.length > 0) {
|
|
616
|
+
items.push({ label: 'Recent', kind: vscode.QuickPickItemKind.Separator });
|
|
617
|
+
for (const historyUrl of history) {
|
|
618
|
+
if (detectedServers.some(s => s.url === historyUrl))
|
|
619
|
+
continue;
|
|
620
|
+
items.push({
|
|
621
|
+
label: `$(history) ${historyUrl}`,
|
|
622
|
+
detail: historyUrl,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
items.push({ label: '', kind: vscode.QuickPickItemKind.Separator });
|
|
627
|
+
items.push({
|
|
628
|
+
label: CUSTOM_URL_LABEL,
|
|
629
|
+
description: 'Type any URL',
|
|
630
|
+
alwaysShow: true,
|
|
631
|
+
});
|
|
632
|
+
const picked = await vscode.window.showQuickPick(items, {
|
|
633
|
+
placeHolder: 'Select a local server or enter a custom URL',
|
|
634
|
+
matchOnDescription: true,
|
|
635
|
+
matchOnDetail: true,
|
|
636
|
+
});
|
|
637
|
+
if (!picked)
|
|
638
|
+
return undefined;
|
|
639
|
+
let url;
|
|
640
|
+
if (picked.label === CUSTOM_URL_LABEL) {
|
|
641
|
+
url = await vscode.window.showInputBox({
|
|
642
|
+
prompt: 'Enter the URL to inspect',
|
|
643
|
+
placeHolder: 'e.g., http://localhost:3000',
|
|
644
|
+
value: history[0] || 'http://localhost:3000',
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
url = picked.detail;
|
|
649
|
+
}
|
|
650
|
+
if (!url)
|
|
651
|
+
return undefined;
|
|
652
|
+
const updatedHistory = [url, ...history.filter(h => h !== url)].slice(0, MAX_HISTORY);
|
|
653
|
+
await this.context.globalState.update(HISTORY_KEY, updatedHistory);
|
|
654
|
+
return url;
|
|
655
|
+
}
|
|
656
|
+
async detectLocalServers() {
|
|
657
|
+
const commonPorts = [
|
|
658
|
+
{ port: 3000, label: 'React / Express' },
|
|
659
|
+
{ port: 3001, label: 'React (alt)' },
|
|
660
|
+
{ port: 4200, label: 'Angular' },
|
|
661
|
+
{ port: 4321, label: 'Astro' },
|
|
662
|
+
{ port: 5000, label: 'Flask / .NET' },
|
|
663
|
+
{ port: 5173, label: 'Vite' },
|
|
664
|
+
{ port: 5174, label: 'Vite (alt)' },
|
|
665
|
+
{ port: 5500, label: 'Live Server' },
|
|
666
|
+
{ port: 8000, label: 'Django / Python' },
|
|
667
|
+
{ port: 8080, label: 'General dev server' },
|
|
668
|
+
{ port: 8081, label: 'General dev server' },
|
|
669
|
+
{ port: 8888, label: 'Jupyter / dev server' },
|
|
670
|
+
{ port: 3333, label: 'Dev server' },
|
|
671
|
+
{ port: 4000, label: 'Phoenix / Gatsby' },
|
|
672
|
+
{ port: 1234, label: 'Parcel' },
|
|
673
|
+
{ port: 9000, label: 'Webpack / PHP' },
|
|
674
|
+
];
|
|
675
|
+
const results = [];
|
|
676
|
+
const checks = commonPorts.map(({ port, label }) => this.isPortOpen(port, 150).then(open => {
|
|
677
|
+
if (open) {
|
|
678
|
+
results.push({ url: `http://localhost:${port}`, label: `Port ${port} · ${label}` });
|
|
679
|
+
}
|
|
680
|
+
}));
|
|
681
|
+
await Promise.all(checks);
|
|
682
|
+
results.sort((a, b) => {
|
|
683
|
+
const portA = parseInt(a.url.split(':').pop());
|
|
684
|
+
const portB = parseInt(b.url.split(':').pop());
|
|
685
|
+
return portA - portB;
|
|
686
|
+
});
|
|
687
|
+
return results;
|
|
688
|
+
}
|
|
689
|
+
isPortOpen(port, timeout) {
|
|
690
|
+
return new Promise(resolve => {
|
|
691
|
+
const socket = new net.Socket();
|
|
692
|
+
socket.setTimeout(timeout);
|
|
693
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
694
|
+
socket.once('timeout', () => { socket.destroy(); resolve(false); });
|
|
695
|
+
socket.once('error', () => { socket.destroy(); resolve(false); });
|
|
696
|
+
socket.connect(port, '127.0.0.1');
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async stopPicker() {
|
|
700
|
+
this.isPickerActive = false;
|
|
701
|
+
if (this.browserSession) {
|
|
702
|
+
await this.browserSession.close();
|
|
703
|
+
this.browserSession = null;
|
|
704
|
+
}
|
|
705
|
+
this.capturedElements = [];
|
|
706
|
+
}
|
|
707
|
+
toggleScreenshot() {
|
|
708
|
+
this.screenshotEnabled = !this.screenshotEnabled;
|
|
709
|
+
}
|
|
710
|
+
isScreenshotEnabled() {
|
|
711
|
+
return this.screenshotEnabled;
|
|
712
|
+
}
|
|
713
|
+
setMode(mode) {
|
|
714
|
+
this.currentMode = mode;
|
|
715
|
+
}
|
|
716
|
+
clearSelection() {
|
|
717
|
+
this.capturedElements = [];
|
|
718
|
+
}
|
|
719
|
+
async enableInspectMode(page) {
|
|
720
|
+
page.on('console', (msg) => {
|
|
721
|
+
const text = msg.text();
|
|
722
|
+
if (text.startsWith('PINPOINT_SELECTED:')) {
|
|
723
|
+
const dataStr = text.replace('PINPOINT_SELECTED:', '').trim();
|
|
724
|
+
try {
|
|
725
|
+
const data = JSON.parse(dataStr);
|
|
726
|
+
this.captureClickedElement(page);
|
|
727
|
+
}
|
|
728
|
+
catch (e) {
|
|
729
|
+
// Fail silently
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else if (text.startsWith('PINPOINT_MODE_CHANGED:')) {
|
|
733
|
+
const mode = text.replace('PINPOINT_MODE_CHANGED:', '').trim();
|
|
734
|
+
if (['pick', 'full'].includes(mode)) {
|
|
735
|
+
this.currentMode = mode;
|
|
736
|
+
vscode.workspace.getConfiguration('pinpoint').update('defaultMode', mode, true);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
else if (text.startsWith('PINPOINT_TARGET_CHANGED:')) {
|
|
740
|
+
const target = text.replace('PINPOINT_TARGET_CHANGED:', '').trim();
|
|
741
|
+
if (['claude-code', 'copilot-chat', 'clipboard'].includes(target)) {
|
|
742
|
+
this.injectionTarget = target;
|
|
743
|
+
vscode.workspace.getConfiguration('pinpoint').update('injectionTarget', target, true);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
async captureClickedElement(page) {
|
|
749
|
+
try {
|
|
750
|
+
// Get the selected mode from the page
|
|
751
|
+
const mode = await page.evaluate(() => {
|
|
752
|
+
return window.pinPointMode || 'pick';
|
|
753
|
+
});
|
|
754
|
+
// Update the current mode
|
|
755
|
+
if (mode && ['pick', 'full'].includes(mode)) {
|
|
756
|
+
this.currentMode = mode;
|
|
757
|
+
}
|
|
758
|
+
// Get the element that was clicked (stored reference, with fallbacks)
|
|
759
|
+
const elementHandle = await page.evaluateHandle(() => {
|
|
760
|
+
const el = window.__pinpoint_clicked;
|
|
761
|
+
if (el) {
|
|
762
|
+
delete window.__pinpoint_clicked;
|
|
763
|
+
return el;
|
|
764
|
+
}
|
|
765
|
+
return document.querySelector('[style*="0ea5e9"]') || document.activeElement;
|
|
766
|
+
});
|
|
767
|
+
if (elementHandle) {
|
|
768
|
+
const tagName = await elementHandle.evaluate((el) => el.tagName.toLowerCase());
|
|
769
|
+
if (tagName === 'body' || tagName === 'html') {
|
|
770
|
+
vscode.window.showWarningMessage('PinPoint: Could not identify the clicked element. Please try again.');
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
await this.captureElement(elementHandle);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
console.error('Failed to capture clicked element:', error);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
ensureGitignore() {
|
|
781
|
+
const gitignorePath = path.join(this.workspaceRoot, '.gitignore');
|
|
782
|
+
const tmpPattern = '.pinpoint/';
|
|
783
|
+
try {
|
|
784
|
+
let gitignoreContent = '';
|
|
785
|
+
if (fs.existsSync(gitignorePath)) {
|
|
786
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
787
|
+
}
|
|
788
|
+
if (!gitignoreContent.includes(tmpPattern)) {
|
|
789
|
+
gitignoreContent += (gitignoreContent ? '\n' : '') + tmpPattern;
|
|
790
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// Silently fail if we can't update gitignore
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async captureElement(elementHandle) {
|
|
798
|
+
try {
|
|
799
|
+
if (!this.browserSession) {
|
|
800
|
+
throw new Error('Browser session not active');
|
|
801
|
+
}
|
|
802
|
+
const page = this.browserSession.getPage();
|
|
803
|
+
if (!page) {
|
|
804
|
+
throw new Error('Page not available');
|
|
805
|
+
}
|
|
806
|
+
// Extract information
|
|
807
|
+
const selectorExtractor = new selectorExtractor_1.SelectorExtractor();
|
|
808
|
+
const domExtractor = new domExtractor_1.DomExtractor();
|
|
809
|
+
const styleExtractor = new styleExtractor_1.StyleExtractor();
|
|
810
|
+
const layoutExtractor = new layoutExtractor_1.LayoutExtractor();
|
|
811
|
+
const screenshotExtractor = new screenshotExtractor_1.ScreenshotExtractor();
|
|
812
|
+
const redactor = new redactor_1.Redactor();
|
|
813
|
+
// Get identity first
|
|
814
|
+
const identity = await elementHandle.evaluate((el) => {
|
|
815
|
+
const dataAttributes = {};
|
|
816
|
+
for (const attr of Array.from(el.attributes)) {
|
|
817
|
+
if (attr.name.startsWith('data-')) {
|
|
818
|
+
dataAttributes[attr.name] = attr.value;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
tag: el.tagName.toLowerCase(),
|
|
823
|
+
id: el.id || undefined,
|
|
824
|
+
classes: Array.from(el.classList),
|
|
825
|
+
role: el.getAttribute('role') || undefined,
|
|
826
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
827
|
+
dataAttributes: Object.keys(dataAttributes).length > 0 ? dataAttributes : undefined,
|
|
828
|
+
text: (el.textContent?.trim().substring(0, 200) || undefined),
|
|
829
|
+
accessibleName: el.ariaLabel || undefined,
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
// Extract all data
|
|
833
|
+
const selectors = await selectorExtractor.extract(elementHandle);
|
|
834
|
+
const dom = await domExtractor.extract(elementHandle, this.contextRadius);
|
|
835
|
+
const styles = await styleExtractor.extract(elementHandle, this.currentMode);
|
|
836
|
+
const layout = await layoutExtractor.extract(elementHandle, page);
|
|
837
|
+
let visual;
|
|
838
|
+
const shouldScreenshot = this.currentMode === 'full' || this.screenshotEnabled;
|
|
839
|
+
if (shouldScreenshot && this.tempDir) {
|
|
840
|
+
try {
|
|
841
|
+
visual = await screenshotExtractor.extract(elementHandle, this.tempDir);
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
console.warn('Screenshot capture failed:', error);
|
|
845
|
+
vscode.window.showWarningMessage('PinPoint: Screenshot capture failed. Other data was captured successfully.');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Detect source file
|
|
849
|
+
let sourceLocation;
|
|
850
|
+
try {
|
|
851
|
+
const sourceLocator = new sourceLocator_1.SourceLocator();
|
|
852
|
+
sourceLocation = await sourceLocator.locate(page, identity, dom, this.workspaceRoot);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
console.warn('Source detection failed:', error);
|
|
856
|
+
}
|
|
857
|
+
// Detect React component name via fiber tree
|
|
858
|
+
let reactComponent;
|
|
859
|
+
try {
|
|
860
|
+
reactComponent = await elementHandle.evaluate((el) => {
|
|
861
|
+
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
|
|
862
|
+
if (!fiberKey)
|
|
863
|
+
return undefined;
|
|
864
|
+
let fiber = el[fiberKey];
|
|
865
|
+
while (fiber) {
|
|
866
|
+
if (fiber.type && typeof fiber.type === 'function') {
|
|
867
|
+
const name = fiber.type.displayName || fiber.type.name;
|
|
868
|
+
if (name && name !== 'Anonymous')
|
|
869
|
+
return name;
|
|
870
|
+
}
|
|
871
|
+
fiber = fiber.return;
|
|
872
|
+
}
|
|
873
|
+
return undefined;
|
|
874
|
+
}) || undefined;
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
console.warn('React component detection failed:', error);
|
|
878
|
+
}
|
|
879
|
+
// Build MaxContext
|
|
880
|
+
let context = {
|
|
881
|
+
meta: {
|
|
882
|
+
url: page.url(),
|
|
883
|
+
timestamp: new Date().toISOString(),
|
|
884
|
+
viewport: layout.viewport || { width: 1920, height: 1080 },
|
|
885
|
+
dpr: layout.devicePixelRatio,
|
|
886
|
+
},
|
|
887
|
+
identity,
|
|
888
|
+
selectors,
|
|
889
|
+
dom,
|
|
890
|
+
layout,
|
|
891
|
+
styles,
|
|
892
|
+
visual,
|
|
893
|
+
sourceLocation,
|
|
894
|
+
reactComponent,
|
|
895
|
+
};
|
|
896
|
+
// Redact sensitive data
|
|
897
|
+
context = redactor.redact(context);
|
|
898
|
+
// Store captured element
|
|
899
|
+
this.capturedElements.push(context);
|
|
900
|
+
// Format and inject to chat
|
|
901
|
+
const formatter = new contextFormatter_1.ContextFormatter();
|
|
902
|
+
const injectedText = formatter.formatForChat(this.capturedElements, this.currentMode, this.workspaceRoot);
|
|
903
|
+
// Inject to focused location (terminal, chat, or clipboard)
|
|
904
|
+
await this.injectToFocusedLocation(injectedText, this.capturedElements.length);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
vscode.window.showErrorMessage(`Failed to capture element: ${error}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async injectToFocusedLocation(injectedText, elementNumber) {
|
|
911
|
+
const target = this.injectionTarget;
|
|
912
|
+
if (target === 'claude-code') {
|
|
913
|
+
try {
|
|
914
|
+
await vscode.env.clipboard.writeText(injectedText + '\n\n');
|
|
915
|
+
await vscode.commands.executeCommand('claude-vscode.focus');
|
|
916
|
+
// Wait for the chat to open and focus (increased delay to handle cold start)
|
|
917
|
+
await new Promise(r => setTimeout(r, 600));
|
|
918
|
+
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
|
919
|
+
// Auxiliary view updates - don't fail the whole block if these error
|
|
920
|
+
try {
|
|
921
|
+
await new Promise(r => setTimeout(r, 100));
|
|
922
|
+
await vscode.commands.executeCommand('list.scrollToBottom');
|
|
923
|
+
await vscode.commands.executeCommand('cursorBottom');
|
|
924
|
+
}
|
|
925
|
+
catch (e) { /* ignore scroll errors */ }
|
|
926
|
+
vscode.window.showInformationMessage(`Captured element #${elementNumber} to Claude Code`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
console.warn('Claude Code injection failed, falling back to clipboard', err);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (target === 'copilot-chat') {
|
|
934
|
+
try {
|
|
935
|
+
await vscode.env.clipboard.writeText(injectedText + '\n\n');
|
|
936
|
+
await vscode.commands.executeCommand('workbench.action.chat.open');
|
|
937
|
+
// Wait enough time for the chat view to focus
|
|
938
|
+
await new Promise(r => setTimeout(r, 600));
|
|
939
|
+
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
|
940
|
+
// Auxiliary view updates - don't fail the whole block if these error
|
|
941
|
+
try {
|
|
942
|
+
await new Promise(r => setTimeout(r, 100));
|
|
943
|
+
await vscode.commands.executeCommand('workbench.action.chat.scrollToBottom');
|
|
944
|
+
}
|
|
945
|
+
catch (e) { /* might not exist in all versions */ }
|
|
946
|
+
try {
|
|
947
|
+
await vscode.commands.executeCommand('list.scrollToBottom'); // Fallback
|
|
948
|
+
await vscode.commands.executeCommand('cursorBottom');
|
|
949
|
+
}
|
|
950
|
+
catch (e) { /* ignore scroll errors */ }
|
|
951
|
+
vscode.window.showInformationMessage(`Captured element #${elementNumber} to Copilot Chat`);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
console.warn('Copilot Chat injection failed, falling back to clipboard', err);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// Clipboard fallback (or explicit clipboard target)
|
|
959
|
+
await vscode.env.clipboard.writeText(injectedText);
|
|
960
|
+
vscode.window.showInformationMessage(`Context copied to clipboard. Paste where needed.`);
|
|
961
|
+
}
|
|
962
|
+
cleanup() {
|
|
963
|
+
if (this.browserSession) {
|
|
964
|
+
this.browserSession.dispose();
|
|
965
|
+
this.browserSession = null;
|
|
966
|
+
}
|
|
967
|
+
// Clean up temp directory
|
|
968
|
+
if (this.tempDir && fs.existsSync(this.tempDir)) {
|
|
969
|
+
try {
|
|
970
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
// Silently fail
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
exports.PickerController = PickerController;
|
|
979
|
+
//# sourceMappingURL=pickerController.js.map
|