dsh-mobilecode 0.11.4 → 0.11.6
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 +19 -14
- package/lib/client.js +93 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,25 +142,30 @@ 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). In **Fit** the stage
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
carries its own copy of these controls (0.11.2). In **Fit** the stage hugs
|
|
146
|
+
the device's real aspect — a poller reads the MJPEG `<img>`'s
|
|
147
|
+
`naturalWidth/naturalHeight`, sets `--mc-ar-n` on the stage, and the stage
|
|
148
|
+
box becomes `width: min(100%, 60vh · ar); aspect-ratio: ar` — no letterbox
|
|
149
|
+
bars, and two devices with different aspect ratios keep equal visible
|
|
150
|
+
height (0.11.5).
|
|
148
151
|
- **Screenshot** captures a still via `POST /stream/still` (a real `screencap`,
|
|
149
152
|
embedded as a data URL up to 4 MB) and flips the stage to a still view with a
|
|
150
153
|
"back to Live" link.
|
|
151
154
|
- **Co-op split view (⧉, v0.9.0)**: with two or more online devices the card
|
|
152
155
|
header grows a ⧉ toggle that docks a second **Device 2** pane beside it —
|
|
153
|
-
its own device select, live `<img>`, tap/drag control,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
its own device select, live `<img>`, tap/drag control, its own Size/Frame
|
|
157
|
+
row (0.11.2), and since 0.11.5 the same **nav toolbar** (Back/Home/Recents/
|
|
158
|
+
Rotate/Refresh) and **☰ device-actions menu** as Device 1. The section is a
|
|
159
|
+
responsive CSS grid `repeat(auto-fit, minmax(min(250px, 100%), 1fr))`, so
|
|
160
|
+
dragging the drawer narrow stacks the panes vertically and widening it puts
|
|
161
|
+
them side by side (0.11.5). Each pane grants its own HMAC capability;
|
|
162
|
+
closing one pane never stalls or disturbs the other stream. A presence
|
|
163
|
+
watcher polls device liveness while streaming: if a device is powered off
|
|
164
|
+
(⏻ off, crash, unplug) its pane drops the frozen frame and the Live badge
|
|
165
|
+
within ~5 s, and stays idle instead of silently grabbing another device —
|
|
166
|
+
both panes guard against the re-grab (0.11.3–4). Both stream captions carry
|
|
167
|
+
the running client version (`· v0.11.x`) so a page refresh is visibly
|
|
168
|
+
proven.
|
|
164
169
|
- **Security**: every stream route sits behind a loopback + trusted-browser
|
|
165
170
|
transport fence (peer address, loopback `Host`, `Sec-Fetch-Site` / `Origin` —
|
|
166
171
|
so a LAN client cannot spoof localhost and a DNS-rebinding `Host` is rejected),
|
package/lib/client.js
CHANGED
|
@@ -121,12 +121,18 @@ window.__ModuleLoader__.load({
|
|
|
121
121
|
.mc-live-stage .mc-live-off { display: flex; align-items: center; justify-content: center; width: 260px; max-width: 100%; height: 460px; color: #9aa0a8; font-size: 13px; line-height: 1.5; text-align: center; }
|
|
122
122
|
/* 0.9.1 co-op: two forced columns — side-by-side MUST survive a narrow drawer. */
|
|
123
123
|
.mc-live-section { display: flex; flex-direction: column; gap: 10px; }
|
|
124
|
-
.mc-live-section.coop { display: grid; grid-template-columns: repeat(
|
|
124
|
+
.mc-live-section.coop { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); gap: 10px; align-items: start; }
|
|
125
125
|
.mc-live-section.coop > .mc-card { min-width: 0; }
|
|
126
|
-
.
|
|
126
|
+
/* 0.11.6: the co-op head must NEVER wrap to a second row — a wrapped
|
|
127
|
+
head offsets one pane's stage ~38px lower than its sibling's, which
|
|
128
|
+
read as "the bars in Device 1 make the heights differ". Children
|
|
129
|
+
shrink instead (picker/select ellipsize). */
|
|
130
|
+
.mc-live-section.coop .mc-card-head { flex-wrap: nowrap; min-width: 0; }
|
|
131
|
+
.mc-card-head .mc-picker { flex: 1 1 auto; min-width: 0; }
|
|
132
|
+
.mc-card-head .mc-picker .mc-btn { min-width: 0; }
|
|
127
133
|
/* No coop-specific height cap: both panes must share Device 1's 60vh rule so
|
|
128
134
|
they match at every size, Fit included (0.11.2). */
|
|
129
|
-
.mc-coop-select { width: auto; padding: 3px 6px; font-size: 12px; }
|
|
135
|
+
.mc-coop-select { width: auto; min-width: 0; flex: 0 1 auto; padding: 3px 6px; font-size: 12px; }
|
|
130
136
|
.mc-live-nav { display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
131
137
|
.mc-live-nav button { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.18)); background: light-dark(#fff, #20242b); color: inherit; border-radius: 999px; width: 40px; height: 32px; cursor: pointer; font-size: 14px; }
|
|
132
138
|
.mc-live-nav button:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
|
|
@@ -171,8 +177,8 @@ window.__ModuleLoader__.load({
|
|
|
171
177
|
.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
178
|
.mc-seg button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 11px; padding: 4px 9px; cursor: pointer; opacity: .6; }
|
|
173
179
|
.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 {
|
|
175
|
-
.mc-live-stage.fit img { max-height: 100%; }
|
|
180
|
+
.mc-live-stage.fit { width: min(100%, calc(60vh * var(--mc-ar-n, 0.45))); aspect-ratio: var(--mc-ar-n, 0.45); display: flex; align-items: center; justify-content: center; margin-inline: auto; }
|
|
181
|
+
.mc-live-stage.fit img { width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }
|
|
176
182
|
.mc-live-stage.bezel { padding: 8px; background: #111; border-radius: 16px; }
|
|
177
183
|
.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); }
|
|
178
184
|
/* ── 0.7.0: compact conversation cards ── */
|
|
@@ -190,7 +196,7 @@ window.__ModuleLoader__.load({
|
|
|
190
196
|
// Shown on both stream captions so a refresh visibly proves which client
|
|
191
197
|
// bundle the browser is actually running. Kept in lockstep with
|
|
192
198
|
// package.json by test/client.mjs.
|
|
193
|
-
const MC_VERSION = "0.11.
|
|
199
|
+
const MC_VERSION = "0.11.6";
|
|
194
200
|
// The panel shows cards for platforms the host can actually drive; the
|
|
195
201
|
// server reports its OS in info.os (iOS tooling is darwin-only).
|
|
196
202
|
const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
|
|
@@ -242,6 +248,58 @@ window.__ModuleLoader__.load({
|
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
// Shared SVG icon paths for the stream toolbars (0.11.5 — both panes
|
|
252
|
+
// carry the same navigation controls; Device 2 used to lack them).
|
|
253
|
+
const NAV_ICONS = {
|
|
254
|
+
back: "M15 18l-6-6 6-6",
|
|
255
|
+
home: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5",
|
|
256
|
+
recents: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5",
|
|
257
|
+
screenshot: "M4 7h4l2-3h4l2 3h4v13H4zM12 17a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
|
|
258
|
+
rotate: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6",
|
|
259
|
+
};
|
|
260
|
+
/** One icon toolbar row; items: {title, icon, run}. Used by both panes. */
|
|
261
|
+
function ToolbarRow({ items }) {
|
|
262
|
+
return h("div", { className: "mc-toolbar" },
|
|
263
|
+
items.map((item) => h("button", { key: item.title, title: item.title, onClick: item.run },
|
|
264
|
+
h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
265
|
+
h("path", { d: item.icon }),
|
|
266
|
+
),
|
|
267
|
+
)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
/** Back/Home/Recents/(Screen)/Rotate built from nav icons (0.11.5). */
|
|
271
|
+
const navToolbar = (control, capture, refresh) => [
|
|
272
|
+
{ title: "Back", icon: NAV_ICONS.back, run: () => control({ kind: "button", name: "back" }) },
|
|
273
|
+
{ title: "Home", icon: NAV_ICONS.home, run: () => control({ kind: "button", name: "home" }) },
|
|
274
|
+
{ title: "Recents", icon: NAV_ICONS.recents, run: () => control({ kind: "button", name: "recents" }) },
|
|
275
|
+
...(capture ? [{ title: "Screenshot", icon: NAV_ICONS.screenshot, run: () => capture() }] : []),
|
|
276
|
+
{ title: "Rotate", icon: NAV_ICONS.rotate, run: () => control({ kind: "rotate" }) },
|
|
277
|
+
{ title: "Refresh", icon: NAV_ICONS.rotate, run: refresh },
|
|
278
|
+
];
|
|
279
|
+
/** ☰ device-actions popover; serial-bound, shared by both panes. */
|
|
280
|
+
function DeviceMenu({ serial, onError }) {
|
|
281
|
+
const [open, setOpen] = useState(false);
|
|
282
|
+
const ACTION_ITEMS = [
|
|
283
|
+
{ action: "notifications", label: "Notifications" },
|
|
284
|
+
{ action: "quick_settings", label: "Quick settings" },
|
|
285
|
+
{ action: "collapse", label: "Collapse" },
|
|
286
|
+
{ action: "lock", label: "Lock" },
|
|
287
|
+
{ action: "wake", label: "Wake" },
|
|
288
|
+
{ action: "assistant", label: "Assistant" },
|
|
289
|
+
];
|
|
290
|
+
const run = async (action) => {
|
|
291
|
+
if (serial === "") return;
|
|
292
|
+
try { await streamPost("/stream/device-action", { device: serial, action }); setOpen(false); }
|
|
293
|
+
catch (err) { onError(err instanceof Error ? err.message : String(err)); }
|
|
294
|
+
};
|
|
295
|
+
return h("div", { className: "mc-menu" },
|
|
296
|
+
h("button", { className: "mc-btn", disabled: serial === "", title: "Device actions", onClick: () => setOpen((v) => !v) }, "☰"),
|
|
297
|
+
open && h("div", { className: "mc-menu-pop" },
|
|
298
|
+
ACTION_ITEMS.map((item) => h("button", { key: item.action, onClick: () => run(item.action) }, item.label)),
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
245
303
|
function PanelController() {
|
|
246
304
|
let panelOpen = false;
|
|
247
305
|
const listeners = new Set();
|
|
@@ -566,6 +624,10 @@ window.__ModuleLoader__.load({
|
|
|
566
624
|
* the full card and by the second co-op pane. */
|
|
567
625
|
function useLiveStream({ serial, onError }) {
|
|
568
626
|
const [streamUrl, setStreamUrl] = useState("");
|
|
627
|
+
// Native aspect ratio (w/h) of the streamed frames, learned from the
|
|
628
|
+
// first decoded MJPEG frame so the Fit stage has the device's real
|
|
629
|
+
// shape — zero letterboxing regardless of pane/device mix (0.11.5).
|
|
630
|
+
const [ar, setAr] = useState(null);
|
|
569
631
|
const imgRef = useRef(null);
|
|
570
632
|
const dragRef = useRef(null);
|
|
571
633
|
const grantTimer = useRef(null);
|
|
@@ -643,7 +705,22 @@ window.__ModuleLoader__.load({
|
|
|
643
705
|
control({ kind: "tap", x: end.x, y: end.y });
|
|
644
706
|
}
|
|
645
707
|
};
|
|
646
|
-
|
|
708
|
+
// Aspect-ratio learner: MJPEG frames decode into the same <img>, so
|
|
709
|
+
// naturalWidth/naturalHeight is live. Poll once a second while the
|
|
710
|
+
// stream is up; stage boxes read --mc-ar-n from here (fit mode).
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (streamUrl === "") return;
|
|
713
|
+
const tick = setInterval(() => {
|
|
714
|
+
const el = imgRef.current;
|
|
715
|
+
if (el && el.naturalWidth > 0 && el.naturalHeight > 0) {
|
|
716
|
+
const a = +(el.naturalWidth / el.naturalHeight).toFixed(4);
|
|
717
|
+
setAr((prev) => (prev === a ? prev : a));
|
|
718
|
+
}
|
|
719
|
+
}, 1000);
|
|
720
|
+
return () => clearInterval(tick);
|
|
721
|
+
}, [streamUrl]);
|
|
722
|
+
|
|
723
|
+
return { streamUrl, grant, reset, control, ar, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
|
|
647
724
|
}
|
|
648
725
|
|
|
649
726
|
/** Device 1 stream: picker-popover header, SVG toolbar pill, device
|
|
@@ -656,7 +733,6 @@ window.__ModuleLoader__.load({
|
|
|
656
733
|
const [serial, setSerial] = useState("");
|
|
657
734
|
const [error, setError] = useState("");
|
|
658
735
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
659
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
660
736
|
const [still, setStill] = useState(null);
|
|
661
737
|
const [view, setView] = useState("live"); // 'live' | 'still'
|
|
662
738
|
// sizeMode/frame are per-pane (0.11.2); StageControls renders the
|
|
@@ -724,12 +800,6 @@ window.__ModuleLoader__.load({
|
|
|
724
800
|
}
|
|
725
801
|
};
|
|
726
802
|
|
|
727
|
-
const menuAction = async (action) => {
|
|
728
|
-
if (serial === "") return;
|
|
729
|
-
try { await streamPost("/stream/device-action", { device: serial, action }); setMenuOpen(false); }
|
|
730
|
-
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
|
731
|
-
};
|
|
732
|
-
|
|
733
803
|
const pick = (device) => { autoPicked.current = true; setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
734
804
|
// 0.10.0 merged "Start server": boot a configured AVD from the picker.
|
|
735
805
|
// The route waits for boot completion; then we re-list and stream it.
|
|
@@ -766,22 +836,7 @@ window.__ModuleLoader__.load({
|
|
|
766
836
|
const sizeStyle = stageSizeStyle(sizeMode);
|
|
767
837
|
const stageClass = stageClassFor(frame, sizeMode.mode === "fit");
|
|
768
838
|
const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
|
|
769
|
-
const toolbar =
|
|
770
|
-
{ title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
|
|
771
|
-
{ title: "Home", icon: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5", run: () => control({ kind: "button", name: "home" }) },
|
|
772
|
-
{ title: "Recents", icon: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5", run: () => control({ kind: "button", name: "recents" }) },
|
|
773
|
-
{ title: "Screenshot", icon: "M4 7h4l2-3h4l2 3h4v13H4zM12 17a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", run: () => capture() },
|
|
774
|
-
{ title: "Rotate", icon: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6", run: () => control({ kind: "rotate" }) },
|
|
775
|
-
{ title: "Refresh", icon: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6", run: () => { if (serial !== "") grant(serial); } },
|
|
776
|
-
];
|
|
777
|
-
const menuItems = [
|
|
778
|
-
{ action: "notifications", label: "Notifications" },
|
|
779
|
-
{ action: "quick_settings", label: "Quick settings" },
|
|
780
|
-
{ action: "collapse", label: "Collapse" },
|
|
781
|
-
{ action: "lock", label: "Lock" },
|
|
782
|
-
{ action: "wake", label: "Wake" },
|
|
783
|
-
{ action: "assistant", label: "Assistant" },
|
|
784
|
-
];
|
|
839
|
+
const toolbar = navToolbar(control, capture, () => { if (serial !== "") grant(serial); });
|
|
785
840
|
|
|
786
841
|
return h("div", { className: "mc-card mc-live" },
|
|
787
842
|
h("div", { className: "mc-card-head" },
|
|
@@ -823,12 +878,7 @@ window.__ModuleLoader__.load({
|
|
|
823
878
|
),
|
|
824
879
|
),
|
|
825
880
|
),
|
|
826
|
-
h(
|
|
827
|
-
h("button", { className: "mc-btn", disabled: serial === "", title: "Device actions", onClick: () => setMenuOpen((v) => !v) }, "☰"),
|
|
828
|
-
menuOpen && h("div", { className: "mc-menu-pop" },
|
|
829
|
-
menuItems.map((item) => h("button", { key: item.action, onClick: () => menuAction(item.action) }, item.label)),
|
|
830
|
-
),
|
|
831
|
-
),
|
|
881
|
+
h(DeviceMenu, { serial, onError: setError }),
|
|
832
882
|
// 0.9.0 co-op: side-by-side second pane (needs two online devices).
|
|
833
883
|
devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch Device 1 + Device 2 side by side", onClick: onToggleCoop }, "⧉"),
|
|
834
884
|
),
|
|
@@ -837,14 +887,8 @@ window.__ModuleLoader__.load({
|
|
|
837
887
|
h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
|
|
838
888
|
// 0.11.0 audit: no ghost affordances — the toolbar appears once a
|
|
839
889
|
// device is selected (disabled-grey read as broken).
|
|
840
|
-
serial !== "" && h(
|
|
841
|
-
|
|
842
|
-
h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
843
|
-
h("path", { d: item.icon }),
|
|
844
|
-
),
|
|
845
|
-
)),
|
|
846
|
-
),
|
|
847
|
-
h("div", { className: stageClass },
|
|
890
|
+
serial !== "" && h(ToolbarRow, { items: toolbar }),
|
|
891
|
+
h("div", { className: stageClass, style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
|
|
848
892
|
view === "still" && still?.dataUrl
|
|
849
893
|
? h("img", { src: still.dataUrl, alt: "still capture", draggable: false })
|
|
850
894
|
: live.streamUrl !== ""
|
|
@@ -929,9 +973,13 @@ window.__ModuleLoader__.load({
|
|
|
929
973
|
!devices.some((d) => d.serial === serial) && h("option", { value: "" }, devices.length === 0 ? "no device" : "— pick device —"),
|
|
930
974
|
devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
|
|
931
975
|
),
|
|
976
|
+
h(DeviceMenu, { serial, onError: setError }),
|
|
932
977
|
),
|
|
933
978
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
934
|
-
|
|
979
|
+
// 0.11.5: Device 2 gets the same nav toolbar as Device 1
|
|
980
|
+
// (back/home/recents/rotate/refresh) — full control of both players.
|
|
981
|
+
serial !== "" && h(ToolbarRow, { items: navToolbar(live.control, undefined, () => { if (serial !== "") grant(serial); }) }),
|
|
982
|
+
h("div", { className: stageClassFor(frame, sizeMode.mode === "fit"), style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
|
|
935
983
|
live.streamUrl !== ""
|
|
936
984
|
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
|
|
937
985
|
: h("div", { className: "mc-live-off" }, devices.filter((d) => d.serial !== excludeSerial).length === 0
|
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.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|