pi-ele-picker 0.4.0 → 0.6.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 +24 -0
- package/client/picker.js +322 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -105,6 +105,18 @@ The launcher starts in the **bottom-right** corner and stays out of the way:
|
|
|
105
105
|
- **写入pi输入框** — pre-fills the current pi input box with the composed prompt; you edit and press Enter.
|
|
106
106
|
- **直接发送** — submits the composed prompt as a real user turn immediately (queued as a follow-up if pi is mid-turn).
|
|
107
107
|
|
|
108
|
+
### Drafts survive reloads
|
|
109
|
+
|
|
110
|
+
Vite reloads the page every time you save a file, so the composer is mirrored into `sessionStorage` and re-attached on the next load:
|
|
111
|
+
|
|
112
|
+
- The description and any attached screenshots come back exactly as they were.
|
|
113
|
+
- The captured element is **snapshotted at pick time and persisted with the draft**, so the element data survives the node being unmounted, an SPA route change, or the reload itself.
|
|
114
|
+
- The captured element is **re-resolved from its selector**, so after an HMR update you land straight back in the panel on the same element — no re-pick.
|
|
115
|
+
- Closing the panel with **✕** also suppresses the automatic reopen; the draft stays reachable through the launcher's hover.
|
|
116
|
+
- **Sending never depends on the element still being on the page.** Pick a button, go copy something from another route, then send — the description goes out with the element as it was when you picked it. A live read is used instead while you are still on the pick-time page, so an HMR swap of the same node is described in its current state; after a route change the snapshot wins, so a lookalike behind the same selector cannot be mistaken for your pick. When a snapshot was used, the status line says so.
|
|
117
|
+
- The draft is per tab and per path (`sessionStorage`, not `localStorage`), and one older than 24h is discarded. A successful send clears it.
|
|
118
|
+
- Screenshots that exceed the ~5MB `sessionStorage` quota are dropped from the draft with a notice rather than silently lost; the description is always kept.
|
|
119
|
+
|
|
108
120
|
### Session status
|
|
109
121
|
|
|
110
122
|
Status polling is **opt-in and off by default**: each cycle opens a loopback request, so it is disabled unless you want it. There are two levels:
|
|
@@ -142,6 +154,18 @@ The same payload is available from the bridge directly:
|
|
|
142
154
|
- If pi restarts while a page is open (new random port + token), the next send re-discovers the bridge and retries once automatically — no page refresh needed. The status poll does the same: a stale port flips to `offline` for one cycle, then rediscovers.
|
|
143
155
|
- The bridge never auto-submits on its own: **写入pi输入框** only fills the editor; **直接发送** is an explicit user action in the page.
|
|
144
156
|
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm install
|
|
161
|
+
npm test # end-to-end suite; starts its own Vite dev server
|
|
162
|
+
npm run typecheck
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The suite drives the real browser client inside [`test/fixture`](test/fixture), a throwaway Vite app: it picks elements, types descriptions, attaches screenshots and reloads, then asserts the draft survived. It runs against `src/` and `client/` directly — no build step is needed first, and a broken client fails the tests rather than the published package.
|
|
166
|
+
|
|
167
|
+
Local runs drive your installed Chrome. To use Playwright's pinned Chromium instead, run `npx playwright install chromium` once and set `PW_BUNDLED=1`; CI always uses the pinned build. `npx playwright test --ui` and `--debug` work as usual, and a failing run keeps a trace in `test-results/`.
|
|
168
|
+
|
|
145
169
|
## Releasing
|
|
146
170
|
|
|
147
171
|
Publishing is token-free: `.github/workflows/release.yml` publishes with **Trusted Publishing (OIDC)**, so `npm publish` authenticates through a short-lived OIDC token minted by GitHub Actions and provenance is attached automatically.
|
package/client/picker.js
CHANGED
|
@@ -152,10 +152,22 @@
|
|
|
152
152
|
// The captured element the panel acts on. Stays put across close/reopen so
|
|
153
153
|
// hovering the launcher can bring the panel back without overwriting it.
|
|
154
154
|
var target = null;
|
|
155
|
+
// The selector `target` was captured through. Kept so the draft can be
|
|
156
|
+
// re-attached after a reload replaces the DOM node `target` points at.
|
|
157
|
+
var targetSelector = null;
|
|
158
|
+
// Everything about the target captured at pick time. Sending must not depend on
|
|
159
|
+
// the node still being on the page — see elementPayload().
|
|
160
|
+
var targetSnapshot = null;
|
|
161
|
+
// The URL the target was picked on. A selector only identifies an element within
|
|
162
|
+
// one document: after a route change the same selector may hit a lookalike.
|
|
163
|
+
var targetUrl = null;
|
|
155
164
|
var shots = [];
|
|
156
165
|
// Set when a pick commits on pointerdown; swallows the follow-up click so the
|
|
157
166
|
// page (links, buttons) never reacts to the click that finished the pick.
|
|
158
167
|
var swallowClick = false;
|
|
168
|
+
// Set when the user closes the panel themselves, so a reload does not pop it
|
|
169
|
+
// back open on top of whatever they moved on to.
|
|
170
|
+
var panelDismissed = false;
|
|
159
171
|
|
|
160
172
|
document.documentElement.appendChild(host);
|
|
161
173
|
|
|
@@ -485,22 +497,46 @@
|
|
|
485
497
|
}
|
|
486
498
|
}
|
|
487
499
|
|
|
488
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* Re-show the panel for the element already captured — no re-pick, no overwrite.
|
|
502
|
+
*
|
|
503
|
+
* A snapshot is enough to reopen: the developer may have navigated away from the
|
|
504
|
+
* page that element lives on, and losing access to their own draft over that
|
|
505
|
+
* would be the wrong trade. The highlight is simply skipped when there is
|
|
506
|
+
* nothing on screen to outline.
|
|
507
|
+
*/
|
|
489
508
|
function openPanel() {
|
|
490
|
-
|
|
509
|
+
var el = resolveTarget();
|
|
510
|
+
if (!el && !targetSnapshot) return false;
|
|
491
511
|
panel.classList.add('open');
|
|
492
|
-
showHighlight(
|
|
512
|
+
if (el) showHighlight(el);
|
|
513
|
+
else hideHighlight();
|
|
493
514
|
positionPanel();
|
|
494
515
|
return true;
|
|
495
516
|
}
|
|
496
517
|
|
|
497
518
|
function select(el) {
|
|
498
519
|
target = el;
|
|
499
|
-
|
|
520
|
+
targetSelector = cssPath(el);
|
|
521
|
+
targetSnapshot = describe(el);
|
|
522
|
+
targetUrl = location.href;
|
|
523
|
+
// A new pick re-arms the draft: the user is working again, so a reload must
|
|
524
|
+
// bring the panel back even if they closed it earlier.
|
|
525
|
+
panelDismissed = false;
|
|
526
|
+
metaEl.innerHTML = '<code>' + escapeHtml(targetSelector) + '</code>' + vueLine(el);
|
|
500
527
|
openPanel();
|
|
528
|
+
saveDraft();
|
|
501
529
|
taEl.focus();
|
|
502
530
|
}
|
|
503
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The element description handed to the bridge is chosen in send(), because the
|
|
534
|
+
* choice between a live read and the snapshot has to be reported to the user.
|
|
535
|
+
* Describing a detached node is never an option: `getComputedStyle()` and
|
|
536
|
+
* `getBoundingClientRect()` return nothing useful once it leaves the document, so
|
|
537
|
+
* the fallback has to be the captured value, not a fresh read.
|
|
538
|
+
*/
|
|
539
|
+
|
|
504
540
|
function vueLine(el) {
|
|
505
541
|
var v = vueInfo(el);
|
|
506
542
|
if (v.componentFile) return '<br>组件: <code>' + escapeHtml((v.componentName ? v.componentName + ' ' : '') + v.componentFile) + '</code>';
|
|
@@ -593,6 +629,7 @@
|
|
|
593
629
|
function addShot(dataUrl) {
|
|
594
630
|
shots.push(dataUrl);
|
|
595
631
|
renderShots();
|
|
632
|
+
saveShots();
|
|
596
633
|
}
|
|
597
634
|
|
|
598
635
|
function renderShots() {
|
|
@@ -609,6 +646,7 @@
|
|
|
609
646
|
x.addEventListener('click', function () {
|
|
610
647
|
shots.splice(i, 1);
|
|
611
648
|
renderShots();
|
|
649
|
+
saveShots();
|
|
612
650
|
});
|
|
613
651
|
d1.appendChild(img);
|
|
614
652
|
d1.appendChild(x);
|
|
@@ -627,6 +665,234 @@
|
|
|
627
665
|
});
|
|
628
666
|
}
|
|
629
667
|
|
|
668
|
+
/* --- draft persistence ---------------------------------------------------
|
|
669
|
+
*
|
|
670
|
+
* Vite reloads the page on every save, and the composer is by far the most
|
|
671
|
+
* expensive thing in this tool to lose: a couple of hundred words of
|
|
672
|
+
* description plus a few pasted screenshots. So the draft is mirrored into
|
|
673
|
+
* `sessionStorage` and re-attached on the next boot.
|
|
674
|
+
*
|
|
675
|
+
* `sessionStorage`, not `localStorage`: a draft belongs to the tab that
|
|
676
|
+
* produced it and must not follow the developer into a fresh window.
|
|
677
|
+
*
|
|
678
|
+
* Two keys on purpose. The text changes on every keystroke while the shots
|
|
679
|
+
* change rarely; re-serialising megabytes of base64 per keypress would both
|
|
680
|
+
* stutter the panel and hit the ~5MB quota needlessly. The text key stays
|
|
681
|
+
* tiny so writing it aggressively is cheap, and the shots key is rewritten
|
|
682
|
+
* only when the shot set actually changes.
|
|
683
|
+
*
|
|
684
|
+
* Everything here is best-effort: a storage failure must never break the
|
|
685
|
+
* editor, only lose the draft.
|
|
686
|
+
*/
|
|
687
|
+
var DRAFT_PREFIX = 'pi-ele-picker:draft:v1';
|
|
688
|
+
var SHOTS_PREFIX = 'pi-ele-picker:shots:v1';
|
|
689
|
+
var SAVE_DEBOUNCE = 250;
|
|
690
|
+
// A draft left over from a tab that stayed open for days is more confusing than
|
|
691
|
+
// helpful: it describes an element that has almost certainly moved on.
|
|
692
|
+
var DRAFT_TTL = 24 * 60 * 60 * 1000;
|
|
693
|
+
var saveTimer = null;
|
|
694
|
+
// True while an edit has not reached storage yet, so a page that is going
|
|
695
|
+
// away flushes exactly once and a delivered draft is not resurrected.
|
|
696
|
+
var draftDirty = false;
|
|
697
|
+
var shotsDropped = false;
|
|
698
|
+
|
|
699
|
+
function draftKey() {
|
|
700
|
+
return DRAFT_PREFIX + ':' + location.pathname;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function shotsKey() {
|
|
704
|
+
return SHOTS_PREFIX + ':' + location.pathname;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Never throws: returns false when storage is full or unavailable. */
|
|
708
|
+
function writeStorage(key, value) {
|
|
709
|
+
try {
|
|
710
|
+
sessionStorage.setItem(key, value);
|
|
711
|
+
return true;
|
|
712
|
+
} catch (e) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function removeStorage(key) {
|
|
718
|
+
try {
|
|
719
|
+
sessionStorage.removeItem(key);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
/* storage unavailable — nothing to clear */
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function readStorage(key) {
|
|
726
|
+
try {
|
|
727
|
+
return sessionStorage.getItem(key);
|
|
728
|
+
} catch (e) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function saveShots() {
|
|
734
|
+
if (!shots.length) {
|
|
735
|
+
removeStorage(shotsKey());
|
|
736
|
+
shotsDropped = false;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (writeStorage(shotsKey(), JSON.stringify(shots))) {
|
|
740
|
+
shotsDropped = false;
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// The images do not fit. Keep the words and say so once, so a reload does not
|
|
744
|
+
// silently eat the screenshots that were just pasted.
|
|
745
|
+
removeStorage(shotsKey());
|
|
746
|
+
if (!shotsDropped) {
|
|
747
|
+
shotsDropped = true;
|
|
748
|
+
setStatus('截图过大,刷新后不会保留(描述仍会保存)', 'err');
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function saveDraft() {
|
|
753
|
+
var rec = {
|
|
754
|
+
selector: targetSelector,
|
|
755
|
+
text: taEl.value,
|
|
756
|
+
// The element as it was when it was picked. Persisted because the reload
|
|
757
|
+
// that restores the draft is often the same one that removed the node —
|
|
758
|
+
// without this the send below could only go out with no element at all.
|
|
759
|
+
element: targetSnapshot,
|
|
760
|
+
url: targetUrl,
|
|
761
|
+
dismissed: panelDismissed,
|
|
762
|
+
savedAt: Date.now(),
|
|
763
|
+
};
|
|
764
|
+
if (!rec.text && !rec.selector && !rec.element) {
|
|
765
|
+
// Nothing worth restoring — do not resurrect an empty panel on reload.
|
|
766
|
+
clearDraft();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Stay dirty when the write fails, so a page that is going away retries.
|
|
770
|
+
if (writeStorage(draftKey(), JSON.stringify(rec))) draftDirty = false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Debounced text write: the keystroke path must stay cheap. */
|
|
774
|
+
function scheduleSave() {
|
|
775
|
+
draftDirty = true;
|
|
776
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
777
|
+
saveTimer = setTimeout(function () {
|
|
778
|
+
saveTimer = null;
|
|
779
|
+
saveDraft();
|
|
780
|
+
}, SAVE_DEBOUNCE);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Write any pending edit right now — the page is being torn down. */
|
|
784
|
+
function flushDraft() {
|
|
785
|
+
if (!draftDirty && !saveTimer) return;
|
|
786
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
787
|
+
saveTimer = null;
|
|
788
|
+
saveDraft();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Forget the draft: it has been delivered, or it is stale garbage. */
|
|
792
|
+
function clearDraft() {
|
|
793
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
794
|
+
saveTimer = null;
|
|
795
|
+
draftDirty = false;
|
|
796
|
+
shotsDropped = false;
|
|
797
|
+
removeStorage(draftKey());
|
|
798
|
+
removeStorage(shotsKey());
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* The live element the panel acts on.
|
|
803
|
+
*
|
|
804
|
+
* `target` is a DOM reference captured at pick time, so an HMR update — or any
|
|
805
|
+
* re-render that swaps the node — leaves it detached. Re-resolving from the
|
|
806
|
+
* remembered selector keeps the panel pointed at the right element without
|
|
807
|
+
* asking the user to pick again, and returns null when the element is really
|
|
808
|
+
* gone, which is what send() and openPanel() need to know.
|
|
809
|
+
*/
|
|
810
|
+
function resolveTarget() {
|
|
811
|
+
if (target && target.isConnected) return target;
|
|
812
|
+
if (targetSelector) {
|
|
813
|
+
try {
|
|
814
|
+
var el = document.querySelector(targetSelector);
|
|
815
|
+
if (el) {
|
|
816
|
+
target = el;
|
|
817
|
+
return el;
|
|
818
|
+
}
|
|
819
|
+
} catch (e) {
|
|
820
|
+
/* not valid CSS (Tailwind-style class names) — cannot re-resolve */
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Re-attach the draft written before this page load.
|
|
828
|
+
*
|
|
829
|
+
* The element is re-resolved from its selector rather than trusted blindly:
|
|
830
|
+
* an HMR update replaces DOM nodes, so a captured `target` reference is dead by
|
|
831
|
+
* the time the page comes back. If the selector no longer resolves, the text
|
|
832
|
+
* and screenshots are still restored into the composer, so the next pick
|
|
833
|
+
* inherits them instead of the user retyping everything.
|
|
834
|
+
*/
|
|
835
|
+
function restoreDraft() {
|
|
836
|
+
var raw = readStorage(draftKey());
|
|
837
|
+
if (!raw) return;
|
|
838
|
+
var rec;
|
|
839
|
+
try {
|
|
840
|
+
rec = JSON.parse(raw);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
clearDraft();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (!rec || typeof rec !== 'object') {
|
|
846
|
+
clearDraft();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (typeof rec.savedAt === 'number' && Date.now() - rec.savedAt > DRAFT_TTL) {
|
|
850
|
+
clearDraft();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
taEl.value = typeof rec.text === 'string' ? rec.text : '';
|
|
855
|
+
|
|
856
|
+
var rawShots = readStorage(shotsKey());
|
|
857
|
+
if (rawShots) {
|
|
858
|
+
try {
|
|
859
|
+
var arr = JSON.parse(rawShots);
|
|
860
|
+
if (arr instanceof Array) {
|
|
861
|
+
shots = arr.filter(function (d) {
|
|
862
|
+
return typeof d === 'string' && d.indexOf('data:image/') === 0;
|
|
863
|
+
});
|
|
864
|
+
renderShots();
|
|
865
|
+
}
|
|
866
|
+
} catch (e) {
|
|
867
|
+
removeStorage(shotsKey());
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (rec.selector) targetSelector = String(rec.selector);
|
|
872
|
+
if (rec.url) targetUrl = String(rec.url);
|
|
873
|
+
if (rec.element && typeof rec.element === 'object') targetSnapshot = rec.element;
|
|
874
|
+
|
|
875
|
+
var el = targetSelector ? resolveTarget() : null;
|
|
876
|
+
if (el) {
|
|
877
|
+
metaEl.innerHTML = '<code>' + escapeHtml(targetSelector) + '</code>' + vueLine(el);
|
|
878
|
+
} else if (targetSnapshot) {
|
|
879
|
+
// Still sendable: the snapshot carries everything the bridge needs.
|
|
880
|
+
metaEl.innerHTML =
|
|
881
|
+
'<code>' + escapeHtml(targetSelector || '') + '</code><br>' +
|
|
882
|
+
'上次选中的元素已不在页面中,发送时会带上抓取时的快照';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (rec.dismissed) return;
|
|
886
|
+
if (openPanel()) return;
|
|
887
|
+
if (!taEl.value && !shots.length) return;
|
|
888
|
+
// No element at all (not even a snapshot) but there are words: show the
|
|
889
|
+
// composer so the draft is visibly still there, and let the next pick attach
|
|
890
|
+
// it to a new element.
|
|
891
|
+
panel.classList.add('open');
|
|
892
|
+
metaEl.textContent = '上次选中的元素已不在页面中,请重新点击选择';
|
|
893
|
+
positionPanel();
|
|
894
|
+
}
|
|
895
|
+
|
|
630
896
|
function loadConfig(force) {
|
|
631
897
|
if (!force && cfg.port) return Promise.resolve(cfg);
|
|
632
898
|
return fetch(CFG_URL, { cache: 'no-store' })
|
|
@@ -643,14 +909,16 @@
|
|
|
643
909
|
}
|
|
644
910
|
|
|
645
911
|
/** POST the payload to the bridge. Never rejects; status 0 means the fetch itself failed. */
|
|
646
|
-
function deliver(c, submit) {
|
|
912
|
+
function deliver(c, submit, element) {
|
|
647
913
|
var payload = {
|
|
648
914
|
text: taEl.value,
|
|
649
915
|
images: shots.map(function (d, i) {
|
|
650
916
|
return { name: 'shot-' + i + '.png', dataUrl: d };
|
|
651
917
|
}),
|
|
652
|
-
element:
|
|
653
|
-
|
|
918
|
+
element: element,
|
|
919
|
+
// The page the element lives on, which is not necessarily the page that is
|
|
920
|
+
// open right now.
|
|
921
|
+
url: targetUrl || location.href,
|
|
654
922
|
title: document.title,
|
|
655
923
|
submit: !!submit,
|
|
656
924
|
};
|
|
@@ -687,6 +955,32 @@
|
|
|
687
955
|
}
|
|
688
956
|
|
|
689
957
|
function send(submit) {
|
|
958
|
+
// Deliberately NOT gated on the element still being on the page. Picking an
|
|
959
|
+
// element and then wandering off to another route to copy something is a
|
|
960
|
+
// normal way to work, and blocking the send there throws away the words the
|
|
961
|
+
// developer typed.
|
|
962
|
+
//
|
|
963
|
+
// Two sources for the element, and the wrong one silently corrupts the payload:
|
|
964
|
+
// the snapshot captured at pick time (a plain value, so it survives an unmount,
|
|
965
|
+
// an SPA route change or a reload), and a live read of whatever the selector
|
|
966
|
+
// resolves to now. A live read wins only while we are still on the pick-time
|
|
967
|
+
// URL — an HMR swap of the same element is the case that needs it, while a
|
|
968
|
+
// route change can put an unrelated lookalike behind the same selector.
|
|
969
|
+
var live = resolveTarget();
|
|
970
|
+
var samePage = targetUrl === null || location.href === targetUrl;
|
|
971
|
+
var element = live && samePage ? describe(live) : targetSnapshot;
|
|
972
|
+
if (element) targetSnapshot = element;
|
|
973
|
+
|
|
974
|
+
if (!element && !taEl.value.trim()) {
|
|
975
|
+
setStatus('请先点击页面元素,或写下要改什么', 'err');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Only ever affects the status line, never whether the send happens.
|
|
980
|
+
var note = '';
|
|
981
|
+
if (element && !live) note = '(元素已不在页面中,已附抓取时的快照)';
|
|
982
|
+
else if (element && !samePage) note = '(页面已切换,已附抓取时的元素快照)';
|
|
983
|
+
|
|
690
984
|
sendBtn.disabled = true;
|
|
691
985
|
sendNowBtn.disabled = true;
|
|
692
986
|
setStatus(submit ? '直接发送中…' : '写入中…');
|
|
@@ -694,7 +988,7 @@
|
|
|
694
988
|
function attempt(forceFresh) {
|
|
695
989
|
return loadConfig(forceFresh).then(function (c) {
|
|
696
990
|
if (!c || !c.port) return { status: 0, body: null, noBridge: true };
|
|
697
|
-
return deliver(c, submit);
|
|
991
|
+
return deliver(c, submit, element);
|
|
698
992
|
});
|
|
699
993
|
}
|
|
700
994
|
|
|
@@ -712,14 +1006,22 @@
|
|
|
712
1006
|
return;
|
|
713
1007
|
}
|
|
714
1008
|
if (out.body && out.body.ok) {
|
|
1009
|
+
// Say which element went out, so a stale pick is visible rather than silent.
|
|
715
1010
|
if (out.body.submitted) {
|
|
716
|
-
setStatus(
|
|
1011
|
+
setStatus(
|
|
1012
|
+
(out.body.queued ? '已发送 ✓(agent 忙碌,已排队为后续任务)' : '已直接发送给 pi 并开始执行 ✓') + note,
|
|
1013
|
+
'ok'
|
|
1014
|
+
);
|
|
717
1015
|
} else {
|
|
718
|
-
setStatus(
|
|
1016
|
+
setStatus(
|
|
1017
|
+
(out.body.inserted ? '已写入 pi 输入框 ✓(去终端按回车发送)' : '已发送给 pi ✓') + note,
|
|
1018
|
+
'ok'
|
|
1019
|
+
);
|
|
719
1020
|
}
|
|
720
1021
|
taEl.value = '';
|
|
721
1022
|
shots = [];
|
|
722
1023
|
renderShots();
|
|
1024
|
+
clearDraft();
|
|
723
1025
|
} else {
|
|
724
1026
|
setStatus('失败: ' + ((out.body && out.body.error) || out.error || 'HTTP ' + out.status), 'err');
|
|
725
1027
|
}
|
|
@@ -777,6 +1079,10 @@
|
|
|
777
1079
|
panel.classList.remove('open');
|
|
778
1080
|
hideHighlight();
|
|
779
1081
|
setStatus('');
|
|
1082
|
+
// Remember the dismissal so a reload does not pop the panel back open, while
|
|
1083
|
+
// the draft stays available through the launcher's hover-to-reopen.
|
|
1084
|
+
panelDismissed = true;
|
|
1085
|
+
saveDraft();
|
|
780
1086
|
});
|
|
781
1087
|
attachBtn.addEventListener('click', function () {
|
|
782
1088
|
fileEl.click();
|
|
@@ -791,6 +1097,8 @@
|
|
|
791
1097
|
sendNowBtn.addEventListener('click', function () {
|
|
792
1098
|
send(true);
|
|
793
1099
|
});
|
|
1100
|
+
taEl.addEventListener('input', scheduleSave);
|
|
1101
|
+
window.addEventListener('pagehide', flushDraft);
|
|
794
1102
|
taEl.addEventListener('paste', function (e) {
|
|
795
1103
|
var items = e.clipboardData && e.clipboardData.items;
|
|
796
1104
|
if (!items) return;
|
|
@@ -944,4 +1252,8 @@
|
|
|
944
1252
|
var override = readStatusOverride();
|
|
945
1253
|
setStatusEnabled(override !== null ? override : cfg && cfg.statusDefault === true, false);
|
|
946
1254
|
});
|
|
1255
|
+
|
|
1256
|
+
// Last: the draft restore depends on state initialised above it (launcher
|
|
1257
|
+
// position, panel offset) and on the draft storage keys being assigned.
|
|
1258
|
+
restoreDraft();
|
|
947
1259
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ele-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Pick an element in your running page, describe the change, and send it to a pi session. Ships a Vite dev plugin (browser picker + discovery endpoint) and the matching pi bridge extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsup",
|
|
56
56
|
"dev": "tsup --watch",
|
|
57
|
-
"typecheck": "tsc --noEmit",
|
|
57
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
58
|
+
"test": "playwright test",
|
|
58
59
|
"prepublishOnly": "npm run build"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
}
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
74
|
+
"@playwright/test": "^1.64.1",
|
|
73
75
|
"@types/node": "^20.11.0",
|
|
74
76
|
"tsup": "^8.0.0",
|
|
75
77
|
"typescript": "^5.4.0",
|