dsh-mobilecode 0.11.4 → 0.11.5

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.
Files changed (2) hide show
  1. package/lib/client.js +85 -43
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -121,7 +121,7 @@ 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(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
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
  .mc-live-section.coop .mc-card-head { flex-wrap: wrap; }
127
127
  /* No coop-specific height cap: both panes must share Device 1's 60vh rule so
@@ -171,8 +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
+ .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; }
175
+ .mc-live-stage.fit img { width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }
176
176
  .mc-live-stage.bezel { padding: 8px; background: #111; border-radius: 16px; }
177
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); }
178
178
  /* ── 0.7.0: compact conversation cards ── */
@@ -190,7 +190,7 @@ window.__ModuleLoader__.load({
190
190
  // Shown on both stream captions so a refresh visibly proves which client
191
191
  // bundle the browser is actually running. Kept in lockstep with
192
192
  // package.json by test/client.mjs.
193
- const MC_VERSION = "0.11.4";
193
+ const MC_VERSION = "0.11.5";
194
194
  // The panel shows cards for platforms the host can actually drive; the
195
195
  // server reports its OS in info.os (iOS tooling is darwin-only).
196
196
  const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
@@ -242,6 +242,58 @@ window.__ModuleLoader__.load({
242
242
  );
243
243
  }
244
244
 
245
+ // Shared SVG icon paths for the stream toolbars (0.11.5 — both panes
246
+ // carry the same navigation controls; Device 2 used to lack them).
247
+ const NAV_ICONS = {
248
+ back: "M15 18l-6-6 6-6",
249
+ home: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5",
250
+ recents: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5",
251
+ screenshot: "M4 7h4l2-3h4l2 3h4v13H4zM12 17a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
252
+ rotate: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6",
253
+ };
254
+ /** One icon toolbar row; items: {title, icon, run}. Used by both panes. */
255
+ function ToolbarRow({ items }) {
256
+ return h("div", { className: "mc-toolbar" },
257
+ items.map((item) => h("button", { key: item.title, title: item.title, onClick: item.run },
258
+ h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
259
+ h("path", { d: item.icon }),
260
+ ),
261
+ )),
262
+ );
263
+ }
264
+ /** Back/Home/Recents/(Screen)/Rotate built from nav icons (0.11.5). */
265
+ const navToolbar = (control, capture, refresh) => [
266
+ { title: "Back", icon: NAV_ICONS.back, run: () => control({ kind: "button", name: "back" }) },
267
+ { title: "Home", icon: NAV_ICONS.home, run: () => control({ kind: "button", name: "home" }) },
268
+ { title: "Recents", icon: NAV_ICONS.recents, run: () => control({ kind: "button", name: "recents" }) },
269
+ ...(capture ? [{ title: "Screenshot", icon: NAV_ICONS.screenshot, run: () => capture() }] : []),
270
+ { title: "Rotate", icon: NAV_ICONS.rotate, run: () => control({ kind: "rotate" }) },
271
+ { title: "Refresh", icon: NAV_ICONS.rotate, run: refresh },
272
+ ];
273
+ /** ☰ device-actions popover; serial-bound, shared by both panes. */
274
+ function DeviceMenu({ serial, onError }) {
275
+ const [open, setOpen] = useState(false);
276
+ const ACTION_ITEMS = [
277
+ { action: "notifications", label: "Notifications" },
278
+ { action: "quick_settings", label: "Quick settings" },
279
+ { action: "collapse", label: "Collapse" },
280
+ { action: "lock", label: "Lock" },
281
+ { action: "wake", label: "Wake" },
282
+ { action: "assistant", label: "Assistant" },
283
+ ];
284
+ const run = async (action) => {
285
+ if (serial === "") return;
286
+ try { await streamPost("/stream/device-action", { device: serial, action }); setOpen(false); }
287
+ catch (err) { onError(err instanceof Error ? err.message : String(err)); }
288
+ };
289
+ return h("div", { className: "mc-menu" },
290
+ h("button", { className: "mc-btn", disabled: serial === "", title: "Device actions", onClick: () => setOpen((v) => !v) }, "☰"),
291
+ open && h("div", { className: "mc-menu-pop" },
292
+ ACTION_ITEMS.map((item) => h("button", { key: item.action, onClick: () => run(item.action) }, item.label)),
293
+ ),
294
+ );
295
+ }
296
+
245
297
  function PanelController() {
246
298
  let panelOpen = false;
247
299
  const listeners = new Set();
@@ -566,6 +618,10 @@ window.__ModuleLoader__.load({
566
618
  * the full card and by the second co-op pane. */
567
619
  function useLiveStream({ serial, onError }) {
568
620
  const [streamUrl, setStreamUrl] = useState("");
621
+ // Native aspect ratio (w/h) of the streamed frames, learned from the
622
+ // first decoded MJPEG frame so the Fit stage has the device's real
623
+ // shape — zero letterboxing regardless of pane/device mix (0.11.5).
624
+ const [ar, setAr] = useState(null);
569
625
  const imgRef = useRef(null);
570
626
  const dragRef = useRef(null);
571
627
  const grantTimer = useRef(null);
@@ -643,7 +699,22 @@ window.__ModuleLoader__.load({
643
699
  control({ kind: "tap", x: end.x, y: end.y });
644
700
  }
645
701
  };
646
- return { streamUrl, grant, reset, control, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
702
+ // Aspect-ratio learner: MJPEG frames decode into the same <img>, so
703
+ // naturalWidth/naturalHeight is live. Poll once a second while the
704
+ // stream is up; stage boxes read --mc-ar-n from here (fit mode).
705
+ useEffect(() => {
706
+ if (streamUrl === "") return;
707
+ const tick = setInterval(() => {
708
+ const el = imgRef.current;
709
+ if (el && el.naturalWidth > 0 && el.naturalHeight > 0) {
710
+ const a = +(el.naturalWidth / el.naturalHeight).toFixed(4);
711
+ setAr((prev) => (prev === a ? prev : a));
712
+ }
713
+ }, 1000);
714
+ return () => clearInterval(tick);
715
+ }, [streamUrl]);
716
+
717
+ return { streamUrl, grant, reset, control, ar, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
647
718
  }
648
719
 
649
720
  /** Device 1 stream: picker-popover header, SVG toolbar pill, device
@@ -656,7 +727,6 @@ window.__ModuleLoader__.load({
656
727
  const [serial, setSerial] = useState("");
657
728
  const [error, setError] = useState("");
658
729
  const [pickerOpen, setPickerOpen] = useState(false);
659
- const [menuOpen, setMenuOpen] = useState(false);
660
730
  const [still, setStill] = useState(null);
661
731
  const [view, setView] = useState("live"); // 'live' | 'still'
662
732
  // sizeMode/frame are per-pane (0.11.2); StageControls renders the
@@ -724,12 +794,6 @@ window.__ModuleLoader__.load({
724
794
  }
725
795
  };
726
796
 
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
797
  const pick = (device) => { autoPicked.current = true; setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
734
798
  // 0.10.0 merged "Start server": boot a configured AVD from the picker.
735
799
  // The route waits for boot completion; then we re-list and stream it.
@@ -766,22 +830,7 @@ window.__ModuleLoader__.load({
766
830
  const sizeStyle = stageSizeStyle(sizeMode);
767
831
  const stageClass = stageClassFor(frame, sizeMode.mode === "fit");
768
832
  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
- ];
833
+ const toolbar = navToolbar(control, capture, () => { if (serial !== "") grant(serial); });
785
834
 
786
835
  return h("div", { className: "mc-card mc-live" },
787
836
  h("div", { className: "mc-card-head" },
@@ -823,12 +872,7 @@ window.__ModuleLoader__.load({
823
872
  ),
824
873
  ),
825
874
  ),
826
- h("div", { className: "mc-menu" },
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
- ),
875
+ h(DeviceMenu, { serial, onError: setError }),
832
876
  // 0.9.0 co-op: side-by-side second pane (needs two online devices).
833
877
  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
878
  ),
@@ -837,14 +881,8 @@ window.__ModuleLoader__.load({
837
881
  h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
838
882
  // 0.11.0 audit: no ghost affordances — the toolbar appears once a
839
883
  // device is selected (disabled-grey read as broken).
840
- serial !== "" && h("div", { className: "mc-toolbar" },
841
- toolbar.map((item) => h("button", { key: item.title, title: item.title, disabled: serial === "", onClick: item.run },
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 },
884
+ serial !== "" && h(ToolbarRow, { items: toolbar }),
885
+ h("div", { className: stageClass, style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
848
886
  view === "still" && still?.dataUrl
849
887
  ? h("img", { src: still.dataUrl, alt: "still capture", draggable: false })
850
888
  : live.streamUrl !== ""
@@ -929,9 +967,13 @@ window.__ModuleLoader__.load({
929
967
  !devices.some((d) => d.serial === serial) && h("option", { value: "" }, devices.length === 0 ? "no device" : "— pick device —"),
930
968
  devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
931
969
  ),
970
+ h(DeviceMenu, { serial, onError: setError }),
932
971
  ),
933
972
  error !== "" && h("div", { className: "mc-error" }, error),
934
- h("div", { className: stageClassFor(frame, sizeMode.mode === "fit") },
973
+ // 0.11.5: Device 2 gets the same nav toolbar as Device 1
974
+ // (back/home/recents/rotate/refresh) — full control of both players.
975
+ serial !== "" && h(ToolbarRow, { items: navToolbar(live.control, undefined, () => { if (serial !== "") grant(serial); }) }),
976
+ h("div", { className: stageClassFor(frame, sizeMode.mode === "fit"), style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
935
977
  live.streamUrl !== ""
936
978
  ? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
937
979
  : 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",
4
+ "version": "0.11.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {