dsh-mobilecode 0.11.2 → 0.11.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/README.md +12 -4
- package/lib/client.js +92 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,17 +142,25 @@ card is **Device 1**; co-op docks a second, **Device 2**. It is produced
|
|
|
142
142
|
its online row, never a duplicate boot entry. A **Live** badge shows the
|
|
143
143
|
streamed serial. Quick sizes (Fit / 100% / S·240 / M·320 presets + a select)
|
|
144
144
|
and frame styles (none / bezel / device) reshape the stage — each stream pane
|
|
145
|
-
carries its own copy of these controls (0.11.2).
|
|
145
|
+
carries its own copy of these controls (0.11.2). In **Fit** the stage locks
|
|
146
|
+
to a shared height, so the two panes sit at identical heights even when the
|
|
147
|
+
devices have different screen aspect ratios (0.11.3).
|
|
146
148
|
- **Screenshot** captures a still via `POST /stream/still` (a real `screencap`,
|
|
147
149
|
embedded as a data URL up to 4 MB) and flips the stage to a still view with a
|
|
148
150
|
"back to Live" link.
|
|
149
151
|
- **Co-op split view (⧉, v0.9.0)**: with two or more online devices the card
|
|
150
152
|
header grows a ⧉ toggle that docks a second **Device 2** pane beside it —
|
|
151
153
|
its own device select, live `<img>`, tap/drag control, and (since 0.11.2) its
|
|
152
|
-
own Size/Frame row mirroring Device 1's; both panes default to Fit
|
|
153
|
-
one
|
|
154
|
+
own Size/Frame row mirroring Device 1's; both panes default to Fit, which
|
|
155
|
+
locks to one stage height, so the pair starts identical and can be styled
|
|
154
156
|
independently. Each pane grants its own HMAC capability; closing one pane
|
|
155
|
-
never stalls or disturbs the other stream.
|
|
157
|
+
never stalls or disturbs the other stream. A presence watcher polls device
|
|
158
|
+
liveness while streaming: if a device is powered off (⏻ off, crash, unplug)
|
|
159
|
+
its pane drops the frozen frame and the Live badge within ~5 s, and stays
|
|
160
|
+
idle instead of silently grabbing another device — both panes guard against
|
|
161
|
+
the re-grab (0.11.3–4). Both stream
|
|
162
|
+
captions carry the running client version (`· v0.11.x`) so a page refresh is
|
|
163
|
+
visibly proven.
|
|
156
164
|
- **Security**: every stream route sits behind a loopback + trusted-browser
|
|
157
165
|
transport fence (peer address, loopback `Host`, `Sec-Fetch-Site` / `Origin` —
|
|
158
166
|
so a LAN client cannot spoof localhost and a DNS-rebinding `Host` is rejected),
|
package/lib/client.js
CHANGED
|
@@ -171,6 +171,8 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
.mc-seg { display: inline-flex; border: 1px solid light-dark(rgba(0,0,0,.18), rgba(255,255,255,.2)); border-radius: 8px; overflow: hidden; }
|
|
172
172
|
.mc-seg button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 11px; padding: 4px 9px; cursor: pointer; opacity: .6; }
|
|
173
173
|
.mc-seg button[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); color: #4285f4; opacity: 1; }
|
|
174
|
+
.mc-live-stage.fit { height: 60vh; display: flex; align-items: center; justify-content: center; }
|
|
175
|
+
.mc-live-stage.fit img { max-height: 100%; }
|
|
174
176
|
.mc-live-stage.bezel { padding: 8px; background: #111; border-radius: 16px; }
|
|
175
177
|
.mc-live-stage.device { padding: 12px; background: linear-gradient(145deg, #2c2f36, #17191d); border-radius: 26px; box-shadow: 0 10px 30px rgba(0,0,0,.45); }
|
|
176
178
|
/* ── 0.7.0: compact conversation cards ── */
|
|
@@ -185,6 +187,10 @@ window.__ModuleLoader__.load({
|
|
|
185
187
|
//#endregion
|
|
186
188
|
|
|
187
189
|
const API_BASE = "/api/dsh-mobilecode";
|
|
190
|
+
// Shown on both stream captions so a refresh visibly proves which client
|
|
191
|
+
// bundle the browser is actually running. Kept in lockstep with
|
|
192
|
+
// package.json by test/client.mjs.
|
|
193
|
+
const MC_VERSION = "0.11.4";
|
|
188
194
|
// The panel shows cards for platforms the host can actually drive; the
|
|
189
195
|
// server reports its OS in info.os (iOS tooling is darwin-only).
|
|
190
196
|
const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
|
|
@@ -195,7 +201,10 @@ window.__ModuleLoader__.load({
|
|
|
195
201
|
: m.mode === "px"
|
|
196
202
|
? { width: m.value + "px", maxWidth: "100%", maxHeight: "60vh" }
|
|
197
203
|
: {});
|
|
198
|
-
|
|
204
|
+
// Fit panes lock to a shared stage height so devices with different
|
|
205
|
+
// aspect ratios still sit at the same height (0.11.3); presets stay
|
|
206
|
+
// width-driven (S·240 / M·320 = device width px).
|
|
207
|
+
const stageClassFor = (f, fit = false) => "mc-live-stage" + (fit ? " fit" : "") + (f !== "none" ? " " + f : "");
|
|
199
208
|
const SIZE_PRESETS = [
|
|
200
209
|
{ label: "Fit", mode: { mode: "fit" } },
|
|
201
210
|
{ label: "100%", mode: { mode: "pct", value: 100 } },
|
|
@@ -585,6 +594,24 @@ window.__ModuleLoader__.load({
|
|
|
585
594
|
};
|
|
586
595
|
const reset = () => { clearTimeout(grantTimer.current); setStreamUrl(""); };
|
|
587
596
|
useEffect(() => () => clearTimeout(grantTimer.current), []);
|
|
597
|
+
// Presence watcher (0.11.3): an MJPEG <img> does NOT fire error when
|
|
598
|
+
// the device dies — it just freezes on the last frame with the Live
|
|
599
|
+
// badge stuck. While a stream is up, poll device presence and drop
|
|
600
|
+
// the stream if the serial vanished (⏻ off, crash, unplug).
|
|
601
|
+
useEffect(() => {
|
|
602
|
+
if (streamUrl === "") return;
|
|
603
|
+
const watch = setInterval(async () => {
|
|
604
|
+
try {
|
|
605
|
+
const r = await streamPost("/stream/devices", {});
|
|
606
|
+
if (!r.devices.some((d) => d.serial === serialRef.current)) {
|
|
607
|
+
clearTimeout(grantTimer.current);
|
|
608
|
+
setStreamUrl("");
|
|
609
|
+
onErrorRef.current("device offline — stream stopped");
|
|
610
|
+
}
|
|
611
|
+
} catch { /* the next tick decides */ }
|
|
612
|
+
}, 5000);
|
|
613
|
+
return () => clearInterval(watch);
|
|
614
|
+
}, [streamUrl]);
|
|
588
615
|
|
|
589
616
|
const control = async (action) => {
|
|
590
617
|
if (serialRef.current === "") return;
|
|
@@ -632,13 +659,16 @@ window.__ModuleLoader__.load({
|
|
|
632
659
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
633
660
|
const [still, setStill] = useState(null);
|
|
634
661
|
const [view, setView] = useState("live"); // 'live' | 'still'
|
|
635
|
-
// sizeMode/frame
|
|
636
|
-
//
|
|
662
|
+
// sizeMode/frame are per-pane (0.11.2); StageControls renders the
|
|
663
|
+
// identical row in both cards, and Fit locks both to one height.
|
|
637
664
|
const [booting, setBooting] = useState(""); // AVD name while POST /boot is in flight
|
|
638
665
|
const [powering, setPowering] = useState(""); // serial while POST /off is in flight
|
|
639
666
|
const live = useLiveStream({ serial, onError: setError });
|
|
640
667
|
const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
|
|
641
668
|
const control = live.control;
|
|
669
|
+
const serialRef = useRef(serial);
|
|
670
|
+
serialRef.current = serial;
|
|
671
|
+
const autoPicked = useRef(false);
|
|
642
672
|
useEffect(() => { if (onSerial) onSerial(serial); }, [serial]);
|
|
643
673
|
|
|
644
674
|
const refreshDevices = async () => {
|
|
@@ -646,7 +676,22 @@ window.__ModuleLoader__.load({
|
|
|
646
676
|
const r = await streamPost("/stream/devices", {});
|
|
647
677
|
setDevices(r.devices);
|
|
648
678
|
setAvds(r.avds ?? []);
|
|
649
|
-
|
|
679
|
+
// Prefer emulators for the welcome auto-pick: a physical
|
|
680
|
+
// serial sorts first in adb order but has no ⏻ off control.
|
|
681
|
+
// (Serial prefix, not the avd field — avdName() can fail.)
|
|
682
|
+
const isEmu = (d) => d.serial.startsWith("emulator-");
|
|
683
|
+
const first = r.devices.find((d) => d.streaming && isEmu(d))?.serial ?? r.devices.find(isEmu)?.serial
|
|
684
|
+
?? r.devices.find((d) => d.streaming)?.serial ?? r.devices[0]?.serial ?? "";
|
|
685
|
+
const cur = serialRef.current;
|
|
686
|
+
if (cur === "") {
|
|
687
|
+
// Auto-stream once per page load (the welcome flow). After
|
|
688
|
+
// an explicit ⏻ off, the card stays idle — it must NOT
|
|
689
|
+
// silently grab the partner device (0.11.3).
|
|
690
|
+
if (!autoPicked.current && first !== "") { autoPicked.current = true; setSerial(first); }
|
|
691
|
+
} else if (!r.devices.some((d) => d.serial === cur)) {
|
|
692
|
+
autoPicked.current = true;
|
|
693
|
+
setSerial(""); live.reset();
|
|
694
|
+
}
|
|
650
695
|
} catch (err) {
|
|
651
696
|
setError(err instanceof Error ? err.message : String(err));
|
|
652
697
|
}
|
|
@@ -685,7 +730,7 @@ window.__ModuleLoader__.load({
|
|
|
685
730
|
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
|
686
731
|
};
|
|
687
732
|
|
|
688
|
-
const pick = (device) => { setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
733
|
+
const pick = (device) => { autoPicked.current = true; setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
689
734
|
// 0.10.0 merged "Start server": boot a configured AVD from the picker.
|
|
690
735
|
// The route waits for boot completion; then we re-list and stream it.
|
|
691
736
|
const boot = async (avd) => {
|
|
@@ -719,7 +764,7 @@ window.__ModuleLoader__.load({
|
|
|
719
764
|
const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
|
|
720
765
|
|
|
721
766
|
const sizeStyle = stageSizeStyle(sizeMode);
|
|
722
|
-
const stageClass = stageClassFor(frame);
|
|
767
|
+
const stageClass = stageClassFor(frame, sizeMode.mode === "fit");
|
|
723
768
|
const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
|
|
724
769
|
const toolbar = [
|
|
725
770
|
{ title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
|
|
@@ -804,14 +849,16 @@ window.__ModuleLoader__.load({
|
|
|
804
849
|
? h("img", { src: still.dataUrl, alt: "still capture", draggable: false })
|
|
805
850
|
: live.streamUrl !== ""
|
|
806
851
|
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "device screen", style: sizeStyle, onError: () => grant(serial) })
|
|
807
|
-
: h("div", { className: "mc-live-off" },
|
|
808
|
-
|
|
809
|
-
|
|
852
|
+
: h("div", { className: "mc-live-off" }, serial === ""
|
|
853
|
+
? (devices.length === 0
|
|
854
|
+
? "No device attached — boot an AVD from the ▾ picker, or press Run app."
|
|
855
|
+
: "Device was shut down — pick one from ▾, or ⏻ boot an AVD.")
|
|
856
|
+
: "Connecting to " + serial + "..."),
|
|
810
857
|
),
|
|
811
858
|
view === "still"
|
|
812
859
|
? h("div", { className: "mc-live-cap" }, "Still captured" + (still?.width ? " (" + still.width + "x" + still.height + ")" : "") + " · ",
|
|
813
860
|
h("a", { className: "mc-log-toggle", onClick: showLive }, "back to Live"))
|
|
814
|
-
: h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive the device"),
|
|
861
|
+
: h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive the device · v" + MC_VERSION),
|
|
815
862
|
h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
|
|
816
863
|
);
|
|
817
864
|
}
|
|
@@ -828,17 +875,35 @@ window.__ModuleLoader__.load({
|
|
|
828
875
|
const [error, setError] = useState("");
|
|
829
876
|
const live = useLiveStream({ serial, onError: setError });
|
|
830
877
|
const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
|
|
878
|
+
// Serial mirror for the 5s poller + one-shot auto-pick guard: this
|
|
879
|
+
// pane may steal a partner while nothing is picked, but it must NOT
|
|
880
|
+
// silently switch to a third device after its own goes offline.
|
|
881
|
+
const serialRef = useRef(serial);
|
|
882
|
+
serialRef.current = serial;
|
|
883
|
+
const autoPicked = useRef(false);
|
|
831
884
|
|
|
832
885
|
const refresh = async () => {
|
|
833
886
|
try {
|
|
834
887
|
const r = await streamPost("/stream/devices", {});
|
|
835
888
|
setDevices(r.devices);
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
889
|
+
// Partner candidates: emulators only (serial prefix, not the
|
|
890
|
+
// flaky avd field) — the co-op partner must be killable from
|
|
891
|
+
// the picker, and never Device 1's own serial.
|
|
892
|
+
const others = r.devices.filter((d) => d.serial !== excludeSerial && d.serial.startsWith("emulator-"));
|
|
893
|
+
const cur = serialRef.current;
|
|
894
|
+
if (cur !== "") {
|
|
895
|
+
// Keep any valid pick — including a manually chosen physical
|
|
896
|
+
// device; only emulators are eligible for auto-pick. A dead
|
|
897
|
+
// or stolen pick clears to the placeholder (never re-grabs).
|
|
898
|
+
if (cur !== excludeSerial && r.devices.some((d) => d.serial === cur)) return;
|
|
899
|
+
autoPicked.current = true;
|
|
900
|
+
setSerial(""); live.reset();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (!autoPicked.current) {
|
|
904
|
+
const next = (others.find((d) => d.streaming) ?? others[0])?.serial ?? "";
|
|
905
|
+
if (next !== "") { autoPicked.current = true; setSerial(next); }
|
|
906
|
+
}
|
|
842
907
|
} catch {
|
|
843
908
|
/* the header keeps the last device list */
|
|
844
909
|
}
|
|
@@ -858,17 +923,24 @@ window.__ModuleLoader__.load({
|
|
|
858
923
|
live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
|
|
859
924
|
h("span", { className: "sp" }),
|
|
860
925
|
h("select", { className: "mc-input mc-coop-select", value: serial, onChange: (e) => { setSerial(e.target.value); live.reset(); } },
|
|
861
|
-
|
|
926
|
+
// An honest empty option: a select with no matching option
|
|
927
|
+
// silently DISPLAYS devices[0] while state is still "" —
|
|
928
|
+
// that made the probe read a phantom pick (0.11.4).
|
|
929
|
+
!devices.some((d) => d.serial === serial) && h("option", { value: "" }, devices.length === 0 ? "no device" : "— pick device —"),
|
|
862
930
|
devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
|
|
863
931
|
),
|
|
864
932
|
),
|
|
865
933
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
866
|
-
h("div", { className: stageClassFor(frame) },
|
|
934
|
+
h("div", { className: stageClassFor(frame, sizeMode.mode === "fit") },
|
|
867
935
|
live.streamUrl !== ""
|
|
868
936
|
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
|
|
869
|
-
: h("div", { className: "mc-live-off" }, devices.
|
|
937
|
+
: h("div", { className: "mc-live-off" }, devices.filter((d) => d.serial !== excludeSerial).length === 0
|
|
938
|
+
? "No second device — boot another emulator."
|
|
939
|
+
: serial === "" || !devices.some((d) => d.serial === serial)
|
|
940
|
+
? "Second device offline — pick one above."
|
|
941
|
+
: "Connecting to " + serial + "..."),
|
|
870
942
|
),
|
|
871
|
-
h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device"),
|
|
943
|
+
h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device · v" + MC_VERSION),
|
|
872
944
|
h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
|
|
873
945
|
);
|
|
874
946
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobilecode",
|
|
3
3
|
"description": "MobileCode for the dsh web GUI: detect iOS/Android projects, run preview servers, and drive the simulator/emulator from the session — 37 agent tools (device_run, device_screen, device_ui_tree, device_ui_rows, device_tap_row, device_tap_element, device_wait_for, device_scroll_to, device_input, device_batch, device_intent, device_connect, device_pair_qr, device_perf, device_meminfo, device_backtrace, device_display, device_avd_create, device_pair_capture, device_app_info, device_install, device_uninstall, device_reboot, device_log, live screen stream, multimodal screenshots) plus a host-mediated co-op mesh hub (random callsigns, JSON pub/sub, tunable latency/jitter/drop/dup/throttle) so two virtual devices — and the AI watching them — can talk in real time. One classified adb boundary and a Wi-Fi connect/pair QR flow in the Connection settings tab. Hot-pluggable — mounted via the profile bundle list + cordis.patch.yml, no dsh source changes.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|