dsh-mobilecode 0.11.2 → 0.11.3
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 +11 -4
- package/lib/client.js +70 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,17 +142,24 @@ 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 the card
|
|
160
|
+
stays idle instead of silently grabbing another device (0.11.3). Both stream
|
|
161
|
+
captions carry the running client version (`· v0.11.x`) so a page refresh is
|
|
162
|
+
visibly proven.
|
|
156
163
|
- **Security**: every stream route sits behind a loopback + trusted-browser
|
|
157
164
|
transport fence (peer address, loopback `Host`, `Sec-Fetch-Site` / `Origin` —
|
|
158
165
|
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.3";
|
|
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,17 @@ window.__ModuleLoader__.load({
|
|
|
646
676
|
const r = await streamPost("/stream/devices", {});
|
|
647
677
|
setDevices(r.devices);
|
|
648
678
|
setAvds(r.avds ?? []);
|
|
649
|
-
|
|
679
|
+
const first = r.devices.find((d) => d.streaming)?.serial ?? r.devices[0]?.serial ?? "";
|
|
680
|
+
const cur = serialRef.current;
|
|
681
|
+
if (cur === "") {
|
|
682
|
+
// Auto-stream once per page load (the welcome flow). After
|
|
683
|
+
// an explicit ⏻ off, the card stays idle — it must NOT
|
|
684
|
+
// silently grab the partner device (0.11.3).
|
|
685
|
+
if (!autoPicked.current && first !== "") { autoPicked.current = true; setSerial(first); }
|
|
686
|
+
} else if (!r.devices.some((d) => d.serial === cur)) {
|
|
687
|
+
autoPicked.current = true;
|
|
688
|
+
setSerial(""); live.reset();
|
|
689
|
+
}
|
|
650
690
|
} catch (err) {
|
|
651
691
|
setError(err instanceof Error ? err.message : String(err));
|
|
652
692
|
}
|
|
@@ -685,7 +725,7 @@ window.__ModuleLoader__.load({
|
|
|
685
725
|
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
|
686
726
|
};
|
|
687
727
|
|
|
688
|
-
const pick = (device) => { setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
728
|
+
const pick = (device) => { autoPicked.current = true; setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
689
729
|
// 0.10.0 merged "Start server": boot a configured AVD from the picker.
|
|
690
730
|
// The route waits for boot completion; then we re-list and stream it.
|
|
691
731
|
const boot = async (avd) => {
|
|
@@ -719,7 +759,7 @@ window.__ModuleLoader__.load({
|
|
|
719
759
|
const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
|
|
720
760
|
|
|
721
761
|
const sizeStyle = stageSizeStyle(sizeMode);
|
|
722
|
-
const stageClass = stageClassFor(frame);
|
|
762
|
+
const stageClass = stageClassFor(frame, sizeMode.mode === "fit");
|
|
723
763
|
const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
|
|
724
764
|
const toolbar = [
|
|
725
765
|
{ title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
|
|
@@ -804,14 +844,16 @@ window.__ModuleLoader__.load({
|
|
|
804
844
|
? h("img", { src: still.dataUrl, alt: "still capture", draggable: false })
|
|
805
845
|
: live.streamUrl !== ""
|
|
806
846
|
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "device screen", style: sizeStyle, onError: () => grant(serial) })
|
|
807
|
-
: h("div", { className: "mc-live-off" },
|
|
808
|
-
|
|
809
|
-
|
|
847
|
+
: h("div", { className: "mc-live-off" }, serial === ""
|
|
848
|
+
? (devices.length === 0
|
|
849
|
+
? "No device attached — boot an AVD from the ▾ picker, or press Run app."
|
|
850
|
+
: "Device was shut down — pick one from ▾, or ⏻ boot an AVD.")
|
|
851
|
+
: "Connecting to " + serial + "..."),
|
|
810
852
|
),
|
|
811
853
|
view === "still"
|
|
812
854
|
? h("div", { className: "mc-live-cap" }, "Still captured" + (still?.width ? " (" + still.width + "x" + still.height + ")" : "") + " · ",
|
|
813
855
|
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"),
|
|
856
|
+
: h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive the device · v" + MC_VERSION),
|
|
815
857
|
h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
|
|
816
858
|
);
|
|
817
859
|
}
|
|
@@ -828,17 +870,22 @@ window.__ModuleLoader__.load({
|
|
|
828
870
|
const [error, setError] = useState("");
|
|
829
871
|
const live = useLiveStream({ serial, onError: setError });
|
|
830
872
|
const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
|
|
873
|
+
// Serial mirror for the interval (avoids stale-closure on setInterval).
|
|
874
|
+
const serialRef = useRef(serial);
|
|
875
|
+
serialRef.current = serial;
|
|
831
876
|
|
|
832
877
|
const refresh = async () => {
|
|
833
878
|
try {
|
|
834
879
|
const r = await streamPost("/stream/devices", {});
|
|
835
880
|
setDevices(r.devices);
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
881
|
+
const others = r.devices.filter((d) => d.serial !== excludeSerial);
|
|
882
|
+
const next = (others.find((d) => d.streaming) ?? others[0])?.serial ?? "";
|
|
883
|
+
const cur = serialRef.current;
|
|
884
|
+
// If the current device is still valid (online and not excluded), keep it.
|
|
885
|
+
if (cur !== "" && cur !== excludeSerial && others.some((d) => d.serial === cur)) return;
|
|
886
|
+
// On any change (death, exclusion, or manual), drop the stream so we
|
|
887
|
+
// don't show a frozen frame under the new serial.
|
|
888
|
+
if (next !== cur) { setSerial(next); live.reset(); }
|
|
842
889
|
} catch {
|
|
843
890
|
/* the header keeps the last device list */
|
|
844
891
|
}
|
|
@@ -863,12 +910,16 @@ window.__ModuleLoader__.load({
|
|
|
863
910
|
),
|
|
864
911
|
),
|
|
865
912
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
866
|
-
h("div", { className: stageClassFor(frame) },
|
|
913
|
+
h("div", { className: stageClassFor(frame, sizeMode.mode === "fit") },
|
|
867
914
|
live.streamUrl !== ""
|
|
868
915
|
? 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.
|
|
916
|
+
: h("div", { className: "mc-live-off" }, devices.filter((d) => d.serial !== excludeSerial).length === 0
|
|
917
|
+
? "No second device — boot another emulator."
|
|
918
|
+
: serial === "" || !devices.some((d) => d.serial === serial)
|
|
919
|
+
? "Second device offline — pick one above."
|
|
920
|
+
: "Connecting to " + serial + "..."),
|
|
870
921
|
),
|
|
871
|
-
h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device"),
|
|
922
|
+
h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device · v" + MC_VERSION),
|
|
872
923
|
h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
|
|
873
924
|
);
|
|
874
925
|
}
|
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.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|